Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-4204Done

(motir-core) app.motir.co/legal/<slug> answers 500 for EVERY slug since the manifest reader deployed — `generateStaticParams` over a runtime-only env var yields no params at build, so the redirect stub renders statically and the locale cookie read throws

Type: code · Parent: MOTIR-3875 (the discovery epic — the edge test says no: nothing in MOTIR-4101 is blocked by this fix, and the deletion card removes the route whether or not it is fixed first) · Discovered in: MOTIR-4102, while reading the deployed application · Repo: motir-core · ONE PR.

The symptom, measured on production (2026-09-02, release v239, image GH_SHA=ac5f9ac1)

GET https://app.motir.co/legal/terms        → HTTP 500
GET https://app.motir.co/legal/dpa          → HTTP 500
GET https://app.motir.co/legal/no-such-doc  → HTTP 500   (should be 404)
GET https://app.motir.co/legal              → HTTP 200   (the index still renders)

fly logs -a motir-core, at the second of each request:

⨯ [Error: An error occurred in the Server Components render. …] { digest: 'DYNAMIC_SERVER_USAGE' }

This is new with the MOTIR-3909 deploy. MOTIR-4012's readback, taken this morning on release v230, recorded /legal/terms200 from the old disk-based reader. Release v239 is the first image built from a commit containing the 3909 merge (8d80ac8d), and it answers 500.

Root cause — VERIFIED against the deployed build, not a reading theory

app/(public)/legal/[slug]/page.tsx was rewritten by MOTIR-4007 into a permanentRedirect(doc.url) stub — and kept its generateStaticParams(), which now returns legalDocumentSlugs(), i.e. a read of MOTIR_LEGAL_DOCUMENTS. That variable is a Fly runtime secret; it is absent when CI runs next build, so at build time generateStaticParams returns [].

Three consequences follow, and the deployed image shows each of them:

  1. The route is classified STATIC with zero prerendered params. /app/.next/prerender-manifest.json inside the running machine lists /legal/[slug] under dynamicRoutes with fallback: null and no entry under routes — a static route whose params are generated on demand. (Read via fly ssh console, 2026-09-02.)
  2. Every request is therefore an on-demand STATIC render of an unlisted param, and in that mode any dynamic API throws. The render reaches one before the page body runs: the (public)/legal layout calls getTranslations (app/(public)/legal/layout.tsx:38, and again in ExploreTopBar/ExploreFooter), and next-intl's request config reads the locale cookie — i18n/request.ts:6, (await cookies()).get('NEXT_LOCALE'). cookies() inside a static render is exactly what Next reports as DYNAMIC_SERVER_USAGE. Neither notFound() nor permanentRedirect() is ever reached, which is why an unknown slug 500s instead of 404ing.
  3. Why it worked before 4007: generateStaticParams used to return the seven slugs read from content/legal/ on disk, which exist at build time. Next tried to prerender them, hit the same cookies() call, and bailed the route out to dynamic rendering at build — the documented behaviour for dynamic-API use during prerender. With no params to attempt, the bail-out never happens and the route ships static.

/legal (the index) has no generateStaticParams, so Next inferred its dynamic-ness from the same cookies() call at build and it renders fine. That asymmetry is the whole diagnosis.

Why nothing masks it in production: the page's own comment says the route is only reached on a build whose MOTIR_PUBLIC_SITE_URL is unset, because proxy.ts:76 otherwise 308s /legal/* to the public host (MOTIR-3884). fly secrets list -a motir-core shows no MOTIR_PUBLIC_SITE_URL — the cutover (MOTIR-3910) is still blocked — so the stub IS the live answer for every old /legal/<slug> link (bookmarks, pre-4010 sign-up notices, emails).

Reproduce

curl -sS -o /dev/null -w '%{http_code}\n' https://app.motir.co/legal/terms     # 500
fly logs -a motir-core --no-tail | grep -A2 'Server Components render'          # digest: 'DYNAMIC_SERVER_USAGE'

Locally: next build with MOTIR_LEGAL_DOCUMENTS unset, then next start with it set and request /legal/terms.

Fix direction

  • Delete generateStaticParams from app/(public)/legal/[slug]/page.tsx. A manifest that is runtime configuration can never yield params on a build server, so the export is dead by construction and its only effect is to mis-classify the route. (export const dynamic = 'force-dynamic' is the equivalent one-liner; removing the misleading export is the honest one.) Update the file's THIS IS A WINDOW comment, which currently claims the stub "stays reachable" while it 500s.
  • A test that would have gone red: a route test that renders /legal/<known slug> with the manifest set and asserts a 308 to the manifest url, and /legal/<unknown> → 404 — run under a build where the manifest is absent at build time (the CI shape). At minimum, assert the page module exports no generateStaticParams.
  • Ordering against the deletion card: that card deletes the whole route, which also closes this. If it merges first, name this key in its PR body and let the sweep close it. If this fixes first (recommended — the 500 is user-visible today and the deletion is a 70-minute sweep behind a human gate), the deletion card's table gains one line: the stub it removes is the fixed one.

Acceptance criteria

  • GET /legal/terms on a build produced WITHOUT MOTIR_LEGAL_DOCUMENTS at build time, started WITH it, answers 308 to https://motir.co/legal/terms; GET /legal/no-such-doc answers 404. Both quoted in the PR body from a local next build && next start.
  • The page module no longer exports generateStaticParams, and a test asserts it (or asserts the route's rendering mode).
  • After deploy: curl -sS -o /dev/null -w '%{http_code}' https://app.motir.co/legal/terms308, pasted on this card.
  • Coverage: ≥90% on any file modified; the five files vitest.config.ts pins stay at their thresholds.

Resolution

open

Context refs

  • motir-core/app/(public)/legal/[slug]/page.tsx — the stub, and the generateStaticParams that mis-classifies it
  • motir-core/app/(public)/legal/layout.tsx:38 · i18n/request.ts:6 — the cookies() the static render meets
  • motir-core/proxy.ts:66-84 — the 308 that would mask this once MOTIR_PUBLIC_SITE_URL is set
  • motir-core/lib/legal/documents.tslegalDocumentSlugs() reads runtime configuration
  • MOTIR-4007 — introduced the stub · MOTIR-4102 — found it · MOTIR-4103 — deletes the route · MOTIR-3910 — the cutover whose 308 would hide it

Advisory dispositions

  • Any reference advisory naming MOTIR-4103 or MOTIR-3910: NOT an owed edge — both sit in other containers (core.md gate 7), and neither must land before this fix; they are named because they change what "fixed" looks like, not because they gate it.