DocsPress is an Automattic project that converts a docs/ folder of Markdown into native WordPress Pages built from Gutenberg blocks, and converts edits made in the WordPress editor back into pull requests. It runs as a GitHub Action, with an optional block theme and blocks plugin installed on the WordPress side.
The entire system turns on one question: which system is allowed to overwrite which page, and when. Everything below is organized around that boundary.
More information: DocsPress documentation
The four parts
DocsPress is not one installable thing. It ships as four separately adopted pieces, and you can use the first without the others.
| Part | Where it runs | What it does |
|---|---|---|
GitHub Action (Automattic/docspress) | GitHub Actions runner | Converts Markdown to Gutenberg blocks, writes Pages over the WordPress REST API, opens reverse-sync pull requests |
Token helper CLI (npx docspress token) | Your machine | Runs the WordPress.com OAuth flow and stores the result as a repository secret |
| DocsPress theme | WordPress | Block theme with docs navigation, command-palette search, table of contents, llms.txt and .md routes |
| DocsPress Blocks plugin | WordPress | 15 documentation blocks: code tabs, API request/response, file trees, terminal sessions, flows, callouts, prompts |
Both WordPress packages require WordPress 6.6+ and PHP 7.4+, and install through the normal theme and plugin upload screens. The Action and CLI are published to npm as docspress.
How a file becomes a Page
Without a manifest, the Action walks every .md and .markdown file under docs-dir and mirrors the directory tree onto the WordPress Page hierarchy.
docs/index.md -> /docs/
docs/getting-started.md -> /docs/getting-started/
docs/guides/index.md -> /docs/guides/
docs/guides/markdown-features.md -> /docs/guides/markdown-features/
Missing intermediate sections are created as managed placeholder Pages. The Page title comes from frontmatter title, then the first H1, then the filename — and when the first H1 is used, it is stripped from the body so the Page does not render the heading twice.
Markdown maps to core blocks rather than one HTML blob: paragraphs to core/paragraph, headings to core/heading, lists (including task lists) to core/list, GFM tables to core/table, fenced code to core/code, images to core/image, rules to core/separator, raw HTML to core/html. Serialized Gutenberg block comments in the Markdown are preserved as-is, which is the escape hatch for anything the converter does not map:
<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>Written as a raw Gutenberg block.</p></blockquote>
<!-- /wp:quote -->
If you need stable slugs, titles, or parent relationships that filenames cannot express, point manifest-file at a JSON file that declares them. Entries without a markdown_source become placeholder Pages. A separate redirects-file keeps old paths alive after renames — though on WordPress.com this creates a managed moved-page placeholder with a link, not a server-level 301.
The sentinel: what DocsPress may touch
Every Page DocsPress creates carries a hidden management sentinel comment. The Action only updates or deletes Pages that contain it. A hand-authored Page sitting at a route DocsPress wants is reported as a conflict and left untouched — zero writes to that path.
The sentinel is also the common ancestor. In reconcile mode it is what lets DocsPress tell a one-sided edit apart from a genuine two-sided conflict.
That single mechanism explains most of the product’s behavior. When only GitHub changed since the baseline, WordPress is updated. When only WordPress changed, a pull request is opened against the repository. When both changed, the run reports a conflict and writes to neither system. The documentation is explicit that you should never hand-inject a sentinel to bypass a conflict — the conflict is a content decision, not a configuration error.
Three modes, three blast radii
| Mode | Writes to WordPress | Writes to GitHub |
|---|---|---|
publish (default) | Yes — Markdown changes become Pages | No |
propose | No — only prepares changes and refreshes sync metadata | Yes — opens the reverse-sync pull request |
reconcile | Yes, for non-conflicting GitHub changes | Yes, for WordPress-only edits |
Reverse sync is not a wholesale regeneration. DocsPress compares the live Gutenberg tree against the tree generated from current Markdown and rewrites only the source regions whose blocks actually changed. Unchanged frontmatter, code-fence languages, tables, and block envelopes stay byte-for-byte identical. If the blocks cannot be mapped back safely, the run fails rather than rewriting the file.
Rolling it out in order
The documented sequence is deliberately slow, and worth following. Each stage widens what the Action is permitted to do.
- Manual dry run.
workflow_dispatch,status: draft,delete-mode: trash,dry-run: true. Nothing is written; the Actions summary shows every planned create, update, and delete. - Manual draft write. Change only
dry-runtofalse. Inspect the Page hierarchy, headings, block rendering, rewritten internal links, and any unmanaged conflicts. - Path-scoped automation. Add a push trigger limited to
docs/**and the workflow file itself, so unrelated commits do not fire a sync. - Reconcile. Add the five-minute schedule and pull-request permissions once one-way publishing is stable.
on:
push:
branches: [main]
paths:
- "docs/**/*.md"
- "docs/**/*.markdown"
- "docs/**/*.json"
- ".github/workflows/sync-docs.yml"
workflow_dispatch:
permissions:
contents: read
Two details in stage 4 are easy to miss. First, the repository setting Settings → Actions → General → Workflow permissions → Allow GitHub Actions to create and approve pull requests must be enabled before the first reverse-sync run. Second, the workflow needs a job condition that skips the push event created when the action-owned docspress/wordpress-sync branch merges — otherwise a merged WordPress proposal is republished straight back to WordPress. The Action has a fallback that recognizes its own merge commit and exits, but the explicit condition is the documented approach.
Pin the SHA
Use full commit SHAs for both actions/checkout and Automattic/docspress. This Action holds a token that can edit and trash Pages on a live site, and dist/index.js is committed to the repository so the runner can execute it without an install step. When you adopt a newer revision, review action.yml and the bundled dist/ diff, then return to a manual dry run before restoring automation.
Authentication
WordPress.com API writes need an OAuth bearer token with the global scope. Create an app at developer.wordpress.com/apps, set the redirect URL to http://localhost:8787/callback, then run the helper — it opens the browser, waits for the local callback, exchanges the code, and can write the secret directly with the GitHub CLI.
npx docspress token \
--client-id YOUR_CLIENT_ID \
--client-secret YOUR_CLIENT_SECRET \
--site example.wordpress.com \
--repo OWNER/REPO \
--set-secret
The secret name is WP_ACCESS_TOKEN. Self-hosted sites are supported through the wordpress-url input, but the site must already have a trusted Bearer-token mechanism — DocsPress does not provision one, and WordPress.com remains the best-tested path.
Two surfaces from one source
This is the part that distinguishes DocsPress from a generic Markdown importer. During sync, the Action stores the original Markdown in the managed Page’s metadata alongside its source path. The theme then serves that stored source through two machine-readable routes.
An llms.txt index at the site root lists the site title, description, and absolute links to every published, source-backed Page. Any Page route with its trailing slash replaced by .md returns the exact UTF-8 source — frontmatter, code fences, tables, supported block comments — with the text/markdown; charset=utf-8 content type and no theme shell.
curl https://docs.example.com/llms.txt
curl --head https://docs.example.com/docs/publish-existing-docs.md
curl https://docs.example.com/docs/publish-existing-docs.md
Expect text/plain; charset=utf-8 for the index and text/markdown; charset=utf-8 for each Page source. Because the response is the reviewed source rather than Markdown reconstructed from rendered HTML, identifiers and examples stay stable for retrieval and citation.
Only published Pages with real Markdown source metadata appear in the index. Placeholder Pages and hand-authored WordPress Pages are omitted, and their .md routes return 404. Note the boundary the docs draw themselves: these are an additional public representation, not an access-control layer and not a statement about third-party model training. Existing sites need one run after installing a theme version that supports the endpoints, to backfill the metadata on Pages whose visible content has not changed.
Where it breaks
| Symptom | Cause and fix |
|---|---|
401 or a required-scope error | Token missing, revoked, or issued without global scope. Verify the secret name with gh secret list — never print the token. |
| Run stops on an unmanaged Page conflict | A manual Page occupies the target route. Change the docs route or root-slug, move the manual Page, or migrate its content deliberately. |
| Children skipped after a parent fails | Pages are processed parent-first. A conflicting or missing parent leaves children without a valid parent ID — fix the parent conflict first. |
| Unexpected deletions planned | Wrong docs-dir checked out, root-slug mismatch, accidental renames, or a missing manifest. Keep dry-run: true; trash is recoverable, force is not. |
| Duplicate H1 on every Page | The theme already prints the Page title as H1. Set create-h1: false and start Markdown bodies at ##. |
| Two files map to one Page | Both index.md and README.md in one folder, or filenames that collide after slug normalization. |
| Custom blocks render as nothing | The DocsPress Blocks plugin is missing or inactive on the target site. |
Whether it fits
The project’s own comparison is against Docusaurus, and it is a fair one to run. The deciding question is not which tool has more features — it is whether you already operate a WordPress site that the documentation should live inside.
DocsPress removes a second public website, hosting target, deployment pipeline, analytics surface, and design system. Editorial review happens through WordPress drafts, roles, and previews rather than deploy previews — useful when publication has a compliance step after code review. Design changes go through the Site Editor and Global Styles, with no React build or theme-options screen.
Docusaurus is the better answer when the documentation must be a standalone React application, when you want extensive MDX component composition, when you need Algolia DocSearch, or when you need mature internationalization. Versioned documentation is a real gap: Docusaurus ships a versioning CLI and versioned directories, while the DocsPress Action does not currently provide built-in versioning, even though the theme documents version-aware URL patterns.
One more input to the decision: DocsPress is early software. The repository describes the core sync loop as working with WordPress.com as the first-class, best-tested target. The Action and CLI are GPL-3.0-or-later; the theme and blocks plugin are GPL-2.0-or-later. Treat it as a promising foundation you are prepared to debug, not a settled dependency.
Questions
No. The Action writes ordinary WordPress Pages made of core blocks, so any theme will render them. You lose the docs navigation, command-palette search, article tools, and the llms.txt and .md endpoints, which are theme features rather than Action features.
The managed Page is scheduled for Trash, or permanently deleted when delete-mode: force. Decide on that policy before enabling the push trigger, because automatic sync includes removals.
Yes, in reconcile mode. Gutenberg edits to managed Pages are collected into one rolling pull request on the docspress/wordpress-sync branch. While that pull request is open, DocsPress leaves the WordPress-only Page alone; other Pages continue publishing from GitHub normally.
The project publishes WordPress Playground blueprints that spin up a temporary site with the theme, the blocks plugin, and the full generated Page tree. You can also run the complete experience locally from a repository clone with the Playground CLI.
Starting
If the repository already has a usable docs/ tree, the shortest path is a manual dry run against a draft Page tree — no agent skills, no theme, no plugin required — and a look at the Actions summary before anything is written. Add the theme and blocks once the hierarchy is what you expected.