Markdown in GitHub. WordPress as the publishing surface.
Sync Markdown documentation into WordPress Pages as Gutenberg-compatible block content.
- Put the Markdown documentation in
docs/. - Create a WordPress access token.
- Add one of the GitHub Actions workflows below.
- Run it first with
status: draftanddry-run: true.
The complete product and setup documentation lives at docs.press/docs.
You do not have to install the DocsPress theme. An unversioned DocsPress sync works with the site's existing theme and native WordPress blocks.
Linchpin sites install both over Composer from packagist.linchpin.com:
composer require linchpin/docspress linchpin/docspress-blocksOr download them directly:
Install the theme for the complete documentation layout, together with the Blocks plugin: the plugin registers the documentation shell the theme's templates compose — navigation, search, breadcrumbs, table of contents, edit links, and Page feedback — and serves /llms.txt and each Page's .md twin. Install the Blocks plugin on its own for rich DocsPress blocks; it is required when API versioning is enabled.
Install the Blocks plugin from this fork, not from upstream. The sync emits blocks upstream does not register — docspress/sentinel and docspress/symbol, and source-provenance attributes on docspress/colorful-code and docspress/code-tabs. Dynamic blocks serialize self-closing, so an unregistered one renders nothing on the front end: the content is simply absent from the page, with no error anywhere. In the editor an unregistered block is worse — it shows the "Your site doesn't include support for this block" card. docspress/sentinel sits at the top of every synced Page, so on a site without this plugin set sentinel-format to comment and the sync writes the legacy HTML comment instead.
Run this from the repository that contains your docs/ directory:
npx @wp-playground/cli@3.1.46 start \
--blueprint=https://raw.githubusercontent.com/Automattic/docspress/main/theme/blueprint-local-docs.json \
--mount="$PWD/docs:/wordpress/docspress-source-docs" \
--no-auto-mount \
--resetWordPress Playground imports the Markdown as editable Pages and opens /docs/.
- Default documentation site
- Contextual sidebars
- Versioned API documentation
- Stock WordPress, no DocsPress theme or plugin
The first two examples include the complete DocsPress presentation layer. The stock WordPress example installs neither optional package and shows repository Markdown as editable native Gutenberg blocks.
Current release: 1.3.0
This fork is its own product line. Upstream versions the theme and plugin in a 0.x series; ours starts at 1.0.0, so an upstream release can never sort above what we publish. Before that break both claimed 0.10.7 and nothing — not Composer, not the WordPress admin, not SatisPress — could tell them apart.
All three artifacts here share one version: the Action, the theme, and the blocks plugin. They have to move together. The Action emits blocks only our plugin registers, so a sync from a newer Action against an older plugin drops that content silently.
Releases are cut by release-please from Conventional Commits on main. Nobody edits a version by hand:
commitlintenforces the message format on commit (feat(LINCHPIN-1234): Subject, orNO-TASK).- release-please opens a release pull request as commits land.
- Merging it tags the release, bumps every version header, and writes
CHANGELOG.md. - The same run then moves the floating
v1, attaches both zips to the release, and installs the theme and plugin on packagist.linchpin.com.
Reference this action as linchpin/docspress@v1.
v1 moves forward as fixes and backwards-compatible inputs land on main. A change that removes or repurposes an input, or alters what a run writes to WordPress, gets v2 instead, so existing workflows keep working until they opt in.
Do not pin a commit SHA. SHA pinning defends against a third party repointing a tag under you; this is a Linchpin fork in the Linchpin organisation, behind branch protection, consumed only by Linchpin repositories — the same trust boundary, so the pin buys nothing and costs a great deal. Seven repositories were pinned to one SHA for a month and none of them received the fix that reports what WordPress actually returned, which made a real sync failure undiagnosable.
Genuinely third-party actions in these workflows, such as actions/checkout, should still be pinned.
upstream is Automattic/docspress. Nothing merges from it automatically — this fork carries changes upstream does not have, and at the time of writing upstream has merged no external pull request. Fetch and review deliberately:
git fetch upstream
git log --oneline main..upstream/mainChanges here that are not Linchpin-specific are worth proposing upstream; a merged one is a change this fork stops carrying.
Create .github/workflows/sync-docs.yml:
name: Publish documentation
on:
push:
branches: [main]
paths:
- "docs/**"
- ".github/workflows/sync-docs.yml"
workflow_dispatch:
permissions:
contents: read
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Automattic/docspress@main
with:
mode: publish
wordpress-site: example.wordpress.com
wordpress-access-token: ${{ secrets.WP_ACCESS_TOKEN }}
docs-dir: docs
root-slug: docs
status: draft
delete-mode: trash
dry-run: trueReview the Actions summary, then set dry-run: false. Publish the Pages only after the draft site looks right.
After one-way publishing is working, use a single reconcile workflow for edits from either side:
name: Reconcile documentation
on:
push:
branches: [main]
paths:
- "docs/**"
- ".github/workflows/sync-docs.yml"
schedule:
- cron: "3/5 * * * *"
workflow_dispatch:
permissions:
contents: write
pull-requests: write
concurrency:
group: docspress-sync
cancel-in-progress: false
jobs:
sync:
if: >-
github.event_name != 'push' ||
!contains(
github.event.head_commit.message,
format('from {0}/docspress/wordpress-sync', github.repository_owner)
)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Automattic/docspress@main
with:
mode: reconcile
wordpress-site: example.wordpress.com
wordpress-access-token: ${{ secrets.WP_ACCESS_TOKEN }}
docs-dir: docs
root-slug: docs
status: publish
delete-mode: trashWordPress-only edits become a rolling Markdown pull request. A true two-sided edit is reported as a conflict instead of overwriting either version.
DocsPress uses a WordPress.com OAuth token stored as the repository secret WP_ACCESS_TOKEN.
- Create an app at WordPress.com Apps.
- Set the redirect URL to
http://localhost:8787/callback. - Authenticate the GitHub CLI with
gh auth login. - Run the token helper from a trusted local terminal:
printf "WordPress.com client secret: "
IFS= read -r -s DOCSPRESS_CLIENT_SECRET
printf "\n"
npx docspress@0.4.1 token \
--client-id YOUR_CLIENT_ID \
--client-secret "$DOCSPRESS_CLIENT_SECRET" \
--site example.wordpress.com \
--repo OWNER/REPO \
--set-secret
unset DOCSPRESS_CLIENT_SECRETThe helper opens WordPress.com for authorization and stores the resulting token without printing it. Confirm only the secret name:
gh secret list --repo OWNER/REPODocsPress can use a REST API key from your host or authentication plugin when it accepts Authorization: Bearer …. Core WordPress does not create this key itself.
Store the key without printing it:
printf "WordPress REST API key: "
IFS= read -r -s WORDPRESS_REST_API_KEY
printf "\n"
printf "%s" "$WORDPRESS_REST_API_KEY" |
gh secret set WP_ACCESS_TOKEN --repo OWNER/REPO
unset WORDPRESS_REST_API_KEYSet wordpress-url to the site origin without /wp-json:
wordpress-url: https://docs.example.com
wordpress-site: docs.example.com
wordpress-access-token: ${{ secrets.WP_ACCESS_TOKEN }}DocsPress calls https://docs.example.com/wp-json/wp/v2/pages. Confirm that the key can read, create, update, and delete Pages before publishing.
For the complete setup, use Authenticate WordPress.
Continue at docs.press/docs for:
