Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
name: CI

on:
pull_request:
push:
branches: [dev, main]

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

# node-version-file (not a bare "22") is what actually enforces the
# >=22.12 floor required by sanity 6 / @sanity/ui 4.
- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "pnpm"

- name: Install dependencies
run: pnpm install --frozen-lockfile

# Generates apps/sanity/extract.json and the site's types.gen.ts, both of
# which are derived artifacts and therefore gitignored. Runs before
# typecheck because the site imports the generated types.
- name: Typegen (Sanity)
run: pnpm typegen
env:
SANITY_STUDIO_PROJECT_ID: hfh83o0w
SANITY_STUDIO_DATASET: production

- name: Typegen (Cloudflare)
run: pnpm --filter @codingcatdev/site cf-typegen

- name: Lint (site)
run: pnpm --filter @codingcatdev/site lint

- name: Typecheck (site)
run: pnpm --filter @codingcatdev/site typecheck

- name: Typecheck (studio)
run: pnpm --filter @codingcatdev/sanity exec tsc --noEmit

- name: Build site
run: pnpm --filter @codingcatdev/site build

- name: Build studio
run: pnpm --filter @codingcatdev/sanity build
env:
SANITY_STUDIO_PROJECT_ID: hfh83o0w
SANITY_STUDIO_DATASET: production

- name: Run E2E Tests
run: pnpm --filter @codingcatdev/site test:e2e

# extract.json is committed and is the single source of truth for typegen.
# The Typegen step above regenerates it; if that changed the file, the
# commit is stale. This is what stopped three divergent copies before.
- name: Verify extract.json is current
run: |
if ! git diff --quiet -- apps/sanity/extract.json; then
echo "::error::apps/sanity/extract.json is stale. Run 'pnpm typegen' and commit the result."
git diff --stat -- apps/sanity/extract.json
exit 1
fi

preview:
needs: verify
if: github.event_name == 'pull_request' && github.base_ref == 'dev'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "pnpm"

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Generate Sanity Types
run: pnpm typegen
env:
SANITY_STUDIO_PROJECT_ID: hfh83o0w
SANITY_STUDIO_DATASET: dev

- name: Generate Cloudflare Types
run: pnpm --filter @codingcatdev/site cf-typegen

- name: Build site (preview)
run: pnpm --filter @codingcatdev/site build

- name: Upload Cloudflare Worker Preview Version
id: upload_preview
run: |
OUTPUT=$(pnpm --filter @codingcatdev/site exec wrangler versions upload --tag "pr-${PR_NUMBER}" --message "PR #${PR_NUMBER}: ${PR_TITLE}")
echo "$OUTPUT"

PREVIEW_URL=$(echo "$OUTPUT" | grep -i "Version Preview URL:" | awk '{print $NF}' | head -n 1)
VERSION_ID=$(echo "$OUTPUT" | grep -i "Worker Version ID:" | awk '{print $NF}' | head -n 1)

echo "preview_url=$PREVIEW_URL" >> $GITHUB_OUTPUT
echo "version_id=$VERSION_ID" >> $GITHUB_OUTPUT

echo "::notice title=Cloudflare Worker Preview URL::${PREVIEW_URL}"

echo "### ⚡ Cloudflare Worker Preview Ready" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Preview URL**: [${PREVIEW_URL}](${PREVIEW_URL})" >> $GITHUB_STEP_SUMMARY
echo "- **Version ID**: \`${VERSION_ID}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Target Worker**: \`codingcatdev-dev\`" >> $GITHUB_STEP_SUMMARY

if [ -n "$PREVIEW_URL" ]; then
{
echo "### ⚡ Cloudflare Worker Preview Ready!"
echo ""
echo "| Environment | URL |"
echo "|---|---|"
echo "| **PR Preview** | [${PREVIEW_URL}](${PREVIEW_URL}) |"
echo "| **Dev Custom Domain** | [https://dev.codingcat.dev](https://dev.codingcat.dev) |"
echo ""
echo "- **Worker Version ID**: \`${VERSION_ID}\`"
echo "- **Protection**: Restricted to \`@codingcat.dev\` accounts via Cloudflare Zero Trust Access"
} > pr_comment.md

COMMENT_ID=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" --jq '.[] | select((.user.login=="github-actions[bot]" or .user.login=="app/github-actions") and (.body | contains("Cloudflare Worker Preview Ready"))) | .id' | tail -n 1)

