docs(blog): how to publish, and why the app has two route groups
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m15s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 36s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m11s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m18s
PR Checks / AI Code Review (Claude) (pull_request) Failing after 2m43s
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m15s
PR Checks / Backend Smoke (pull_request) Successful in 10s
PR Checks / Build Backend (no push) (pull_request) Successful in 36s
PR Checks / Build Frontend (no push) (pull_request) Successful in 1m11s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 1m18s
PR Checks / AI Code Review (Claude) (pull_request) Failing after 2m43s
PUBLISHING.md carries the house style with the posts, so the standard survives without the design doc to hand — including the rule that a post states what a metric does not show, which is the strongest signal a human wrote it. CLAUDE.md gains the two constraints that are invisible from the code and expensive to rediscover: metadata file conventions break if moved into a route group, and the build must keep succeeding with DATABASE_URL unset. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017YmbBhr8s7GusjDE12hrZM
This commit is contained in:
1 parent
b793640507
commit
07d586d0ad
2 files changed
+122
No files matched your search
@@ -23,6 +23,46 @@ Key files:
|
||||
- `backend/data_loader.py` - Data queries, geocoding, legacy DataFrame compatibility
|
||||
- `backend/schemas.py` - Column mappings, metric definitions, LA code mappings
|
||||
|
||||
### Content / CMS (Payload)
|
||||
|
||||
Payload CMS runs **inside** the Next.js app — one image, one container, no
|
||||
separate service. It powers `/blog`; `/about` is a plain coded page.
|
||||
|
||||
- **Admin panel:** `/admin`. The only authenticated surface on the site.
|
||||
`noindex` via both `robots.txt` and `X-Robots-Tag`.
|
||||
- **CMS API:** `/cms-api`, **not** `/api`. `/api/*` is a catch-all proxy to
|
||||
FastAPI (`app/(frontend)/api/[...path]`) which would silently swallow every
|
||||
admin call and forward it to the backend. Mount points are defined once in
|
||||
`lib/payloadRoutes.ts`.
|
||||
- **Database:** the existing Postgres, in its own `payload` schema, so no
|
||||
pipeline operation on `public` — including
|
||||
`scripts/migrate_csv_to_db.py --drop` — can reach blog content.
|
||||
- **Uploads:** the `payload_media` Docker volume at `/app/media`. Not
|
||||
reproducible from the pipeline; must be backed up.
|
||||
- **New env vars:** `DATABASE_URL` and `PAYLOAD_SECRET` on the frontend service.
|
||||
Staging must use a different `PAYLOAD_SECRET` from production.
|
||||
- Publishing workflow and house style: `nextjs-app/docs/PUBLISHING.md`.
|
||||
|
||||
### Two route groups
|
||||
|
||||
`nextjs-app/app/` has no root `layout.tsx`. It cannot: Payload's admin panel
|
||||
ships its own root layout rendering `<html>`/`<body>`, and Next permits
|
||||
multiple root layouts only when no `app/layout.tsx` exists.
|
||||
|
||||
- `app/(frontend)/` — the site. Its `layout.tsx` is the site's root layout.
|
||||
- `app/(payload)/` — the admin panel and `/cms-api`.
|
||||
|
||||
Route groups are invisible to routing, so every public URL is unchanged.
|
||||
|
||||
**The metadata file conventions stay at the `app/` root** — `robots.ts`,
|
||||
`opengraph-image.tsx`, `icon.png`, `apple-icon.png`. Inside a route group Next
|
||||
treats them as segment-scoped: it renames `/icon.png` to `/icon-<hash>.png` and
|
||||
drops `/robots.txt` entirely. Route handlers are unaffected.
|
||||
|
||||
The build must succeed with `DATABASE_URL` unset, because CI builds it that
|
||||
way. Never call `getCachedPayload()` at module scope, and never add
|
||||
`generateStaticParams` to a DB-backed route.
|
||||
|
||||
### Frontend (Vanilla JS)
|
||||
- Single-page application with hash-based routing
|
||||
- Chart.js for data visualization
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Publishing to the blog
|
||||
|
||||
The blog is Payload CMS, running inside the Next.js app. There is no separate
|
||||
service and no second deploy — writing a post is done in the browser and takes
|
||||
effect on the live site within seconds.
|
||||
|
||||
## Signing in
|
||||
|
||||
`https://www.schoolcompare.co.uk/admin` — one account, no registration. If you
|
||||
need the account seeded on a fresh environment, run against the container:
|
||||
|
||||
```bash
|
||||
npx payload create-first-user
|
||||
```
|
||||
|
||||
Staging has its own admin panel, its own database and its own credentials at
|
||||
`https://stx.schoolcompare.co.uk/admin`. Never reuse production's secret or
|
||||
password there.
|
||||
|
||||
## Writing a post
|
||||
|
||||
**Posts → Create New.** The fields:
|
||||
|
||||
| Field | Notes |
|
||||
|---|---|
|
||||
| **Title** | The `<h1>` and the browser tab. |
|
||||
| **Slug** | The URL segment, in the sidebar. **Never change it after publishing** — it is the canonical URL, and changing it breaks every existing link and discards the page's accumulated search signal. |
|
||||
| **Published at** | The date shown on the post and in the feed. |
|
||||
| **Excerpt** | Max 200 characters. Shown on the index *and* used as the meta description, so write it as a standalone sentence rather than a teaser. |
|
||||
| **Hero image** | Optional. Becomes the social share image; without one, the site's generated card is used. |
|
||||
| **Content** | Rich text. `/` inserts a block. |
|
||||
|
||||
**Save as draft** while you're working — drafts are not public. **Publish** when
|
||||
it's ready.
|
||||
|
||||
### The callout block
|
||||
|
||||
One custom block, `Callout`, with two tones:
|
||||
|
||||
- **Caveat** — what a number does *not* show. This is the one that matters: it
|
||||
is how a post states a limitation in context rather than burying it in a
|
||||
closing paragraph.
|
||||
- **Note** — a useful aside.
|
||||
|
||||
### Images
|
||||
|
||||
Every image requires alt text; the editor will not let you save without it.
|
||||
Uploads go to a Docker volume on the host, which is backed up separately from
|
||||
Postgres — an image is not reproducible from the pipeline the way school data
|
||||
is.
|
||||
|
||||
## How publishing reaches the live site
|
||||
|
||||
- `/blog`, `/blog/rss.xml` and `/content-sitemap.xml` are rendered per request,
|
||||
so a new post appears immediately.
|
||||
- `/blog/[slug]` is cached after its first request. Publishing or editing fires
|
||||
a `revalidatePath` from the collection's `afterChange` hook, which drops that
|
||||
cached copy — so edits appear immediately too.
|
||||
|
||||
If a change doesn't show, it is far more likely the post is still a draft than
|
||||
that the cache is stale.
|
||||
|
||||
## House style
|
||||
|
||||
These rules are why the blog exists. A post that ignores them makes the site
|
||||
read more machine-generated, not less.
|
||||
|
||||
- **First person singular.** "I built", "I found" — never "we provide".
|
||||
- **Concrete over general.** "When we were looking at schools in Wandsworth"
|
||||
beats any amount of stated warmth.
|
||||
- **State limits before someone else finds them.** Every post that presents a
|
||||
metric says what it does not show. This is the single strongest signal that a
|
||||
human wrote it: generated content does not volunteer its own weaknesses.
|
||||
- **No mission statements, no "passionate about", no invented team.** There is
|
||||
one person here.
|
||||
- **Short sentences.**
|
||||
- **Never publish a surname, an employer, or a child's name.** The site's author
|
||||
is "Tudor". See `/about`.
|
||||
- **Never invent a figure**, even illustratively. On a site whose whole
|
||||
proposition is official data, a made-up number attached to a real school is
|
||||
the one thing it cannot do — and no illustrative intent survives being
|
||||
screenshotted.
|
||||
Reference in new issue
Block a user