A fully open-source, WordPress-compatible CMS built on Astro — admin and public frontend in a single deployment.
No PHP. No legacy baggage. TypeScript, Astro 4 SSR, Drizzle ORM, and your choice of hosting.
| Platform | One-click |
|---|---|
| Cloudflare Pages | |
| Railway | |
| Render | |
| Docker | docker compose up — see Docker below |
AstroPress is a modern CMS that speaks WordPress — same wp_* database schema, same mental model — but runs on the edge with zero PHP. Developers get the extensibility of WordPress; users get a fast, cheap, globally-distributed site.
Key features:
- WordPress-compatible
wp_*schema (Drizzle ORM + SQLite/D1) - Visual block-based page and theme editor (full-screen ThemeEditor)
- Gutenberg block editor for posts and classic content
- Custom post types, taxonomies, custom fields (ACF-style) — all managed via UI
- WPForms-style form builder with entries, conditional logic, multi-page
- Navigation menus with drag-and-drop reorder and submenu nesting
- Plugin system — drop a package in
/plugins, register inapps/admin/src/plugins.ts - Single installation — admin (
/admin/*) and public frontend (/*) in one app - Cloudflare-native: D1 database + R2 object storage + Pages hosting
- Session-based auth (Lucia v3)
AstroPress runs as a single Astro SSR app that serves both the admin dashboard and the public-facing website:
/ → public homepage (blog list or static front page)
/blog/[slug] → blog post
/[slug] → page (supports visual block editor)
/forms/[id] → standalone form page
/admin/* → CMS dashboard (auth required)
/api/* → REST API (most endpoints require auth)
Everything shares one database. No separate frontend deployment needed.
- Node.js 20+
- pnpm 9+
git clone https://github.com/awsmin/AstroPress
cd astropress
pnpm installpnpm db:setup # runs migrations against local.db
pnpm db:seed # optional: seeds demo contentpnpm dev| URL | Description |
|---|---|
| http://localhost:4321 | Admin + public site |
| http://localhost:4321/admin | CMS dashboard |
Visit http://localhost:4321 — the setup wizard runs on first boot to create your admin account.
No CLI or terminal needed. After clicking the button, follow the steps below.
- Click the deploy button above
- Authorize GitHub and Cloudflare when prompted
- Cloudflare forks the repo and creates a Pages project
In the Pages project → Settings → Build & Deployments:
| Setting | Value |
|---|---|
| Root directory | apps/admin |
| Build command | ASTRO_ADAPTER=cloudflare pnpm build |
| Build output directory | dist |
| Deploy command | (leave empty) |
Go to Settings → Functions and add the following bindings. Create each resource in Cloudflare first if it doesn't exist yet.
D1 Database (required — stores all CMS content)
| Field | Value |
|---|---|
| Variable name | DB |
| D1 database | Create a new database named astropress |
R2 Bucket (required for media uploads)
| Field | Value |
|---|---|
| Variable name | R2 |
| R2 bucket | Create a new bucket named astropress-media |
Workers AI (optional — enables built-in AI with no external API key)
| Field | Value |
|---|---|
| Variable name | AI |
| Binding type | AI |
If you skip the AI binding, you can still use any external provider (Anthropic, OpenAI, Gemini, Mistral, Groq) by adding an API key in Admin → Settings → AI.
Save settings and trigger a new deployment. On first visit, AstroPress automatically creates all database tables and redirects you to the setup wizard to create your admin account.
Once the AI binding is added:
- Go to Admin → Settings → AI
- Select Cloudflare Workers AI (no API key needed)
- Choose a model:
- Llama 3.1 8B — fast, great for most tasks
- Llama 3.3 70B — smarter, slower
- Mistral 7B — good alternative
- Click Save Settings
The AI assistant, block generator, and chat widget will all use your Workers AI binding at no extra per-token cost beyond your Cloudflare plan.
cd apps/admin
npx wrangler d1 create astropress
npx wrangler r2 bucket create astropress-media
npx wrangler pages project create astropress
# Build and deploy
ASTRO_ADAPTER=cloudflare pnpm build
npx wrangler pages deploy distAdd D1, R2, and AI bindings in the Cloudflare dashboard after the project is created (CLI does not set bindings for Pages projects).
Or manually:
- Push the repo to GitHub
- Create a new Railway project → Deploy from GitHub repo
- Set environment variables in the Railway dashboard:
DATABASE_URL— e.g.file:./data/astropress.db(Railway persistent volume) or a PostgreSQL URLAUTH_SECRET— a random 32+ character string
Railway auto-detects railway.toml and builds with Docker.
Render reads render.yaml automatically. Set DATABASE_URL in the Render dashboard after the first deploy. A 1 GB persistent disk is attached at /app/data for the SQLite database.
cp .env.example .env # edit AUTH_SECRET
docker compose upOpen http://localhost:4321 — database tables are created automatically on first boot, then the setup wizard runs to create your admin account.
docker build -t astropress .
docker run -p 4321:4321 \
-e DATABASE_URL=file:./data/astropress.db \
-e AUTH_SECRET=your-secret-here \
-v $(pwd)/data:/app/data \
astropress| Variable | Description |
|---|---|
DATABASE_URL |
SQLite: file:./data/astropress.db · PostgreSQL: postgres://... |
AUTH_SECRET |
32+ char string for session signing |
On Cloudflare Pages, the D1 database is bound automatically via wrangler.toml — no DATABASE_URL needed.
astropress/
├── apps/
│ └── admin/ # Single Astro SSR app (admin + public frontend)
│ └── src/
│ ├── components/ # BlockRenderer.astro
│ ├── islands/ # React islands (BlockEditor, FormBuilder, ThemeEditor …)
│ ├── layouts/ # AdminLayout.astro, BaseLayout.astro
│ ├── lib/ # icons.ts, posts.ts, public-query.ts, formRenderer.ts …
│ ├── middleware.ts
│ ├── pages/
│ │ ├── index.astro # public homepage
│ │ ├── [slug].astro # public pages
│ │ ├── blog/[slug].astro # blog posts
│ │ ├── forms/[id].astro # standalone form page
│ │ ├── admin/ # CMS dashboard pages
│ │ └── api/ # REST endpoints
│ └── plugins.ts
├── packages/
│ ├── core/ # Drizzle schema, registry, query helpers, types
│ ├── auth/ # Lucia v3 session auth
│ ├── api/ # Hono router foundation
│ └── ui/ # Shared React components
├── plugins/
│ └── seo/ # First-party SEO plugin
├── themes/
│ └── default/ # Default front-end theme styles
├── Dockerfile
├── docker-compose.yml
├── railway.toml
├── render.yaml
└── wrangler.toml
Import from @astropress/core/query in any Astro page:
---
import { queryPosts, getField, getPostTerms, getSiteInfo } from "@astropress/core/query";
const db = Astro.locals.db;
// Like WP_Query
const { posts, total, pages } = await queryPosts(db, {
type: "book",
perPage: 12,
orderBy: "title",
order: "asc",
});
// Like ACF get_field / get_post_meta
const price = await getField(db, post.id, "price");
// Like get_the_terms
const categories = await getPostTerms(db, post.id, "category");
// Like get_bloginfo
const site = await getSiteInfo(db);
---| Function | WP equivalent |
|---|---|
queryPosts(db, args) |
WP_Query |
getPost(db, idOrSlug, type?) |
get_post() |
getPostById(db, id) |
get_post() |
getPostBySlug(db, slug, type?) |
get_page_by_path() |
getField(db, postId, key) |
ACF get_field() |
getFields(db, postId) |
all meta as Record<string,string> |
getTerms(db, taxonomy, args?) |
get_terms() |
getPostTerms(db, postId, taxonomy) |
get_the_terms() |
getOption(db, name, fallback?) |
get_option() |
getSiteInfo(db) |
get_bloginfo() |
- Create a package in
/plugins/my-plugin/ - Export a plugin config:
// plugins/my-plugin/src/index.ts
import { definePlugin, registerPostType } from "@astropress/core";
export default definePlugin({
name: "My Plugin",
version: "1.0.0",
register() {
registerPostType("product", {
label: "Product",
pluralLabel: "Products",
icon: "bag",
public: true,
showInMenu: true,
supports: ["title", "editor", "thumbnail", "custom-fields"],
});
},
});- Load it in
apps/admin/src/plugins.ts:
import myPlugin from "@astropress/my-plugin";
loadPlugin(myPlugin);See CONTRIBUTING.md and docs/.
MIT — see LICENSE.