Skip to content

Commit 1b01b48

Browse files
codercatdevclaude
andcommitted
ci: add build/lint/typecheck gate and repo agent docs
The repo had no CI at all — no lint, typecheck, build, or test ran on any push. Two latent breakages had accumulated unnoticed as a result: - apps/web/biome.json is invalid for Biome 2 (`jsxRuntime: "automatic"` is not an accepted value), so `pnpm lint` there has been failing on a config error rather than linting anything. Left as-is since apps/web is being retired; apps/site gets a corrected Biome 2 config (`assist.actions.source`, `files.includes` with negations, `jsxRuntime: "transparent"`). - apps/sanity/extract.json can silently drift from the schema, which is how three divergent copies accumulated. CI now re-runs the extract and fails if the result differs from what is committed. ci.yml runs lint + typecheck + build for the site, typecheck + build for the Studio, and the extract drift check. Uses node-version-file: .nvmrc rather than a bare "22", which is what actually enforces the >=22.12 floor that sanity 6 and @sanity/ui 4 require; deploy.yml switched to the same. deploy.yml also gains a paths filter so site-only changes stop redeploying the Studio on every push. Adds a root CLAUDE.md (the repo had no root docs) recording the package layout, the in-flight migration, where the vendored Sanity skills live, and the conventions that have already caused real bugs here — no getStaticPaths under output: "server", GROQ must live in .ts files for TypeGen, never read secrets from import.meta.env, bindings are not inherited into env.production, and stegaClean anything bound to an attribute or feed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HZ4S1uTJwFdbv3A6PHWfw2
1 parent 4a95ece commit 1b01b48

8 files changed

Lines changed: 768 additions & 61 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [dev, main]
7+
8+
concurrency:
9+
group: ci-${{ github.ref }}
10+
cancel-in-progress: true
11+
12+
jobs:
13+
verify:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- uses: pnpm/action-setup@v4
19+
20+
# node-version-file (not a bare "22") is what actually enforces the
21+
# >=22.12 floor required by sanity 6 / @sanity/ui 4.
22+
- uses: actions/setup-node@v4
23+
with:
24+
node-version-file: ".nvmrc"
25+
cache: "pnpm"
26+
27+
- name: Install dependencies
28+
run: pnpm install --frozen-lockfile
29+
30+
- name: Lint (site)
31+
run: pnpm --filter @codingcatdev/site lint
32+
33+
- name: Typecheck (site)
34+
run: pnpm --filter @codingcatdev/site typecheck
35+
36+
- name: Typecheck (studio)
37+
run: pnpm --filter @codingcatdev/sanity exec tsc --noEmit
38+
39+
- name: Build site
40+
run: pnpm --filter @codingcatdev/site build
41+
42+
- name: Build studio
43+
run: pnpm --filter @codingcatdev/sanity build
44+
env:
45+
SANITY_STUDIO_PROJECT_ID: hfh83o0w
46+
SANITY_STUDIO_DATASET: production
47+
48+
# extract.json is the single source of truth for typegen. If it drifts
49+
# from the schema, generated types silently go stale — which is how three
50+
# divergent copies accumulated before.
51+
- name: Verify extract.json is current
52+
run: |
53+
pnpm --filter @codingcatdev/sanity typegen:extract
54+
if ! git diff --quiet -- apps/sanity/extract.json; then
55+
echo "::error::apps/sanity/extract.json is stale. Run 'pnpm typegen' and commit the result."
56+
git diff --stat -- apps/sanity/extract.json
57+
exit 1
58+
fi
59+
env:
60+
SANITY_STUDIO_PROJECT_ID: hfh83o0w
61+
SANITY_STUDIO_DATASET: production

‎.github/workflows/deploy.yml‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ name: Deploy Sanity Studio
33
on:
44
push:
55
branches: [dev, main]
6+
paths:
7+
- "apps/sanity/**"
8+
- "pnpm-lock.yaml"
9+
- ".github/workflows/deploy.yml"
610

