Skip to content

awsmin/AstroPress

Repository files navigation

AstroPress

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.


Deploy

Platform One-click
Cloudflare Pages Deploy to Cloudflare
Railway Deploy on Railway
Render Deploy to Render
Docker docker compose up — see Docker below

What it is

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 in apps/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)

Architecture

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.


Quick Start

Prerequisites

  • Node.js 20+
  • pnpm 9+

1. Install dependencies

git clone https://github.com/awsmin/AstroPress
cd astropress
pnpm install

2. Set up the local database

pnpm db:setup        # runs migrations against local.db
pnpm db:seed         # optional: seeds demo content

3. Start dev server

pnpm 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.


Deploy to Cloudflare Pages

One-click deploy

Deploy to Cloudflare

No CLI or terminal needed. After clicking the button, follow the steps below.


Step-by-step setup

1. Connect your repo

  1. Click the deploy button above
  2. Authorize GitHub and Cloudflare when prompted
  3. Cloudflare forks the repo and creates a Pages project

2. Configure build settings

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)

3. Add bindings

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.

4. Deploy

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.


Enable Cloudflare Workers AI in the admin

Once the AI binding is added:

  1. Go to Admin → Settings → AI
  2. Select Cloudflare Workers AI (no API key needed)
  3. Choose a model:
    • Llama 3.1 8B — fast, great for most tasks
    • Llama 3.3 70B — smarter, slower
    • Mistral 7B — good alternative
  4. 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.


Manual CLI deploy

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 dist

Add D1, R2, and AI bindings in the Cloudflare dashboard after the project is created (CLI does not set bindings for Pages projects).


Deploy to Railway

Deploy on Railway

Or manually:

  1. Push the repo to GitHub
  2. Create a new Railway project → Deploy from GitHub repo
  3. Set environment variables in the Railway dashboard:
    • DATABASE_URL — e.g. file:./data/astropress.db (Railway persistent volume) or a PostgreSQL URL
    • AUTH_SECRET — a random 32+ character string

Railway auto-detects railway.toml and builds with Docker.


Deploy to Render

Deploy to Render

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.


Docker

Quick start

cp .env.example .env     # edit AUTH_SECRET
docker compose up

Open http://localhost:4321 — database tables are created automatically on first boot, then the setup wizard runs to create your admin account.

Build image manually

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

Environment Variables

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.


Monorepo Structure

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

Data Layer — Query Helpers

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()

Plugin System

  1. Create a package in /plugins/my-plugin/
  2. 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"],
    });
  },
});
  1. Load it in apps/admin/src/plugins.ts:
import myPlugin from "@astropress/my-plugin";
loadPlugin(myPlugin);

Contributing

See CONTRIBUTING.md and docs/.

License

MIT — see LICENSE.

About

AstroPress

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors