AIYA CMS - Headless Core self-updates
Headless-first administration and content framework for AIYA CMS.
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/yeraph-plus/aiya-cms-core/archive/refs/heads/main.zipShips its own WordPress updater (Plugin Update Checker), so new versions show up under Dashboard → Updates.
Headless-first WordPress plugin for AIYA CMS (WP 6.4+, developed and
running against WP 7.1, runtime PHP 8.5, Requires PHP: 8.5): the
admin half of a decoupled site — settings, content domains, media
pipeline, community, notifications and a versioned REST contract
(aiya/core/v1) consumed by the Astro front end. No Gutenberg, no
React in the admin; native admin styles only.
Ecosystem
- this repository — WordPress backend: content domains, settings,
the versioned
aiya/core/v1contract aiya-cms-station— the Astro SSR front end that consumes the contract; its README is the authoritative deployment doc (nginx topology, environment variables, the siteurl-origin rule)aiya-cms-resource-publisher— local desktop publisher for resource posts; the companionaiya-publish/v1WP plugin ships from that same repo
Layout
src/— plugin code (runtime, Settings framework, Admin surfaces, Domain modules, Api contract/presenter/rest layers); autoloadAiya\Core\plusAiya\Infra\frompackages/packages/— WordPress-free infrastructure packages, eight of them:aiya/image-processor,aiya/slug-toolkit,aiya/typesetting,aiya/opencc-convert(unwired by decision),aiya/openlist,aiya/gofile-api,aiya/payment-epay,aiya/payment-afdian—packages/README.mdis the authoritative table. They are shipped in-tree and NOT composer-installed: the plugin autoloader reads each package's own composer.json for its PSR-4 prefix, and a package's third-party dependencies are declared in the root composer.jsonthemes/aiya-headless/— the companion shell theme; sync source for its runtime locationwp-content/themes/aiya-headless/(see its README)assets/— admin CSS/JS (Backbone + jQuery UI + code editor)languages/— zh_CN translations (.mobuilt, see the i18n skill)tests/Unit/— PHPUnit suite (WP shim bootstrap, no install needed)docs/— ROADMAP (milestone log), ARCHITECTURE (module wiring and conventions), MIGRATION (legacy disposition)
Development
composer install # deps + phpunit/phpcs/phpstan tooling
composer php:unit # PHPUnit (Unit suite)
composer php:cs # WordPressCodingStandard pass
composer php:stan # PHPStan (level 8, WP stubs)
i18n # see .agents/skills/aiya-i18n-zh-cn (host python)
After activation the plugin runs its five clean-install migrations
(0.80.0 pure CREATE TABLE statements — no upgrade steps, no data
conversions), defaults permalinks to /%postname%/ when empty and
schedules its crons. See docs/ARCHITECTURE.md for module wiring and
docs/ROADMAP.md for the milestone-by-milestone iteration log.
Releases
Releases are cut by pushing a version tag — git tag v0.95.0 && git push origin v0.95.0. The tag must equal the plugin header Version:
and the AIYA_CORE_VERSION constant; the workflow's gate step refuses
to build otherwise. GitHub Actions (.github/workflows/release.yml)
then:
- installs production dependencies (
composer install --no-dev— the shippedvendor/carries only imagine + pinyin, not the dev toolchain) and compiles the zh_CN.mowithmsgfmt(*.mois gitignored, so the release build is the only place it exists); - stages a slim copy (no
.git,.github,tests/,docs/, dev configs or root composer files — the packages' owncomposer.jsonmanifests stay, the lazy autoloader reads them) and zips it with the top-levelaiya-core/folder; - publishes a GitHub release with the zip, a commit-log changelog since the previous tag and GitHub's generated notes.
workflow_dispatch runs the same build without publishing and uploads
the zip as a workflow artifact for inspection.
Online updates
The plugin never ships through wordpress.org, so it checks its own
releases: the bundled Plugin Update Checker library
(yahnis-elsts/plugin-update-checker, composer-installed) watches this
repository's GitHub Releases and surfaces updates in the normal Plugins
screen. The update source is built in (yeraph-plus/aiya-cms-core);
a site may repoint it per environment — define('AIYA_CORE_UPDATE_REPO', 'owner/repo'); in wp-config.php, or filter aiya_core_update_repo —
an empty value turns the checker off. The Update URI: false plugin header keeps wordpress.org out of the picture, and each
release carries exactly one zip asset: the checker requires that zip and
hands it to the updater — a release cut without it offers no update at
all, never GitHub's source archive for the tag (which carries no
composer vendor/ tree and no compiled .mo files). The aiya-headless
shell theme is deliberately not distributed in releases — it is a
placeholder, mirrored from themes/ by hand.
Deployment
The site is two hosts: this WordPress backend (content, media, users,
versioned REST) and the Astro SSR application (front-station/) that
renders the public site. The wp-content/themes/aiya-headless/ shell
theme only catches direct hits on the WP host.
1. Backend (WordPress + aiya-core)
docker compose up -d # wordpress:php8.5-apache + mariadb + phpmyadmin + wpcli
docker compose run --rm wpcli plugin activate aiya-core
wp-content/is bind-mounted; deploy by syncing the plugin folder (andthemes/aiya-headless/) into it — no image rebuild needed.- Schema: five
0.80.0CREATE migrations run once through the schema version runner; a failed migration holds the stored version back and retries on the next request. There are no upgrade paths to maintain. - Production checklist:
WP_DEBUG/WP_DEBUG_LOGoff (dev-only);- behind a CDN/reverse proxy, bridge the real client IP through the
aiya_core_client_ipfilter (rate limiting and guest dedup key on it —REMOTE_ADDRwould collapse everyone onto the proxy IP); - narrow the web server's forwarded-header trust to the real front
proxy only. The official
wordpress:*-apacheimage ships mod_remoteip trusting every RFC1918 range, so any client that can reach WP directly rewritesREMOTE_ADDRwith a bareX-Forwarded-Forheader — noAIYA_PROXY_SECRETneeded — and forges a fresh identity per request. Override/etc/apache2/conf-available/remoteip.conf(bind-mount a replacement) withRemoteIPHeader X-Forwarded-For+RemoteIPInternalProxy <real proxy segment>only; the secret bridge is evaluated in PHP after Apache has already decided whatREMOTE_ADDRis, so a wide server-level trust defeats it. In front of nginx keep$proxy_add_x_forwarded_for(appending) — spoofed entries always end up left of the real IP and mod_remoteip's rightmost resolution ignores them; - leave
WP_DEBUGundefined (webhook logs stay off); /wp/v2is gated to logged-in editors automatically; first-party namespaces self-announce viaaiya_core_firstparty_rest_namespaces.
2. Frontend (aiya-cms-station)
The Astro SSR application (front-station/ in the dev workspace).
Its README is the authoritative deployment doc; the short version:
cd front-station
npm install
cp .env.example .env # server-side only: AIYA_SITE_URL / AIYA_WP_API_URL /
# AIYA_PROXY_SECRET (+ optional timeout / visitor-IP
# header / session cookie domain)
npm run verify # astro check + vitest + prettier + build
npm start # node dist/server/entry.mjs (env-file aware)
- The
.envis server-side only and never reaches the browser bundle; production takes real environment variables. - When the backend is unreachable the site serves a 503 gate page with
Retry-Afterand auto-recovers — no crash loops.
3. Contract sync loop
The frontend's zod schemas are enforced against a backend-generated snapshot:
docker compose run --rm wpcli aiya contracts snapshot # from D:\WordPress_Dev
# → copy the output to front-station/src/lib/core/contracts.snapshot.json
cd front-station && npm test # shape drift turns the suite red
The v1 lock (contracts.snapshot.v1.json, additive-only) is amended
only by owner decision, recorded in the ROADMAP.
4. Shell theme sync
Edit either copy of aiya-headless (repo themes/ or runtime
wp-content/themes/) and mirror to the other — the three runtime files
(style.css / functions.php / index.php) must stay byte-identical
(the README exists only in the repo copy).