Astroload is an Astro 7 + Payload CMS starter template, built as a pnpm workspace on MongoDB. It includes live preview, single- or multi-locale routing, SEO output, S3 storage, forms with basic spam checks, build-time redirects, deploy webhooks, and a typed CMS data layer with LRU caching.
Note
This template is in active development and in production use. The API, content model, and project structure may still change between releases. Pin a tag or commit if you build on top of it. Changes per release are tracked in astroload/CHANGELOG.md.
- Page builder with rich text, image, form, and dynamic posts/authors list blocks
- Drafts and autosave on Pages, Posts, and Authors
- Role-based access (
admin,editor) plus separate API keys for read-only and preview reads. The Astro side uses these scoped keys, soweb/.envholds no admin-capable Payload token - Editor-managed Header, Footer, Labels, and SiteSettings globals
- Seed script for an admin user, API keys, and demo content
- Astro 7 with prerendered pages in production. Dev renders on demand so CMS edits show up without restarts
- Tailwind v4 via
@tailwindcss/vite, no PostCSS layer - View transitions via
<ClientRouter /> - Lexical rich text with custom block and upload renderers
- Typed data layer backed by an LRU cache, bypassed in dev and for preview reads
- Autosave-driven preview on Pages, Posts, and Authors
- Mobile, tablet, and desktop breakpoints preconfigured in the admin
- Editor toolbar overlay on the standalone preview tab, hidden inside the Payload iframe
- Preview route guarded by a shared secret with
crypto.timingSafeEqual, served withCache-Control: no-store,X-Robots-Tag: noindex, nofollow, andReferrer-Policy: no-referrer
- Configurable locale set (
enanddeby default). With more than one locale, URLs carry a/{locale}segment and content pages gethreflangplusx-defaultalternates (error and root pages carry none) - With a single locale the prefix is omitted, so URLs read
/aboutrather than/en/about, and the switcher and alternates are off. A single-locale project that may add languages later can setFORCE_URL_PREFIXincms/src/site-config.tsto keep the/{locale}prefix. URLs then stay the same when a locale is added and need no redirects - Editable SEO metadata (title, description, image) on Pages, Posts, and Authors, with fallbacks
- JSON-LD output:
WebSiteandOrganizationon home,Articleon posts,Personon authors - Sitemaps with
lastmod: a single sitemap for unprefixed single-locale builds, otherwise a sitemap index plus per-locale sitemaps with alternate-locale links robots.txtcontrolled by a SiteSettings toggle so staging stays out of search- Language switcher in the header when more than one locale is configured
- Form builder with text, email, number, textarea, select, checkbox, and message fields
- Client-side submission to the CMS endpoint (JavaScript required)
- Hidden honeypot field plus a minimum-submit-time check, both stripped server-side
- Build-time redirects fetched from a
Redirectscollection, no runtime hop - Deploy webhook (
DEPLOY_HOOK_URL) for any plain endpoint. Repeated edits are throttled to at most two webhook calls per five-minute window - Optional S3 storage, on when its env vars are set. Without it, uploads go to the local filesystem under
cms/media/ - Optional Resend email, on when its env vars are set. Without it, emails are logged to the console
- Optional Umami analytics, Cloud or self-hosted: cookieless, proxied through first-party routes so content blockers that match the Umami hostnames miss it
- Locale-aware custom 404 and 500 pages
- Astro 7 with the Node adapter (
@astrojs/node) - Payload 3 on Next 16 and React 19
- MongoDB 8 via
@payloadcms/db-mongodb, standalone (transactions disabled). Postgres remains available as a documented alternative, seeastroload/maintenance.md - Tailwind CSS v4 via
@tailwindcss/vite - TypeScript 5.7
- pnpm 10 workspaces
- Node
>=22.12(see.nvmrcfor the pinned version)
Prerequisites: Node >=22.12 (see .nvmrc), pnpm >=9, Docker. The package scripts assume a POSIX shell, so on Windows run them under WSL or Git Bash.
# 1. Get the code
git clone https://github.com/woerndl/astroload.git
cd astroload
# 2. Install dependencies
pnpm install
# 3. Start local MongoDB
docker compose up -d
# 4. Set up env files
cp cms/.env.example cms/.env
cp web/.env.example web/.env
# In cms/.env, set PAYLOAD_SECRET (openssl rand -base64 32)
# In both files, set the same PREVIEW_SECRET (openssl rand -hex 32)
# 5. Seed the database (creates admin user, API keys, demo content)
pnpm --filter @astroload/cms seed
# The seed prints PAYLOAD_READ_KEY and PAYLOAD_PREVIEW_KEY.
# Paste both into web/.env.The seed creates an admin user admin@example.com / admin1234 unless
PAYLOAD_ADMIN_EMAIL and PAYLOAD_ADMIN_PASSWORD are set in cms/.env.
Change the password before the instance is reachable.
Then run the two dev servers in separate terminals:
# Terminal 1: Payload admin at http://localhost:3000/admin
pnpm --filter @astroload/cms dev# Terminal 2: Astro site at http://localhost:4321
pnpm --filter @astroload/web devOnce users exist, re-seeding needs --force or SEED_FORCE=1, which clears the seeded collections and recreates users, API keys, and demo content.
Variables are declared in cms/.env.example and web/.env.example, which are the source of truth.
DATABASE_URIMongoDB connection string. The docker-compose service usesmongodb://127.0.0.1:27330/astroload.PAYLOAD_SECRETadmin session secret.SERVER_URLorigin the admin is served from. Must match the browser origin for CSRF checks.WEBSITE_URLAstro frontend origin used bygeneratePageURL.PREVIEW_SECRETshared secret for the/previewroute. Must matchweb/.env.DEPLOY_HOOK_URLoptional. Any plain webhook endpoint.RESEND_API_KEY,RESEND_FROM_ADDRESS,RESEND_FROM_NAMEoptional.S3_BUCKET,S3_ENDPOINT,S3_REGION,S3_ACCESS_KEY_ID,S3_SECRET_ACCESS_KEYoptional.
PAYLOAD_READ_KEY,PAYLOAD_PREVIEW_KEYminted by the seed.PREVIEW_SECRETmatches the CMS value.CMS_URL,WEBSITE_URLorigins.UMAMI_WEBSITE_IDoptional.CMS_URL,WEBSITE_URL, andUMAMI_WEBSITE_IDare public values baked in at build time, see Deployment.
flowchart LR
Editor -->|HTTPS| CMS["cms<br/>Payload + Next.js admin"]
CMS <-->|mongoose| Mongo[(MongoDB)]
CMS -->|REST| Web["web<br/>Astro + Node adapter"]
Visitor -->|HTTPS| Web
Two Node processes, one MongoDB database. cms/ owns the admin, the API, and the schema. web/ reads from the CMS over HTTP at build time (for prerendered pages and redirects) and at request time (for /preview and any opted-in SSR route). See astroload/architecture.md for the longer version.
.
βββ cms/ Payload application (admin and REST API)
βββ web/ Astro frontend (public site and /preview)
βββ docker-compose.yml Local MongoDB
βββ pnpm-workspace.yaml
Both apps are Node servers with no serverless adapter or provider-specific build step, so a plain Node host is enough. For containers, each app has a Dockerfile and deploy/docker-compose.production.yml is a scaffold to copy. See astroload/deployment.md.
pnpm --filter @astroload/cms buildthenpnpm --filter @astroload/cms start.- Needs
DATABASE_URI,PAYLOAD_SECRET,SERVER_URL,WEBSITE_URL,PREVIEW_SECRETat runtime. Boot aborts with one consolidated error listing any that are missing. - S3 and Resend turn on when their env vars are set.
- There is no bundled rate limiter. Rate limiting belongs at the edge or in a proxy in front of the CMS, see
astroload/security.md.
pnpm --filter @astroload/web buildthenpnpm --filter @astroload/web start. Thestartscript binds0.0.0.0:4321by default.HOSTandPORToverride that, and container hosts usually inject them.CMS_URL,WEBSITE_URL, andUMAMI_WEBSITE_IDareastro:env/clientpublic values, inlined into the output atastro build. The web app must be built with the production values. Injecting them only atstarthas no effect.- The Astro standalone server does not auto-load
.env, unlike the CMS'snext start. The host injects the runtime server vars (PAYLOAD_READ_KEY,PAYLOAD_PREVIEW_KEY,PREVIEW_SECRET, plusUMAMI_HOST_URLfor a self-hosted Umami). For local production testing, export them or runnode --env-file=web/.env web/server.mjsfrom the repo root. - Content pages, the sitemap index, and
robots.txtare prerendered. The per-locale sitemaps and the/previewroute are SSR, so the CMS must stay reachable for them at runtime. A client-side script POSTs form submissions as JSON to Payload's/api/form-submissionsendpoint, soweb/needs no submission route. Submission requires JavaScript. astro buildreads redirects from theRedirectscollection through the CMS REST API. The CMS must be reachable during build.- The web app compresses its own responses:
web/server.mjswraps the standalone handler and negotiates brotli or gzip per request. A proxy or CDN in front passes the already-encoded responses through.astroload/maintenance.mdshows how to verify compression end to end.
- Setting
DEPLOY_HOOK_URLin the CMS fires a POST when published content changes or is deleted, when Media, Forms, or Redirects change, or when any global changes. Works with Railway, Vercel, Coolify, or any plain webhook endpoint as-is. A burst of edits triggers one immediate call plus at most one trailing call per five-minute window while edits continue. Seecms/.env.examplefor URL shapes anddocs/astroload/maintenance.mdfor how long publishes take to go live and for usingCONTENT_BUILD_IDto avoid stale build caches.
Issues and pull requests are welcome. Run pnpm lint, pnpm check, and pnpm test before opening a PR.
jhb-software/payload-astro-website-templatefor the starting reference.@jhb.software/payload-pages-pluginfor the URL tree.@jhb.software/astro-payload-richtext-lexicalfor Lexical rendering.astro-seo-schemafor JSON-LD output.
MIT Β© Alexander WΓΆrndl
