The /docs deploy from #3860 still returned HTTP 500 with
`EvalError: Code generation from strings disallowed for this context`
(captured via `wrangler tail`).
`next-mdx-remote/rsc` compiles MDX to JSX *at runtime* using
`new Function()` style code generation. Cloudflare Workers' security
sandbox bans all dynamic code generation from strings, even from inside
trusted code, so any worker invocation that touched the docs page
threw immediately.
Switch to a build-time markdown -> HTML pipeline:
- Drop `next-mdx-remote` and `gray-matter`. Add `marked` (pure-JS,
no eval).
- `web/scripts/generate-docs-content.mjs` now runs each markdown
source through `marked.parse()` (gfm enabled) at build time and
writes the resulting HTML strings into
`web/lib/docs-content.generated.ts`.
- `web/app/[locale]/docs/page.tsx` renders each section as
`<article className="docs-content" dangerouslySetInnerHTML={{ __html: section.html }} />`.
No MDX runtime, no JSX compilation at request time, just static HTML
injection.
- `web/app/globals.css` adds a `@layer components` block targeting
`.docs-content h1..h4, p, a, ul, ol, li, blockquote, code, pre, table,
thead, th, td, hr, strong`. Same shadcn-themed look that the dropped
`mdx-components.tsx` provided, applied via CSS instead of React
component overrides.
- `web/components/docs/mdx-components.tsx` removed.
We lose MDX features (JSX inside markdown), but the docs are pure
markdown anyway. `docs/` remains the SoT; the marketing site renders
identical content with no eval and no fs at runtime.
The first build of #3860 failed at type-check because the generated
`lib/docs-content.generated.ts` is gitignored (regenerated on every
build) and CI's `type-check` step runs before `build`. Two fixes:
- web-ci.yml: explicit `Generate docs content from repo-root docs/`
step right after `bun install` so format-check, lint, and type-check
all see the file.
- web/package.json: add `prepare` lifecycle script. `bun install`
invokes it automatically, so a fresh local checkout boots into a
working state too.
Build still re-runs the generator via prebuild, so docs/ edits land in
the bundle without an explicit dev action.
Replace the bespoke 16-section /docs page that pulled prose from
`messages/{locale}.json` with a build-time MDX renderer that reads the
canonical markdown in repo-root `docs/`. Each markdown file becomes
one section of the docs page, scrolled-to via the existing DocsShell
sidebar. Section data structure stays in `lib/docs-sections.ts` so the
sidebar / scroll-spy keeps working with no client changes.
Why this layout:
- One source of truth: `docs/guide/*.md`, `docs/reference/*.md`,
`docs/manifesto.md`. Edits land in one place; the website redeploys
pick them up automatically via the existing web-deploy workflow.
- Build-time only: `MDXRemote` is rendered inside an RSC and the page
is statically generated (`●` SSG). Cloudflare Workers serves the
rendered HTML; no MDX compiler runs at request time.
- next-intl unchanged for everything else: only the docs prose moves
out. `mobileHeader` and `searchPlaceholder` strings stay in
`messages/{locale}.json`; the 18 stale section keys are removed.
Files:
- web/lib/docs-sections.ts: 9 sections matching docs/ files, typed
`DocSection` with `{ id, title, file }`.
- web/lib/docs-source.ts: `loadDocSource(file)` reads
`<repo-root>/docs/<file>` at build time via `node:fs/promises`.
- web/components/docs/mdx-components.tsx: shadcn-styled overrides for
every markdown element (h1-h4, p, a, ul/ol/li, blockquote, code, pre,
table, hr, strong) so the rendered output matches the rest of the
site.
- web/app/[locale]/docs/page.tsx: rewritten as an async RSC that loads
every section's source in parallel and renders one MDXRemote per
section inside DocsShell.
- web/messages/{en,ja,ko,zh}.json: `docs` key trimmed from 20 entries
to 2 (mobileHeader, searchPlaceholder).
- web/package.json: + next-mdx-remote, + gray-matter.
Local verification: `bun run format:check`, `bun run lint`,
`bun run type-check`, `bun run build`, `bunx opennextjs-cloudflare
build` all pass; `/[locale]/docs` builds as static for all 4 locales
at 4.12 kB / 132 kB First Load.
The first auto-deploy from PR #3855 returned HTTP 500 on every page
with the runtime error `TypeError: components.ComponentMod.handler is
not a function`. Captured via `wrangler tail`.
Root cause: Next.js 16.2.6 was published 2026-05-07 19:01 UTC, *after*
@opennextjs/cloudflare 1.19.8 was published earlier the same day at
11:33 UTC. OpenNext 1.19.8's peerDependency declares
`next: '>=15.5.16 <16 || >=16.2.5'` — 16.2.6 falls inside the range
syntactically, but the route component module export shape changed in
that patch and OpenNext has not caught up yet.
Pin Next + eslint-config-next to 15.5.18 (latest 15.x LTS, the other
half of OpenNext's supported range). Revert the migration-only changes
that came with the 16 bump:
- eslint.config.mjs: `nextPlugin.configs["core-web-vitals"]` (v16
shape) -> `nextPlugin.flatConfig.coreWebVitals` (v15 shape).
- tsconfig.json: `jsx: "react-jsx"` -> `jsx: "preserve"` (Next 15
default).
- tsconfig.json: add `noUncheckedSideEffectImports: false` because
TypeScript 6 enabled this option under `strict` and Next 15's
bundled types do not declare ambient CSS modules (Next 16 does).
All other web/ deps stay at latest. lucide-react remains pinned at
0.577.0 from #3853 for the same brand-icon reason. Re-evaluate Next 16
when @opennextjs/cloudflare ships a release explicitly tested against
\>= 16.2.6.
"lucide-react": "0" is npm shorthand for >=0.0.0 <1.0.0, so the
dependency was still floating across all 0.x releases. A future
lockfile refresh could pull a newer 0.x that quietly changes brand-icon
inventory.
Pin exact 0.577.0 (no caret) so the lockfile cannot drift until we
explicitly migrate to a brand-icon library compatible with lucide-react
v1.x (which removed Github, X, etc.).
Identified by cubic.
Bumped via `bun update --latest` then resolved breakage from two
major-version jumps:
1. Next.js 15.5 → 16.2 + @next/eslint-plugin-next 16: dropped the
`flatConfig` namespace. Updated web/eslint.config.mjs to use
`nextPlugin.configs["core-web-vitals"]` per the new export shape.
2. lucide-react 0.553 → 1.x: lucide upstream removed all brand icons
(Github, etc.) — they are now expected to come from a separate brand
icon library. Pinned lucide-react at the last 0.x (0.577.0) for now;
migrating to a brand-icon library is tracked as a follow-up.
All other deps to latest:
- react/react-dom 19.2.4 → 19.2.6
- next-intl 4.8 → 4.11
- motion 12.35 → 12.38
- tailwind-merge 3.4 → 3.5
- geist 1.5 → 1.7
- @radix-ui/* unchanged (already latest within their ranges)
- @opennextjs/cloudflare 1.17 → 1.19.8
- @playwright/test 1.56 → 1.59
- @tailwindcss/postcss + tailwindcss 4.1 → 4.2.4
- @types/node 22 → 25.6
- @types/react 19 → 19.2.14
- eslint 9 → 10.3 (works because we now reference @next/eslint-plugin-next
configs directly, not eslint-config-next)
- eslint-plugin-prettier 5.5.4 → 5.5.5
- globals 16 → 17.6
- postcss 8.5.6 → 8.5.14
- prettier 3.6.2 → 3.8.3 (no formatting changes detected by --check)
- prettier-plugin-tailwindcss 0.6 → 0.8
- typescript 5.9.3 → 6.0.3
- typescript-eslint 8.56 → 8.59
- wrangler 4.71 → 4.90
Verified locally:
- bun install --frozen-lockfile: 685 packages, no errors
- bun run format:check: pass (no diffs after `bun run format`)
- bun run lint: pass
- bun run type-check: pass (TypeScript 6 + @types/node 25)
- bun run build: pass (Next 16 build, 21 static pages, all 4 locales)
- bunx opennextjs-cloudflare build: pass (.open-next/worker.js produced)
Note: Next 16 `build` log relabels the Middleware row to "Proxy
(Middleware)" — purely cosmetic, no behavior change.
Imports the public marketing site previously living in
../oh-my-opencode-web. Independent of the npm plugin: own package.json,
bun.lock, tsconfig.json. Not included in the published package — root
files: array still only ships dist/, bin/, postinstall.mjs.
Stack:
- Next.js 15.5 App Router + RSC, deployed to Cloudflare Workers via
@opennextjs/cloudflare (build target .open-next/worker.js).
- Tailwind v4 + shadcn/ui primitives.
- next-intl with 4 locales (en/ja/ko/zh) under app/[locale]/.
- Playwright e2e tests under web/e2e/.
- Custom domains ohmyopenagent.com (primary) and ohmyopencode.org
(legacy alias) declared in web/wrangler.toml.
Source files were re-formatted via `bun run format` to bring them in
line with the existing .prettierrc (singleQuote: false). Functional code
unchanged.