if [ -n "$COMMENT_ID" ]; then
gh api -X PATCH "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" -f body="$(cat pr_comment.md)"
else
gh pr comment "$PR_NUMBER" --body-file pr_comment.md
fi
fi
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}

83 changes: 83 additions & 0 deletions .github/workflows/deploy-workers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: Deploy Site to Cloudflare Workers

on:
push:
branches: [dev, main]
paths:
- "apps/site/**"
- "packages/**"
- "pnpm-lock.yaml"
- ".github/workflows/deploy-workers.yml"

concurrency:
group: deploy-site-${{ github.ref }}
cancel-in-progress: true

jobs:
deploy-dev:
if: github.ref == 'refs/heads/dev'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "pnpm"

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Generate Sanity Types
run: pnpm typegen
env:
SANITY_STUDIO_PROJECT_ID: hfh83o0w
SANITY_STUDIO_DATASET: dev

- name: Generate Cloudflare Types
run: pnpm --filter @codingcatdev/site cf-typegen

- name: Build site (dev)
run: pnpm --filter @codingcatdev/site build

- name: Deploy to Cloudflare Workers (dev.codingcat.dev)
run: pnpm --filter @codingcatdev/site exec wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

deploy-production:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "pnpm"

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Generate Sanity Types
run: pnpm typegen
env:
SANITY_STUDIO_PROJECT_ID: hfh83o0w
SANITY_STUDIO_DATASET: production

- name: Generate Cloudflare Types
run: pnpm --filter @codingcatdev/site cf-typegen

- name: Build site (production)
run: pnpm --filter @codingcatdev/site build:production

- name: Deploy to Cloudflare Workers (codingcat.dev)
run: pnpm --filter @codingcatdev/site exec wrangler deploy --env production
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
15 changes: 12 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
name: Deploy Sanity Studio

on:
workflow_dispatch:
push:
branches: [dev, main]
paths:
- "apps/sanity/**"
- "pnpm-lock.yaml"
- ".github/workflows/deploy.yml"

jobs:
deploy-dev:
if: github.ref == 'refs/heads/dev'
if: github.ref == 'refs/heads/dev' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
environment: dev

Expand All @@ -17,7 +22,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: "22"
node-version-file: ".nvmrc"
cache: "pnpm"

- name: Install dependencies
Expand All @@ -29,6 +34,8 @@ jobs:
env:
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
SANITY_STUDIO_HOSTNAME: ${{ vars.SANITY_STUDIO_HOSTNAME }}
SANITY_STUDIO_DATASET: ${{ vars.SANITY_STUDIO_DATASET }}
SANITY_STUDIO_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }}

deploy-production:
if: github.ref == 'refs/heads/main'
Expand All @@ -42,7 +49,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: "22"
node-version-file: ".nvmrc"
cache: "pnpm"

