Skip to content

Commit 361be00

Browse files
committed
Build the docs site: Fumadocs static export fed by sqlc-dev/sqlc docs/
Scaffold a Fumadocs (Next.js static export) site mounted at the domain root, plus everything around it: - scripts/ingest.mjs: checks out sqlc docs (or takes --src), derives frontmatter titles from each page's # heading, converts toc.yaml into the sidebar meta.json (sections as separators, unlisted pages kept out of the sidebar), and fails loudly on contract drift - pages stay .md so fumadocs-mdx compiles them as Markdown, not MDX; literal {...} and <...> in prose render as written - lib/remark-github-alerts.ts maps GitHub alert blockquotes to Callouts; lib/relative-link.tsx resolves relative .md links (bare ones included) to routes at render time - versioned builds via NEXT_PUBLIC_BASE_PATH with noindex and a version switcher driven by a runtime-fetched /versions.json - scripts/redirects.mjs emits meta-refresh stubs for the legacy Read the Docs /en/latest/*.html URLs and the old rediraffe rule - workflows: CI build on PRs, latest deploy on repository_dispatch/cron, tag deploys that sync to a version prefix and append versions.json Generated output (content/, out/, .source/) is never committed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Thc5nWNbxfQ67pVDcNSkxC
1 parent 1ae5c63 commit 361be00

32 files changed

Lines changed: 7763 additions & 1 deletion

‎.github/workflows/ci.yml‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [main]
7+
8+
jobs:
9+
build:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
14+
- name: Check out sqlc docs
15+
uses: actions/checkout@v4
16+
with:
17+
repository: sqlc-dev/sqlc
18+
path: .cache/sqlc
19+
sparse-checkout: docs
20+
21+
- uses: actions/setup-node@v4
22+
with:
23+
node-version: 22
24+
cache: npm
25+
26+
- run: npm ci
27+
28+
- name: Ingest
29+
run: node scripts/ingest.mjs --src .cache/sqlc/docs
30+
31+
- name: Build
32+
run: npm run build
33+
34+
- name: Type check
35+
run: npm run types:check

‎.github/workflows/deploy.yml‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
name: Deploy latest docs
2+
3+
on:
4+
# Sent by sqlc-dev/sqlc when a push to main touches docs/** (see README).
5+
repository_dispatch:
6+
types: [docs-updated]
7+
workflow_dispatch:
8+
# Safety net in case a dispatch is missed.
9+
schedule:
10+
- cron: '17 6 * * *'
11+
12+
concurrency:
13+
group: deploy-latest
14+
cancel-in-progress: true
15+
16+
jobs:
17+
deploy:
18+
runs-on: ubuntu-latest
19+
env:
20+
# Used to skip the deploy step on forks / before R2 is provisioned.
21+
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- name: Check out sqlc docs
26+
uses: actions/checkout@v4
27+
with:
28+
repository: sqlc-dev/sqlc
29+
path: .cache/sqlc
30+
sparse-checkout: docs
31+
32+
- uses: actions/setup-node@v4
33+
with:
34+
node-version: 22
35+
cache: npm
36+
37+
- run: npm ci
38+
39+
- name: Ingest
40+
run: node scripts/ingest.mjs --src .cache/sqlc/docs
41+
42+
- name: Build
43+
run: npm run build
44+
45+
- uses: actions/upload-artifact@v4
46+
with:
47+
name: site
48+
path: out/
49+
retention-days: 7
50+
51+
- name: Deploy to R2
52+
if: ${{ env.R2_ACCESS_KEY_ID != '' }}
53+
env:
54+
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
55+
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
56+
AWS_DEFAULT_REGION: auto
57+
# The root prefix is the latest build; versioned snapshots live under
58+
# v*/ and versions.json is owned by the release workflow — never
59+
# delete either from here.
60+
run: |
61+
aws s3 sync out/ "s3://${{ secrets.R2_BUCKET }}/" \
62+
--endpoint-url "https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com" \
63+
--delete \
64+
--exclude "v*" \
65+
--exclude "versions.json"