711
jobs:
812
deploy-dev:
@@ -17,7 +21,7 @@ jobs:
1721

1822
- uses: actions/setup-node@v4
1923
with:
20-
node-version: "22"
24+
node-version-file: ".nvmrc"
2125
cache: "pnpm"
2226

2327
- name: Install dependencies
@@ -42,7 +46,7 @@ jobs:
4246

4347
- uses: actions/setup-node@v4
4448
with:
45-
node-version: "22"
49+
node-version-file: ".nvmrc"
4650
cache: "pnpm"
4751

4852
- name: Install dependencies

‎CLAUDE.md‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# codingcat.dev
2+
3+
pnpm monorepo. Node >= 22.12 (see `.nvmrc`); pnpm 10.34.1.
4+
5+
## Packages
6+
7+
| Path | Package | What it is |
8+
|---|---|---|
9+
| `apps/site` | `@codingcatdev/site` | **Astro 7 SSR on Cloudflare Workers** — the public site. Active development. |
10+
| `apps/web` | `@codingcatdev/web` | Next.js 16 app. Being retired: its public half is replaced by `apps/site`; the dashboard + sponsor portal move to `admin.codingcat.dev`. |
11+
| `apps/sanity` | `@codingcatdev/sanity` | Sanity Studio v6, deployed to Sanity hosting. |
12+
13+
Root scripts target `apps/site`. Use `pnpm dev:dashboard` / `build:dashboard` for the Next app, `pnpm dev:studio` for the Studio.
14+
15+
## In-flight migration
16+
17+
Moving from Next.js on Vercel to Astro 7 SSR on Cloudflare Workers, with Sanity
18+
upgraded to v6 and the video/AI pipeline (Remotion, Gemini, ElevenLabs,
19+
NotebookLM, GCS, YouTube upload) dropped. Auth moves from Supabase to
20+
better-auth on Cloudflare D1.
21+
22+
Branch note: `dev` still holds the **pre-Next Astro app** and `main` holds the
23+
Next app — `main` is ahead. Harvest old Astro code with `git show dev:<path>`,
24+
never by checking `dev` out over the working tree.
25+
26+
## Conventions that bite
27+
28+
- **`output: "server"` — there is no `getStaticPaths` anywhere.** Pagination is a
29+
runtime bounds check on `Astro.params`, not a build-time param list.
30+
- **GROQ queries must live in `.ts` files.** Sanity TypeGen cannot parse `.astro`
31+
frontmatter, so `defineQuery` calls in `.astro` files are silently skipped.
32+
- **Never read secrets from `import.meta.env`.** Vite inlines non-`PUBLIC_` vars
33+
into the server bundle, baking them into the deployed Worker script. Read them
34+
per-request from `locals.runtime.env`.
35+
- **Cloudflare bindings are not inherited into `env.production`.** Anything added
36+
to the top level of `wrangler.jsonc` must be repeated there.
37+
- **`stegaClean` anything bound to an attribute**, `<meta>`, JSON-LD, or XML.
38+
Stega's zero-width characters are harmless in text nodes and corrupting in
39+
`href`, `src`, and feeds.
40+
- **`@sanity/icons` v5 has no root-entry icon exports.** Import from subpaths:
41+
`import {UserIcon} from "@sanity/icons/User"`.
42+
- `apps/sanity/plugins/podcast-rss/` is vendored, not an npm dep — see its README.
43+
44+
## Agent skills
45+
46+
Sanity guidance is vendored in `.agents/skills/` (pinned by `skills-lock.json`).
47+
The Astro-relevant ones for this migration:
48+
49+
- `.agents/skills/portable-text-serialization/rules/astro.md` — `astro-portabletext`
50+
usage. Note it uses **singular** `type` / `block` / `mark` keys, unlike
51+
`@portabletext/react`'s plural `types` / `marks`.
52+
- `.agents/skills/sanity-best-practices/references/astro.md`
53+
- `.agents/skills/content-modeling-best-practices/`, `seo-aeo-best-practices/`
54+
55+
`.agents/skills/sanity-live-cache-components/` documents the Next.js
56+
`cacheComponents` + `next-sanity` pattern being removed. It applies only to
57+
`apps/web` and should be deleted with it.
58+
59+
Cloudflare guidance comes from the Claude Code cloudflare plugin — invoke the
60+
`cloudflare`, `workers-best-practices`, and `wrangler` skills rather than
61+
guessing Workers APIs.
62+
63+
## Verifying
64+
65+
`pnpm lint`, `pnpm typecheck`, `pnpm build` cover `apps/site`; CI also builds the
66+
Studio and fails if `apps/sanity/extract.json` has drifted from the schema.
67+
68+
Bindings only resolve under `wrangler dev`, not `astro dev` — verify anything
69+
touching `locals.runtime.env`, D1, or OG image generation against a real Worker.
70+
71+
`apps/site/baseline/` holds pre-migration production `sitemap.xml` and RSS
72+
snapshots (472 URLs, 50 items per feed). Diff against them before cutover; a
73+
changed podcast GUID re-publishes every episode to Apple/Spotify.

‎apps/site/.gitignore‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
dist/
2+
.astro/
3+
.wrangler/
4+
.dev.vars
5+
worker-configuration.d.ts
6+
src/lib/sanity/types.gen.ts

‎apps/site/astro.config.mjs‎

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { defineConfig } from "astro/config";
1+
import fs from "node:fs";
2+
import path from "node:path";
23
import cloudflare from "@astrojs/cloudflare";
34
import react from "@astrojs/react";
45
import tailwindcss from "@tailwindcss/vite";
5-
import fs from "node:fs";
6-
import path from "node:path";
6+
import { defineConfig } from "astro/config";
77

88
/**
99
* Inline font files as Uint8Array at build time.
@@ -13,45 +13,45 @@ import path from "node:path";
1313
* around Satori/workers-og and should not be re-derived.
1414
*/
1515
function rawFonts(extensions) {
16-
return {
17-
name: "vite-plugin-raw-fonts",
18-
enforce: "pre",
19-
resolveId(id, importer) {
20-
if (extensions.some((ext) => id.includes(ext))) {
21-
if (id.startsWith(".")) {
22-
return path.resolve(path.dirname(importer), id);
23-
}
24-
return id;
25-
}
26-
},
27-
load(id) {
28-
if (extensions.some((ext) => id.includes(ext))) {
29-
const buffer = fs.readFileSync(id);
30-
return `export default new Uint8Array([${Array.from(buffer).join(",")}]);`;
31-
}
32-
},
33-
};
16+
return {
17+
name: "vite-plugin-raw-fonts",
18+
enforce: "pre",
19+
resolveId(id, importer) {
20+
if (extensions.some((ext) => id.includes(ext))) {
21+
if (id.startsWith(".")) {
22+
return path.resolve(path.dirname(importer), id);
23+
}
24+
return id;
25+
}
26+
},
27+
load(id) {
28+
if (extensions.some((ext) => id.includes(ext))) {
29+
const buffer = fs.readFileSync(id);
30+
return `export default new Uint8Array([${Array.from(buffer).join(",")}]);`;
31+
}
32+
},
33+
};
3434
}
3535

3636
export default defineConfig({
37-
site: process.env.SITE_URL ?? "https://codingcat.dev",
38-
output: "server",
39-
adapter: cloudflare({
40-
// Images are Sanity CDN URLs already transformed by @sanity/image-url;
41-
// do not route them through Cloudflare Images.
42-
imageService: "passthrough",
43-
platformProxy: { enabled: true },
44-
}),
45-
integrations: [react()],
46-
redirects: {
47-
"/blog/page": "/blog",
48-
"/podcasts/page": "/podcasts",
49-
"/authors/page": "/authors",
50-
"/guests/page": "/guests",
51-
"/sponsors/page": "/sponsors",
52-
},
53-
vite: {
54-
plugins: [tailwindcss(), rawFonts([".ttf", ".otf"])],
55-
assetsInclude: ["**/*.wasm"],
56-
},
37+
site: process.env.SITE_URL ?? "https://codingcat.dev",
38+
output: "server",
39+
adapter: cloudflare({
40+
// Images are Sanity CDN URLs already transformed by @sanity/image-url;
41+
// do not route them through Cloudflare Images.
42+
imageService: "passthrough",
43+
platformProxy: { enabled: true },
44+
}),
45+
integrations: [react()],
46+
redirects: {
47+
"/blog/page": "/blog",
48+
"/podcasts/page": "/podcasts",
49+
"/authors/page": "/authors",
50+
"/guests/page": "/guests",
51+
"/sponsors/page": "/sponsors",
52+
},
53+
vite: {
54+
plugins: [tailwindcss(), rawFonts([".ttf", ".otf"])],
55+
assetsInclude: ["**/*.wasm"],
56+
},
5757
});

‎apps/site/biome.json‎

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
{
22
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
3+
"vcs": {
4+
"enabled": true,
5+
"clientKind": "git",
6+
"defaultBranch": "main",
7+
"useIgnoreFile": true
8+
},
9+
"files": {
10+
"includes": [
11+
"**/*.ts",
12+
"**/*.tsx",
13+
"**/*.js",
14+
"**/*.mjs",
15+
"!**/node_modules",
16+
"!**/dist",
17+
"!**/.astro",
18+
"!**/.wrangler",
19+
"!src/lib/sanity/types.gen.ts",
20+
"!worker-configuration.d.ts",
21+
"!baseline"
22+
]
23+
},
324
"formatter": {
425
"enabled": true,
526
"useEditorconfig": true
@@ -20,35 +41,27 @@
2041
"noUnusedFunctionParameters": "warn"
2142
},
2243
"style": {
23-
"noUnusedTemplateLiteral": {
24-
"level": "error",
25-
"fix": "safe"
26-
},
44+
"noUnusedTemplateLiteral": { "level": "error", "fix": "safe" },
2745
"noNonNullAssertion": "warn",
2846
"useBlockStatements": "error"
29-
},
47+
},
3048
"a11y": {
3149
"noSvgWithoutTitle": "off"
3250
}
3351
}
3452
},
35-
"organizeImports": {
36-
"enabled": true
37-
},
38-
"files": {
39-
"ignore": ["node_modules/**", "sanity/types.ts", "supabase/**", "playwright-report/**", "test-results/**"]
53+
"assist": {
54+
"actions": {
55+
"source": {
56+
"organizeImports": "on"
57+
}
58+
}
4059
},
4160
"javascript": {
42-
"jsxRuntime": "automatic",
61+
"jsxRuntime": "transparent",
4362
"formatter": {
4463
"trailingCommas": "all",
4564
"semicolons": "always"
4665
}
47-
},
48-
"vcs": {
49-
"enabled": true,
50-
"clientKind": "git",
51-
"defaultBranch": "main",
52-
"useIgnoreFile": true
5366
}
5467
}

‎apps/site/package.json‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
"name": "@codingcatdev/site",
33
"type": "module",
44
"private": true,
5-
"engines": { "node": ">=22.12" },
5+
"engines": {
6+
"node": ">=22.12"
7+
},
68
"scripts": {
79
"dev": "astro dev",
810
"build": "astro build",
@@ -28,6 +30,7 @@
2830
"tailwindcss": "^4.2.1"
2931
},
3032
"devDependencies": {
33+
"@astrojs/check": "^0.9.10",
3134
"@biomejs/biome": "2.2.4",
3235
"@tailwindcss/typography": "^0.5.19",
3336
"typescript": "^5.9",

0 commit comments

Comments
 (0)