- name: Install dependencies
Expand All @@ -54,3 +61,5 @@ jobs:
env:
SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }}
SANITY_STUDIO_HOSTNAME: ${{ vars.SANITY_STUDIO_HOSTNAME }}
SANITY_STUDIO_DATASET: ${{ vars.SANITY_STUDIO_DATASET }}
SANITY_STUDIO_PROJECT_ID: ${{ vars.SANITY_STUDIO_PROJECT_ID }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ yarn-error.log*
!.vscode/mcp.json
# pnpm workspace — root pnpm-lock.yaml is the lockfile
package-lock.json

# agents teamwork
.agents/
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22.23.2
88 changes: 88 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# codingcat.dev

pnpm monorepo. Node >= 22.12 (see `.nvmrc`); pnpm 10.34.1.

## Packages

| Path | Package | What it is |
|---|---|---|
| `apps/site` | `@codingcatdev/site` | **Astro 7 SSR on Cloudflare Workers** — the public site. Active development. |
| `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`. |
| `apps/sanity` | `@codingcatdev/sanity` | Sanity Studio v6, deployed to Sanity hosting. |

Root scripts target `apps/site`. Use `pnpm dev:dashboard` / `build:dashboard` for the Next app, `pnpm dev:studio` for the Studio.

## In-flight migration

Moving from Next.js on Vercel to Astro 7 SSR on Cloudflare Workers, with Sanity
upgraded to v6 and the video/AI pipeline (Remotion, Gemini, ElevenLabs,
NotebookLM, GCS, YouTube upload) dropped. Auth moves from Supabase to
better-auth on Cloudflare D1.

Branch note: `dev` still holds the **pre-Next Astro app** and `main` holds the
Next app — `main` is ahead. Harvest old Astro code with `git show dev:<path>`,
never by checking `dev` out over the working tree.

## Conventions that bite

- **`output: "server"` — there is no `getStaticPaths` anywhere.** Pagination is a
runtime bounds check on `Astro.params`, not a build-time param list.
- **GROQ queries must live in `.ts` files.** Sanity TypeGen cannot parse `.astro`
frontmatter, so `defineQuery` calls in `.astro` files are silently skipped.
- **Never read secrets from `import.meta.env`.** Vite inlines non-`PUBLIC_` vars
into the server bundle, baking them into the deployed Worker script. Read them
from `import { env } from "cloudflare:workers"` — `@astrojs/cloudflare` v14
reduced `Runtime` to `{ cfContext }` and deprecated `locals.runtime`.
- **Cloudflare bindings are not inherited into `env.production`.** Anything added
to the top level of `wrangler.jsonc` must be repeated there.
- **The environment is chosen at BUILD time, via `CLOUDFLARE_ENV`.** The adapter
flattens `wrangler.jsonc` into `dist/server/wrangler.json` and writes
`.wrangler/deploy/config.json`, which redirects wrangler away from the source
config — so `wrangler deploy --env production` on a default build silently
ships the *dev* dataset under the *dev* worker name. Use
`pnpm --filter @codingcatdev/site build:production`.
- **`Astro.site` is baked in at build time**, so it cannot carry a per-env
origin. Canonicals, `og:url`, feeds and the sitemap read `Astro.locals.siteUrl`,
resolved per request from the `SITE_URL` var in middleware.
- **`stegaClean` anything bound to an attribute**, `<meta>`, JSON-LD, or XML.
Stega's zero-width characters are harmless in text nodes and corrupting in
`href`, `src`, and feeds.
- **`@sanity/icons` v5 has no root-entry icon exports.** Import from subpaths:
`import {UserIcon} from "@sanity/icons/User"`.
- **`astro-portabletext` uses singular `type` / `block` / `mark` keys**, and mark
components read the mark definition from `node.markDef`, not `node`. Both fail
silently — the component simply never runs.
- **A self-closing `<script />` in an `.astro` file breaks props inference**, so
`Astro.props` degrades to `Record<string, any>`. Use paired tags.
- `apps/sanity/plugins/podcast-rss/` is vendored, not an npm dep — see its README.

## Agent skills

Sanity guidance is vendored in `.agents/skills/` (pinned by `skills-lock.json`).
The Astro-relevant ones for this migration:

- `.agents/skills/portable-text-serialization/rules/astro.md` — `astro-portabletext`
usage. Note it uses **singular** `type` / `block` / `mark` keys, unlike
`@portabletext/react`'s plural `types` / `marks`.
- `.agents/skills/sanity-best-practices/references/astro.md`
- `.agents/skills/content-modeling-best-practices/`, `seo-aeo-best-practices/`

`.agents/skills/sanity-live-cache-components/` documents the Next.js
`cacheComponents` + `next-sanity` pattern being removed. It applies only to
`apps/web` and should be deleted with it.

Cloudflare guidance comes from the Claude Code cloudflare plugin — invoke the
`cloudflare`, `workers-best-practices`, and `wrangler` skills rather than
guessing Workers APIs.

## Verifying

`pnpm lint`, `pnpm typecheck`, `pnpm build` cover `apps/site`; CI also builds the
Studio and fails if `apps/sanity/extract.json` has drifted from the schema.

Bindings only resolve under `wrangler dev`, not `astro dev` — verify anything
touching `locals.runtime.env`, D1, or OG image generation against a real Worker.

`apps/site/baseline/` holds pre-migration production `sitemap.xml` and RSS
snapshots (472 URLs, 50 items per feed). Diff against them before cutover; a
changed podcast GUID re-publishes every episode to Apple/Spotify.
1 change: 0 additions & 1 deletion apps/sanity/components/CodeSandboxPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from "react";
import { RenderPreviewCallbackProps } from "sanity";
// TODO: Add stronger typing

const CodePenPreview = (props: any) => {
Expand Down
Loading
Loading