feat(landing): migrate to vinext and fix prod regressions (#4882)
This commit is contained in:
parent
5e26d1d33d
commit
21fb6dfac0
|
|
@ -1,17 +1,14 @@
|
|||
import NextBundleAnalyzer from '@next/bundle-analyzer'
|
||||
import { codeInspectorPlugin } from 'code-inspector-plugin'
|
||||
import { config } from 'dotenv'
|
||||
import type { NextConfig } from 'next'
|
||||
import createNextIntlPlugin from 'next-intl/plugin'
|
||||
|
||||
process.title = 'Folo Landing (NextJS)'
|
||||
process.title = 'Folo Landing (vinext)'
|
||||
|
||||
const env = config().parsed || {}
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
|
||||
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts')
|
||||
|
||||
let nextConfig: NextConfig = {
|
||||
const nextConfig = {
|
||||
reactStrictMode: false,
|
||||
productionBrowserSourceMaps: true,
|
||||
output: 'standalone',
|
||||
|
|
@ -46,18 +43,6 @@ let nextConfig: NextConfig = {
|
|||
],
|
||||
}
|
||||
},
|
||||
turbopack: {
|
||||
rules: codeInspectorPlugin({
|
||||
bundler: 'turbopack',
|
||||
hotKeys: ['altKey'],
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
if (process.env.ANALYZE === 'true') {
|
||||
nextConfig = NextBundleAnalyzer({
|
||||
enabled: true,
|
||||
})(nextConfig)
|
||||
}
|
||||
|
||||
export default withNextIntl(nextConfig)
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
import { defineCloudflareConfig } from '@opennextjs/cloudflare'
|
||||
|
||||
export default defineCloudflareConfig()
|
||||
|
|
@ -4,11 +4,12 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production next build",
|
||||
"cf:build": "pnpm exec opennextjs-cloudflare build",
|
||||
"cf:deploy": "pnpm exec wrangler deploy",
|
||||
"cf:deploy:dev": "pnpm exec wrangler deploy --env dev",
|
||||
"dev": "cross-env NODE_ENV=development next dev -p 4399",
|
||||
"build": "cross-env NODE_ENV=production vinext build",
|
||||
"cf:build": "cross-env NODE_ENV=production vinext build",
|
||||
"cf:deploy": "pnpm exec vinext deploy",
|
||||
"cf:deploy:dev": "pnpm exec vinext deploy --preview",
|
||||
"dev": "cross-env NODE_ENV=development vinext dev -p 4399",
|
||||
"start": "cross-env NODE_ENV=production vinext start -p 4399",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
@ -32,7 +33,6 @@
|
|||
"jotai": "2.15.0",
|
||||
"js-cookie": "3.0.5",
|
||||
"motion": "12.23.24",
|
||||
"next": "16.0.11",
|
||||
"next-intl": "4.4.0",
|
||||
"next-themes": "0.4.6",
|
||||
"ogl": "1.0.11",
|
||||
|
|
@ -54,23 +54,24 @@
|
|||
"tailwind-merge": "3.3.1",
|
||||
"unified": "11.0.5",
|
||||
"usehooks-ts": "3.1.1",
|
||||
"vaul": "1.1.2"
|
||||
"vaul": "1.1.2",
|
||||
"vinext": "0.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/vite-plugin": "1.25.5",
|
||||
"@egoist/tailwindcss-icons": "1.9.0",
|
||||
"@iconify-json/lucide": "1.2.71",
|
||||
"@iconify-json/mingcute": "1.2.5",
|
||||
"@iconify-json/simple-icons": "1.2.56",
|
||||
"@iconify/tailwind": "1.2.0",
|
||||
"@innei/prettier": "1.0.0",
|
||||
"@next/bundle-analyzer": "16.0.11",
|
||||
"@opennextjs/cloudflare": "1.16.5",
|
||||
"@tailwindcss/postcss": "4.1.16",
|
||||
"@tailwindcss/typography": "0.5.19",
|
||||
"@tanstack/react-query-devtools": "5.90.2",
|
||||
"@types/node": "24.9.1",
|
||||
"@types/react": "19.1.17",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-rsc": "0.5.21",
|
||||
"autoprefixer": "10.4.21",
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"code-inspector-plugin": "1.2.10",
|
||||
|
|
@ -87,6 +88,7 @@
|
|||
"tailwindcss-animate": "1.0.7",
|
||||
"tailwindcss-safe-area": "1.1.0",
|
||||
"typescript": "catalog:",
|
||||
"wrangler": "4.67.0"
|
||||
"vite": "7.3.1",
|
||||
"wrangler": "4.68.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
'use server'
|
||||
|
||||
export async function getGithubStar() {
|
||||
return await fetch(`https://api.github.com/repos/RSSNext/Folo`, {
|
||||
headers: {
|
||||
Authorization: process.env.GITHUB_TOKEN
|
||||
? `Bearer ${process.env.GITHUB_TOKEN}`
|
||||
: '',
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.message) {
|
||||
throw new Error(data.message)
|
||||
}
|
||||
|
||||
return data.stargazers_count as number
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
return -1
|
||||
})
|
||||
}
|
||||
|
|
@ -4,14 +4,24 @@ import { getTranslations } from 'next-intl/server'
|
|||
|
||||
import { DownloadHero } from '~/components/widgets/download/DownloadHero'
|
||||
import { PlatformDownloads } from '~/components/widgets/download/PlatformDownloads'
|
||||
import { defaultLocale, locales } from '~/i18n/routing'
|
||||
import { detectPlatform } from '~/lib/platform'
|
||||
|
||||
type LocaleParams = { locale?: string }
|
||||
|
||||
const localeSet = new Set(locales)
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
params: Promise<LocaleParams> | LocaleParams | undefined
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await params
|
||||
const localeFromParams = params ? (await params).locale : undefined
|
||||
const locale =
|
||||
localeFromParams &&
|
||||
localeSet.has(localeFromParams as (typeof locales)[number])
|
||||
? localeFromParams
|
||||
: defaultLocale
|
||||
const t = await getTranslations({ locale, namespace: 'download.metadata' })
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { Root } from '~/components/layout/root/Root'
|
|||
import { LightRays } from '~/components/ui/light-rays'
|
||||
import { LandingHeader } from '~/components/widgets/landing/Header'
|
||||
import { siteInfo } from '~/constants/site'
|
||||
import { locales } from '~/i18n/routing'
|
||||
import { defaultLocale, locales } from '~/i18n/routing'
|
||||
import { sansFont } from '~/lib/fonts'
|
||||
|
||||
import { Providers } from '../../providers/root'
|
||||
|
|
@ -18,14 +18,30 @@ import { InitInClient } from '../InitInClient'
|
|||
|
||||
init()
|
||||
|
||||
type LocaleParams = { locale?: string }
|
||||
|
||||
type MaybeAsyncLocaleParams = LocaleParams | Promise<LocaleParams> | undefined
|
||||
|
||||
const localeSet = new Set(locales)
|
||||
|
||||
const resolveLocale = async (params: MaybeAsyncLocaleParams) => {
|
||||
const locale = params ? (await params).locale : undefined
|
||||
|
||||
if (locale && localeSet.has(locale as (typeof locales)[number])) {
|
||||
return locale
|
||||
}
|
||||
|
||||
return defaultLocale
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return locales.map((locale) => ({ locale }))
|
||||
}
|
||||
|
||||
export async function generateMetadata(
|
||||
params: Promise<{ locale: string }>,
|
||||
): Promise<Metadata> {
|
||||
const { locale } = await params
|
||||
export async function generateMetadata(props: {
|
||||
params: MaybeAsyncLocaleParams
|
||||
}): Promise<Metadata> {
|
||||
const locale = await resolveLocale(props.params)
|
||||
const t = await getTranslations({ locale, namespace: 'metadata' })
|
||||
|
||||
const title = t('title', { defaultValue: siteInfo.title })
|
||||
|
|
@ -107,10 +123,10 @@ export default async function LocaleLayout({
|
|||
params,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
params: Promise<{ locale: string }>
|
||||
params: MaybeAsyncLocaleParams
|
||||
}) {
|
||||
const { locale } = await params
|
||||
const messages = await getMessages()
|
||||
const locale = await resolveLocale(params)
|
||||
const messages = await getMessages({ locale })
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,29 @@
|
|||
'use client'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
import { getGithubStar } from '~/actions/github-star'
|
||||
type GithubRepoStatsResponse = {
|
||||
repo?: {
|
||||
stars?: number
|
||||
}
|
||||
}
|
||||
|
||||
export const useGithubStar = () =>
|
||||
useQuery({
|
||||
queryKey: ['github-star'],
|
||||
queryFn: () => getGithubStar(),
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await fetch('https://ungh.cc/repos/RSSNext/Folo')
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch repo stats: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = (await response.json()) as GithubRepoStatsResponse
|
||||
return typeof data.repo?.stars === 'number' ? data.repo.stars : -1
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return -1
|
||||
}
|
||||
},
|
||||
staleTime: 1000 * 60 * 10,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,25 @@
|
|||
import { getRequestConfig } from 'next-intl/server'
|
||||
|
||||
export default getRequestConfig(async ({ locale, requestLocale }) => {
|
||||
const localeValue = locale || (await requestLocale) || 'en'
|
||||
type RequestConfigParams = {
|
||||
locale?: string
|
||||
requestLocale?: Promise<string | undefined> | string | undefined
|
||||
}
|
||||
|
||||
const SUPPORTED_LOCALES = ['en', 'zh', 'jp'] as const
|
||||
const DEFAULT_LOCALE = 'en'
|
||||
const localeSet = new Set<string>(SUPPORTED_LOCALES)
|
||||
|
||||
export default getRequestConfig(async (params?: RequestConfigParams) => {
|
||||
const localeFromParam = params?.locale
|
||||
const requestLocaleValue = params?.requestLocale
|
||||
const localeFromRequest = requestLocaleValue
|
||||
? await requestLocaleValue
|
||||
: undefined
|
||||
const requestedLocale = localeFromParam || localeFromRequest
|
||||
const localeValue =
|
||||
requestedLocale && localeSet.has(requestedLocale)
|
||||
? requestedLocale
|
||||
: DEFAULT_LOCALE
|
||||
|
||||
return {
|
||||
locale: localeValue,
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
import createMiddleware from 'next-intl/middleware'
|
||||
|
||||
import { routing } from '~/i18n/routing'
|
||||
|
||||
export default createMiddleware(routing)
|
||||
|
||||
export const config = {
|
||||
matcher: ['/((?!_next|api|apple-app-site-association|.*\\..*).*)'],
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
|
||||
import { defaultLocale, locales } from '~/i18n/routing'
|
||||
|
||||
const localeSet = new Set(locales)
|
||||
const rscSuffix = '.rsc'
|
||||
const bypassPrefixes = ['/_next', '/_vinext', '/api']
|
||||
const bypassExactPaths = new Set([
|
||||
'/apple-app-site-association',
|
||||
'/.well-known/apple-app-site-association',
|
||||
])
|
||||
|
||||
const getLogicalPathname = (pathname: string) => {
|
||||
if (!pathname.endsWith(rscSuffix)) {
|
||||
return pathname
|
||||
}
|
||||
|
||||
const logicalPathname = pathname.slice(0, -rscSuffix.length)
|
||||
return logicalPathname === '' ? '/' : logicalPathname
|
||||
}
|
||||
|
||||
export function proxy(request: Request) {
|
||||
const url = new URL(request.url)
|
||||
const rawPathname = url.pathname
|
||||
const pathname = getLogicalPathname(rawPathname)
|
||||
|
||||
if (bypassPrefixes.some((prefix) => pathname.startsWith(prefix))) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
if (bypassExactPaths.has(pathname)) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
const hasFileExtension = /\.[^/]+$/.test(rawPathname)
|
||||
if (hasFileExtension && !rawPathname.endsWith(rscSuffix)) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
const firstSegment = pathname.split('/').find(Boolean)
|
||||
if (firstSegment && localeSet.has(firstSegment as (typeof locales)[number])) {
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
const rewritePath =
|
||||
pathname === '/' ? `/${defaultLocale}` : `/${defaultLocale}${pathname}`
|
||||
const rewriteUrl = new URL(rewritePath, request.url)
|
||||
rewriteUrl.search = url.search
|
||||
|
||||
return NextResponse.rewrite(rewriteUrl)
|
||||
}
|
||||
|
||||
export const middleware = proxy
|
||||
export default proxy
|
||||
|
||||
export const config = {
|
||||
matcher: ['/:path*'],
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
@import 'tailwindcss';
|
||||
@import 'tailwindcss-safe-area';
|
||||
@import './pastel-theme-oklch.css';
|
||||
|
||||
@plugin "@tailwindcss/typography";
|
||||
@plugin '@egoist/tailwindcss-icons';
|
||||
@plugin "tailwind-scrollbar";
|
||||
@plugin 'tailwindcss-animate';
|
||||
@import './pastel-theme-oklch.css';
|
||||
|
||||
@source "./src/**/*.{js,jsx,ts,tsx}";
|
||||
@source "../**/*.{js,jsx,ts,tsx}";
|
||||
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));
|
||||
|
||||
@theme {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { cloudflare } from '@cloudflare/vite-plugin'
|
||||
import vinext from 'vinext'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vinext(),
|
||||
cloudflare({
|
||||
viteEnvironment: {
|
||||
name: 'rsc',
|
||||
childEnvironments: ['ssr'],
|
||||
},
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'next-intl/config': fileURLToPath(
|
||||
new URL('src/i18n/request.ts', import.meta.url),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import handler from 'vinext/server/app-router-entry'
|
||||
import { handleImageOptimization } from 'vinext/server/image-optimization'
|
||||
|
||||
export default {
|
||||
async fetch(request, env) {
|
||||
const url = new URL(request.url)
|
||||
|
||||
if (url.pathname === '/_vinext/image') {
|
||||
return handleImageOptimization(request, {
|
||||
fetchAsset: (path) =>
|
||||
env.ASSETS.fetch(new Request(new URL(path, request.url))),
|
||||
transformImage: async (body, { width, format, quality }) => {
|
||||
const result = await env.IMAGES.input(body)
|
||||
.transform(width > 0 ? { width } : {})
|
||||
.output({ format, quality })
|
||||
return result.response()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return handler.fetch(request)
|
||||
},
|
||||
}
|
||||
|
|
@ -1,13 +1,17 @@
|
|||
{
|
||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||
"name": "landing-next",
|
||||
"main": ".open-next/worker.js",
|
||||
"name": "landing-vinext",
|
||||
"main": "./worker/index.js",
|
||||
"compatibility_date": "2026-02-01",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
"account_id": "1f1d1678a2413a54c944b3081bab5c84",
|
||||
"assets": {
|
||||
"directory": ".open-next/assets",
|
||||
"directory": "./dist/client",
|
||||
"binding": "ASSETS",
|
||||
"not_found_handling": "none",
|
||||
},
|
||||
"images": {
|
||||
"binding": "IMAGES",
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
|
|
|
|||
2884
pnpm-lock.yaml
2884
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue