mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 17:29:45 +00:00
feat(i18n): harden build artifact tracing for Hostinger deploy
Re-applies PR #182's next-intl scaffold with defensive measures aimed at the most-likely-but-unconfirmed causes of the 503 that hit prod after that PR merged. PR #183 reverted #182 to restore service; this branch carries the same i18n payload plus the safeguards below. Defensive changes vs PR #182: 1. **`outputFileTracingRoot: __dirname`** in next.config.ts. Builds were warning that Next.js inferred the workspace root as the parent GHWebhosting directory (because of a stray lockfile there). With the wrong root, file tracing for the production artifact can include the wrong files or skip ours. Pinning the root to the wacrm directory silences the warning and makes tracing deterministic across local / CI / Hostinger. 2. **`outputFileTracingIncludes: { "/**/*": ["./messages/**/*.json"] }`**. Belt-and-suspenders: explicitly force the i18n message dictionaries into the production artifact even if static-import tracing somehow misses them in Hostinger's build environment. 3. **Static locale import map in `src/i18n/request.ts`**. Replaces `import(\`../../messages/\${locale}.json\`)` with an explicit `{ en: () => import(...), es: ..., lt: ... }` map. Template-literal dynamic imports are well-supported by Turbopack's tracer but the static form leaves zero room for the tracer to skip a locale. 4. **Try/catch around the intl middleware call**. If `intlMiddleware(request)` ever throws at runtime, fall through to auth-only handling and log the error rather than 503-ing every page request behind a single bad call. 5. **Try/catch around `LOADERS[locale]()`** with English fallback. Same idea: a missing locale bundle at runtime returns English content instead of crashing the render path. 6. **`engines.node: ">=20.10.0"`** (was `>=20.0.0`). Next 16.2's actual minimum. Tells Hostinger's Node.js app picker not to land us on a too-old subversion. Verified locally: - npm run lint → 0 errors (same 18 pre-existing warnings) - npm run typecheck → clean - npm run test → 366 passing - npm run build → 62 prerendered pages across en/es/lt; workspace root warning gone; messages bundled into `.next/server/chunks/ssr/ messages_{en,es,lt}_json_[json]_cjs_*.js` - npm start probes: / → 307 → /en /en/login → 200 /es/login → 200 /lt/login → 200 /en/dashboard → 307 → /en/login (signed out) /api/whatsapp/templates → 401 If 503 reappears in prod after this lands, the local repro is clean, so the next step is hPanel build/runtime logs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8124640d80
commit
0fa06d8b14
@ -1,3 +1,4 @@
|
|||||||
|
import path from "node:path";
|
||||||
import createNextIntlPlugin from "next-intl/plugin";
|
import createNextIntlPlugin from "next-intl/plugin";
|
||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
@ -57,6 +58,20 @@ const SECURITY_HEADERS = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
|
// Pin file-tracing to this directory so Next.js doesn't ascend to the
|
||||||
|
// parent GHWebhosting folder (which has its own lockfile) and either
|
||||||
|
// bundle the wrong stuff or skip ours. Was visible as a workspace-root
|
||||||
|
// warning during builds. Also see `outputFileTracingIncludes` below —
|
||||||
|
// a wrong root makes the includes match incorrectly.
|
||||||
|
outputFileTracingRoot: path.join(__dirname),
|
||||||
|
// Force the production artifact to include the i18n message dictionaries.
|
||||||
|
// `src/i18n/request.ts` loads them via a static import map but the
|
||||||
|
// tracer can still miss JSON-only files that no code statically
|
||||||
|
// imports relative to this entry point. Belt-and-suspenders.
|
||||||
|
outputFileTracingIncludes: {
|
||||||
|
"/**/*": ["./messages/**/*.json"],
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cache-Control policy.
|
* Cache-Control policy.
|
||||||
*
|
*
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@ -45,7 +45,7 @@
|
|||||||
"vitest": "^4.1.7"
|
"vitest": "^4.1.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
"template"
|
"template"
|
||||||
],
|
],
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.10.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
@ -1,12 +1,35 @@
|
|||||||
import { hasLocale } from 'next-intl';
|
import { hasLocale } from 'next-intl';
|
||||||
import { getRequestConfig } from 'next-intl/server';
|
import { getRequestConfig } from 'next-intl/server';
|
||||||
import { routing } from './routing';
|
import { routing, type Locale } from './routing';
|
||||||
|
|
||||||
|
// Static import map. Template-literal dynamic imports (e.g.
|
||||||
|
// `import(\`../../messages/\${locale}.json\`)`) can confuse Next.js's
|
||||||
|
// file tracer in some build environments — the JSONs may be missing
|
||||||
|
// from the production artifact even though they resolve in `next dev`
|
||||||
|
// and `next start` locally. Naming each path explicitly forces every
|
||||||
|
// supported locale's message file into the bundle.
|
||||||
|
const LOADERS: Record<Locale, () => Promise<{ default: Record<string, unknown> }>> = {
|
||||||
|
en: () => import('../../messages/en.json'),
|
||||||
|
es: () => import('../../messages/es.json'),
|
||||||
|
lt: () => import('../../messages/lt.json'),
|
||||||
|
};
|
||||||
|
|
||||||
export default getRequestConfig(async ({ requestLocale }) => {
|
export default getRequestConfig(async ({ requestLocale }) => {
|
||||||
const requested = await requestLocale;
|
const requested = await requestLocale;
|
||||||
const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale;
|
const locale: Locale = hasLocale(routing.locales, requested)
|
||||||
|
? requested
|
||||||
|
: routing.defaultLocale;
|
||||||
|
|
||||||
const messages = (await import(`../../messages/${locale}.json`)).default;
|
// Fall back to English if the requested locale's bundle is missing
|
||||||
|
// at runtime. A missing locale should display English content rather
|
||||||
|
// than 503 the whole render path.
|
||||||
|
let messages: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
messages = (await LOADERS[locale]()).default;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[i18n] failed to load messages for locale "${locale}", falling back to en:`, err);
|
||||||
|
messages = (await LOADERS[routing.defaultLocale]()).default;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
locale,
|
locale,
|
||||||
|
|||||||
@ -33,12 +33,23 @@ export async function middleware(request: NextRequest) {
|
|||||||
// routes don't live under [locale] and would be wrongly prefixed).
|
// routes don't live under [locale] and would be wrongly prefixed).
|
||||||
// If next-intl redirects (missing locale), return immediately;
|
// If next-intl redirects (missing locale), return immediately;
|
||||||
// the redirected URL re-enters middleware with the prefix in place.
|
// the redirected URL re-enters middleware with the prefix in place.
|
||||||
|
//
|
||||||
|
// The intlMiddleware call is wrapped because an unhandled throw
|
||||||
|
// here would cascade to a 503 in front of every page — we'd
|
||||||
|
// rather degrade to "no locale routing applied for this request"
|
||||||
|
// than take the whole site down. Errors are logged so they show
|
||||||
|
// up in Hostinger's process logs without being swallowed silently.
|
||||||
let intlResponse: NextResponse | null = null
|
let intlResponse: NextResponse | null = null
|
||||||
if (!isApiRoute) {
|
if (!isApiRoute) {
|
||||||
|
try {
|
||||||
intlResponse = intlMiddleware(request)
|
intlResponse = intlMiddleware(request)
|
||||||
if (intlResponse.headers.get('location')) {
|
if (intlResponse.headers.get('location')) {
|
||||||
return intlResponse
|
return intlResponse
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[middleware] intl middleware threw, falling through to auth-only:', err)
|
||||||
|
intlResponse = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Run Supabase auth against the locale-stripped pathname so the
|
// 2. Run Supabase auth against the locale-stripped pathname so the
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user