The public site and the Keystatic editor are deployed together as a Next.js application on Cloudflare Workers. Editors can publish blog posts and team-member profiles from preferred.ai/keystatic; GitHub pull requests provide validation, approvals, and an audit trail.
Install Node.js 22 or newer and pnpm, then run:
pnpm install
pnpm devOpen http://localhost:3000 for the website or http://localhost:3000/keystatic for the local editor. Local Keystatic writes directly to the working tree, so review the changed files before committing them.
Useful checks before opening a pull request:
pnpm check
pnpm build:workerpnpm check runs linting, type checking, Markdown security checks, People-content validation, and publishing-policy tests. The Worker build also regenerates the blog and People aggregate data used in production.
Go to preferred.ai/keystatic and sign in with GitHub. Saving an entry creates or updates a branch whose name begins with keystatic/. The publishing automation then:
- Creates or reuses a pull request for the complete branch diff.
- Classifies every changed path as blog, People, mixed editorial, or non-editorial.
- Runs
Test Build, including content validation, dependency audit, image optimization, and the full Cloudflare Worker build. - Enables squash auto-merge only when the diff is limited to approved editorial paths.
- Deploys the production Worker after the pull request merges into
main.
- Open Posts and choose Create post.
- Enter the title first; it becomes the post slug and image-folder name.
- Choose or drop a featured image into Featured image.
- Write the post in Content. Use the image toolbar, paste an image, or drag and drop it into the editor.
- Add useful alt text, preview the post, and save.
New posts and images are stored together:
content/posts/my-new-post.md
public/uploads/my-new-post/cover.jpg
public/uploads/my-new-post/diagram-one.png
Existing posts may keep their legacy /uploads/YYYY/MM/... paths. Leave Existing featured image path (legacy) empty for a new post.
A blog-only pull request merges automatically after all required checks pass; it does not need a peer approval.
- Open People, then create or select a profile.
- Enter the person's name and confirm the URL-safe slug. The slug also names the managed photo directory.
- Choose Professor, Research Staff, PhD Candidate, PhD Co-supervisee, or Alumni.
- Upload or replace the profile photo and optionally add a title and personal or professional URL.
- Preview
/people, then save.
Each profile and its photo are stored under the same slug:
content/people/jane-doe.json
public/team/members/jane-doe/photo.jpg
Delete profiles and replace photos through Keystatic so the record and managed image stay in sync.
A People pull request currently merges automatically after all required checks pass, without a peer approval. Mixed blog-and-People changes follow the same check-only editorial policy.
| Complete pull-request diff | Merge requirement |
|---|---|
content/posts/**, public/uploads/** |
Required checks |
content/people/**, public/team/members/** |
Required checks |
| A mixture of the two editorial groups | Required checks |
| Any other path | Editorial auto-merge is disabled and need approval |
The classifier evaluates the complete diff, so adding a code or configuration file to an editorial branch cannot bypass technical review.
The local Keystatic editor is the preferred fallback when the hosted editor is unavailable: run pnpm dev, edit at http://localhost:3000/keystatic, preview the result, and commit the generated content and images to a new branch.
Blog posts can also be edited directly in content/posts/ using .github/BLOG_POST_TEMPLATE.md. Put their images in public/uploads/, preview locally, run the checks above, and open a pull request. People records should be changed through Keystatic because it keeps their JSON and managed photos aligned.
Keystatic needs server-side API routes and GitHub OAuth, so production must use the full-stack Worker build rather than a static export. Configure these GitHub Actions variables and secrets:
NEXT_PUBLIC_KEYSTATIC_STORAGE_KIND=github
KEYSTATIC_GITHUB_CLIENT_ID=...
KEYSTATIC_GITHUB_CLIENT_SECRET=...
KEYSTATIC_SECRET=...
NEXT_PUBLIC_KEYSTATIC_GITHUB_APP_SLUG=...Use .env.example as the local checklist. Give the GitHub App access only to PreferredAI/PreferredAI.github.io; browser editors must have repository write access.
For initial GitHub App setup, run:
NEXT_PUBLIC_KEYSTATIC_STORAGE_KIND=github pnpm devThen open http://127.0.0.1:3000/keystatic/setup. The storage-kind value is public configuration; the client secret and KEYSTATIC_SECRET must remain secret.