Repository navigation
refactor(website): migrate to Next.js 15 + Cloudflare Workers CI - #4305
Conversation
Replace the Vite + React 18 SPA with a Next.js 15 App Router app (React 19) deployed to Cloudflare Workers via @opennextjs/cloudflare. The site is now SSR-rendered with full SEO surface area: Metadata API with OG/Twitter cards, JSON-LD SoftwareApplication schema, sitemap and robots routes. Markdown demos are pre-rendered at build time by scripts/prerender-markdown.ts (marked + prismjs + KaTeX) so crawlers receive real HTML on first paint and the runtime bundle no longer carries the markdown engine. Theme CSS is similarly baked into a generated module to remove the Vite-only ?inline imports. Mermaid still hydrates client-side because it needs DOM. Component split: Sponsor stays a Server Component; TitleBar, Theme, Footer, and Feature are marked 'use client' for state / animations / Mermaid runs. Per-component CSS imports moved to layout.tsx (Next.js App Router only allows global CSS from there). Assets moved from src/assets/ to public/ and referenced by string path. CI: new .github/workflows/website-deploy.yml runs lint + typecheck + Next build + OpenNext bundle on every push to develop and on PRs that touch packages/website (or its inputs). develop deploys to production via 'wrangler deploy'; PR builds upload a versioned preview and post a sticky comment with the preview URL. Requires CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID repo secrets. wrangler.toml binds marktext.me as a Worker Custom Domain so Cloudflare manages the route + DNS record automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…build The root postinstall.ts rebuilds Electron-native modules (native-keymap, keytar, ced) on every pnpm install. On Ubuntu runners these need libx11-dev, libxkbfile-dev, and libsecret-1-dev or node-gyp fails before the website build even starts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR migrates the website package from a Vite/React SPA to a Next.js App Router site with build-time markdown prerendering and Cloudflare Workers deployment support.
Changes:
- Adds Next.js app structure, metadata routes, prerendered markdown/theme generation, and OpenNext/Workers config.
- Updates website components to use client/server component boundaries and public asset paths.
- Adds website deployment CI and updates website dependencies/tooling.
Reviewed changes
Copilot reviewed 29 out of 51 changed files in this pull request and generated 7 comments.
Show a summary per file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "dev": "next dev --port 3000", | ||
| "build": "next build", | ||
| "start": "next start", | ||
| "lint": "next lint --max-warnings 0", |
There was a problem hiding this comment.
Fixed in 0866c24 — switched the lint script to eslint . --max-warnings 0 and added a flat ESLint 9 config (eslint.config.mjs) that re-exports the next/core-web-vitals preset via @eslint/eslintrc FlatCompat. .eslintrc.json removed.
| "build": "next build", | ||
| "start": "next start", | ||
| "lint": "next lint --max-warnings 0", | ||
| "type-check": "tsc --noEmit", |
There was a problem hiding this comment.
Already fixed in earlier commit b4cc5c8 — workflow now runs tsx scripts/prerender-markdown.ts as a dedicated "Prerender content" step before lint / type-check / build, so the gitignored src/generated/* modules exist when tsc reads them.
| cache: 'pnpm' | ||
|
|
||
| # The root postinstall script rebuilds the desktop app's Electron-native | ||
| # modules (native-keymap, keytar, ced). They need these system libraries |
There was a problem hiding this comment.
Already fixed in earlier commit 70e43f2 — added an "Install desktop-rebuild system libs" step that apt-get installs libx11-dev libxkbfile-dev libsecret-1-dev before pnpm install, so the root postinstall's Electron rebuild of native-keymap/keytar/ced succeeds on the Ubuntu runner.
| /<pre><code class="language-mermaid">__MERMAID_BLOCK__([A-Za-z0-9+/=]+)__END__\s*<\/code><\/pre>/g, | ||
| (_, b64) => { | ||
| const source = Buffer.from(b64, 'base64').toString('utf-8') | ||
| return `<div class="mermaid">${escapeHtml(source.trim())}</div>` |
There was a problem hiding this comment.
Fixed in 0866c24 — the prerender script now routes both code paths through a mermaidDiv(source) helper that emits <div class="mermaid" data-source="...">, so Theme.tsx can restore the diagram text after mermaid.run() swaps .textContent for an <svg>.
| .replace(/"/g, '"') | ||
| .replace(/'/g, "'") | ||
| .trim() | ||
| return MERMAID_HEAD.test(text) ? `<div class="mermaid">${escapeHtml(text)}</div>` : full |
There was a problem hiding this comment.
Fixed in 0866c24 — the fallback regex path now also goes through the shared mermaidDiv() helper, so detected-by-content Mermaid blocks get data-source too.
| "next": "^15.5.0", | ||
| "react": "^19.0.0", | ||
| "react-dom": "^19.0.0" |
| { title: 'Diagram', description: 'Support Flowchart, Sequence diagram, Gantt diagram, Vega chart.', key: 'diagram' }, | ||
| { title: 'Inline Formats', description: 'Support CommonMark and GitHub Flavored Markdown Spec.', key: 'inlineFormats' }, | ||
| { title: 'Math Formula', description: 'Markdown extensions math expressions (KaTeX)', key: 'mathFormula' }, | ||
| { title: 'Code Block', description: 'Support GFM code fense, highlight by prismjs.', key: 'codeBlock' } |
src/generated/{markdown-html,theme-css}.ts are gitignored and only created
by the prerender script; without an explicit step before type-check, tsc
fails on the missing modules.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Build artifacts for PR #4305: Run: https://github.com/marktext/marktext/actions/runs/26622440621
|
- Mermaid prerender now emits data-source on each <div class="mermaid"> so Theme.tsx can restore the diagram text after mermaid.run() swaps it for an <svg>. Both the marked-emitted path and the fallback regex-detected path go through the same mermaidDiv() helper. - Switch lint script from deprecated `next lint` to `eslint` CLI with a flat ESLint 9 config (eslint.config.mjs) that re-exports the next/core-web-vitals preset via @eslint/eslintrc FlatCompat. Removes the legacy .eslintrc.json. - Feature.tsx: fix typo "fense" -> "fence". - website-deploy.yml: handle the first-ever PR run, when the Worker hasn't been bootstrapped on Cloudflare yet. `wrangler versions upload` only updates existing Workers; on that specific error we now soft-fail and post a sticky comment explaining that the next push to develop will bootstrap the Worker. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Move the mermaid runtime behind a single dynamic import in a shared packages/website/src/lib/mermaid.ts helper. Mermaid is ~500 KB and was previously bundled into the initial JS for `/`; Feature and Theme now both use `useMermaidRender` (or `runMermaid` + `setMermaidTheme`) and the chunk only loads when a diagram actually appears. - Theme.tsx: split the page-CSS swap and the mermaid repaint into separate effects. Mermaid only repaints when the theme darkness flips, not on every light↔light switch, since mermaid's 'default' / 'dark' palettes don't distinguish individual themes. - Drop `src/lib/markdown.ts` (a zero-value re-export) and import the generated map directly from `@/generated/markdown-html`. - Drop `src/types/assets.d.ts` (declares the Vite-only `?inline` CSS query that no source file uses anymore). - prerender-markdown.ts: ship only the prism-javascript grammar; bash / ts / json / jsx / tsx / css / markdown / yaml were imported but the demo files don't use any of them. Bundle impact: `/` First Load JS drops from 274 kB to 115 kB. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ktext#4305) Migrates packages/website from Vite + React 18 SPA to Next.js 15 App Router (React 19) with SSR on Cloudflare Workers via @opennextjs/cloudflare. Markdown demos are prerendered at build time (marked + prismjs + KaTeX) for SEO; mermaid is dynamic-imported. Adds .github/workflows/website-deploy.yml: develop push → production via wrangler deploy; PRs touching packages/website/** → preview upload + sticky comment with preview URL. Binds marktext.me as a Worker Custom Domain. Full SEO surface: Metadata API (OG/Twitter), JSON-LD SoftwareApplication, sitemap + robots routes. Bundle: / First Load JS 274 kB → 115 kB. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Summary
packages/website/from Vite + React 18 SPA to Next.js 15 App Router (React 19) with SSR — full SEO surface: Metadata API (OG/Twitter), JSON-LDSoftwareApplication, sitemap + robots routes.scripts/prerender-markdown.tsusingmarked+prismjs+ KaTeX) so crawlers see real HTML on first paint and the runtime bundle no longer ships the markdown engine. Mermaid still hydrates client-side..github/workflows/website-deploy.yml: push todevelop→ production deploy viawrangler deploy; PRs touchingpackages/website/**→ preview upload + sticky comment with preview URL. Powered by@opennextjs/cloudflare.marktext.meas a Worker Custom Domain inwrangler.toml.What changed
src/app/(layout, page, sitemap, robots),src/lib/markdown.ts,src/generated/*(build-time output, git-ignored),scripts/prerender-markdown.ts,next.config.ts,open-next.config.ts,wrangler.toml,.eslintrc.json,.github/workflows/website-deploy.yml.index.html,vite.config.ts,src/main.tsx,src/App.tsx,src/utils/markdownToHtml.ts,tsconfig.node.json,vite-env.d.ts.Sponsorstays a Server Component;TitleBar/Theme/Footer/Featuremarked'use client'. CSS imports centralized inapp/layout.tsx(App Router requirement for global CSS).src/assets/*→public/assets/*; CSSurl()refs updated to absolute/assets/....pnpm-workspace.yaml:sharp,unrs-resolver,workerdadded toallowBuilds.Required before this can deploy
Repo secrets (already configured):
CLOUDFLARE_API_TOKEN— scoped to Workers Scripts Edit + Workers Routes Edit + Account Settings ReadCLOUDFLARE_ACCOUNT_IDTest plan
pnpm installsucceeds (sharp / workerd / unrs-resolver builds approved)pnpm --filter marktext-website run type-checkpassespnpm --filter marktext-website run lintpassespnpm --filter marktext-website run buildproduces a successful Next build (all routes static)pnpm --filter marktext-website run cf:buildemits.open-next/worker.js+.open-next/assets/Deploy websiteworkflow runs green on this PRview-source:on preview URL shows OG / Twitter meta, JSON-LD, prerendered demo markdown (not empty divs)Notes on Cloudflare custom domain
The first production deploy will bind
marktext.meas a Worker Custom Domain. If the apex currently has a conflicting A/AAAA/CNAME record, the deploy will fail until that record is removed in the Cloudflare DNS dashboard.🤖 Generated with Claude Code