This repo is a Turborepo monorepo (npm workspaces + Turbo task orchestration). It does not run a long-lived Node server in production. Everything is pre-rendered at build time into plain HTML/CSS/JS files, bundled into a tarball, uploaded to S3, and served by nginx on DuckDuckGo's edge.
flowchart LR
subgraph monorepo [Monorepo]
SP["apps/static-pages<br/>Pages Router"]
NS["nextssg<br/>App Router"]
PKGS["packages<br/>polyfills, inline-scripts, design-system, utils"]
end
subgraph build [Build]
TB["turbo run merge-output"]
OUT["apps/static-pages/out/"]
end
subgraph ci [CI on main]
TAR["build-output.tar.gz"]
S3["S3 artifacts/SHA.tar.gz"]
end
subgraph prod [Production]
CACHE["/usr/local/ddg.cache/static-pages/"]
NGINX["nginx rewrites URLs to .html files"]
end
PKGS --> SP
PKGS --> NS
SP --> TB
NS --> TB
TB --> OUT
OUT --> TAR --> S3 --> CACHE --> NGINX
There are two Next.js apps that both emit static files, plus shared packages they depend on. At the end of a full build, there is one deployable directory: apps/static-pages/out/.
| Piece | Role |
|---|---|
apps/static-pages |
Main site — hundreds of marketing, subscription, and in-browser pages (/about, /pro, /app, DBP, etc.) |
apps/nextssg |
Performance-focused homepage rebuild (currently the noai homepage only) |
apps/benchmark |
Lighthouse regression tooling for nextssg only — not part of production output |
packages/* |
Shared code: design system components, polyfill bootloader, inline IIFE scripts, utilities, static file server |
Turbo wires dependency order: app build tasks depend on ^build (build workspace dependencies first). Packages like @staticpages/polyfills and @staticpages/inline-scripts are compiled with esbuild/vite before the Next apps consume them.
Both apps use Next.js with output: "export" in production — meaning next build walks the route tree and writes finished HTML files to an out/ folder. No SSR at request time.
- Router: Next.js Pages Router (
src/pages/) - Route types:
- Unlocalized pages at the top level (
src/pages/about.tsx→/about.html) - Localized pages under
src/pages/[locale]/(/en-US/app.html,/es-ES/app.html, …) - Experiment variants as nested dynamic routes (e.g.
experiment_appbranding/control.html)
- Unlocalized pages at the top level (
- SSG hooks: Pages export
getStaticPaths/getStaticPropsvia helpersmakeGetStaticPaths()andmakeGetStaticProps(). At build time, Next enumerates every locale × variant combination and renders each page to HTML. - i18n: Strings go through react-intl + FormatJS. Before build,
npm run i18n:compileturns Smartling JSON into compiled AST message files intext-compiled/. - Build command:
npm run build→ compile i18n →next build --no-lint→ writes toapps/static-pages/out/
- Router: Next.js App Router (
src/app/) - Scope: Small — noai homepage, mobile homepage, a few related routes
- Asset prefix: Production assets are prefixed with
/static-pages/turboso they don't collide with the main app's_next/bundles - Build command:
next build→ browser-compat post-processing → writes toapps/nextssg/out/ - Extra packages: Pulls in polyfills and inline-scripts more aggressively for perf
The two apps build independently, then get stitched together:
npm run merge-output
# equivalent to:
# rm -rf apps/static-pages/out/turbo
# cp -r apps/nextssg/out/ apps/static-pages/out/turbo/In Turbo, this is the root task //#merge-output, which depends on both static-pages#build and nextssg#build. The convenience wrapper is:
just build # runs: npx turbo run merge-output
npm run build # runs: turbo run build (apps only, no merge)Resulting layout:
apps/static-pages/out/
├── en-US/
│ ├── app.html
│ ├── pro/login.html
│ └── ...
├── about.html
├── _next/ ← static-pages JS/CSS bundles
├── static-assets/ ← images, sprites, etc.
└── turbo/ ← entire nextssg output, mounted at /static-pages/turbo/
├── en-US/
│ └── index.html ← noai homepage
└── _next/ ← nextssg JS/CSS bundles (prefixed paths)
Nginx decides which tree to serve for a given URL — the main site for most routes, the turbo/ subtree for the optimized homepage.
just install # npm install
just dev # turbo run dev — static-pages on :3000nextssg dev runs separately via its workspace (ddg-serve out 3001 --base-path /static-pages/turbo after a build, or next dev in that app).
just build # turbo merge-output (builds both apps + merges)
just compress # gzip + brotli every .html/.js/.css in out/
just deploy-from-dev # rsync out/ → /usr/local/ddg.cache/static-pages/npx turbo run lint check-types lint:css format-check test
npx turbo run merge-output
npx turbo run test:integration --filter=static-pages
just test-smoke # lightweight node tests against deployed HTMLOn every PR and push to main, GitHub Actions roughly does:
- Build job:
npx turbo run merge-output - Tarball:
tar -czf build-output.tar.gz -C ./apps/static-pages/out . - Upload artifact (retained ~10 days for PR deploys and test jobs)
- Lint, unit tests, integration tests, nextssg e2e, smoke tests run in parallel / downstream
- On main only, after all green: download artifact →
just out-uploadwhich compresses and uploads{git-sha}.tar.gztos3://ddg-static-pages/artifacts/
The tarball contents are exactly what's in apps/static-pages/out/ — HTML files, _next/ chunks, pre-compressed .gz and .br siblings, static assets, and the turbo/ subtree.
| Question | Answer |
|---|---|
| What folder on disk? | /usr/local/ddg.cache/static-pages/ (rsync'd from the tarball) |
| Who serves it? | nginx (config lives in the separate ddg repo, not this one) |
| How do URLs map? | nginx rewrites friendly URLs to file paths, e.g. /app → /static-pages/en-US/app.html. Locale is chosen via an nginx variable ($static_pages_locale). |
| Homepage routing | The optimized nextssg homepage is served from the turbo/ prefix under /static-pages/turbo/… |
| Is Node running? | No — pure static files. Pre-compressed gzip/brotli variants let nginx serve compressed responses without on-the-fly compression. |
This repo builds and uploads artifacts. Actual production rollout is driven by the ddg repo's release workflow:
- Finds a recent commit whose tarball exists in S3
- Creates a GitHub release tag like
static-pages_20240916_172053_ET - Passes that ref to
rex deploy --static-pages-ref=…on staging/canary/production - Updates
s3://ddg-static-pages/latest.txtto point at the deployed SHA
Rollback is also orchestrated from ddg (redeploy a previous tag, mark bad releases).
These aren't pages themselves, but they shape what ends up in the HTML:
| Package | Purpose |
|---|---|
design-system |
Shared Button, Text, Heading, Card — imported by both apps |
polyfills |
Legacy-browser bootloader + per-locale intl chunks (esbuild-built IIFEs) |
inline-scripts |
Tiny IIFEs inlined in <head> (theme, device classes, error handler, pixels) |
browser-compat |
Post-build checks/fixes for ES5 compatibility |
ddg-serve |
Dev static server with autocomplete proxy (nextssg + benchmark) |
utils |
Shared helpers |
- Multiple apps can coexist if their outputs are namespaced (
turbo/subdirectory + asset prefix). - Turbo orchestrates build order and caching across workspaces — one
merge-outputtask is the "release build." - The final artifact is boring on purpose: a directory of HTML + hashed JS/CSS + compressed copies. No container, no runtime.
- Routing is split: Next.js decides file paths at build time; nginx decides public URLs at serve time.
- CI produces immutable SHA-keyed tarballs; deployment is "pick a tarball, rsync to cache dir, nginx serves it."