‎.github/workflows/release.yml‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
name: Deploy versioned docs
2+
3+
on:
4+
# Sent by sqlc-dev/sqlc on release, with {"tag": "v1.32.0"} as payload.
5+
repository_dispatch:
6+
types: [docs-release]
7+
workflow_dispatch:
8+
inputs:
9+
tag:
10+
description: 'sqlc tag to build (e.g. v1.32.0)'
11+
required: true
12+
13+
concurrency:
14+
group: deploy-release
15+
cancel-in-progress: false
16+
17+
jobs:
18+
deploy:
19+
runs-on: ubuntu-latest
20+
env:
21+
TAG: ${{ inputs.tag || github.event.client_payload.tag }}
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- name: Check out sqlc docs at tag
26+
uses: actions/checkout@v4
27+
with:
28+
repository: sqlc-dev/sqlc
29+
ref: ${{ env.TAG }}
30+
path: .cache/sqlc
31+
sparse-checkout: docs
32+
33+
# Versioning starts at the first tag that carries the plain-markdown
34+
# content contract; older tags are not backfilled.
35+
- name: Check content contract
36+
run: |
37+
if [ ! -f .cache/sqlc/docs/toc.yaml ]; then
38+
echo "::error::${TAG} predates docs/toc.yaml; nothing to build"
39+
exit 1
40+
fi
41+
42+
- uses: actions/setup-node@v4
43+
with:
44+
node-version: 22
45+
cache: npm
46+
47+
- run: npm ci
48+
49+
- name: Ingest
50+
run: node scripts/ingest.mjs --src .cache/sqlc/docs
51+
52+
- name: Build
53+
run: npm run build
54+
env:
55+
# Static export bakes absolute asset/link/search paths, so the
56+
# version prefix must be set at build time.
57+
NEXT_PUBLIC_BASE_PATH: /${{ env.TAG }}
58+
59+
- uses: actions/upload-artifact@v4
60+
with:
61+
name: site-${{ env.TAG }}
62+
path: out/
63+
retention-days: 7
64+
65+
- name: Deploy to R2
66+
env:
67+
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
68+
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
69+
AWS_DEFAULT_REGION: auto
70+
ENDPOINT: https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
71+
BUCKET: ${{ secrets.R2_BUCKET }}
72+
run: |
73+
aws s3 sync out/ "s3://${BUCKET}/${TAG}/" \
74+
--endpoint-url "$ENDPOINT" --delete
75+
76+
# Append this version to versions.json at the domain root; every
77+
# snapshot fetches it at runtime, so old snapshots list new versions.
78+
aws s3 cp "s3://${BUCKET}/versions.json" versions.json \
79+
--endpoint-url "$ENDPOINT" || echo '{"versions":[]}' > versions.json
80+
node -e '
81+
const fs = require("fs");
82+
const file = JSON.parse(fs.readFileSync("versions.json", "utf8"));
83+
const versions = new Set(file.versions ?? []);
84+
versions.add(process.argv[1]);
85+
const sorted = [...versions].sort((a, b) =>
86+
b.localeCompare(a, undefined, { numeric: true }));
87+
fs.writeFileSync("versions.json", JSON.stringify(
88+
{ latest: sorted[0], versions: sorted }, null, 2) + "\n");
89+
' "$TAG"
90+
aws s3 cp versions.json "s3://${BUCKET}/versions.json" \
91+
--endpoint-url "$ENDPOINT" --content-type application/json

‎.gitignore‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Dependencies
2+
node_modules/
3+
4+
# Next.js build output
5+
.next/
6+
out/
7+
8+
# fumadocs-mdx generated files
9+
.source/
10+
next-env.d.ts
11+
12+
# Ingested content — generated by `npm run ingest` from sqlc-dev/sqlc docs/.
13+
# Never commit it; never hand-edit it.
14+
content/
15+
16+
# Local sqlc checkout used by the ingest script
17+
.cache/
18+
19+
# TypeScript incremental build info
20+
tsconfig.tsbuildinfo

‎README.md‎

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,144 @@
1-
Hi
1+
# sqlc docs site
2+
3+
This repository builds and deploys <https://docs.sqlc.dev>. It contains **no
4+
documentation content**. Content lives in
5+
[sqlc-dev/sqlc](https://github.com/sqlc-dev/sqlc) under `docs/` as plain
6+
GitHub-flavored Markdown; this repo ingests it, renders it with
7+
[Fumadocs](https://fumadocs.dev) on Next.js, and publishes a fully static
8+
site.
9+
10+
```
11+
sqlc-dev/sqlc this repo
12+
┌─────────────────────────┐ ┌──────────────────────────────┐
13+
│ docs/*.md (content) │ checkout │ ingest: md → content tree │
14+
│ docs/toc.yaml (nav) │ ─────────► │ build: Fumadocs / Next.js │
15+
│ internal/docs (Go lint) │ │ deploy: static files → CDN │
16+
└─────────────────────────┘ └──────────────────────────────┘
17+
```
18+
19+
The split exists so that sqlc contributors never need a JavaScript toolchain:
20+
the Go linter in `sqlc/internal/docs` (runs in sqlc's `go test ./...`)
21+
enforces the content contract, and anything that passes it must render here.
22+
If a change to this repo would reject content the linter accepts, that's a
23+
bug in this repo — or a proposed contract change that goes to the linter
24+
first.
25+
26+
## The content contract
27+
28+
Enforced upstream by `sqlc/internal/docs`:
29+
30+
- Plain CommonMark + GFM. No MDX, no JSX, no raw HTML except HTML comments.
31+
- Every page starts with exactly one `#` heading — the page title.
32+
- Admonitions use GitHub alert syntax: `> [!NOTE]`, `> [!TIP]`, `> [!WARNING]`.
33+
- Relative links (`../reference/config.md#database`) resolve to real files;
34+
anchors match GitHub-style heading slugs.
35+
- `docs/toc.yaml` lists every page exactly once: an `index`, titled
36+
`sections`, and `unlisted` (published, not in the sidebar).
37+
38+
Two implementation details here are load-bearing for that contract:
39+
40+
- Ingested pages keep their `.md` extension, so fumadocs-mdx compiles them as
41+
**Markdown, not MDX** — literal `{...}` and `<...>` in prose (which the
42+
linter rightly does not reject) render fine.
43+
- `lib/remark-github-alerts.ts` maps `> [!NOTE]` blockquotes to Fumadocs
44+
`<Callout>` components, and `lib/relative-link.tsx` resolves relative `.md`
45+
links (bare ones included) to page routes at render time.
46+
47+
## Local development
48+
49+
```sh
50+
npm ci
51+
npm run ingest # sparse-clones sqlc-dev/sqlc@main into .cache/
52+
npm run dev
53+
```
54+
55+
`npm run ingest -- --ref v1.30.0` builds another ref;
56+
`npm run ingest -- --src ../sqlc/docs` uses a local checkout (CI does this).
57+
58+
The ingest step (`scripts/ingest.mjs`) writes `content/docs/`: it derives
59+
each page's frontmatter `title` from its `#` heading and strips it, copies
60+
the body verbatim, and converts `toc.yaml` into a Fumadocs `meta.json`
61+
(sections become sidebar separators; `unlisted` pages get routes but stay out
62+
of the sidebar). Anything unexpected — an unknown `toc.yaml` field, a page
63+
without a title — fails the build loudly: that's the contract-drift alarm.
64+
`content/` is generated output; never commit or hand-edit it.
65+
66+
`npm run build` emits the static site to `out/` (plain HTML/CSS/JS, servable
67+
from any static host that resolves `/howto/select` to `howto/select.html`),
68+
then `scripts/redirects.mjs` adds redirect stubs for legacy URLs.
69+
70+
## URL scheme and redirects
71+
72+
Pages are mounted at the domain root: `docs.sqlc.dev/howto/select`. The old
73+
Read the Docs URLs redirect:
74+
75+
```
76+
/en/latest/<path>.html → /<path> (301)
77+
/en/latest/ → /
78+
/howto/upload → /howto/push (carried over from the old rediraffe config)
79+
```
80+
81+
These should be real 301s at the hosting edge (e.g. Cloudflare Bulk
82+
Redirects). As a belt-and-braces fallback the build also emits meta-refresh
83+
stubs (with canonical links and `noindex`) under `out/en/latest/`, so legacy
84+
links work on any static host even before edge rules exist. Old RTD tag URLs
85+
(`/en/v1.29.0/...`) can redirect to `/` until versioned builds cover them.
86+
87+
## Versioning
88+
89+
Versions are immutable build artifacts, not branches:
90+
91+
- The root serves latest, rebuilt on every `main` docs change.
92+
- The release workflow builds a tag with `NEXT_PUBLIC_BASE_PATH=/v1.32.0`
93+
(static export bakes absolute asset/link/search paths, so the prefix is a
94+
build-time setting) and uploads it to the `/v1.32.0/` prefix, once, forever.
95+
- `versions.json` at the domain root, appended by each release, drives the
96+
version-switcher dropdown; every snapshot fetches it at runtime so old
97+
snapshots list new versions.
98+
- Versioned builds set `noindex` so stale versions never outrank current
99+
docs in search engines.
100+
- Versioning starts at the first tag that contains `docs/toc.yaml`; older
101+
tags are not backfilled.
102+
103+
## CI
104+
105+
- **CI** (`ci.yml`): ingest + build + typecheck on every PR and push to main.
106+
- **Deploy latest docs** (`deploy.yml`): runs on `repository_dispatch`
107+
(type `docs-updated`), manual dispatch, and a daily cron as a safety net.
108+
Ingests sqlc@main, builds, syncs `out/` to the R2 bucket root — excluding
109+
`v*/` and `versions.json`, which belong to releases.
110+
- **Deploy versioned docs** (`release.yml`): takes a tag (manual input or
111+
`repository_dispatch` type `docs-release` with `{"tag": "v1.32.0"}`),
112+
builds it under its version prefix, syncs to that prefix, and appends the
113+
tag to `versions.json`.
114+
115+
Deploys need these repository secrets: `R2_ACCOUNT_ID`, `R2_ACCESS_KEY_ID`,
116+
`R2_SECRET_ACCESS_KEY`, `R2_BUCKET`. Until they exist, the deploy step is
117+
skipped and the workflows just prove the build is green.
118+
119+
The sqlc repo side is a tiny workflow that notifies this repo when docs
120+
change. `DOCS_DISPATCH_TOKEN` is a fine-grained PAT scoped to this repo only,
121+
with Contents read/write permission (what `repository_dispatch` requires):
122+
123+
```yaml
124+
# .github/workflows/docs-dispatch.yml in sqlc-dev/sqlc
125+
name: Notify docs site
126+
on:
127+
push:
128+
branches: [main]
129+
paths: ['docs/**']
130+
jobs:
131+
dispatch:
132+
runs-on: ubuntu-latest
133+
steps:
134+
- run: |
135+
curl -sS -X POST \
136+
-H "Authorization: Bearer ${{ secrets.DOCS_DISPATCH_TOKEN }}" \
137+
-H "Accept: application/vnd.github+json" \
138+
https://api.github.com/repos/sqlc-dev/docs/dispatches \
139+
-d '{"event_type": "docs-updated"}'
140+
```
141+
142+
and the release equivalent sending
143+
`{"event_type": "docs-release", "client_payload": {"tag": "${{ github.ref_name }}"}}`
144+
on tag push.

0 commit comments

Comments
 (0)