This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Hill People is a blog platform with an Astro frontend (https://hillpeople.net) and Strapi CMS backend (https://journal.hillpeople.net). The frontend fetches data via SSR, and Strapi renders the frontend in an iframe for preview mode.
make pm2 # Start frontend + backend via PM2 (local Strapi)
make pm2-prod # Start frontend via PM2 (production Strapi, no local backend)
make pm2-frontend # Start/restart just frontend
make pm2-backend # Start/restart just backend
make pm2-stop # Stop all PM2 processes
make pm2-kill # Kill PM2 daemon + all processes
make pm2-logs # Tail all logs
make pm2-logs-frontend # Tail frontend logs
make pm2-logs-backend # Tail backend logs
make pm2-status # Show PM2 process table
make sync-data # Pull production data to local Strapi
make upgrade-strapi # Upgrade Strapi to latest version
make build # Build frontend + backendPM2 targets use startOrRestart so they're idempotent — safe to call multiple times without spawning duplicate processes.
npm run dev # Start dev server with hot reload
npm run build # Build for production
npm run deploy # Build and deploy to Cloudflarenpm run develop # Start Strapi with autoReload
npm run build # Build admin panel
npm run transfer # Fetch data from production (requires STRAPI_PRODUCTION_URL and STRAPI_TRANSFER_TOKEN in .env)
npm run upgrade # Upgrade Strapi to latest version- Frontend fetches posts from Strapi API (
/api/posts?populate=*) - Post content is stored as HTML in Strapi using CKEditor (
richContentfield) - Images are served with responsive formats (xlarge, large, medium, small, xsmall)
lib/api.ts- Strapi API client:fetchPosts(),fetchPostBySlug(),fetchSingleType()— all accept optionallocalsparam for DO cachinglib/do-cache.ts- Durable Object cache helpers:getCached(),setCached(),clearDOCache()lib/cache.ts- Cache invalidation: Cloudflare edge purge + DO cache clearlib/imageUrl.ts- Utilities for constructing Strapi image URLs and selecting formatsdurable-objects/StrapiCache.ts- Durable Object class for unified cross-isolate cachingworker.ts- Custom Worker entry point that exports DO alongside Astro SSR handlerpages/blog/[slug].astro- Dynamic post pages; supports?status=draftfor preview mode
post/- Blog post collection type with custom attribution middlewareabout/- Single type for the about pagepost/middlewares/postAttributionMiddleware.ts- Auto-populatescreatedBy/updatedByfields
Strapi admin generates preview URLs like /blog/{slug}?status=draft. The frontend checks for status=draft query param to fetch and render draft content.
- Always develop on a new branch checked out from latest
main - All changes require human review and testing before merge
- PRs are merged via the GitHub UI (not via CLI)
- Never push directly to
main
When adding a new page to the frontend:
- Add the URL to
.github/workflows/lighthouse.ymlin theurlarray (Lighthouse CI) - Add the URL to
frontend/src/pages/sitemap.xml.ts
The frontend uses a two-layer caching strategy to minimize Strapi API calls while ensuring content freshness.
A Cloudflare Durable Object (StrapiCache) provides a single shared cache instance across all Worker isolates. Located in frontend/src/durable-objects/StrapiCache.ts.
- TTL: 12 hours
- Scope: Global — shared across all Worker isolates (unlike per-isolate caching)
- Storage: SQLite-backed with in-memory read-through cache (persists across DO evictions)
- Skips caching: 404s and empty responses
- Invalidation: Automatic on TTL expiry, or manual via
?bustcachequery param on any page - Access: All data-fetching functions accept
Astro.localsto access the DO binding
HTTP Cache-Control headers enable Cloudflare's edge caching for rendered pages.
| Page | Cache-Control |
|---|---|
/blog/[slug] |
public, s-maxage=3600, stale-while-revalidate=86400 |
/blog/[slug]?status=draft |
private, no-store |
/api/posts |
public, s-maxage=300, stale-while-revalidate=600 |
/api/climbing-ticks |
public, s-maxage=300, stale-while-revalidate=600 |
Manual: Append ?bustcache to any URL (e.g., https://hillpeople.net/climbing?bustcache) to clear both caches:
- Clears the Durable Object cache (affects all isolates instantly)
- Purges Cloudflare edge cache via API (requires
CLOUDFLARE_ZONE_IDandCLOUDFLARE_API_TOKEN) - Redirects back to the clean URL
Automatic: Strapi lifecycle hooks call /api/revalidate when content changes, purging specific URLs:
| Content Type | URLs Purged |
|---|---|
post |
/, /api/posts, /blog/{slug} |
about |
/about |
home-page |
/ |
site-settings |
/, /about, /climbing, /privacy |
climbing-tick |
/climbing, /api/ticklist-data |
climbing-goal |
/climbing, /api/ticklist-data |
person |
/climbing, /api/ticklist-data |
privacy-policy |
/privacy |
| Variable | Location | Purpose |
|---|---|---|
CLOUDFLARE_ZONE_ID |
Cloudflare Pages | Zone ID for cache purge API |
CLOUDFLARE_API_TOKEN |
Cloudflare Pages | Token with "Zone.Cache Purge" permission |
REVALIDATE_SECRET |
Both | Shared secret for webhook auth |
FRONTEND_REVALIDATE_URL |
Strapi Cloud | https://hillpeople.net/api/revalidate |
When adding a new page to the frontend:
- Lighthouse CI: Add the URL to
.github/workflows/lighthouse.ymlin theurlarray - Cache invalidation: Add the URL to
ALL_CACHEABLE_URLSinfrontend/src/lib/cache.ts - Cache-Control headers: If the page should be cached at the edge, add appropriate
Cache-Controlheaders
When adding a new content type in Strapi that affects frontend pages:
-
Lifecycle hooks: Create
backend/src/api/{content-type}/content-types/{content-type}/lifecycles.ts:import { invalidateCache } from '../../../../utils/cache-invalidation'; export default { async afterCreate() { await invalidateCache('{content-type}'); }, async afterUpdate() { await invalidateCache('{content-type}'); }, async afterDelete() { await invalidateCache('{content-type}'); }, };
-
URL mapping: Add the content type to
CONTENT_TYPE_URLSinfrontend/src/lib/cache.ts:'{content-type}': ['/affected-page', '/api/affected-endpoint'],
The newsletter system is a Strapi plugin at backend/src/plugins/newsletter/. It sends emails via Resend when new posts are published.
- Cron job checks for eligible posts (published,
newsletterSent=false, past cooldown period) on a configurable interval - Admin UI at Newsletter in the Strapi sidebar: Send tab (manual send, test send, email preview), History tab, Settings tab
- Confirm/unsubscribe endpoints are proxied through the Astro frontend (
/newsletter/confirm/[token],/newsletter/unsubscribe/[token]) to keep Strapi unexposed - Subscriber lifecycle in
backend/src/api/subscriber/sends confirmation emails via the plugin service on new signups
server/src/services/email-service.ts— Resend API wrapperserver/src/services/template-service.ts— HTML email generation (branded, with dark mode)server/src/services/newsletter-service.ts— Core orchestration (send, confirm, unsubscribe)server/src/controllers/— Admin routes + content-api routesserver/src/bootstrap.ts— Cron registrationadmin/src/— React admin UI
After modifying plugin source files, rebuild and restart Strapi:
cd backend/src/plugins/newsletter && npm run build
# Then restart Strapi: make pm2-backenddocuments().update()only modifies the draft — callpublish()after to propagate to the published version- Document Service API returns relative media URLs (e.g.,
/uploads/img.jpg), unlike the REST API which resolves them to absolute. Usestrapi.config.get('server.url')to resolve. - Query published documents with
status: 'published'param, not a filter
| Variable | Location | Purpose |
|---|---|---|
RESEND_API_KEY |
Strapi Cloud | Resend API key for sending emails |
CLIENT_URL |
Strapi Cloud | Frontend URL (shared with admin preview config) |
STRAPI_PUBLIC_URL |
Strapi Cloud | Strapi public URL for resolving media URLs in emails |
Four GitHub Actions workflows run on pull requests:
| Workflow | File | Trigger | Purpose |
|---|---|---|---|
| Build | build.yml |
PR opened/sync | Validates frontend + backend build (parallel jobs) |
| Preview | preview.yml |
PR opened/sync/reopen/close | Deploys preview worker per PR; cleans up on close |
| Lighthouse | lighthouse.yml |
PR (frontend changes) | Performance audits on built site |
| Claude Review | claude-review.yml |
PR | AI code review |
Each PR gets a preview worker at hillpeople-preview-pr-{N}.workers.dev. The preview worker does not include Durable Object caching (Cloudflare preview URLs don't support DO bindings) — requests go directly to Strapi. The STRAPI_API_TOKEN secret is provisioned automatically via wrangler secret put. The worker is deleted when the PR is closed.
| Secret | Purpose |
|---|---|
CLOUDFLARE_WORKERS_API_TOKEN |
Cloudflare API token with Workers Scripts permissions |
CLOUDFLARE_ACCOUNT_ID |
Cloudflare account ID for Workers API |
STRAPI_API_TOKEN |
Strapi API token (used by Lighthouse workflow) |
LHCI_GITHUB_TOKEN |
Lighthouse CI GitHub App token |
Both services auto-deploy on merge to main:
- Frontend: Cloudflare Pages
- Backend: Strapi Cloud
Issues live in GitHub (evannoronha/hillpeople). See docs/agents/issue-tracker.md.
Five canonical labels: needs-triage, needs-info, ready-for-agent, ready-for-human, wontfix. See docs/agents/triage-labels.md.
Single-context layout — root CONTEXT.md and docs/adr/. See docs/agents/domain.md.