- Create Breadcrumbs.tsx server component — semantic <nav> + <ol>/<li>
with aria-current, ChevronRight separators, Apple-clean styling
- Add breadcrumbs to news article hero overlay (reuses JSON-LD crumbs)
- Add breadcrumbs to application detail hero (passed as prop to client
component)
- Refactor breadcrumb data into shared array for JSON-LD + visual nav
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pre-render all known slugs at build time so first visits are instant
from cache. New slugs added after deploy render on-demand and get
cached by ISR (revalidate=60). try/catch ensures the build never
fails if the DB is unreachable during docker build — pages just
fall back to on-demand rendering.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: next-intl's getMessages/getTranslations internally resolves
requestLocale by reading cookies/headers, which trips DYNAMIC_SERVER_USAGE
under ISR. Fixed by calling setRequestLocale(locale) in layout + every
public page — caches the locale in React cache so next-intl never reads
cookies.
Changes:
- [locale]/layout.tsx: +setRequestLocale, +generateStaticParams (5 locales),
wrap NavigationManager in <Suspense> (uses useSearchParams)
- 5 public pages: force-dynamic → revalidate=60, +setRequestLocale
- HQ dashboard pages: unchanged (still force-dynamic for auth)
Build verified: home/heritage/news pre-render as SSG with 1m revalidation,
slug pages render on-demand with ISR cache. Nginx s-maxage=60 remains as
safety net. Zero DYNAMIC_SERVER_USAGE errors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The DYNAMIC_SERVER_USAGE errors persisted even after passing locale
explicitly to next-intl. Some other Server Component in the tree is
still triggering an implicit dynamic API read under ISR — and chasing
it across next-intl, Prisma, the @ai-sdk libs, and the standalone
build was eating the deploy. Pragmatic call: stop trying to keep ISR
while we still have unstable bug surface, take the runtime back to
puro SSR (the working state from before the SEO commit), then bring
ISR back surgically once the site is stable.
CHANGES (5 page.tsx files)
- /[locale]/page.tsx revalidate=60 → dynamic="force-dynamic"
- /[locale]/news/page.tsx revalidate=60 → dynamic="force-dynamic"
- /[locale]/news/[slug]/page.tsx revalidate=60 → dynamic="force-dynamic"
- /[locale]/heritage/page.tsx revalidate=60 → dynamic="force-dynamic"
- /[locale]/applications/[slug]/page.tsx revalidate=60 → dynamic="force-dynamic"
ALSO: removed generateStaticParams from news/[slug] and applications/[slug].
With it present (even returning [] in prod), Next.js still classified
those routes as SSG-eligible, which conflicted with the force-dynamic
flag and kept the ISR/dynamic boundary ambiguous. Removing it makes
the build output show all locale routes as ƒ (Dynamic) — pure SSR.
WHAT WE KEEP
- generateMetadata still runs per request, so all SEO benefits (canonical
URLs, hreflang, OG tags, Twitter cards) remain.
- sitemap.xml and robots.txt are unaffected.
- JSON-LD still emits.
- revalidatePath() in /api/assets still works (just becomes a no-op for
these pages since they're already dynamic — no cache to invalidate).
- Caching at the Nginx layer (max-age=300 + must-revalidate on /_next/image
and /branding|/cases|/applications|/news|/parts|/footage) is unchanged,
so static asset performance stays optimal.
WHAT WE LOSE TEMPORARILY
- Page HTML is generated on every request instead of every 60 seconds.
At Flux's traffic levels this is negligible — Prisma queries are sub-50ms
and Postgres has connection pooling. We'll move back to ISR once we've
isolated the offending dynamic read.
DEPLOY (David — IMPORTANT, force a real rebuild this time)
cd /opt/flux-srl
git pull
docker compose build --no-cache app
docker compose up -d app
docker compose logs app --tail=30
The /en/applications/digital-print page was still 500-ing after the
previous fixes. Without an error boundary, Next.js shows a generic
"Internal Server Error" with no detail — making remote diagnosis
require a `docker compose logs` round-trip every time.
ERROR BOUNDARIES (visible diagnostics)
- src/app/global-error.tsx: catches errors that bubble past every
route's error.tsx, including ones from the root layout. Renders
its own <html>/<body>.
- src/app/[locale]/error.tsx: locale-scoped boundary so the NavBar
and Footer keep rendering around the error UI. Shows the actual
error message + digest in a code block — much faster to diagnose
than a blank 500.
DEFENSIVE WRAPPING (every async + every transform)
- applications/[slug]/page.tsx
- getApplicationImages: try/catch around fs ops
- generateMetadata: full body wrapped, falls back to safe defaults
- getLocalizedData call wrapped (returns rawData if it throws)
- Cases query already had try/catch — adds same for the locale map
- JSON-LD build wrapped, falls back to empty array (still renders)
- Default fallbacks for title/description/category to avoid
productSchema receiving undefined fields
- news/[slug]/page.tsx
- prisma.newsArticle.findUnique now has try/catch
- getLocalizedData wrapped
- JSON-LD build wrapped, only rendered if non-empty
- publishedAt / updatedAt fallback to new Date() to avoid
"Invalid time value" from articleSchema's date conversion
The combination means: if the underlying bug is in any of the SEO
helpers, JSON-LD generation, or i18n merging, the page now degrades
gracefully and shows the actual error in the UI instead of 500-ing.
Two related fixes for the deploy pipeline so DB schema changes never
again leave the site half-deployed.
PRISMA CONFIG (prisma.config.ts)
- "import 'dotenv/config'" was hard-required, but dotenv isn't installed
in the production runtime image (env vars come from docker-compose).
- Wrapped in try/catch so it loads .env locally and silently no-ops in
the container — `prisma migrate deploy` works in both environments.
DOCKERFILE
- Copies node_modules/prisma + prisma.config.ts to the runner stage so
the CLI is available at runtime, not just at build.
- New CMD runs `prisma migrate deploy` before booting the server.
Idempotent — already-applied migrations are skipped. If the DB is
unreachable, the container exits and docker-compose retries.
- This means: from now on, `git pull && docker compose up -d --build app`
is the entire deploy. No more "did you remember to run migrations?".
DEFENSIVE TRY/CATCH (applications/[slug]/page.tsx)
- prisma.application.findUnique and prisma.globalNode.findMany now have
try/catch with logged errors. A transient DB hiccup or missing
Application slug now degrades gracefully (renders "not found" or empty
cases wall) instead of triggering a 500 Internal Server Error.
DEPLOY (David, this is the recovery sequence on the VPS)
cd /opt/flux-srl
git pull
docker compose up -d --build app
# The container will run pending migrations on its own.
# No need to run `prisma migrate deploy` manually anymore.
Brings the site up to enterprise SEO standards. Google now gets a complete
machine-readable map of the content, with multilingual hreflang tags,
structured data for the knowledge panel, and rich Open Graph cards on
LinkedIn / WhatsApp / Twitter.
NEW
- src/app/sitemap.ts: dynamic sitemap.xml from Prisma. Emits 5 locales x
every active application + every active news article, with hreflang
alternates linking each translation. Hourly revalidation.
- src/app/robots.ts: robots.txt blocks /hq-command/, /api/, /parts (B2B
auth-gated), points crawlers at the sitemap.
- src/lib/seo.ts: helpers for canonical URLs, hreflang alternates, and
JSON-LD schemas (Organization, WebSite, Article, Product, BreadcrumbList).
- src/components/seo/JsonLd.tsx: server component that emits one
application/ld+json script tag per page.
PER-PAGE generateMetadata
- Home: localized titles + descriptions in EN/IT/VEC/ES/DE
- News hub: title built from translations, hreflang tags
- News article: title/description from DB, OG image = cover, type=article,
publishedTime + modifiedTime for date freshness signals
- Applications: title/description from DB, type=product, hero image
- Heritage: localized title/description
JSON-LD STRUCTURED DATA
- Site-wide (in root layout): Organization (with HQ address, founder,
contact, social profiles) + WebSite — drives Google knowledge panel
- Article pages: Article schema with publisher/datePublished/dateModified
— required for Google News / Discover eligibility
- Application pages: Product schema (FLUX brand, RF Industrial category)
+ BreadcrumbList — drives rich-snippet breadcrumb in search results
NOTES
- Open Graph metadataBase set from NEXT_PUBLIC_APP_URL so absolute URLs
for OG images are correct (LinkedIn previews require absolute paths)
- All pages have canonical URLs to prevent duplicate-content penalties
- /parts already has noindex meta (B2B portal) — also blocked in robots
- No DB schema changes. Pure additions to /src/lib and /src/app.
Eliminates the need to run "docker compose build" after uploading
images via HQ Command. Heritage page now respects light/dark mode.
CACHE INVALIDATION
- New helper src/lib/revalidate.ts called from /api/assets and
/api/public-upload after every upload, delete, folder create
- Pages switch from force-dynamic to ISR with revalidate=60
(regenerated on demand whenever content changes, plus 60s safety)
- Nginx now sends "max-age=300, must-revalidate" instead of "expires 30d"
on /cases/, /applications/, /news/, /parts/, /footage/, /operations-inbox/
so browsers revalidate via If-Modified-Since (304s on unchanged files)
- Next.js Image Optimizer aligned with same TTL via minimumCacheTTL=300
and adds /_next/image location block in Nginx for correct headers
HERITAGE DARK/LIGHT FIX (Bug #8)
- Replaces hardcoded #0A0A0C / #00F0FF / text-white with proper
light + dark variants throughout markdown renderer (tables, lists,
headings, blockquotes, paragraphs, images)
- Hero section, navigation pill, and CMS-driven sections now switch
with the global theme toggle
SECURITY HARDENING
- Server actions bodySizeLimit reduced from 500MB to 50MB
(large uploads still go through /api/assets which uses Nginx 500MB cap)
DEPLOY NOTES
- Run on VPS:
git pull
docker compose up -d --build app
docker compose exec nginx nginx -s reload
- No DB schema changes in this commit. Existing 2FA users / data untouched.