ChangelogChangelog

Changelog

Release history for every TAW piece: taw/core and its taw command, TAW Theme, TAW Gutenberg, taw-fleet and the TAW Companion.

2026-10-27taw/core v1.100.0: a pull then a publish changes nothing. taw-fleet v1.34.1, taw-theme v1.12.80 and taw-gutenberg v0.3.81.
taw/coretaw-fleettaw-themetaw-gutenbergfix

October 2026: a pull then a publish changes nothing

  • taw/core v1.100.0: content snapshots name a post's featured image by its path in uploads (2026/06/hero.webp), not just its file name (schema 1.9). With two pictures of one name in different months, a pull could give a post the other one, and the publish review then listed the same picture as a change. An older snapshot's name still works when only one file has it; when several do, the post keeps its featured image and the import says so. Setting a featured image also cleans up the duplicate rows an old migration can leave.
  • taw/core v1.100.0: links to the site keep their http or https. An http:// link to the site's own domain used to come back as https://, so a publish listed it as changed.
  • taw/core v1.100.0: when the site you import into doesn't have a post's author, a post already there keeps its author. It used to go to whoever ran the import, which a publish then offered to write to production. New posts still get the importing user.
  • taw-fleet v1.34.1: the publish review ties a featured image to its own file, and taw-fleet refuses to publish featured images to a production taw/core older than 1.100.0, which looks them up by name. Update production with the same release (u, then merge and deploy).
  • taw-theme v1.12.80 and taw-gutenberg v0.3.81 lock taw/core 1.100.0.
2026-10-27taw/core v1.99.2: no more random critical errors on sites where a plugin bundles Composer. taw-theme v1.12.79 and taw-gutenberg v0.3.80 lock it.
taw/coretaw-themetaw-gutenbergfix

October 2026: no random critical errors with Wordfence

  • taw/core v1.99.2: on a site running Wordfence (or another plugin that bundles its own Composer), about one page load in 150 ended on WordPress's "There has been a critical error" page, fine on reload. taw/core asked Composer for its own version, and on those requests the plugin's copy of Composer answered that taw/core isn't installed. taw/core now reads its version from its own vendor/ folder and never fails over it. taw --version, taw sync, taw inspect and content snapshots also report the right version on such sites.
  • taw-theme v1.12.79 and taw-gutenberg v0.3.80 lock taw/core 1.99.2.
2026-10-15taw-fleet v1.34.0: site notes, written by you, Claude and taw-fleet.
taw-fleetfeature

October 2026: site notes

  • taw-fleet v1.34.0: each site keeps notes on what happened to it, so you, or Claude in a later session, can catch up before working on it. N in the dashboard adds one, and the details' new NOTES section shows the newest three. In a terminal, taw-fleet notes <site> shows them and taw-fleet notes <site> "…" adds one.
  • taw-fleet v1.34.0: Claude reads a site's notes before working on it (site_notes, show_site, a skill's first message) and adds its own with add_note when it finishes something or finds something worth knowing. Adding a note doesn't ask: it only writes the notes file.
  • taw-fleet v1.34.0: taw-fleet notes what it does to a site itself: updates (and where one stopped), merges and their deploys, publishes, undos, content pulls, restores, scaffold applies, deploy setups and new repositories.
  • Notes live in ~/TAW Fleet/sites/<site>/notes.md, newest first, as plain Markdown you can edit, never in the theme or its repository. See Site notes.
2026-10-15taw-fleet v1.33.0: the dashboard can open when you log in to your Mac.
taw-fleetfeature

October 2026: open at login

  • taw-fleet v1.33.0: taw-fleet config open-at-login on opens the dashboard in its own window each time you log in to your Mac; off stops it. It sets the new open_at_login setting and adds a login agent (~/Library/LaunchAgents/com.relmaur.taw-fleet.login.plist). In the dashboard, the : menu's Open at login entry toggles it. Nothing opens right away, only from your next login. See Open at login.
2026-10-14taw-fleet v1.32.0: tags and grouping in popups; the chat's input stays on screen.
taw-fleetfeature

October 2026: tags and grouping in popups

  • taw-fleet v1.32.0: T opens the selected site's tags in a popup: the fleet's tags to tick, with how many sites have each, and new ones typed below. # still opens it, now also on keyboards where # is Option+3. = opens a popup of the ways to group the list (none, tag, kind, production, each tag prefix) instead of cycling through them.
  • taw-fleet v1.32.0: in the chat, a command with line breaks no longer pushes the input off the screen, so you can always queue a message or answer Claude. Skills and tool searches show by name instead of as JSON.
2026-10-14taw-fleet v1.31.2: Claude's questions in the chat are a list to pick from.
taw-fleetfix

October 2026: Claude's questions, readable

  • taw-fleet v1.31.2: when Claude asks you to choose in the chat (>), you now get its options as a list instead of raw JSON. Move with ↑ ↓ and press enter, or press a number; space ticks several when it allows more than one; the last option types your own answer, and esc leaves it unanswered. Before, a yes sent Claude no answer at all.
2026-10-13taw-fleet v1.31.1: the publish review lists pictures' details and uploads only the files that go.
taw-fleetfix

October 2026: pictures in the publish review

  • taw-fleet v1.31.1: the publish review now lists each picture's details (alt text, title, caption, description) under Pictures and files, unticked, so production keeps its own unless you tick one. Each new file says which records use it and goes up only with one of them ticked; the file count follows what you tick. taw-fleet publish --decisions and Claude's publish_content take a picture by its key (media:2026/09/team.webp). taw-fleet refuses to publish pictures to a taw/core older than 1.99.1, which wrote them all whatever was ticked. If production still needs a file taw-fleet didn't tie to a ticked record, taw-fleet sends it, tries once more and says so.
  • taw-fleet v1.31.1: an MCP server started before a taw-fleet upgrade now says to reconnect it, instead of failing on settings the new version wrote (“unknown keys: group_by”).
2026-10-13taw/core v1.99.1: a publish writes only the media it was asked for. taw-theme v1.12.78 and taw-gutenberg v0.3.79 lock it.
taw/coretaw-themetaw-gutenbergfix

October 2026: publishing one page changes only that page

  • taw/core v1.99.1: publishing one page from Local also changed the alt text and titles of other images, and uploaded a file nothing approved used (a site's icon). Now a published record brings only the images it uses, an image's own details change only when you approve that image, and only the files that go are uploaded.
  • taw-theme v1.12.78 and taw-gutenberg v0.3.79 lock taw/core 1.99.1.
2026-10-13taw-fleet v1.31.0: tags, and a list grouped by tag, kind, production or client.
taw-fleetfeature

October 2026: tags and groups

  • taw-fleet v1.31.0: give each site tags (retainer, es, client:acme) with # in the dashboard or taw-fleet tag. They show on the site's card, the filter finds them (#retainer, tag:client:), and taw-fleet list --tag lists those sites. = groups the list by tag (a site with several shows under each), kind, production or client, with a header per group that ← folds and → opens; the dashboard remembers the grouping. Claude reads the tags and sets them with set_tags. See Tags and groups.
2026-10-13taw/core v1.99.0: a json field, and fields that keep their saved value when a check fails. taw-theme v1.12.77 and taw-gutenberg v0.3.78 lock it.
taw/coretaw-themetaw-gutenberg

October 2026: a JSON field that can't break the page

  • taw/core v1.99.0: a new json field type. You edit JSON in a code box with a Format button, and a mistake shows its line and column as you type. Invalid JSON is never saved: the page keeps using the last good value, and the field shows what you typed with “Not saved: …” under it, so nothing is lost. Sites can add their own check (validate), such as a JSON Schema. Any field can work this way with 'on_invalid' => 'keep'. A site's practice previews asked for it.
  • Also fixed: in the block editor, a field's validation error now shows when you reload the page. Before, the editor's save swallowed it.
  • taw-theme v1.12.77 and taw-gutenberg v0.3.78 lock taw/core 1.99.0.
2026-10-13taw/core v1.98.1: a repeater inside a repeater row on an options page works. taw-theme v1.12.76 and taw-gutenberg v0.3.77 lock it.
taw/coretaw-themetaw-gutenbergfix

October 2026: nested repeaters on options pages

  • taw/core v1.98.1: an options page whose repeater has a repeater inside each row (a list of widgets, each with its own links) showed “critical error”. It now shows and saves the nested rows. What's stored doesn't change. taw update also brings taw-theme 1.12.75's tests/TestCase.php to sites with the scaffold's earlier copy. A client site reported it.
  • taw-theme v1.12.76 and taw-gutenberg v0.3.77 lock taw/core 1.98.1.
2026-10-13taw-fleet v1.30.0: a filter that combines words and conditions.
taw-fleetfeature

October 2026: find any site

  • taw-fleet v1.30.0: / now takes several words, which must all match, and - leaves out. Conditions ask about each site's state: is:behind, is:dirty, is:down, is:failing, has:comments, has:prs, kind:block, core:<1.95 and more. mx -is:live or core:<1.95 has:comments narrow a big fleet in a few keys. The footer counts the matches and names anything it didn't understand. See Filter.
2026-10-13TAW Theme v1.12.75: block tests work on taw/core 1.98.
taw-themefix

October 2026: block tests on taw/core 1.98

  • TAW Theme v1.12.75: the theme's tests/TestCase.php stubs get_post() in stubBlockConstructor(), which taw/core 1.98 calls when a block reads its fields. Without it, a block test that reads a MetaBlock's fields stopped with a fatal error. A site whose TestCase.php is unedited gets it from taw update; a site that edited it can add the same stub.
2026-10-13taw-fleet v1.29.1: follow-ups in the chat show while Claude works, pastes work, ctrl+n starts a new chat.
taw-fleetfix

October 2026: a chat you can follow up in

  • taw-fleet v1.29.1: in the > chat, a message sent while Claude is still working used to go nowhere. It now shows at once, marked queued, and goes to Claude as soon as it finishes. Pasting into the chat works. ctrl+n starts a new chat (the old one stays in Claude Code's history), and ctrl+c in the chat only stops Claude: it no longer quits the dashboard. See Chat with Claude.
2026-10-13Publish Local content to production, after a field-by-field review: taw/core v1.98.0, TAW Companion v0.5.0, taw-fleet v1.29.0. taw-theme v1.12.74 and taw-gutenberg v0.3.76 lock it.
taw/coretaw-themetaw-gutenbergtaw-fleettaw-hub-companionsecurity

October 2026: publish content to production

  • taw-fleet v1.29.0: p publishes what changed in a site's Local content to production. A review opens in your browser: every record by its title, every change by its field (Fieldset › Label) with production's value beside Local's and the changed words marked, images side by side, and the files to upload. A value production changed too since your last pull shows both sides, and you pick one. Nothing is written until you press Publish. The review is kept in ~/TAW Fleet/sites/<site>/publish/ as the record of what went out, and x undoes the last publish, keeping anything edited since. In a terminal, taw-fleet publish <site> (--no-review prints the review); in the chat, publish_content. See Publish content to production.
  • TAW Companion v0.5.0: signed publish routes (plan, files, apply with a one-time token, undo), off until a theme's composer.json says "publish": true under extra.taw-companion. Files are matched by their sha256, and production plans the approved records again before writing: if one changed since the review, nothing is written. See Publishing content.
  • taw/core v1.98.0: Content Interchange 1.8. A dry run names every change by its field, says which values the target changed too since a base (--base, content:diff --with-base), can keep the target's value (--keep), takes files from disk (--staged), and previews an undo. Tools → TAW Data gets a checkbox per record and Keep this site's / Use the file's for values changed on both sides. Block themes get the content:* commands too. See Review details, conflicts and publishing.
  • taw-theme v1.12.74 and taw-gutenberg v0.3.76 lock taw/core 1.98.0 (which also carries 1.97.1).
2026-10-13taw/core v1.97.1: a custom post type's edit screen can follow its template's block order.
taw/core

October 2026: box order for custom post types

  • taw/core v1.97.1: MetaboxOrder::lockFromTemplate() keeps a page's edit-screen boxes in the order its blocks render. It now also works for a custom post type rendered by single-{type}.php: add MetaboxOrder::lockFromTemplate('<post type>'); to inc/customizations.php. A client site's role pages asked for it. The themes pick it up with their next taw/core lock.
2026-10-13taw-fleet v1.28.0: the launch report, and a new site from a link all the way to production.
taw-fleet

October 2026: the launch report

  • taw-fleet v1.28.0: once a site is live, I checks production and opens its launch report: HTTPS and the redirect to it, the certificate, search engines let in (it catches WordPress's "Discourage search engines" left on), the last deploy, the companion, plugins and security, PageSpeed with a picture of production beside the Local build, and what's left for you, each with how. In a terminal, taw-fleet launch-report <site>. taw-fleet comments link links a site to its BugSmash project. In the chat, the new-site skill now goes all the way: the brief, the Local build, GitHub, the deploy to your host and the launch report, stopping for you six times. See A new site from a link.
2026-10-12taw/core v1.97.0 and taw-fleet v1.27.0: set up a site's deploy to any host. taw-theme v1.12.73 and taw-gutenberg v0.3.75 lock it.
taw/coretaw-themetaw-gutenbergtaw-fleet

October 2026: deploy to any host

  • taw/core v1.97.0: a shared deploy, the way CI is shared: after CI passes, the built theme goes to its host by SSH + rsync (WPMU DEV, WP Engine, Kinsta, SiteGround, Cloudways, a VPS…) or by git push. The server's identity is pinned when the deploy is set up, and a deploy carrying a database dump, .sync/, .env files or a key is refused. Sites with their own deploy keep it.
  • taw-fleet v1.27.0: I on the SETUP checklist's Deploy step sets it up: pick the host (any SSH host or git remote works), fill in the details, add the deploy key taw-fleet shows you at the host, test, and taw-fleet stores everything in GitHub and opens the pull request whose merge is the first deploy. It also adds the production URL to your settings. In the chat, the add-production skill does the same. See Set up a site.
  • taw-theme v1.12.73 and taw-gutenberg v0.3.75 lock taw/core 1.97.0.
2026-10-12taw-fleet v1.26.0: the SETUP checklist, and a new site on GitHub with one key.
taw-fleet

October 2026: what each site still needs

  • taw-fleet v1.26.0: a site's details end with a SETUP checklist: repository, secrets check, deploy, production, companion, bot, BugSmash and skills, each done or with how to do it. I does the next step. For a new site, that's GitHub: a private repository, its branches pushed, and the dump check required before anything merges, as on the fleet's other sites. taw-fleet ready prints the checklist in a terminal; Claude can read it and create the repository too, asking first. See Set up a site.
2026-10-12taw-fleet v1.25.0: make a new site from a link, in the fleet chat.
taw-fleet
  • taw-fleet v1.25.0: in the fleet chat, “make me this site:” with a link to a Claude design or artifact, a Figma file or an existing website (or a brief in words) runs the new new-site fleet skill. It writes a brief for you to approve, creates the Local site, builds it with the theme's build-site skill (taw/core 1.96), and opens the reference beside the build. AI apps can create a Local site too (create_site, which asks first). See A new site from a link.
2026-10-12taw/core v1.96.0: the build-site skill. taw-theme v1.12.72 and taw-gutenberg v0.3.74 lock it.
taw/coretaw-themetaw-gutenberg

October 2026: a skill that builds a whole site

  • taw/core v1.96.0: every TAW theme gets a third site skill, build-site. Give it a brief (usually the one taw-fleet's new-site skill writes from a link) and it builds the whole site in Local, on a branch: the colours and fonts, the site options, header, footer and menus, then each page with its content and images. It ends with the theme's checks and every page answering. Classic themes build with the theme's own page skills; block themes with theme.json, templates and block content. See Site skills.
  • taw-theme v1.12.72 and taw-gutenberg v0.3.74 lock taw/core 1.96.0 and have the skill.
2026-10-12taw-fleet v1.24.0: the fleet folder (~/TAW Fleet) and the first fleet skills.
taw-fleet

October 2026: the fleet folder and fleet skills

  • taw-fleet v1.24.0: ~/TAW Fleet/ is now where Claude works on the whole fleet. It holds a CLAUDE.md made from your settings (how to work, your production sites, where things are), taw-fleet's tools for Claude, and three fleet skills: fleet-check (the morning check: what needs you, most urgent first), update-fleet (update every site, then merge and deploy them one at a time, asking each time) and onboard-mac (a new Mac ready for the fleet). a lists them under Fleet skills, and they run in the fleet's chat. taw-fleet init writes the folder, and the dashboard keeps it current. See The fleet folder.
2026-10-12taw-fleet v1.23.0: chat with Claude in the dashboard (>), for any TAW job on any site.
taw-fleet

October 2026: chat with Claude in taw-fleet

  • taw-fleet v1.23.0: > opens a chat with your own Claude Code inside the dashboard, signed in with your Claude account. From the list it's about the whole fleet; from a site's details, about that site. Ask in your own words: what needs attention, update the sites that are behind, merge and deploy, change something on a page, resolve the client's comments. Claude uses taw-fleet's own tools, and it asks before it changes anything: an update or a merge asks the dashboard's own question, and anything else shows exactly what would run. esc leaves it working; taw-fleet chat --prompt prints what Claude is told. See Chat with Claude.
2026-10-12taw-fleet v1.22.2: after a merge, M reminds you to clear the page cache once the deploy has finished.
taw-fleet

October 2026: M reminds you about the page cache

  • taw-fleet v1.22.2: while a merge deploys, its MERGE section says to clear the site's page cache once production has the change, not before. Once it does, it says to clear it now (Hummingbird, WP Rocket, the host's cache), and so does the message at the bottom. A cache cleared during the deploy is rebuilt from the old code. taw-fleet merge and the MCP merge tool, which don't wait for the deploy, end with the same sentence as taw/core's update report.
2026-10-12taw/core v1.95.2: a metabox's defaults stay on the pages it applies to. taw-theme v1.12.71 and taw-gutenberg v0.3.73 lock it.
taw/coretaw-themetaw-gutenberg

October 2026: defaults stay on their own pages

  • taw/core v1.95.2: a field's default text now shows only on the pages its metabox covers. Sites that give each page its own version of a block, with the same field names, could show one page's fallback text on another page; a client site saw it on three blocks. If a page relied on text from another page's default, add that page to the metabox's screens. On the other site using defaults, nothing changes.
  • taw-theme v1.12.71 and taw-gutenberg v0.3.73 lock taw/core 1.95.2.
2026-10-12taw/core v1.95.1: the update report says to clear page caches after the deploy. taw-theme v1.12.70 and taw-gutenberg v0.3.72 lock it.
taw/coretaw-themetaw-gutenberg

October 2026: clear the cache after the deploy, not during it

  • taw/core v1.95.1: an update's report now ends its next steps with a reminder: clear the site's page cache once the deploy has finished, not while it runs. A cache cleared during the deploy is rebuilt from the old code, and the site keeps showing the old pages until it's cleared again.
  • taw-theme v1.12.70 and taw-gutenberg v0.3.72 lock taw/core 1.95.1.
2026-10-12taw/core v1.95.0: link previews get a JPEG or PNG, not the AVIF. taw-theme v1.12.69 and taw-gutenberg v0.3.71 lock it.
taw/coretaw-themetaw-gutenberg
  • taw/core v1.95.0: TAW serves uploaded images as AVIF, which pages load faster, but Facebook, WhatsApp, LinkedIn and X can't read AVIF in a link preview, so shared pages showed no picture. The social image (og:image, twitter:image) now points at the original JPEG or PNG you uploaded, which WordPress keeps. It works in TAW's own SEO tags and in Yoast, Rank Math and SmartCrawl. To check, paste a page into a preview tool such as the Facebook Sharing Debugger.
  • taw-theme v1.12.69 and taw-gutenberg v0.3.71 lock taw/core 1.95.0.
2026-10-12taw/core v1.94.1: taw sync survives a GitHub rate limit. taw-theme v1.12.68 and taw-gutenberg v0.3.70 lock it.
taw/coretaw-themetaw-gutenberg

October 2026: updates don't stop at GitHub's rate limit

  • taw/core v1.94.1: GitHub allows 60 API requests an hour without a token, and updating the whole fleet can use them up. taw sync then crashed, so the update stopped at its sync step and blamed the network. It now says what GitHub answered and goes on, and it signs in by itself when it can (GITHUB_TOKEN, GH_TOKEN, or your gh login), for 5,000 requests an hour. If you see the rate-limit message, run gh auth login once.
  • taw-theme v1.12.68 and taw-gutenberg v0.3.70 lock taw/core 1.94.1.
2026-10-12taw-fleet v1.22.1: doctor checks that taw/core's pre-commit check is on.

October 2026: doctor watches the pre-commit check

  • taw-fleet v1.22.1: on themes with taw/core 1.94 or newer, doctor reports git.no-hook when the pre-commit check that refuses dumps, .env files and keys isn't on, and git.hook-own when the clone has git hooks of its own, so the check needs adding by hand. list --json shows each theme's pre_commit_hook.
  • The report's terminal summary now counts framework updates, maintenance and the site's own changes separately.
2026-10-12taw/core v1.94.0: a pre-commit hook refuses commits of database dumps and credentials. taw-theme v1.12.67 and taw-gutenberg v0.3.69 lock it.
taw/coretaw-themetaw-gutenberg

October 2026: a leak stops at the commit

  • taw/core v1.94.0: the check CI runs on every push (database dumps, .sync/ snapshots, remote.env, .env, wp-config.php, private keys) now also runs before each commit. Updating a theme (taw update, taw-fleet's u or U, or composer update taw/core) installs a git pre-commit hook from taw/core, so such a file is refused before the commit exists and never leaves your Mac. A theme that already has its own git hooks is left alone, with the one line to add to them. CI keeps checking every push: git commit --no-verify skips a hook, nothing skips CI. See Updating a site.
  • taw-theme v1.12.67 and taw-gutenberg v0.3.69 lock taw/core 1.94.0.
2026-10-12taw-fleet v1.22.0: monthly client reports. Availability, updates and changes, review comments, speed and security, in English or Spanish, ready to send.

October 2026: a monthly report for each client

  • taw-fleet v1.22.0: m in the dashboard, or taw-fleet report <site>, makes a site's monthly report: availability from watch, framework updates and the site's own changes, how often it was published, BugSmash comments received and resolved, PageSpeed scores, and the live site's versions, plugins and known vulnerabilities.
  • Ready to send: Markdown to paste into an email and a single HTML page to attach or print, in ~/TAW Fleet/reports/. Technical work is summed up in one line, and anything that couldn't be measured says so plainly.
  • Every month on its own: on the 1st, watch makes last month's report for each production site and tells you. report_language (en, es, also per site) and report_from set the language and the name. See Monthly reports.
2026-10-12taw-fleet v1.21.0: restore points. Before an update, a merge or a content import, taw-fleet saves the Local database and the theme's branch; z puts one back.

October 2026: a way back from every risky step

  • taw-fleet v1.21.0: before an update (u, U), a merge (M) and a content import (C), taw-fleet saves a restore point: the Local site's database and the theme's branch and commit. It keeps the newest 5 per site (restore_keep).
  • Kept where nothing can leak it: the database copy goes straight from wp db export into a private, compressed file in ~/Library/Application Support/taw-fleet/restore/. taw-fleet refuses to save one inside a site, a theme or any git repository, and then the step doesn't run.
  • z in the dashboard, or taw-fleet restore <site>, puts one back after asking. A stopped update offers 3 Put it back. Every restore saves how things are first, so it can be undone. See Restore points.
2026-10-12taw/core v1.93.4: the faster form stylesheet is opt-in, with what critical CSS needs first; queued critical blocks inline their CSS again.
taw/coretaw-themetaw-gutenberg

October 2026: faster first paint, without the jump

  • taw/core v1.93.4: 1.93.3 loaded the form stylesheet in the background on every site. That sped up first paint, but on most sites the desktop layout then jumped when the full stylesheet arrived (layout shift around 1.0), because the theme's critical CSS had gaps the blocking stylesheet used to hide. The plain stylesheet is the default again. Once a theme's critical CSS covers the top of every page, add_filter('taw_form_css_async', '__return_true'); turns the faster loading on. The theme docs, the perf-audit skill and taw make:block --help say what the critical CSS needs: the header positioned exactly like the full stylesheet, each template's top section, and protected bool $critical = true; on blocks right under the banner.
  • Fixed: a $critical block queued with BlockRegistry::queue() now inlines its CSS in <head> as documented, instead of adding a render-blocking stylesheet.
  • taw-theme v1.12.66 and taw-gutenberg v0.3.68 lock taw/core 1.93.4.
2026-10-12taw-fleet v1.20.0: taw-fleet watch sends a Mac notification when a deploy fails, production goes down, a client comments or an update is ready to merge.

October 2026: the fleet tells you, even with the dashboard closed

  • taw-fleet v1.20.0: taw-fleet watch install checks the fleet every 5 minutes in the background and sends a macOS notification for each change, once: a failed deploy, CI failing on the default branch, a production site down or back, a site not verified, new client comments, an update ready to merge.
  • Clicking a notification opens the dashboard on that site, its details showing. The Homebrew cask now installs terminal-notifier for that. taw-fleet --site <site> does the same from a terminal.
  • watch status shows whether it's on, the last check and what it told lately. watch test sends a notification so you can see it arrive. See Notifications.
2026-10-12taw/core v1.93.3: the form stylesheet no longer blocks rendering. taw-theme v1.12.65 and taw-gutenberg v0.3.67 lock it.
taw/coretaw-themetaw-gutenberg

October 2026: forms no longer slow down first paint

  • taw/core v1.93.3: the form stylesheet used to load as a plain <link> on every page, even pages without a form, and held up first paint (on one live site it was the only thing blocking it: 452 ms on mobile). It now loads in the background, in the same place in <head>, so your own form styles still win and forms look exactly as before. If your theme already made it async with a style_loader_tag filter, you can drop that line. To keep the old behaviour: add_filter('taw_form_css_async', '__return_false');.
  • taw-theme v1.12.65 and taw-gutenberg v0.3.67 lock taw/core 1.93.3.
2026-10-12taw-fleet v1.19.0: AI apps can update, merge, start and stop sites over MCP, and every action asks first, like the dashboard.

October 2026: AI apps that ask before they act

  • taw-fleet v1.19.0: taw-fleet mcp now serves the dashboard's actions too: update_site, update_all, merge_pr, site_branch, start_site, stop_site, sync_apply and pull_content.
  • Every action asks first. The first call returns the dashboard's own question and a one-time token, and nothing changes. Only a second call with that token runs it, after the AI app has asked you.
  • The taw tool runs a theme's read-only commands at once, and asks before any other.
  • New commands: taw-fleet branch <site> (back to the site's branch, D in the dashboard) and taw-fleet comments <site> --prompt (the prompt X hands Claude). Both are also MCP pieces: the site_branch tool and the resolve_comments prompt.
2026-10-12taw-fleet v1.18.0: taw-fleet mcp, an MCP server so AI apps can read the whole fleet with typed tools.

October 2026: the fleet, for AI apps

  • taw-fleet v1.18.0: taw-fleet mcp serves the TAW sites on your Mac over the Model Context Protocol. Add it once with claude mcp add taw-fleet -- taw-fleet mcp, or in any app's MCP settings.
  • Tools: list_sites, show_site, doctor, pull_requests, comments, production, and taw for a theme's own read-only commands. Each one is a taw-fleet command you can run yourself, and answers with its --json. taw-fleet mcp tools lists them.
  • Read-only for now. Writes that ask first, as the dashboard does, come next. See AI Integration.
2026-10-12taw/core v1.93.2: every instruction says taw, the command taw-fleet 1.17.0 installs. taw-theme v1.12.64 and taw-gutenberg v0.3.66 lock it.
taw/coretaw-themetaw-gutenberg

October 2026: one command, taw

  • The docs, the themes' agent docs and skills, and taw/core's help now write commands as taw …. taw comes with taw-fleet 1.17.0 and runs the theme's own vendor/bin/taw with the site's PHP, from the theme or from anywhere with taw @<theme>. See The taw command, rewritten around it.
  • Nothing changes in a theme: vendor/bin/taw is the same command where taw isn't installed, like CI.
  • taw/core v1.93.2, taw-theme v1.12.64 (skills and README) and taw-gutenberg v0.3.66.
2026-10-12taw-fleet v1.17.0: the taw command. One command on the PATH runs any theme's own vendor/bin/taw, with its site's PHP and database socket.

October 2026: one taw command

  • taw-fleet v1.17.0 installs a second command, taw. It runs the theme's own vendor/bin/taw, so every site keeps its own taw/core version.
  • Where: in a theme folder, or any folder under it: taw list, taw make:block Hero. From anywhere: taw @john-seed list. For a site with two themes, taw @taw/taw-theme list. Outside a theme, taw lists every theme by its short name.
  • The right PHP: on a Local site, taw uses the site's own PHP and MySQL socket, so commands that start WordPress reach that site's database. A halted site gets one line saying so, then the command runs anyway.
  • Older themes (taw/core before 1.90) run their bin/taw. A theme without vendor/ is told to run composer install.
  • TAW_DEBUG=1 taw … prints the exact command it runs. CI keeps calling vendor/bin/taw, which is the same command.
  • Install: brew upgrade taw-fleet installs taw too. taw-fleet self-update puts it next to taw-fleet.
2026-10-12taw-fleet v1.16.0: merge several sites at once, each followed on its card and in a MERGE section, from GitHub to production.

October 2026: merges you can follow

  • taw-fleet v1.16.0: M no longer waits for another merge, or for an update, to finish. Merge site after site: each merge runs on its own.
  • On the card: a spinner with “merging #8…”, then “deploying…”, then ✓ or ✗ for 15 minutes.
  • In the detail pane: a MERGE section lists each step, from the merge on GitHub to production, with the time each one took. The bottom line lists every merge in progress.
  • Back on the site's branch: after a merge, untracked files (the update's .taw/ report) no longer keep the theme on its update branch. Only changes to tracked files do.
2026-10-10taw/core v1.93.1: sync keeps a tests/TestCase.php with the site's own helpers; CI on Node 24. taw-theme v1.12.63 and taw-gutenberg v0.3.65 lock it.
taw/coretaw-themetaw-gutenberg

October 2026: updates keep your own test helpers

  • taw/core v1.93.1: updating a site used to overwrite tests/TestCase.php, so helpers you added there were lost. Now the update replaces it only when it is a copy TAW shipped. A copy with your own changes is kept and listed as "has this site's own changes, left as is", and the weekly framework-sync pull request lists it under "Kept". See Updating a site.
  • CI: the shared theme workflows run their GitHub actions on Node 24 (GitHub is retiring Node 20). The smoke test runs on Ubuntu 24.04, whose image has the MySQL it uses (ubuntu-latest moves to Ubuntu 26 on October 19).
  • Theme repositories: a pull request can't be merged until the check for committed dumps and credentials passes.
  • taw-theme v1.12.63 and taw-gutenberg v0.3.65 lock taw/core 1.93.1.
2026-10-09taw/core v1.93.0: theme CI refuses committed database dumps and credentials. taw-theme v1.12.62 and taw-gutenberg v0.3.64 lock it.
taw/coretaw-themetaw-gutenberg

October 2026: no database dump reaches a theme repository

  • taw/core v1.93.0: every theme's CI gets a new check. It fails when the repository holds a database dump (*.sql, a *.wpress backup, or a file that looks like one), anything under .sync/ (content snapshots, remote.env), a .env or wp-config.php file, or a private key. On a pull request it checks every commit, so a dump added and then removed again still fails. It needs no Composer install, so it runs even when the rest of the build can't. See Updating a site.
  • Why: a theme repository can be public, and a full database dump once reached one through a broken .gitignore line. Every TAW theme ignores /.sync/ now (taw-theme v1.12.61, taw-gutenberg v0.3.63); this check catches what an ignore rule can't.
  • taw-theme v1.12.62 and taw-gutenberg v0.3.64 lock taw/core 1.93.0.
2026-10-09taw-fleet v1.15.1: a warning when a theme is on another repository’s branch, D to switch back, and M keeps a copy in step.
taw-fleet

October 2026: back on the right branch

  • taw-fleet v1.15.1: a theme whose checked-out branch tracks another repository gets a warning: ≠ after the branch on its card, “tracks owner/name” in the details, and doctor code git.foreign-upstream.
  • D, Back to the site's branch: switches the theme back to its default branch after asking. It refuses with uncommitted changes, and deletes the stray branch only when that branch tracks another repository and has nothing that repository lacks. See Back to the site's branch.
  • M with a delivery remote: when taw.json's update.remote isn't origin, M also brings origin's default branch up to date after the merge, fast-forward only. A copy that has diverged is left alone, with how to realign it once.
  • The : menu is grouped by meaning: Site · Local, Site · Production, Site · Feedback, Theme · Code, Theme · Update, Theme · Git & GitHub, App. The keys are the same.
  • Fixes: u is always available; block themes' update question no longer mentions framework files; /block finds block themes; an unknown icons value is an error; help texts point to the theme's resolve-comments skill.
2026-10-09taw-fleet v1.15.0: update every theme without an agent, follow taw.json’s delivery remote, and optional Nerd Font icons.
taw-fleet

October 2026: update the whole fleet, no agent needed

  • taw-fleet v1.15.0: U (or taw-fleet update --all) updates every client theme that needs it through vendor/bin/taw update, one after another, each ending in its own pull request. One screen shows a line per theme; enter shows a theme's steps, o its pull request, r its report. A theme that stops doesn't stop the others, and Fix with Claude is offered at the end. Themes with uncommitted changes or off their default branch are left out, with the reason. The agent round is still there: taw-fleet handoff --all --launch.
  • The delivery remote: pull requests, M, the deploy status and doctor follow taw.json's update.remote (taw/core 1.92).
  • Calmer rows: one update icon in the card's margin; the details pane opens with an UPDATE section.
  • Icons: plain symbols by default. taw-fleet config icons nerd switches to Nerd Font icons.
  • See Dashboard and keys.
2026-10-09taw/core v1.92.0: taw.json names the git remote an update goes to. taw-theme v1.12.60 and taw-gutenberg v0.3.62 lock it.
taw/coretaw-themetaw-gutenberg

October 2026: updates reach the repository that deploys

  • taw/core v1.92.0: a new taw.json setting, "remote" (default "origin"). vendor/bin/taw update pushes its branch to that remote and opens the pull request on that remote's GitHub repository. For a site that deploys from an agency's organization while origin is a copy, set "remote": "origin-agency", or the pull request opens where merging deploys nothing. If the remote doesn't exist, the update refuses before changing anything. See Updating a site.
  • taw-theme v1.12.60 and taw-gutenberg v0.3.62 lock taw/core 1.92.0.
2026-10-09taw-fleet v1.14.1: an update screen that shows every step as it runs.
taw-fleet

October 2026: watch an update happen

  • taw-fleet v1.14.1: "Update this site" gets its own screen: the versions and the taw.json policy at the top, a progress bar, a checklist of every step with its time and detail ("not run here" with the reason for a skipped check), and the latest output (l for the full log).
  • When it ends, a result panel: o opens the pull request, r the report, and the "For you" steps or the error are shown in full. esc goes back to the list while it runs.
2026-10-09taw-fleet v1.14.0: Update this site. One question, then vendor/bin/taw update does the rest.
taw-fleet

October 2026: update a site from the dashboard

  • taw-fleet v1.14.0: u (or taw-fleet update <site>) runs the theme's vendor/bin/taw update with the site's PHP and Local's Composer, after one question worded from its taw.json. Uncommitted changes are refused before anything starts.
  • A theme on an older taw/core updates in one step too: taw-fleet installs the newest taw/core in vendor/ first, puts composer.lock back, and lets the new command do the rest.
  • When an update stops: Fix with Claude (the report is Claude's prompt; it asks before pushing and never merges), or Do it myself (the report opens in your editor). taw-fleet update exits with 1.
  • See Updating a site.
2026-10-09taw/core v1.91.2: update reads taw/core’s version from composer.lock. taw-theme v1.12.59 and taw-gutenberg v0.3.61 lock it.
taw/coretaw-themetaw-gutenberg

October 2026: the update report starts from what git has

  • taw/core v1.91.2: vendor/bin/taw update reads the installed taw/core version from composer.lock, and from vendor/ only when there's no lock. That version drives the report's “from → to”, the patch range and a pin, so the update is right even when vendor/ is ahead of the lock.
  • taw-theme v1.12.59 and taw-gutenberg v0.3.61 lock taw/core 1.91.2, so new sites start with it.
2026-10-09taw/core v1.91.1: migrations follow taw.json, edited agent docs are kept, and update shows its progress.
taw/core

October 2026: updates that respect the site's choices

  • taw/core v1.91.1: migrations follow taw.json wherever they run: in vendor/bin/taw update, in the Composer hook and in vendor/bin/taw upgrade --apply. With "scaffold": "off" or "docs": "off", the matching migration is held back, with a line that says how to do it by hand. See Updating a site.
  • Edited agent docs are kept. The 1.91.0/agent-docs migration and docs:sync --apply replace only copies identical to a version the scaffolds shipped. An edited copy stays, with the steps to move your notes; docs:sync --apply --force replaces it anyway. In 1.91.0 they replaced edited copies too: if that happened to a theme, its notes are in git history.
  • sync --apply now only reports the agent docs and the configs; docs:sync and configs:sync write them. The weekly framework-sync workflow runs vendor/bin/taw upgrade --apply after sync.
  • vendor/bin/taw update prints its progress as it goes (on stderr with --json), keeps quoted parts of --composer whole, so Local's paths with spaces work, and --plan --json prints JSON.
2026-10-09Docs: the taw command and taw-fleet at the center, every page checked against the code.
docs

October 2026: the docs, built around the tools

  • New navigation: Start here, then the taw command, then taw-fleet, then the themes, the data layer, the block editor and the taw/core PHP reference. Every existing page keeps its URL.
  • New pages: How TAW fits together, the taw command reference (all 33 commands with their options), and for taw-fleet Dashboard and keys, the Command reference and Production, PRs and deploys.
  • Rewritten: the Introduction and the Quickstart (create a site with taw-fleet or Composer, then vendor/bin/taw), and the upgrading guide.
  • Fixed: the pages that said TAW has no Gutenberg blocks (true only of TAW Theme), the UI Block example (defaults(), not defaultData()), the Vite dev-server port notes, the companion's version constraint, and links to pages that don't exist. Commands are written vendor/bin/taw throughout.
  • Removed: the documentation template's sample pages (authentication, notice, components).
2026-10-09taw/core v1.91.0: one-step updates. taw.json decides, vendor/bin/taw update does it all, and a written guide covers what a person must finish.
taw/coretaw-themetaw-gutenberg

October 2026: a site updates like a package

  • taw/core v1.91.0: vendor/bin/taw update updates a theme in one step. It works on a new branch: it updates taw/core, applies the framework's files, runs the migrations and checks, commits, and opens a pull request, without asking anything. See Updating a site.
  • taw.json is the site's update policy: the taw/core version range, what an update may change, which checks run, and whether it opens a pull request or merges one itself. vendor/bin/taw policy explains it in words.
  • When an update stops, nothing is pushed. .taw/update-report.md says what failed and how to fix, check, finish or undo it, step by step. The same file is what Claude gets with "Fix with Claude".
  • composer update runs the migrations: each change a release makes to a theme is code that explains itself (vendor/bin/taw upgrade --explain <id>) and never overwrites your edits.
  • The framework's part ships with taw/core: agent docs, the Vite and PHPStan base, and theme CI (shared workflows at @v1). Themes keep short files of their own.
  • taw-theme v1.12.58 and taw-gutenberg v0.3.60 start with taw.json, the CI stubs, the short docs and configs, and the Composer hook.
2026-10-09taw/core v1.90.0: the taw command ships with taw/core (vendor/bin/taw); add your own commands.
taw/coretaw-themetaw-gutenberg

October 2026: the taw command lives in taw/core

  • taw/core v1.90.0 ships the taw command as vendor/bin/taw, so new commands arrive with composer update taw/core. Add your own with TAW\CLI\CommandRegistry. See The taw command.
  • taw-theme v1.12.57 and taw-gutenberg v0.3.59: bin/taw now hands over to it, so php bin/taw keeps working.
  • This is the first step toward updates that need no questions: a TAW update becomes a plain composer update.
2026-10-09taw-fleet v1.13.0: one palette for every action and skill, with the reason when one isn’t available.
taw-fleet

October 2026: menus that explain themselves

  • taw-fleet v1.13.0: : (all actions) and a (ask Claude…) open one two-pane palette. The list is on the left, in groups: Recent, Site, Theme, Code & GitHub, Production, Claude and App. The highlighted entry's full description is on the right; for a skill, that includes where it comes from (taw/core, the scaffold or the site itself) and how to ask for it.
  • Entries that can't run right now stay listed, dimmed, with the reason, such as "no open pull request". Typing filters the list, and recently used entries come first.
  • See Dashboard and keys.
2026-10-09Site skills: every TAW theme carries its AI tools; taw-fleet v1.12.0 runs them in the theme folder.
taw/coretaw-themetaw-gutenbergtaw-fleet

October 2026: AI tools in every site

  • taw/core v1.89.0 ships the site skills every TAW site gets: resolve-comments (client comments from BugSmash) and perf-audit (GTMetrix and PageSpeed). bin/taw sync installs them in classic themes, and the new php bin/taw skills:sync --apply installs them in block themes. See Site skills.
  • taw-theme v1.12.56 and taw-gutenberg v0.3.58 lock taw/core 1.89.0 and carry both skills, so new sites start with them.
  • taw-fleet v1.12.0: press a to start Claude Code in a theme folder on one of its skills, and X now runs resolve-comments there too. taw-fleet wp-remote writes to the live site as a bot user whose password stays in the Keychain, and doctor reports skills a theme hasn't installed.
2026-10-09taw-fleet v1.11.0: Claude gets its first message again; no more fighting Local's leftover processes.
taw-fleet

October 2026 — starts and stops that don't hang

  • taw-fleet v1.11.0: X, U and handoff --launch opened Claude Code without its first message. It arrives again.
  • Leftover processes: after many starts and stops, Local can leave nginx or php-fpm processes behind that keep a site from starting or stopping. taw-fleet now finds them before and after every start, stop and restart, and names them instead of trying again. The dashboard asks to end them and then does what you asked; on the command line, run taw-fleet unstick <site>. Only Local's own processes are touched.
2026-10-09taw-fleet v1.10.0: open and resolve client comments from the dashboard; a clearer table and detail pane.
taw-fleet

October 2026 — act on client comments from the dashboard

  • taw-fleet v1.10.0: F opens a site's BugSmash review page, and X hands its open comments to Claude Code in the TAW umbrella with the taw-resolve-comments skill. The skill shows its plan and asks before anything reaches production.
  • A clearer table: the columns span their panel, the selected card is tinted, and with many sites the cards sit tight with the position in the rule. The TAW ecosystem's own themes come last under their own label, and the kind reads as a quiet classic or block.
  • The detail pane says where things come from: LOCAL, LIVE, FEEDBACK and FINDINGS sections, with comments shown as quotes.
  • A shorter bottom line: the everyday keys only. : finds the rest.
2026-10-09taw/core v1.88.0: a pull carries everything in one run; the fidelity suite runs on every change.
taw/coretaw-themetaw-gutenberg

October 2026 — content moves, tested end to end

  • Fidelity suite: every change to taw/core now runs a real round trip between two WordPress sites in CI, and fails unless a second import reports nothing to change, every value points at the same record on both sites, the copy imports back unchanged, and undo restores it. See How it's tested.
  • taw/core v1.88.0 fixes what the suite found. A page already on your site whose only change is a link to a page the same pull brings now gets that link in the same pull, not the next one. Term fields get it too, and so do the front page, posts page and sticky posts on a site that had none.
  • Files keep their place: a downloaded file goes into the same month folder as at the source, so two files with one name stay two files, and a large image arrives under its own name instead of -scaled-1.
  • An option the source never set is no longer carried, and the site address at the end of a sentence is mapped.
  • taw-theme and taw-gutenberg lock taw/core 1.88.0. Live sites don't need it to send content; the site you pull into does.
2026-10-09taw-fleet v1.9.0: open BugSmash comments per site; two-line dashboard rows.
taw-fleet

October 2026 — client feedback in the morning check

  • taw-fleet v1.9.0: sites with a BugSmash project (bugsmash_project in the settings) show their open review comments: an FB column (a warning once the oldest has waited more than two days), a Feedback block with the latest comments and their page, doctor findings (comments.open, comments.unreachable, comments.no-key), and taw-fleet comments. It only reads; the API key stays in the Keychain (taw-fleet comments key import).
  • Dashboard: each theme is now a two-line card: the site and its columns on top, and underneath its theme, kind, production host, the oldest open comment's age and the last commit's age.
2026-10-09taw/core v1.87.2: pulls settle when a host drops large files. taw-theme v1.12.53 (chat guardrails); a client site live on taw/core 1.87.1.
taw/coretaw-theme

October 2026 — pulls settle on slow hosts

  • taw/core v1.87.2: a content import tries a media download three times before giving up, because some hosts cut large files off mid-transfer, and its warning now says why a download failed. A post whose comment or ping status is stored empty at the source no longer shows as changed on every pull.
  • taw-theme v1.12.53: taw/core 1.87.1 and the chat guardrails (rag:usage, the chatbot session flow).
  • A client site runs taw/core 1.87.1. With 1.87.2 on the receiving site, a second pull from it reports no changes.
2026-10-09taw/core v1.87.1: a post picked inside a repeater row is saved. Live sites on taw/core 1.87.0.
taw/core

October 2026 — repeater rows keep their selected posts

  • taw/core v1.87.1: a post selector inside a repeater row could lose the editor's pick, because the row only saved it if something else changed afterwards. The picker now signals the change, as the image and file pickers do.
  • Live sites: four production sites run taw/core 1.87.0, so taw-fleet pull gets everything from Phases 5–8 (page trees, the site icon, logo and menus). Their pages were compared before and after each deploy, and were unchanged.
2026-10-09taw/core v1.87.0: content moves carry the site icon, logo and menus (schema 1.7). taw-gutenberg v0.3.54.
taw/coretaw-gutenberg

October 2026 — the site icon, logo and menus come along

  • taw/core v1.87.0: a pull, push or full migration now also carries the site icon and logo, the menus built in wp-admin and which location each sits in, reusable blocks, block navigation menus, and footnotes.
  • Menus a theme builds in code are left alone. Another plugin's data (an SEO plugin's titles) travels only when asked, with --meta.
  • taw-gutenberg v0.3.54: taw/core 1.87.0.
2026-10-09taw/core v1.86.0: content imports can be undone. taw-gutenberg v0.3.53.
taw/coretaw-gutenberg

October 2026 — an undo button for content imports

  • taw/core v1.86.0: every import keeps a journal of what it changed. content:import --undo --yes, or Undo this import in Tools → TAW Data, reverses it: changed values go back, what it created is deleted, what it deleted comes back, and anything edited by hand since is kept.
  • A record that fails no longer stops the import; it's listed as failed.
  • If the safety net (the rollback snapshot and the journal) can't be written, nothing is imported.
  • taw-gutenberg v0.3.53: taw/core 1.86.0.
2026-10-09taw/core v1.85.0: scoped exports and pushes (schema 1.6). taw-theme v1.12.52, taw-gutenberg v0.3.52.
taw/coretaw-themetaw-gutenberg

October 2026 — moving a few posts, or a change, without side effects

  • taw/core v1.85.0: exporting some posts (--posts, --types, --since) leaves out options and keeps only the categories and tags those posts use, so importing them can't overwrite the rest of the site. --with-options / --with-terms bring them back.
  • Change-sets from content:diff carry the source, its references and the media they use: pushing one to another site maps IDs, rewrites links and downloads the files.
  • An import never sets the front page to a page the site doesn't have.
  • A file an import downloaded is recognised next time even if WordPress renamed it.
  • taw-theme v1.12.52, taw-gutenberg v0.3.52: taw/core 1.85.0.
2026-10-09taw/core v1.84.0: content imports keep page and category trees (schema 1.5). taw-theme v1.12.51, taw-gutenberg v0.3.51.
taw/coretaw-themetaw-gutenberg

October 2026 — page and category trees that survive a move

  • taw/core v1.84.0: pages are matched by their path (about/team), so two pages with the same slug under different parents no longer overwrite each other, and a page moved at the source is moved here, not duplicated.
  • Parents are set whatever order the source lists pages and categories in, and a parent removed at the source is removed here.
  • Category and tag meta is imported (it was exported but never written), and terms of private taxonomies arrive with their names.
  • Snapshot schema 1.5. Older snapshots still import; production sites need this release for their snapshots to carry page paths.
  • taw-theme v1.12.51, taw-gutenberg v0.3.51: taw/core 1.84.0.
2026-10-09taw/core v1.83.0: content imports carry media metadata. taw-gutenberg v0.3.50.
taw/coretaw-gutenberg

October 2026 — images keep their alt text and captions across a move

  • taw/core v1.83.0: an image already on the site gets the source's alt text, caption, title and description when they differ; captions keep their links.
  • The import preview (and taw-fleet pull's) lists files to download, files missing, and image metadata changes.
  • A reference to a file the site can't get is cleared, instead of pointing at whatever image has that number here.
  • taw-gutenberg v0.3.50: taw/core 1.83.0.
2026-10-09taw/core v1.82.0–v1.82.1: content imports map references and URLs (schema 1.4). taw-theme v1.12.50, taw-gutenberg v0.3.48–v0.3.49.
taw/coretaw-themetaw-gutenberg
  • taw/core v1.82.0: a snapshot from another site (a taw-fleet pull, a staging copy) now lands with its references pointing at this site's records: selected posts, query loops, navigation links and images find their counterparts by slug or file, not by the other site's IDs.
  • Links to the source site become links to this one, and images become this site's copies. Files this site doesn't have yet keep loading from the source.
  • Images with the same name in different month folders no longer swap. Importing the same snapshot twice reports nothing to change the second time.
  • Snapshot schema 1.4. Older snapshots still import; production sites need this release for their snapshots to carry the new references.
  • taw-theme v1.12.50, taw-gutenberg v0.3.48: taw/core 1.82.0.
  • taw/core v1.82.1: a pull settles in one import: files arriving in the import are downloaded before it decides what already matches. taw-gutenberg v0.3.49: taw/core 1.82.1.
  • Live: one production site on 1.82.0, two on 1.82.1 — their snapshots carry the new references; pages unchanged.
2026-10-09taw-fleet v1.8.0: refresh everything at once (ctrl+r, taw-fleet refresh).
taw-fleet

October 2026 — refresh every site at once

  • taw-fleet v1.8.0: ctrl+r reads everything again for every site, skipping the caches: the Local sites with the newest versions from GitHub, pull requests, CI and deploys, the production sites, and the sync check of every classic theme. The bottom line shows what's still running, then one summary. taw-fleet refresh does the same on the command line.
2026-10-09taw/core v1.80.0–v1.80.1: field defaults, saved as records. taw-theme v1.12.48–v1.12.49, taw-gutenberg v0.3.46–v0.3.47.
taw/coretaw-themetaw-gutenberg

October 2026 — content from code into the database

  • taw/core v1.80.0: metaboxes and options pages take a defaults key: what a field reads, and shows in the editor, while nothing is stored. It replaces getMeta(...) ?: 'text' fallbacks in blocks.
  • Tools → TAW Data → Defaults (or php bin/taw content:defaults --apply) writes the defaults into the empty fields, so content managed in code becomes records you can edit and export. It shows what it would write first, and the last save can be undone.
  • taw-theme v1.12.48: taw/core 1.80.0, bin/taw content:defaults, and CI installs WP-CLI directly (the old download stopped working). taw-gutenberg v0.3.46: taw/core 1.80.0.
  • taw/core v1.80.1: a field falls back to its default for every value a ?: fallback treated as empty, including '0' (an image saved blank) and '[]' (a repeater saved with no rows). content:defaults runs as the first administrator. taw-theme v1.12.49, taw-gutenberg v0.3.47: taw/core 1.80.1.
2026-10-09taw/core v1.79.0: content imports keep what they're given. taw-theme v1.12.47, taw-gutenberg v0.3.45.
taw/coretaw-themetaw-gutenberg

October 2026 — safer content imports

  • taw/core v1.79.0: the first round of Content Interchange fixes, on the import side:
    • comments are no longer duplicated on every import;
    • content keeps embeds and &;
    • password-protected posts stay protected;
    • date and term changes travel;
    • a failed write never lands on another post.
  • Snapshot schema 1.3. Older snapshots still import.
  • taw-theme v1.12.47, taw-gutenberg v0.3.45: taw/core 1.79.0.
2026-10-09taw-fleet v1.7.0: check every theme at once; a menu of all actions.
taw-fleet

October 2026 — check every theme, find any action

  • taw-fleet v1.7.0: Y (or taw-fleet sync --all) checks every classic TAW theme against the taw-theme scaffold at once, in seconds, and keeps the SYNC column current.
  • : opens a list of every action: type to find one, and enter runs it on the selected theme.
2026-10-09taw-fleet v1.6.0: pull production content into Local; other GitHub accounts marked. TAW companion v0.4.0.
taw-fleettaw-hub-companion

October 2026 — production content into Local

  • taw-fleet v1.6.0: C (or taw-fleet pull <site>) copies a live site's published content into its Local copy. It shows a preview of what changes, asks, then imports with a rollback snapshot saved first.
  • Themes whose repository belongs to another GitHub account show @account in the dashboard (/other filters to them).
  • TAW companion v0.4.0: a signed, read-only GET /content route with the site's published content snapshot. All four live sites run it.
2026-10-09taw-fleet v1.5.0: pull requests and deploys in the dashboard; merge with M.
taw-fleet

October 2026 — finish updates from the dashboard

  • taw-fleet v1.5.0: each theme's open pull requests with their CI, and whether production has the newest commit (PR and DEPLOY columns, taw-fleet prs).
    • M merges a pull request once CI passed (it asks, naming the production site it deploys), pulls the default branch into the local theme, and follows the deploy until production has it.
    • doctor warns about failed deploys, failing or conflicting pull requests, and commits not deployed.
2026-10-09taw-fleet v1.4.0: start working on a site with one key.
taw-fleet

October 2026 — one key to start working

  • taw-fleet v1.4.0: w starts the site, opens the theme in your editor, runs Vite in its own window, and opens the site with hot reload. Press it again to stop Vite and the site. On the command line: taw-fleet work <site>.
    • The dashboard shows vite after a theme's name while its dev server runs; /vite filters to those.
  • taw-fleet v1.3.1: production sites come first in the dashboard; /live filters to them.
2026-10-08taw-fleet v1.3.0: update every theme at once. taw/core v1.78.1, taw-theme v1.12.46, taw-gutenberg v0.3.44.
taw-fleettaw/coretaw-themetaw-gutenberg

October 2026 — update every theme at once

  • taw-fleet v1.3.0: U updates every client TAW theme that needs it in one Claude Code session.
    • One subagent per theme follows the update-theme skill's batch mode.
    • You decide docs changes, sites to start and pushes once, at the end.
    • u now moves taw/core's dependencies too, and says so when the version didn't move.
  • taw/core v1.78.1 (docs): upgrade with composer update taw/core --with-dependencies. From before v1.77.0, a plain update changes nothing.
  • taw-theme v1.12.46: batch mode fixes from a real rehearsal. taw-gutenberg v0.3.44: taw/core v1.78.1.
2026-10-08taw/core v1.78.0, taw-theme v1.12.45, taw-gutenberg v0.3.43: safer theme updates, ready for updating every site at once.
taw/coretaw-themetaw-gutenberg

October 2026 — safer theme updates

  • taw/core v1.78.0: php bin/taw sync compares composer.json and package.json with the scaffold by rule.
    • It suggests only what the scaffold has and your site lacks: missing keys or repositories, and raised version constraints.
    • It never proposes removing your own packages, and never reads extra (where the companion key lives).
    • Optional starter features (Reactiph, the chatbot) are marked optional.
    • --apply-manifests writes the suggestions.
  • taw-theme v1.12.45: the update-theme skill has a batch mode for an agent updating several themes at once.
    • It skips a theme with uncommitted changes instead of asking.
    • It returns docs changes as proposals.
    • It commits on its own branch, never pushes, and reports in a fixed format.
  • taw-gutenberg v0.3.43: taw/core v1.78.0.
2026-10-08taw-fleet v1.2.0: its own window, update with agent, and a start/restart fix.
taw-fleet

October 2026 — taw-fleet v1.2.0

  • Its own window. taw-fleet opens the dashboard in a new terminal window that it fills, and gives the current terminal back; quitting closes the window. --window=false, or window = false in the config, keeps it in the current terminal.
  • Update with agent. A opens Claude Code in the theme folder with the update prompt, in a window beside the dashboard (left and right halves of the screen in Terminal). When Claude exits, the dashboard rescans. u offers it as an answer too.
  • Fix: starting a site no longer reports "Local isn't open" while Local is starting it, and a restart no longer leaves Local showing "restarting".
2026-10-08taw-hub retired: taw-fleet reads the live sites through the companion, now shipped with each TAW theme as an mu-plugin. taw/core v1.77.0, companion 0.3.0, taw-theme v1.12.44, taw-gutenberg v0.3.42.
taw-fleettaw/coretaw-hub-companiontaw-themetaw-gutenberg

October 2026 — taw-hub retired, the live sites in taw-fleet

The TAW Hub control app is retired. taw-fleet now checks the production sites itself.

  • taw-fleet v1.1.0:
    • taw-fleet live shows each production site's WordPress, PHP, taw/core and companion versions, plugins with updates waiting, known vulnerabilities and recent logs;
    • in the dashboard that's the LIVE column and a Production section;
    • doctor adds live.* findings.
    • Requests are signed with a key kept in your macOS Keychain, and every answer is checked against the site's pinned key.
  • Companion 0.3.0 ships with the theme as an mu-plugin:
    • a Composer requirement, the fleet key in extra.taw-companion.keys, and the deploy copying its one-file loader;
    • it updates with composer.lock.
    • The four live client sites run it.
  • taw/core v1.77.0:
    • hub:install and hub:enroll are retired (hidden stubs that explain what replaced them);
    • enshrined/svg-sanitize 1.0.0 fixes three advisories that current Composer refuses to install.
  • taw-theme v1.12.44 and taw-gutenberg v0.3.42 take taw/core v1.77.0. taw-theme's bin/taw drops the hub commands, and the hub-connect skill is gone.
2026-10-08taw-fleet v1.0.0: a terminal dashboard for every TAW site on your Mac.
taw-fleetfeaturedocs

October 2026 — taw-fleet

New tool: taw-fleet, installed with brew install Relmaur/tap/taw-fleet. It reads Local by Flywheel's own files, so it needs no setup.

  • Dashboard:
    • every TAW theme with its site's status, taw/core version, scaffold check and git state;
    • a detail pane, a filter, and shortcuts to the editor, Finder, terminal, site, wp-admin, production and GitHub.
  • Updates:
    • update runs composer update taw/core and lists the UPGRADING.md sections to check;
    • sync compares a theme with the taw-theme scaffold;
    • handoff gives the whole update to an agent with a prompt that has the site's context and your rules.
  • Sites:
    • start, stop and restart go through the Local app;
    • wp runs wp-cli with the site's PHP and database;
    • create makes a new Local site with taw-theme or taw-gutenberg, activated and committed.
  • Checks: doctor lists what needs attention, with stable codes for scripts.
  • Updating taw-fleet: brew upgrade taw-fleet, or taw-fleet self-update without Homebrew.
2026-10-07taw/core v1.76.1 / taw-gutenberg 0.3.41 / taw-theme v1.12.43: tabs that look like tabs; dynamic block settings docs and examples.
taw/coretaw-gutenbergtaw-themefixdocs

October 2026 — Tabs, and dynamic block settings wrapped up

Tagged taw/core v1.76.1, TAW Gutenberg v0.3.41 and taw-theme v1.12.43.

  • Options pages and metabox tabs look like tabs: one row of labels with the active one underlined in the admin color, instead of bordered boxes with faded labels. On narrow screens the row scrolls sideways. Keyboard use is unchanged.
  • New page: Dynamic block settings. Dynamic tags covers the new Use it for targets.
  • TAW Gutenberg's Single book template shows them:
    • genre badges in each genre's own color (a new Color field on genres);
    • an is-in-print/is-out-of-print class on the details column;
    • the cover's caption from a new Cover credit field;
    • a sponsored rel on the Buy button;
    • an #awards anchor.
2026-10-07taw/core v1.76.0 / taw-theme v1.12.42: the Code of Canon Law corpus.
taw/coretaw-themefeature

October 2026 — Code of Canon Law corpus

Tagged taw/core v1.76.0 and taw-theme v1.12.42.

  • New and opt-in: TAW\Core\Rest\CanonLawEndpoint::enable() serves taw/v1/canon-law/* from a Code installed with bin/taw canon-law:install <edition> <.sqlite|.json>. On hosts without pdo_sqlite, it installs into MySQL.
  • bin/taw gains canon-law:install and canon-law:export.
  • With the endpoint on and an edition installed, the chatbot can look canons up (lookup_canon_law).
  • Sites that don't enable it are unchanged.
2026-10-07taw/core v1.75.0 / taw-gutenberg 0.3.40 / taw-theme v1.12.41: dynamic block settings in the editor.
taw/coretaw-gutenbergtaw-themefeature

October 2026 — Dynamic block settings in the editor

Tagged taw/core v1.75.0, TAW Gutenberg v0.3.40 and taw-theme v1.12.41.

  • Every block has a TAW dynamic settings panel in its sidebar, below TAW visibility. Set its classes, text color, background, border color and HTML attributes (anchor, title, ARIA label and description, data-…) from values, e.g. a background from the genre’s color.
  • Each setting shows its answer for the post you’re editing: the classes it adds, a color swatch, the attribute’s value, or why it’s ignored (“Not a color”, “palette colors only on this site”).
  • The canvas previews the classes and colors as the site will show them.
  • The TAW data popup’s Use it for gains Open in new tab and Link rel on buttons, Caption and Title on images, Link on navigation links, and Date on the Post Date block.
2026-10-03taw/core v1.74.0 / taw-gutenberg 0.3.39 / taw-theme v1.12.40: dynamic block settings, server side.
taw/coretaw-gutenbergtaw-themefeature

October 2026 — Dynamic block settings (server side)

Tagged taw/core v1.74.0, TAW Gutenberg v0.3.39 and taw-theme v1.12.40. This is the first of three steps; the editor panel comes next.

  • Any block can take CSS classes, text, background and border colors and HTML attributes (anchor, title, ARIA label and description, data-*) from expressions, stored in the block's metadata.tawSettings. Each item of a TAW Loop or Query Loop gets its own values.
  • Colors are a palette name (accent) or a real CSS color. Anything that could inject CSS is dropped. When the site's editing policy turns custom colors off, only palette colors apply.
  • Button open in new tab and rel, and the post date, can take expressions too.
2026-10-03taw/core v1.73.2 / taw-gutenberg 0.3.38 / taw-theme v1.12.39: the TAW data popup opens in the middle of the screen; a hover fix for conditional blocks.
taw/coretaw-gutenbergtaw-themefix

October 2026 — TAW data popup, centered

Tagged taw/core v1.73.2, TAW Gutenberg v0.3.38 and taw-theme v1.12.39.

  • The TAW data popup opens as a dialog in the middle of the screen instead of hanging off the block toolbar. Close it with the × in its header, with Escape, or by clicking outside it.
  • The box that opens when you click a value in the text (TAW value) has a × too.
  • Hovering a conditional block in the editor no longer shows a big blue box. The "Conditional" label stays in its corner.
2026-09-30taw/core v1.73.1 / taw-gutenberg 0.3.37 / taw-theme v1.12.38: taw/core fully translated into Spanish (Mexico).
taw/coretaw-gutenbergtaw-themei18n

September 2026 — Spanish (Mexico), complete

Tagged taw/core v1.73.1, TAW Gutenberg v0.3.37 and taw-theme v1.12.38.

  • Every taw/core string now has an es_MX translation (631 of 631; 247 were missing). This covers TAW Data (import and export), Edición TAW, TAW Media folders, SEO & Social, knowledge bases, forms and page passwords, and the data panel's controls.
  • The TAW Editing screen's layer headings (Site structure, Design, Editor features) are now translatable too.
2026-09-30taw/core v1.73.0 / taw-gutenberg 0.3.36 / taw-theme v1.12.37: the function picker; expressions docs and examples.
taw/coretaw-gutenbergtaw-themefeaturedocs

September 2026 — The function picker

Tagged taw/core v1.73.0, TAW Gutenberg v0.3.36 and taw-theme v1.12.37. This completes formulas and functions in expressions.

  • ƒ Functions in the Expression tab lists every function by group, with what it does and an example; your site's own functions appear under This site. Typing @ suggests functions too (@cur → @currency(…)).
  • A misspelled function gets a hint: "Did you mean @round(…)?"
  • @ago(@book_year) counts calendar years for a year: 1994 is "32 years ago" this year.
  • Docs: Formulas and functions, Links and images, Your own functions, and the function reference.
  • TAW Gutenberg's Single book template: "First published 1994, 32 years ago", an awards heading that counts ("3 awards"), and a More by button that links to a search for the author.
2026-09-30taw/core v1.72.0 / taw-gutenberg 0.3.35 / taw-theme v1.12.36: expressions in links and images.
taw/coretaw-gutenbergtaw-themefeature

Tagged taw/core v1.72.0, TAW Gutenberg v0.3.35 and taw-theme v1.12.36.

  • Links: a button's link can be an expression: /?s=@urlencode(@post.title), mailto:…, @coalesce(@book_buy_link, @option.store_url). Unsafe or empty links keep the button's own link.
  • Images: an image block can show the image an expression names, such as the cover or a placeholder: @coalesce(@book_cover, 49). Its alt text comes along. Images attached to posts a visitor can't read are left out.
  • In the TAW data popup, pick Block text, then Use it for: a button's text or link, an image's image or alt text.
  • A TAW Loop's Order by has Custom expression…, e.g. @(0 - @row.year) for the newest first.
2026-09-30taw/core v1.71.0 / taw-gutenberg 0.3.34 / taw-theme v1.12.35: about 40 new expression functions, and your own.
taw/coretaw-gutenbergtaw-themefeature

September 2026 — Expression functions

Tagged taw/core v1.71.0, TAW Gutenberg v0.3.34 and taw-theme v1.12.35.

  • Numbers and money: @(@price * 1.16).currency('MXN') → $116.58, @number(1234567.8), @percent(0.25), round, min, max…
  • Dates: @ago(@book_year) → "62 years ago", @until(@event_date), @days_between(…), @add_days(@date.today, 10), @weekday(@post.date)… in your site's language and time zone.
  • Text: capitalize, words, replace, strip, slug, plural(@count(@book_awards), 'award', 'awards') → "3 awards"…
  • Lists: @count(@book_awards), @join(@column(@book_awards, 'name'), ' / '), @terms('genre'), sort, contains, sum, avg… Repeaters, related posts and files read as lists.
  • Your own functions: a theme registers them in PHP with the taw_expression_functions filter, and they work everywhere expressions do.
  • The TAW data popup's Fields tab now lists Visitor and date values (logged in, role, today, now).
2026-09-30taw/core v1.70.0 / taw-gutenberg 0.3.33 / taw-theme v1.12.34: formulas and if() in expressions.
taw/coretaw-gutenbergtaw-themefeature

September 2026 — Formulas and if() in expressions

Tagged taw/core v1.70.0, TAW Gutenberg v0.3.33 and taw-theme v1.12.34. Everything you've written so far keeps working the same way.

  • Formulas: @( … ) does arithmetic and comparisons: Item @(@loop.index + 1) of @loop.count, @(@price * 1.16).
  • If: @if(@stock > 0, 'In stock', 'Sold out') chooses text. Also coalesce() (the first value that isn't empty) and empty().
  • Formulas work in text, block text, visibility rules (@(@book_year + 1) · is · 1966) and a loop's order.
  • The Expression tab shows problems only the site can find, like dividing by zero or doing arithmetic with text, and has @( formula ) and @if( … ) buttons.

More functions (numbers and money, dates, text, lists), expressions in links and images, and a function picker come next.

2026-09-30taw/core v1.69.3 / taw-gutenberg 0.3.32 / taw-theme v1.12.33: Performance loads only when used.
taw/coretaw-gutenbergtaw-themeinternal

September 2026 — Performance loads only when used

Tagged taw/core v1.69.3, TAW Gutenberg v0.3.32 and taw-theme v1.12.33.

  • TAW\Support\Performance is now loaded by the class autoloader instead of on every request. Nothing changes for themes: the same hooks register, and TAW_PERFORMANCE_AUTOLOAD still works.
2026-09-30taw/core v1.69.2 / taw-gutenberg 0.3.31 / taw-theme v1.12.32: repeater rows keep gradient text and HubSpot form edits.
taw/coretaw-gutenbergtaw-themefix

September 2026 — Repeater rows keep every edit

Tagged taw/core v1.69.2, TAW Gutenberg v0.3.31 and taw-theme v1.12.32.

  • Inside a repeater row, a gradient text or HubSpot form change that didn't come from typing (for example, removing a segment) is now saved in the block editor.
2026-09-29taw/core v1.69.1 / taw-gutenberg 0.3.30 / taw-theme v1.12.31: TAW Loop docs and book template examples.
taw/coretaw-gutenbergtaw-themedocs

September 2026 — TAW Loop: docs and examples

Tagged taw/core v1.69.1, TAW Gutenberg v0.3.30 and taw-theme v1.12.31. See TAW Loop.

  • An empty loop without a No items block now shows nothing at all, including a heading inside it, so a section disappears when it has nothing to show.
  • TAW Gutenberg's Single book template shows the book's Genres, its Awards (with "No awards yet.") and More books like this (same genre, not this book).
2026-09-29taw/core v1.69.0 / taw-gutenberg 0.3.29 / taw-theme v1.12.30: the TAW Loop in the editor.
taw/coretaw-gutenbergtaw-themefeature

September 2026 — TAW Loop in the editor

Tagged taw/core v1.69.0, TAW Gutenberg v0.3.29 and taw-theme v1.12.30.

  • Insert TAW Loop from the inserter: choose what to loop over, the field (or taxonomy, or post type), and a starting design: List, Cards or Start blank.
  • Live previews: the first item is the one you edit; every other item shows its real values, and they refresh as you edit.
  • Sidebar: order by any value, show at most, skip, items per page with page links, a filter built with conditions, list or grid.
  • Inside an item, the TAW data popup lists its Row and Loop values first.
2026-09-29taw/core v1.68.0 / taw-gutenberg 0.3.28 / taw-theme v1.12.29: the TAW Loop blocks (server side).
taw/coretaw-gutenbergtaw-themefeature

September 2026 — TAW Loop (server side)

Tagged taw/core v1.68.0, TAW Gutenberg v0.3.28 and taw-theme v1.12.29. A TAW Loop repeats a design for each item:

  • Sources: a repeater's rows, related posts (a post select field), a query (post type, genres like this post's, author, search…), terms, and a gallery's images.
  • Item values: @row.name, @row.year… and @loop.index, @loop.first, @loop.last in values, expressions, bindings and conditions. A post item is the post, so its title, date and fields just work.
  • Options: order by any value, limit, offset, filter with conditions, pagination (each loop pages on its own), list or grid, loops inside loops, and a "no items" state.
  • Up to 200 items per loop. Each theme gains two hooks.

This release is the front end; the editor (setup, sidebar, live previews) comes next.

2026-09-26taw/core v1.67.0–v1.67.1 / taw-gutenberg 0.3.26–0.3.27 / taw-theme v1.12.27–v1.12.28: conditions in the editor.
taw/coretaw-gutenbergtaw-themefeature

September 2026 — Conditions in the editor

Tagged taw/core v1.67.0 and v1.67.1, TAW Gutenberg v0.3.26–v0.3.27 and taw-theme v1.12.27–v1.12.28. See Conditions.

  • Show only when… in the TAW data popup's Expression tab and in a value's editor: a rule builder (value · comparison · what to compare with; all or any; groups) with Otherwise show text.
  • Visibility tab in the popup, and TAW visibility in every block's sidebar: show a whole block only when rules hold.
  • A live answer, Shown / Hidden for this post; conditional blocks and values are marked in the canvas.
  • TAW Gutenberg's book template hides the subtitle when there is none, shows "First published …" only with a year, and adds a Buy this book button (a new book_buy_link field) that shows only with a link.
  • v1.67.1: in the Site Editor, conditions are checked against the latest post of the template's type.
2026-09-26taw/core v1.66.0 / taw-gutenberg 0.3.25 / taw-theme v1.12.26: conditions (server side).
taw/coretaw-gutenbergtaw-themefeature

September 2026 — Conditions

Tagged taw/core v1.66.0, TAW Gutenberg v0.3.25 and taw-theme v1.12.26. A chip, a block's bound text, or a whole block can show only when rules hold:

  • “First published 1965” only when the year is set; a Buy button only when the buy link isn't empty; a “New” badge only for posts from the last 30 days.
  • 23 comparisons: empty / not empty, text (equals, contains, starts or ends with), lists (is one of, has), numbers (greater or less than, between), dates (before, after, on, between; with today or -30 days), yes / no.
  • Rules combine with All or Any, one group deep. A hidden chip can show other text instead.
  • New values: @viewer.logged_in, @viewer.role, @date.today, @date.now.
  • Each theme gains one hook; content without conditions renders as before.

This release is the server side. The rule builder in the TAW data popup comes next.

2026-09-26taw/core v1.65.1 / taw-gutenberg 0.3.24 / taw-theme v1.12.25: chip popover fixes.
taw/coretaw-gutenbergtaw-themefix

September 2026 — Chip popover fixes

Tagged taw/core v1.65.1, TAW Gutenberg v0.3.24 and taw-theme v1.12.25.

  • A chip's popover closes after Save or Refresh, and on Escape or a click elsewhere. Before, it flickered and stayed open.
  • Removing a chip (Remove, Backspace or Delete) no longer throws an error in the editor.
2026-09-26taw/core v1.63.0–v1.65.0 / taw-gutenberg 0.3.20–0.3.23 / taw-theme v1.12.22–v1.12.24: dynamic tags and expressions.
taw/coretaw-gutenbergtaw-themefeature

September 2026 — Dynamic tags and expressions

Tagged taw/core v1.63.0, v1.64.0 and v1.65.0. See Dynamic tags and expressions.

  • Values inside a sentence: “Published on September 26, 2026 by Frank Herbert”. Each value is an atomic chip in the editor and always live on the front end, with the same privacy rules as Block Bindings.
  • Expressions: text and values in one string, e.g. Published on @post.date.format('F j, Y') by @post.author, with format, upper, lower, default and truncate.
  • The TAW data popup (the block toolbar's database button): a Fields tab grouped by fieldset and an Expression tab with suggestions, a live preview and error hints. Insert inline or use as the block text.
  • TAW Gutenberg's book template shows “Published on …” with an expression.
  • Hook lists gain three lines in total; content without tags renders exactly as before.
2026-09-25taw/core v1.60.0–v1.62.0 / taw-gutenberg 0.3.16–0.3.18 / taw-theme v1.12.19–v1.12.21: Block Bindings.
taw/coretaw-gutenbergtaw-themefeature

September 2026 — Block Bindings: TAW fields in core blocks

Tagged taw/core v1.60.0, v1.61.0 and v1.62.0, TAW Gutenberg v0.3.16–v0.3.18 and taw-theme v1.12.19–v1.12.21. See Block Bindings.

  • v1.60.0: a taw/field Block Bindings source. Paragraphs, headings, list items, buttons, images and post dates can show post, option, term and author fields, in templates, patterns and Query Loops too. Fields can opt out with "bindings": false; user fields opt in with "bindings": true.
  • v1.61.0: the editor side. Bound blocks preview their real value, a TAW field toolbar button (and ⋮ → Connect to TAW field…) binds a block in one pick, and the Attributes panel lists TAW fields.
  • v1.62.0: editing policies gain allowBound: blocks clients may add only bound to a field, offered as Field text, Field image… in the inserter.
  • TAW Gutenberg ships templates/single-book.html, built from bound core blocks, as the reference.
  • Both themes' hook lists gain three lines in total; nothing renders differently until a block uses a binding.
2026-09-25taw/core v1.59.2 / taw-gutenberg 0.3.15 / taw-theme v1.12.18: fixes from the first fleet upgrade.
taw/coretaw-gutenbergtaw-themefix

September 2026 — Fixes from the first fleet upgrade

Tagged taw/core v1.59.2, TAW Gutenberg v0.3.15 and taw-theme v1.12.18. Seven TAW sites were upgraded to v1.59.1 with the upgrade guide, and these are the problems they found:

  • taw-core's Spanish translation now loads in classic themes. From v1.56.0 to v1.59.1 only a site's own .po translated taw-core text.
  • php bin/taw wp db … works under Local, and bin/taw wp tells you when the Local site is stopped.
  • bin/taw sync keeps a site's own skill when a framework skill with the same name appears, and reports the clash.
  • Metabox and options-page tabs work by keyboard. They look the same.
  • UPGRADING.md has clearer checks:
    • a command that finds group values stored under the wrong key, and how to fix them;
    • form checks that don't send email;
    • notes on the optional Reactiph and chatbot starter features.
2026-09-25taw/core v1.59.0 / taw-gutenberg 0.3.13 / taw-theme v1.12.16: a link field type.
taw/coretaw-gutenbergtaw-themefeature

Tagged taw/core v1.59.0, TAW Gutenberg v0.3.13 and taw-theme v1.12.16.

  • link is a new field type: a URL, its text and “open in a new tab”, edited together and stored as one value. It works in metaboxes, options pages, repeater rows, term and user screens and the data panel. Relative paths like /contact work, and required means a URL.
  • echo Taw::post()->field('cta') prints the escaped <a>; ->link() gives its parts. See Reading fields.
  • Existing fields render and save exactly as before.
2026-09-25taw/core v1.58.0 / taw-gutenberg 0.3.12 / taw-theme v1.12.15: typed reads for terms, users and options.
taw/coretaw-gutenbergtaw-themefeature

September 2026 — Typed reads everywhere

Tagged taw/core v1.58.0, TAW Gutenberg v0.3.12 and taw-theme v1.12.15.

  • Taw::term() (the queried term when called without an ID), Taw::user(), Taw::option('company_phone') and Taw::options('site') read fields the same way as Taw::post(). See Reading fields.
2026-09-25taw/core v1.57.0 / taw-gutenberg 0.3.11 / taw-theme v1.12.14: typed, escaped field reads with Taw::post().
taw/coretaw-gutenbergtaw-themefeature

September 2026 — Taw::post()

Tagged taw/core v1.57.0, TAW Gutenberg v0.3.11 and taw-theme v1.12.14.

  • TAW\Taw::post()->field('x') returns a typed value: ->image(), ->rows(), ->posts(), ->bool(), ->text(), and echo escaped for the field's type. In a MetaBlock, $this->fields($postId) does the same. See Reading fields.
  • Nothing throws: a missing post or field reads as empty. The existing helpers are unchanged.
  • Updating an older site? See Upgrading taw/core on an existing site.
2026-09-25taw/core v1.56.0 / taw-gutenberg 0.3.10 / taw-theme v1.12.13: taw-core has its own text domain and ships its own translations.
taw/coretaw-gutenbergtaw-themefeature

September 2026 — taw-core translations

Tagged taw/core v1.56.0, TAW Gutenberg v0.3.10 and taw-theme v1.12.13.

  • Every taw-core string uses the taw-core text domain. Before, most used the theme's taw-theme domain, and the form and password-gate messages used a taw domain that nothing loaded, so they were always English.
  • Translations ship with the package (languages/) and load on their own in the site's language. Spanish (es_MX) starts with the strings TAW sites already translated. The data panel gets them too.
  • Your theme's translation wins. When a theme's languages/es_MX.po translates a taw-core string, that wording keeps showing, so no site loses or changes a translation. See Translations.
  • One visible change on Spanish sites: the form message "%s is required." is now translated.
2026-09-25taw/core v1.55.0 / taw-gutenberg 0.3.9 / taw-theme v1.12.12: options pages can be exposed over REST, per page.
taw/coretaw-gutenbergtaw-themefeature

September 2026 — Options pages over REST

Tagged taw/core v1.55.0, TAW Gutenberg v0.3.9 and taw-theme v1.12.12.

  • rest: "private" adds an options page's values to /wp/v2/settings, for administrators, with the page's own validation and sanitizing.
  • rest: "public" also serves them read-only at GET /wp-json/taw/v1/options/<page id>, to anyone. Use it only for pages with nothing private on them.
  • Pages without rest expose nothing, as before. See Options pages over REST.
2026-09-25taw/core v1.54.0 / taw-gutenberg 0.3.8 / taw-theme v1.12.11: fieldsets on user profiles.
taw/coretaw-gutenbergtaw-themefeature

September 2026 — Fields on users

Tagged taw/core v1.54.0, TAW Gutenberg v0.3.8 and taw-theme v1.12.11.

  • "on": ["user"] shows a fieldset on Profile, Edit User and Add New User, with every field type, conditions and validation. See Fields on users.
  • Over REST, user fields appear only to people who can edit that user. They never show on an author's public /wp/v2/users/<id>.
  • Content snapshots carry them with --with-users.
2026-09-25taw/core v1.53.1 / taw-gutenberg 0.3.7 / taw-theme v1.12.10: unchecking a checkbox hides its conditional fields again.
taw/coretaw-gutenbergtaw-themefix

September 2026 — Checkbox conditions

Tagged taw/core v1.53.1, TAW Gutenberg v0.3.7 and taw-theme v1.12.10.

  • Fix: in the metabox form, unchecking a checkbox now hides the fields whose conditions depend on it, at the top level and inside repeater rows. Saving was already correct.
2026-09-25taw/core v1.53.0 / taw-gutenberg 0.3.6 / taw-theme v1.12.9: fieldsets on taxonomy terms.
taw/coretaw-gutenbergtaw-themefeature

September 2026 — Fields on terms

Tagged taw/core v1.53.0, TAW Gutenberg v0.3.6 and taw-theme v1.12.9.

  • "on": ["term:genre"] shows a fieldset on a taxonomy's Add and Edit screens, alone or next to post types, with every field type, conditions and validation. See Fields on terms.
  • Read values with Metabox::get_term() or Metabox::term($id). They're in REST on the term, and content snapshots carry them.
  • Under the hood, the metabox engine now reads and writes through a storage context (post, term or user meta). Post metaboxes render and save byte-for-byte as before.
2026-09-25taw/core v1.52.1 / taw-gutenberg 0.3.5 / taw-theme v1.12.8: content import finds drafts and private posts by slug from the CLI.
taw/coretaw-gutenbergtaw-themefix

September 2026 — Import matches drafts

Tagged taw/core v1.52.1, TAW Gutenberg v0.3.5 and taw-theme v1.12.8.

  • Fix: bin/taw content:import runs without a user, so it couldn't see drafts and private posts, and planned them as new posts. It now matches them by slug, and references to them (parent, page_on_front, comments) resolve.
2026-09-25taw/core v1.52.0 / taw-gutenberg 0.3.4 / taw-theme v1.12.7: qualified field ids, and content snapshots carry fields with any prefix.
taw/coretaw-gutenbergtaw-themefeaturefix

September 2026 — Qualified field ids

Tagged taw/core v1.52.0, TAW Gutenberg v0.3.4 and taw-theme v1.12.7.

  • Every field also has a qualified id, "{fieldset}.{field}". REST meta, content export and import, the data panel, the visual editor and fields:get / fields:set look fields up per post type, so two fieldsets can share a field id and each keeps its own type and sanitizer. See Field ids.
  • Content interchange 1.2: fields with a prefix other than _taw_ are exported, keyed by their meta key. _taw_ fields keep their bare-id keys, and the importer still reads 1.0 and 1.1 files. See Content Interchange.
  • Fix: fields:set and the importer wrote group sub-fields to the wrong meta key (_taw_{sub} instead of _taw_{group}_{sub}).
2026-09-25taw/core v1.51.1 / taw-gutenberg 0.3.3 / taw-theme v1.12.6: icon fields work in block themes.
taw/coretaw-gutenbergtaw-themefix

September 2026 — Icon fields in block themes

Tagged taw/core v1.51.1, TAW Gutenberg v0.3.3 and taw-theme v1.12.6.

  • Boot::data() now registers the icon search (/taw/v1/icons) when Lucide::enable() has been called. Before, only Theme::boot() did, so icon fields in TAW Gutenberg showed a picker that couldn't load icons, in the metabox and in the data panel.
  • Nothing changes for sites that don't call Lucide::enable(), and taw-theme behaves exactly as before.
2026-09-25taw/core v1.51.0 / taw-gutenberg 0.3.2 / taw-theme v1.12.5: the data panel shows TAW fields in a block-editor sidebar, off by default.
taw/coretaw-gutenbergtaw-themefeaturefix

September 2026 — The data panel

Tagged taw/core v1.51.0, TAW Gutenberg v0.3.2 and taw-theme v1.12.5.

  • The data panel shows fieldsets in a “TAW Data” sidebar in the block editor instead of as metaboxes under the canvas. See Data panel.
    • It's off by default. Switch it on per fieldset ("ui": "panel"), for the site (a settings schema file with fieldsetUi), or per install (TAW_DATA_UI in wp-config.php).
    • Every field type works, including groups, repeaters (with nested ones) and conditions. Values are stored exactly as the metabox stores them.
    • wysiwyg fields open a small block editor in a modal and keep the classic editor's storage format, so templates render them as before.
    • The server checks required, validate callbacks and read-only fields on block-editor saves. In the editor, saving is locked while a required field is empty.
  • Fix: repeater values written outside the metabox form (REST, the visual editor, fields:set, content:import) no longer lose nested repeaters, or files and multiple post_select values, inside rows.
  • Fix: REST writes of gradient_text and hubspot_form are cleaned the way the metabox cleans them (for example, an empty HubSpot region becomes na1).
  • Both themes only update their taw/core lock. The panel stays off in both, and their hooks are unchanged.
2026-09-25taw/core v1.50.0 / taw-gutenberg 0.3.1 / taw-theme v1.12.4: themeBlocks keeps a theme's own blocks available when the editor is locked down.
taw/coretaw-gutenbergtaw-themefeature

September 2026 — Theme blocks in locked-down editors

Tagged taw/core v1.50.0, TAW Gutenberg v0.3.1 and taw-theme v1.12.4.

  • themeBlocks is a new editing-policy key: your theme's own blocks, as names or globs (["taw-gutenberg/*"]).
    • They're added to every allow list, the curated one included, so clients at guided and above can still insert them.
    • Levels that allow every block are unchanged, and it still applies when TAW_EDITING_PRESET sets the level.
    • See Editing policies.
  • TAW Gutenberg 0.3.1 ships themeBlocks: ["taw-gutenberg/*"], so its Callout block stays available to locked clients.
  • Tools → TAW Editing shows the theme blocks and warns when a pattern matches no registered block.
2026-09-25taw/core v1.49.0–v1.49.1 / taw-gutenberg 0.3.0 / taw-theme v1.12.3: a shared Vite adapter, and custom blocks with Vite and TypeScript in TAW Gutenberg.
taw/coretaw-gutenbergtaw-themefeature

September 2026 — Vite for block themes

Tagged taw/core v1.49.0 and v1.49.1, TAW Gutenberg v0.3.0 and taw-theme v1.12.3.

  • TAW\Core\Assets\Vite (v1.49.0) is a Vite adapter any TAW theme can use:
    • script(), style() and url(), plus block(), which loads a block.json's file: assets through Vite;
    • scripts load as ES modules and keep their translations;
    • without a build, the site still renders and administrators see what's missing.
  • One dev-server check for both loaders: the check that only trusts the project's own dist/hot file now lives in Assets\DevServer, and the classic ViteLoader uses it too. taw-theme behaves exactly as before.
  • Shared Vite config in vendor/taw/core/resources/vite/taw-vite.mjs: wordpressExternals() (use the React and @wordpress/* copies WordPress already loads), hotFile() and phpReload(). v1.49.1 makes it tree-shakeable, so a block keeps only the names it imports (the example block went from 16.56 kB to 1.71 kB).
  • TAW Gutenberg 0.3.0 builds custom blocks with Vite and TypeScript, and ships an example Callout block. create-project builds them for you. See TAW Gutenberg.
2026-09-24taw/core v1.45.0–v1.48.0 / taw-gutenberg 0.2.0–0.2.1: editing policies lock the block editor down for a client, layer by layer, with presets from open to locked.
taw/coretaw-gutenbergfeature

September 2026 — Editing policies

Tagged v1.45.0 to v1.48.0, with TAW Gutenberg v0.2.0 and v0.2.1. An editing policy decides how much a client can change in the block editor, in four layers: page content, site structure, design tokens and editor features. See Editing policies.

  • Presets: open → guided → structured → locked. Override any layer or single setting in taw-schema/editing.json (new JSON kind editing), with Schema::editing() in PHP, or per post type with PostType::editing(). schema:validate checks the file.
  • Per install: TAW_EDITING_PRESET in wp-config.php picks the level. TAW_EDITING_BYPASS_USERS names who stays unlocked, even when clients are Administrators. TAW_EDITING_OFF turns everything off.
  • Enforced on the server: a save that adds a block the policy doesn't allow gets a 400, and writes to locked templates, template parts, Global Styles or Navigation get a 403. At locked, the Site Editor is hidden and refused.
  • Content-only pages on WordPress 7.1 (v1.48.0): the editor now ignores a page-level contentOnly lock, so taw/core locks the layout and switches each block to content-only editing with a small script.
  • Tools → TAW Editing shows the policy in effect, whether you bypass it, and warnings.
  • Opt-in: the policy applies only when a theme calls \TAW\Core\Boot::editing(). TAW Gutenberg does, and ships the open preset. taw-theme doesn't, and its hooks are unchanged.
2026-09-23taw-theme v1.12.1: composer create-project no longer prints a warning about a missing .env.example file.
taw-themefix

September 2026 — taw-theme install fix

Tagged v1.12.1. composer create-project taw/theme no longer prints a copy() warning. The install script tried to copy .env.example to .env, but the theme ships no such file and nothing reads a .env, so the step is gone.

2026-09-23taw-create 0.1.0: one command starts either TAW theme. composer create-project taw/create asks for taw-theme or taw-gutenberg.
taw-createtaw-gutenbergfeature

September 2026 — One installer for both TAW themes

taw-create 0.1.0 is a new installer package. Composer can't pass arguments to a project's scripts, so the choice comes from a prompt, or from TAW_STARTER when there's no terminal:

cd wp-content/themes
composer create-project taw/create my-theme \
  --repository='{"type":"vcs","url":"https://github.com/Relmaur/taw-create"}'
  • Pick a theme: taw-theme (classic, the default when non-interactive) or taw-gutenberg (block theme). TAW_STARTER=theme|gutenberg skips the question, and TAW_STARTER_VERSION pins a version.
  • Safe by design: it never touches a folder that already has other files in it. A failed install leaves your folder as it was, and no shell is involved.
  • TAW Gutenberg is now tagged v0.1.0, with a GitHub Release, so it installs as a stable version.
  • The direct commands (composer create-project taw/theme …, taw/gutenberg …) still work.
2026-09-23taw/core v1.44.0 / taw-theme v1.12.0 / taw-gutenberg 0.1.0: define your data model as JSON in taw-schema/, validate it without WordPress, and meet TAW Gutenberg, the new block theme.
taw/coretaw-themetaw-gutenbergfeature

September 2026 — JSON schema definitions, and TAW Gutenberg

Tagged v1.44.0, with a paired taw-theme release tagged v1.12.0. It covers the whole data-layer series (v1.42.0–v1.44.0) plus the branding and PHP-requirement cleanups below.

  • JSON definitions: post types, taxonomies, fieldsets and options pages can live as one JSON file each in a taw-schema/ folder (child theme, parent theme, or wp-content/). PHP definitions outrank every file. An invalid file is skipped with a notice. See Data layer.
  • php bin/taw schema:validate checks those files without WordPress, pointing at each problem (/fields/2/type). resources/schema/taw-schema-1.0.json gives editors autocomplete.
  • TAW Gutenberg 0.1.0: a new block (full site editing) theme that uses only taw/core's data layer. See TAW Gutenberg.
  • Scope: taw/core is now documented as serving TAW sites only (taw-theme, taw-gutenberg and their child themes).
  • taw-theme: registers schema:validate in bin/taw. The README and style.css no longer spell out “TAW”, and both now state the real PHP requirement (8.2+).
2026-09-23taw/core v1.43.0: a schema registry — define post types, taxonomies, fieldsets and options pages in PHP; they compile into the existing Metabox and OptionsPage engines.
taw/corefeature

September 2026 — Schema registry

Tagged v1.43.0. Define your content model in one place:

add_action('taw_schema_register', function (\TAW\Core\Schema\Registry $schema): void {
    $schema->add(Schema::postType('book')->labels('Book', 'Books'));
    $schema->add(Schema::taxonomy('genre')->for('book')->labels('Genre', 'Genres'));
    $schema->add(Schema::fieldset('book_details')->on('book')->fields([Field::text('book_author')]));
});

Fieldsets become regular Metaboxes and options pages become OptionsPages, so storage, the admin UI, REST meta and content export behave exactly like hand-written ones. Post types default to public + show_in_rest, and always get custom-fields support (WordPress hides registered meta from REST without it). Invalid keys are rejected up front, field-id collisions are reported, and rewrite rules flush once, on the next admin request, when post types change.

2026-09-23taw/core v1.42.0: Boot::data(), a data-only entry point for TAW block themes. Performance optimizations now register on boot, not at autoload.
taw/corefeature

September 2026 — Boot::data() and a data layer for block themes

Tagged v1.42.0. taw/core is now split into a data layer (fields, REST meta, content interchange) and the classic-theme toolkit (PHP blocks, Vite, Alpine, performance, visual editor):

  • \TAW\Core\Boot::data() boots only the data layer, for block themes. Theme::boot() calls it too, so classic sites are unchanged.
  • Behavior note: Performance optimizations (which dequeue the block library CSS, among other things) used to register the moment Composer loaded the package. They now register in Theme::boot() / bootstrapFullSite(). taw-theme sites see no difference (verified: identical hooks, in identical order). If a setup relied on the old behavior without ever calling boot(), define TAW_PERFORMANCE_AUTOLOAD as true in wp-config.php.
  • Framework::url() resolves package assets from a child theme too. dump()/dd() no longer clash with other libraries. ext-pdo_sqlite is now optional (only the RAG and corpus features use it).
2026-09-20taw/core v1.41.0 / taw-theme v1.11.14: fix — the documented Metabox 'tabs' option never rendered; a tabbed metabox now shows its tab UI instead of a flat field list.
taw/coretaw-themebugfix

September 2026 — Metabox tabs now render

Metabox accepted a tabs config (documented in the README and on the Metabox pages), but render() never called render_tabs() — it was dead code, so a tabbed metabox silently rendered as a flat field list. Tagged v1.41.0, with a paired taw-theme release tagged v1.11.14.

render() now resolves each tab's fields and renders the tab UI. Fields no tab lists stay visible above the tab bar rather than being dropped (saving walks every field, so an unposted field would be blanked); a tab whose fields match nothing is skipped, and a metabox with no usable tab renders flat. Every tab's inputs stay in the DOM — only the active panel is shown — so the metabox still saves as one form and conditional fields keep working.

Behavior note: a site whose Metabox already carried a tabs config now gets the tab UI it configured instead of a flat list.

2026-09-20taw/core v1.40.0 / taw-theme v1.11.13: Form::register() gains per-form 'class' / 'button_class' options and emits field width as a --taw-span custom property instead of an inline grid-column. taw-theme: new figma-fidelity and figma-image-crop-fix skills.
taw/coretaw-themefeature

September 2026 — Form styling hooks + Figma fidelity skills

Tagged v1.40.0, with a paired taw-theme release tagged v1.11.13. Flagged while pixel-matching a Figma Contact form: the submit button was hardcoded taw-btn taw-btn-primary with no per-form hook, and per-field width was an inline grid-column: span N that no stylesheet rule could beat without !important.

Form::register([
    'id'           => 'contact',
    'class'        => 'contact-form',   // appended to the <form>'s `taw-form`
    'button_class' => 'btn-black',      // appended to `taw-btn taw-btn-primary` (submit, and Next on multi-step)
    'fields'       => [...],
]);

Both options add to the built-in classes, never replace them. A field's width is now the --taw-span custom property, turned into grid-column by form.css, so a theme can override the column with an ordinary selector. Behavior note: every grid cell (fields, html, heading, divider) now collapses to full width below 640px — previously only fields did, and the rule needed !important.

taw-theme gains two canonical Claude Code skills distilled from a long Figma-exact pass on a client site: figma-fidelity (measure the live DOM against Figma's own numbers on desktop and mobile frames, close gaps, report drift) and figma-image-crop-fix (derive the exact pan/zoom or object-fit: fill stretch treatment for an image, including the object-position derivation for fluid containers). AGENTS.md § "CSS entry points" now documents that critical.scss is unlayered and beats Tailwind utilities — use modifier classes, not utilities, to vary above-the-fold blocks.

2026-09-15taw/core v1.39.0 / taw-theme v1.11.12: Form::register() gains a submit_icon option — a Lucide icon name or raw SVG/HTML rendered after the submit button's label.
taw/coretaw-themefeature

September 2026 — submit_icon on Form::register()

Form::renderSubmitButton() hardcoded its markup to a spinner + label span, with no slot for an icon — any site wanting a non-default submit button icon had to fork Form.php. Tagged v1.39.0, with a paired taw-theme release tagged v1.11.12. Flagged while pixel-matching a Contact form's submit button (a trailing "send" icon) against a Figma spec.

Form::register([
    'id'           => 'contact',
    'submit_label' => 'Send message',
    'submit_icon'  => 'send',
    'fields'       => [...],
]);

submit_icon accepts a Lucide icon name (rendered via Lucide::render() — no Lucide::enable() needed, same as any direct template call to it) or raw '<svg>...</svg>'/HTML, printed as-is inside a decorative <span class="taw-btn-icon" aria-hidden="true">, after the label. Nothing renders when unset, empty, or an unrecognized icon name.

2026-09-15taw/core v1.37.1 / taw-theme v1.11.11: fix — gradient_text destroyed the word-boundary space around a highlighted phrase sitting mid-sentence, its primary use case.
taw/coretaw-themebugfix

September 2026 — gradient_text destroyed word-boundary whitespace

sanitizeGradientTextValue() ran each segment's text through sanitize_text_field(), whose own trim() silently ate a segment's leading/trailing space — exactly where it sits whenever a highlighted phrase is mid-sentence rather than the trailing segment, gradient_text's primary use case (e.g. "Three ways we help you own more and rent less."). renderGradientText() concatenates segments with no separator of its own, so the words rendered smashed together with no space at all. Tagged v1.37.1, with a paired taw-theme release tagged v1.11.11. Reported (with a clean repro) while migrating real heading+highlight pairs on a client site to gradient_text — no content had shipped on the buggy behavior yet.

Fixed by restoring a single boundary space wherever the original segment text had one — never anything else from the original string, so the fix can't reopen anything sanitize_text_field() just closed.

2026-09-15taw/core v1.37.0 / taw-theme v1.11.10: fields:set/fields:get gain an 'options' scope for site-wide OptionsPage fields, plus two new field types — gradient_text (multi-segment gradient headings) and hubspot_form (HubSpot embed config).
taw/coretaw-themefeature

September 2026 — OptionsPage support for fields:set/get, plus gradient_text and hubspot_form

fields:set/fields:get only ever accepted a numeric post ID and wrote via update_post_meta() — there was no CLI path to write a site-wide OptionsPage field, despite the CLI's own docs claiming Metabox/OptionsPage support. Tagged v1.37.0, with a paired taw-theme release tagged v1.11.10.

Pass the literal options in place of the post ID to target a site-wide field instead — resolved via new OptionsPage::getFieldConfig() (a bare-id lookup) and written via new OptionsPage::writeOption() (update_option(), sanitized through the same Metabox::sanitizeForStorage() Content\Importer already uses for options-registry values):

php bin/taw fields:set options company_phone "555-1234"

Also two new field types, wired into both Metabox and OptionsPage:

  • gradient_text — an ordered list of {text, highlighted} segments, replacing the old convention of a plain heading field plus a single always-trailing highlight field (which can't express a heading like "How can we help?", where the highlighted run isn't last). Render with Metabox::renderGradientText($segments, $highlightClass).
  • hubspot_form — {portal_id, form_id, region}, rendered via new TAW\Core\Integrations\Hubspot::render()/isConfigured(), for a HubSpot embed with a fallback to the site's native Form block when unconfigured.
2026-09-14taw/core v1.33.1 / taw-theme v1.11.9: new taw_critical_css_entry filter on ViteLoader's critical CSS inlining, so a full-page CPT template with its own above-the-fold hero can swap in a different critical file per request.
taw/coretaw-themefeature

September 2026 — per-request override for critical CSS via taw_critical_css_entry

enqueueThemeAssets()'s call to ViteLoader::inlineCriticalCss() hardcoded resources/scss/critical.scss with no way to override it — a client theme building full-page event-invite microsites with three distinct visual templates, each needing its own correctly-styled above-the-fold hero, had no way to load a different critical file without one site-wide critical.scss covering every template's layout at once. Tagged v1.33.1, with a paired taw-theme release tagged v1.11.9.

The call site now resolves through apply_filters('taw_critical_css_entry', 'resources/scss/critical.scss') — purely additive, unhooked behavior unchanged. A theme can now do:

add_filter('taw_critical_css_entry', function (string $entry) {
    return is_singular('event_invite') ? 'resources/scss/critical-event-invite.scss' : $entry;
});
2026-09-14taw/core v1.33.0 / taw-theme v1.11.8: on_submit callbacks now receive the new submission's post ID as a second, optional argument; SubmissionsHandler::saveSubmission() also records a _taw_user_agent meta key.
taw/coretaw-themefeature

September 2026 — on_submit callbacks now receive the submission's post ID

Form::process() called SubmissionsHandler::saveSubmission() but discarded its return value entirely, so an on_submit callback only ever received the submitted $data — never the ID of the taw_submission record just created for it, with no way to correlate a callback's own side effects back to that record without re-querying by form ID and a timestamp guess. Tagged v1.33.0, with a paired taw-theme release tagged v1.11.8.

The callback's signature is now function(array $data, int|false $postId): void — false if the save itself failed. Every existing on_submit callback written against the old one-argument signature keeps working unchanged: PHP silently ignores extra arguments passed to a closure declaring fewer parameters than it's invoked with. Also new: saveSubmission() now records a _taw_user_agent meta key from $_SERVER['HTTP_USER_AGENT'], alongside the existing _taw_user_ip/_taw_page_url — generic technical context useful for any form submission, not specific to one theme.

2026-09-14taw/core v1.32.1 / taw-theme v1.11.7: fix — rich-text metabox/options-page values left raw HTML entities (e.g. ") for the browser to decode back into JS-breaking characters inside Alpine's x-data JSON; also adds the Catechism Reader Corpus (v1.32.0) to README.md.
taw/coretaw-themebugfix

September 2026 — rich-text metabox values left raw HTML entities for the browser to decode

Metabox::render()'s and OptionsPage::render_repeater_row()'s Alpine x-data seed values came straight from get_post_meta()/get_option() — for a wysiwyg (or any rich-text) field, that value already carries HTML entities baked in (a literal " saved as &quot;). esc_attr() left that &quot; in the rendered markup, and the browser HTML-decoded it right back into a raw, JSON-breaking " inside x-data — a silent Alpine initialization failure on any block with a rich-text field. Tagged v1.32.1, which also documents the Catechism Reader Corpus (v1.32.0, below) in README.md; a paired taw-theme release tagged v1.11.7 picks up both.

Fixed by decoding entities (html_entity_decode(..., ENT_QUOTES | ENT_HTML5, 'UTF-8')) before seeding both metabox and options-page repeater initial values, so the x-data JSON sees the raw value a <textarea>/<input> would actually expose via .value.

2026-09-14taw/core v1.32.0: new Catechism Reader Corpus — edition-parameterized part→section→chapter→question/answer reader mirroring the Bible corpus's SQLite/MySQL dual-backend architecture, applying every lesson from that corpus's production incidents from day one.
taw/corefeature

September 2026 — Catechism Reader Corpus, edition-parameterized from day one

A generic TAW\Core\Corpus\Catechism\* namespace mirroring Bible Reader Corpus's architecture one hierarchy level deeper — part → section → chapter → numbered question/answer, matching a catechism's own structure. Landed via Relmaur/taw-core#28, tagged v1.32.0.

Every reader/installer/endpoint method takes an $edition slug (registered in CatechismEditions, currently just pius-x — the Catechism of Saint Pius X) rather than assuming a single installed catechism, unlike the Bible corpus's single-corpus design — adding a second edition later (Saint John Paul II's, say) needs one new registry entry plus an installed file, no new code. The MySQL fallback ships alongside SQLite from the start this time, rather than arriving as a follow-up after a production incident: innodb_ft_min_token_size short-word filtering and checked $wpdb queries with real post-import row counts are both built in from day one. One genuine improvement over the Bible installer: since Catechism's MySQL tables are shared across editions (edition + source_id columns), reloading one edition uses a scoped DELETE rather than TRUNCATE — DML, not DDL, so the transaction wrapping around a reload is a real cross-table atomicity guarantee, not just a best-effort one.

2026-09-11taw/core v1.31.2 / taw-theme v1.11.6: fix — MysqlBibleInstaller could report a clean success even when the MySQL import actually wrote nothing, since every $wpdb->query() call went unchecked and the CLI's success message was built from the input file's own row counts rather than anything read back from MySQL.
taw/coretaw-themebugfix

September 2026 — MysqlBibleInstaller could report false success

A production install against WPMUdev/MariaDB 10.6.23 once printed a clean [OK] Imported 73 books, 1334 chapters... while GET /wp-json/taw/v1/bible/books kept 404ing with "No Bible corpus is installed" — the table didn't exist at all. A second run genuinely succeeded. The transient root cause was never pinned down, but the reporting bug was real and deterministic regardless: the success message was built from the input JSON's own row counts, never anything read back from MySQL, and every $wpdb->query() call ran unchecked — $wpdb->query() returns false on failure and doesn't throw, so a silently-failed statement just fell through to a normal return. Landed via Relmaur/taw-core#27 (closes #26), tagged v1.31.2, with a paired taw-theme release tagged v1.11.6.

Every query in MysqlBibleInstaller now throws (with $wpdb->last_error) on a false result, and install() returns row counts read back via COUNT(*) after the import — corpus:install reports those, catching a real failure as a clean CLI error instead of a false success or a raw stack trace. Also documented: TRUNCATE TABLE is DDL on InnoDB/MariaDB and causes an implicit commit, so the transaction wrapping around the five-table reload was never a true cross-table atomicity guarantee — the loud-failure behavior is the real safety net now, not the transaction.

2026-09-11taw/core v1.31.1 / taw-theme v1.11.5: fix — MySQL FULLTEXT boolean-mode search silently dropped words shorter than innodb_ft_min_token_size (default 3), so any Spanish query containing de/la/el/en/un/... returned zero results. Confirmed against a real MySQL server.
taw/coretaw-themebugfix

September 2026 — MySQL FULLTEXT search ignored short/unindexed words

Verified against a real MySQL server (a client theme's session): books()/chapter()/full corpus import all confirmed byte-for-byte correct, but +amor +de +Dios returned zero results even though three verses genuinely contain all three words — +amor +Dios (dropping "de") returned the correct three. Landed via Relmaur/taw-core#25 (closes #24), tagged v1.31.1, with a paired taw-theme release tagged v1.11.5.

InnoDB's FULLTEXT indexer never indexes a word shorter than innodb_ft_min_token_size (default 3) at all — a required +word term for a word that was never indexed can never match, so any AND query containing a short word (Spanish is full of them: de/la/el/en/un/...) returned nothing regardless of whether the rest of the query genuinely matched. Not fixable via server config on managed hosting (a global my.cnf setting requiring a restart and full index rebuild). MysqlBibleReader::significantWords() now reads the actual configured innodb_ft_min_token_size at query time and drops any word shorter than it from the required set before searching — the same "silently ignore, don't exclude everything" handling a real search engine applies to short/stop words.

2026-09-11taw/core v1.31.0 / taw-theme v1.11.4: MySQL-backed fallback for the Bible Reader Corpus when pdo_sqlite isn't available (confirmed absent on real WPMUdev managed hosting) — corpus:export produces a portable JSON export, corpus:install now accepts either format, BibleEndpoint resolves the right backend transparently.
taw/coretaw-themefeature

September 2026 — MySQL-backed fallback for the Bible Reader Corpus

bin/taw corpus:install against the real ~18MB Straubinger corpus file on a live client site (WPMUdev managed hosting) fataled with PDOException: could not find driver — pdo_sqlite/sqlite3 are missing from both PHP-FPM and CLI on that host, on two PHP versions, and WPMUdev declined to add them. $wpdb (mysqli) is guaranteed on every WordPress host since WP core itself can't function without it; pdo_sqlite is not guaranteed anywhere. Landed via Relmaur/taw-core#23 (closes #22), tagged v1.31.0, with a paired taw-theme release tagged v1.11.4.

  • ProtectedSqlite::isAvailable() — real capability detection: extension_loaded('pdo_sqlite') and a real sqlite::memory: connection attempt, since a loaded extension isn't always functional on every host build.
  • TAW\Core\Corpus\Bible\BibleReaderInterface extracted (deferred in the original design until a second implementation existed to shape it against — that point has arrived); BibleReader implements it with zero internal changes, so every host where pdo_sqlite already works sees no behavior change at all.
  • MysqlBibleReader — an independent, MySQL-backed mirror of the same read contract: books()/chapter() via $wpdb joins, searchVerses()/searchNotes() via MySQL boolean-mode FULLTEXT (+word1 +word2, translating the same AND-of-words semantics the SQLite search fix already established) with hand-built <mark> excerpts (MySQL has no snippet()).
  • bin/taw corpus:export <sqlite-path> <json-path> — dumps a corpus to portable JSON on a machine that has pdo_sqlite, for installing on a target host that doesn't.
  • bin/taw corpus:install — now content-sniffs .sqlite vs. portable JSON and routes to the matching backend; a .sqlite source on a host without pdo_sqlite now fails with a clear message pointing at corpus:export, instead of a bare PDOException.
  • BibleEndpoint resolves SQLite first (unconditionally, whenever available and installed), MySQL otherwise.

See Bible Reader Corpus for the full reference, and docs/adr/0002-corpus-mysql-fallback.md in taw-core for the architectural reasoning. Rag\Storage's vector chunks remain pdo_sqlite-only — a MySQL-backed vector store is a plausible follow-up, flagged as separate, harder scope (continuous writes from ongoing post saves don't fit the export-once-to-JSON approach this fix relies on).

2026-09-11taw/core v1.30.0 / taw-theme v1.11.3: the RAG chatbot is now opt-in (RagSettings::enable()), matching every other optional subsystem — settings page, knowledge bases, WP-content ingestion, and POST taw/v1/chat all stay off by default.
taw/coretaw-themefeature

September 2026 — RAG chatbot becomes opt-in

Theme::boot() unconditionally wired the entire Sovereign Hybrid-RAG Chatbot subsystem — every TAW site got the Settings → TAW Chatbot page, WP-content ingestion, and the public POST taw/v1/chat endpoint whether or not the developer wanted it. Lucide, MediaFolders, and the Bible Reader Corpus's BibleEndpoint all follow an explicit enable() opt-in flag; the chatbot — arguably the subsystem with the highest bar to actually need (an LLM endpoint + TAW_RAG_API_KEY) — was the one exception. Landed via Relmaur/taw-core#20 (closes #19), tagged v1.30.0, with a paired taw-theme release (Relmaur/taw-theme main, tagged v1.11.3).

  • RagSettings::enable()/isEnabled() — same shape as Lucide::enable()/MediaFolders::enable()/BibleEndpoint::enable(). Call it in inc/customizations.php before Theme::boot().
  • taw-theme's Blocks/Chatbot widget now checks RagSettings::isEnabled() in both header.php (asset enqueue) and footer.php (render) — previously it always rendered and would have POSTed to a REST route that no longer registers on a site that hasn't opted in.
  • inc/customizations.php gets a commented-out RagSettings::enable(); line, matching the Lucide/icon field type's posture (off by default, one line to turn on).
2026-09-11taw/core v1.29.3 / taw-theme v1.11.2: two bugs found running bin/taw corpus:install on a live client site — ProtectedSqlite/Corpus\Storage silently killed pre-boot CLI usage, and corpus:install was never registered in taw-theme's bin/taw. Also fixed: Bible search required an exact phrase match.
taw/coretaw-themebugfix

September 2026 — corpus:install pre-boot fix, bin/taw registration, and Bible search matching

Two bugs found running bin/taw corpus:install against the real ~18MB Straubinger corpus file on a live client site, plus one more found while fixing them. Landed via Relmaur/taw-core#18 (closes #17), tagged v1.29.3; bin/taw registration landed directly on taw-theme main, tagged v1.11.2.

  • ProtectedSqlite/Corpus\Storage silently killed pre-boot CLI usage. Both carried if (!defined('ABSPATH')) exit;, but CorpusInstallCommand::execute() calls ProtectedSqlite::looksLikeSqliteFile() before require $wpLoad — so the guard fired and exited the whole process with zero output. The exact pitfall this repo's own CLAUDE.md already documents for Content\* (v1.25.1). Fixed by dropping the guard from both, matching Content\*'s precedent — neither class has a WordPress dependency of its own. A subprocess regression test (Storage\PreBootAutoloadTest, mirroring Content\PreBootAutoloadTest) now runs with no ABSPATH defined at all, the one test shape that can actually catch this.
  • corpus:install was never wired into bin/taw. The command class shipped in taw-core v1.29.0's src/CLI/, but canonical taw-theme's bin/taw never got the matching registration (compare content:reindex-kb, which got both a class and a registration when it shipped) — the command was unreachable from any fresh checkout.
  • Bible search required an exact contiguous phrase. BibleReader::escapeFtsPhrase() wrapped the whole query as one FTS5 literal phrase, so searchVerses('Dios amor') silently returned nothing unless those words were adjacent in that exact order — which fails for almost any realistic multi-word search. Every word is now quoted independently and ANDed together.
2026-09-11taw/core v1.29.0: Bible Reader Corpus — a framework-level, developer-installed reference-corpus subsystem (storage, CLI install, a Straubinger-schema Bible reader, and a rate-limited REST surface), built for a client site's Bible reader.
taw/corefeature

September 2026 — Bible Reader Corpus

A read-only REST surface over a developer-installed reference corpus — currently a Straubinger-translation Spanish Catholic Bible, built for a client site's Bible reader (theme-side page/Block shipping separately) but framework-level and reusable at the storage layer. Landed via Relmaur/taw-core#16 (closes #15), tagged v1.29.0.

  • TAW\Core\Storage\ProtectedSqlite — the RAG chatbot's "protected uploads directory + exception-mode PDO open + magic-byte SQLite validation" plumbing, extracted and shared rather than duplicated for this new subsystem.
  • TAW\Core\Corpus\Storage — wp-content/uploads/taw-private/corpus/, deliberately separate from the RAG chatbot's taw-private/rag/: a reference corpus meant for direct reading and a RAG knowledge base meant for chatbot embedding are different concerns that happen to share storage mechanics.
  • php bin/taw corpus:install <path> <filename> — CLI-only, not a wp-admin upload form. A curated dataset a developer places once per environment, not end-user content.
  • TAW\Core\Corpus\Bible\BibleReader — books grouped by testament/division, per-chapter verses + overlapping section headings + overlapping notes, and FTS5 search over verses or notes. Not final — subclassable so a theme can point at a different installed filename or override read behavior.
  • GET /taw/v1/bible/books, .../books/{slug}/chapters/{n}, .../search — opt-in (BibleEndpoint::enable()), public (no auth — this is public Scripture text), rate-limited regardless. The endpoint resolves its reader through apply_filters('taw_corpus_bible_reader', ...).

See Bible Reader Corpus for the full reference, and docs/adr/0001-reference-corpus-storage.md in taw-core for the architectural reasoning behind each of these decisions.

2026-09-11taw/core v1.28.1 / taw-theme v1.11.1: fix — a post that lost RAG eligibility (unpublished, trashed, password-protected) after being indexed stayed searchable via the chatbot indefinitely. It's now de-indexed immediately.
taw/coretaw-themebugfix

September 2026 — RAG chatbot: de-index posts that become ineligible

PostIndexer::onSavePost() only ever handled the "should this post be indexed" path — a post that lost eligibility after being indexed (unpublished, moved to draft/private, trashed, or password-protected) stayed searchable through search_knowledge_base indefinitely, since the method just returned without touching its existing vectors. A real privacy gap, not just a staleness one. Landed via Relmaur/taw-core#13, tagged v1.28.1, with a matching taw-theme v1.11.1 (composer.lock only — no theme code changed).

  • Eligibility now also excludes password-protected posts outright (post_password === ''), not just publish status + indexed post type.
  • Any post that isn't (or is no longer) eligible has its vectors removed immediately, inline — unlike the embed-on-publish path, a delete has no external API call to avoid blocking on, so there's no reason to defer it to WP-Cron.
2026-09-11taw/core v1.28.0: Sovereign Hybrid-RAG Chatbot — content-agnostic knowledge bases. Upload any .sqlite file in wp-admin; a single search_knowledge_base tool answers from it, the site's own content, or both.
taw/coretaw-themefeature

September 2026 — Sovereign Hybrid-RAG Chatbot: content-agnostic knowledge bases

A visitor-facing chat widget that answers from any number of named knowledge bases — the site's own WordPress content, plus any .sqlite file an admin uploads from wp-admin — with an OpenAI-compatible LLM doing semantic search across whichever one a question calls for. Landed via Relmaur/taw-core#12 (closes #11), tagged v1.28.0, with a paired taw-theme release (Relmaur/taw-theme#3).

This supersedes v1.27.0 (#10, tagged the same day), which shipped the same chat widget and REST endpoint but with the reference-data layer hardcoded to two specific schemas (lookup_bible/lookup_catechism). On review that was the wrong shape — v1.27.0 was superseded before any real consumer integrated against it. If you're on v1.27.0, upgrade straight to v1.28.0; there's no migration path from the old schema-specific tools, only a clean replacement.

  • Content-agnostic by design. No particular schema is assumed or required of an uploaded .sqlite file — every table is scanned, every column with SQLite TEXT affinity is extracted, chunked, embedded, and made searchable. A table with no text column is skipped.
  • Settings → TAW Chatbot → Knowledge Bases — upload a .sqlite file with a name and description; that's the entire setup. Ingestion runs via WP-Cron, never on the upload request. The upload handler validates the SQLite magic-byte header before accepting a file.
  • The site's own content is just another knowledge base — a built-in, non-deletable wp-content entry, unified into the same registry and the same search tool as uploaded ones, rather than a separate dedicated tool.
  • One tool, not three. search_knowledge_base(knowledge_base, query) replaces the old lookup_bible/lookup_catechism/search_unstructured_archive trio. Its knowledge_base enum is built fresh from the registry on every request, so a newly-uploaded knowledge base is searchable the moment ingestion finishes.
  • Settings → TAW Chatbot — LLM base URL/model, indexed post types for the site's own content, chunk size/overlap, max tool-call iterations, and whether anonymous visitors can chat. The LLM API key is wp-config-constant-only (TAW_RAG_API_KEY), never an options-table field.
  • POST /wp-json/taw/v1/chat — public by default, defended by unconditional rate limiting (20 requests/10 min per IP) rather than a login requirement.
  • Blocks/Chatbot (taw-theme) — a site-wide floating widget, unchanged by this redesign since it only depends on the /taw/v1/chat request/response shape. Markdown replies render via marked, sanitized with DOMPurify.

Downstream: content:import-reference is gone (upload replaces it); content:reindex-kb <id> is the new manual re-ingestion escape hatch for one uploaded knowledge base, alongside the existing content:reindex for the site's own content.

2026-09-10 (pending release)taw/core v1.26.0: Content Interchange becomes a whole-site state migration path — post authorship, opt-in users / comments / environment settings, a --migrate shorthand, Metabox-backed CPT auto-export, and a slug-less-draft fix.
taw/coretaw-themefeature

September 2026 — Content Interchange → whole-site state migration

Content Interchange now moves a site's state, not just its content. All new behaviour is opt-in and backward compatible; the importer accepts schema 1.0 and 1.1. Landed via Relmaur/taw-core#8 (closes #7), tagged v1.26.0, with a paired taw-theme release.

Acceptance criterion: content:export --migrate then content:import --yes against the same site reports 0 created / 0 updated / 0 deleted.

  • Post authorship. Every exported post now carries author ({login, email} — never a numeric ID) plus comment_status / ping_status. On import the author resolves by login, then email, then falls back to the importing user with a warning.
  • --with-users exports a users[] section (login, email, display_name, roles[], profile meta, user_registered). Password hashes only with the separate --with-user-passwords. Import matches by login → email, creates if absent, and sanitises roles against the roles the target site actually defines — a role the target lacks is never granted.
  • --with-comments exports comments on the exported posts with parent_ref threading; import remaps them to their post by slug, rebuilds comment_parent, and recomputes counts.
  • --with-settings exports a second, opt-in option allowlist for environment settings — permalink_structure, timezone_string, sticky_posts (as slugs), date_format, and more — plus a taw_content_export_settings_options filter. These are import-gated too: content:import skips them unless you also pass --with-settings.
  • content:export --migrate = --with-users --with-settings --all-media --include-drafts (not passwords or comments — those stay explicit). The automatic rollback snapshot is now maximal-scope, so undoing a --migrate import is complete.
  • Slug-less drafts no longer duplicate on re-import. draft / pending / auto-draft posts are excluded by default; --include-drafts re-adds them and matches slug-less posts on a composite (type, sha1(title|date_gmt)) key.
  • Metabox-backed CPTs auto-export. Any post type with a TAW\Core\Metabox\Metabox attached is now included even when public => false. Sites that added a manual taw_content_export_post_types filter just for a content CPT (for example an event schedule) can drop it.
  • --all-media also exports attachments not referenced by any post/field; media records now carry title / description, set on the sideloaded attachment.
  • Import ordering is deterministic and dependency-first: users → terms → media → posts → comments → settings.
  • content:diff diffs the new users and comments sections.
  • The Tools → TAW Data export screen gains checkboxes for the new scopes (passwords / comments behind an "Advanced" disclosure). GET /wp-json/taw/v1/content/export stays content-only — no user or settings export over REST.
  • Downstream: the taw-theme scaffold bin/taw already registers content:*. Any site keeping an owner: site content-sync skill should update its pull / push sections for the new --with-* / --migrate flags.
2026-09-10 (pending release)taw/core v1.25.1: content-interchange round-trip fixes — content:import (and content:diff) no longer print nothing, and export → import of the same site is now a verified no-op.
taw/coretaw-themebugfix

September 2026 — Content interchange round-trip fixes

Three bugs in the v1.25.0 Content Interchange, all reproduced live against a populated site and regression-tested. Landed via Relmaur/taw-core#6 (closes #5), tagged v1.25.1.

  • content:import printed nothing (exit 0, no plan table, no "no changes" line). ContentImportCommand references a TAW\Core\Content\* class before require wp-load.php; that autoloaded the class file, whose if (!defined('ABSPATH')) exit; guard fired (WordPress not booted yet) and silently killed the process. The guard is removed from the six Content\* classes the content:* commands load before boot — they are pure class definitions, like TAW\Helpers\Framework / TAW\CLI\WpLoader, which already omit it. This also fixes content:diff, which was silent for the same reason.
  • page_on_front / page_for_posts broke the round-trip. The exporter renders them as page slugs (portable); the importer compared the slug against the stored integer ID → a false "changed", and an apply would have written the slug into page_on_front, breaking the static front page. The importer now resolves these back to the local post ID before both the diff and the write. post.parent (slug) and featured_media (filename) were audited — already resolved on write; featured_media is now also diffed by filename.
  • Empty / default values showed as changes. A field stored as "" (or absent, or at its default) compared unequal to the same empty value in the snapshot. Both sides now normalize through FieldCodec::decode plus a canonical key that collapses "" / null / [] / false / missing to one token, and aligns "1" ↔ true, "[…]" ↔ the decoded array, "42" ↔ 42.

Also: Importer::apply() now skips records the dry-run shows unchanged, so a clean export → import writes nothing and re-running an import doesn't churn post_modified dates; a stray "Undefined array key policy" warning is gone; and content:import --json no longer trails the "Dry run" note after the JSON.

taw-theme picks up taw/core v1.25.1 (v1.9.1).

2026-09-10 (pending release)taw/core v1.25.0: portable content interchange — a Content Exporter/Importer, a Tools → TAW Data screen, bin/taw content:export | content:import | content:diff, and REST-registered field meta for headless integrations.
taw/coretaw-themefeature

September 2026 — Portable content interchange

TAW sites had no first-class way to move content between environments or to hand it to a code agent. wp export (WXR) doesn't understand _taw_* meta; a full DB copy is all-or-nothing and needs SSH. taw/core v1.25.0 generalizes TAW's own serialize → review → apply pattern (seo:extract/inject, fields:get/set) to whole-site content.

TAW\Core\Content\Exporter builds a portable JSON snapshot: meta (schema 1.0 + source + a block/field registry_fingerprint for drift warnings), options (every _taw_* decoded, plus an allowlisted core set with page_on_front/page_for_posts resolved to slugs), terms, posts (pages/posts + public CPTs except taw_submission; every _taw_* field decoded by its registered type), and media (referenced attachments, keyed by filename). Users, revisions, comments, transients and nav_menu are never exported. Scope with --types / --since / --posts / --no-media.

TAW\Core\Content\Importer consumes a snapshot or a change-set (content:diff emits one). Records match by natural key — never numeric ID. Media is matched by filename or sideloaded, and the old → new ID map is rewritten into post_content and image/files values before any write. plan() is a mandatory dry-run (field-level diff, zero writes); apply() writes a full rollback snapshot to wp-content/uploads/taw-private/ first, then writes meta through the new Metabox::writeMeta() primitive. Per-record conflict policy update / create / skip.

wp-admin: Tools → TAW Data — an Export button and an Import flow that shows the dry-run diff as a review table with an explicit "Apply N changes" (no one-click apply). Plus GET /wp-json/taw/v1/content/export (capability export).

REST-registered field meta: Theme::boot() now exposes every TAW field over wp/v2 — register_post_meta() for scalars (with the field's sanitizer and an edit_post auth_callback), register_rest_field() for the decoded repeater/files/post_select shapes, and register_setting(… show_in_rest) for OptionsPage fields. This unblocks headless front-ends and external tooling. It does not add a mobile editing UI. Opt out with add_filter('taw_register_meta_in_rest', '__return_false').

Also: Metabox::writeMeta() / sanitizeForStorage() — one shared meta-write primitive (sanitize + wp_slash + update_post_meta, matching Metabox::save()); fields:set moved onto it, fixing a latent bug where a repeater value containing " or \ was corrupted by update_post_meta's internal wp_unslash.

taw-theme picks up taw/core v1.25.0 and registers the three content:* commands in bin/taw (v1.9.0). Landed via Relmaur/taw-core#4 (closes #3).

See Content Interchange.

2026-09-09 (pending release)taw/core v1.24.0: user-enumeration hardening — the public /wp/v2/users REST routes are now hidden from anonymous requests by default, closing the ?rest_route=/wp/v2/users leak that host WAFs miss. The taw-theme scaffold gains a site-owned inc/security.php.
taw/coretaw-themesecurity

September 2026 — User-enumeration lockdown

A client-site incident surfaced a gap that host WAFs and “hide users endpoint” plugins routinely leave open: they match only the /wp-json/wp/v2/users path form, so GET /?rest_route=/wp/v2/users (the query-routed REST form) still returns 200 and leaks id, name, and the author slug (≈ login name) for every user on the site.

taw/core v1.24.0 adds TAW\Core\Security\Hardening::hideUsersEndpoint(), wired into Theme::boot() by default. It filters at rest_endpoints — REST dispatch, after the route is resolved — so one filter closes every routing form at once: /wp-json/, ?rest_route=, and /batch/v1.

  • Only anonymous requests are affected. Any logged-in user keeps the full API, so the block editor's /wp/v2/users?who=authors author selector still works for Editors (who lack list_users).
  • /wp/v2/users/me always stays available — the mobile apps, Jetpack, and the block editor need the authenticated self-lookup, and it already returns 401 without a login.
  • Escape hatch for headless / integration sites: add_filter('taw_security_hide_users_endpoint', '__return_false').

taw-theme scaffold gains inc/security.php — site-owned (never touched by update-theme, same as inc/performance.php), required from inc/customizations.php. It calls the new core helper and adds a parse_request handler that 301-redirects logged-out ?author=N probes to the home page. That redirect stays scaffold-level on purpose: it is site policy, it kills author-archive query URLs, and it overlaps with security-plugin behavior.

See Security / Hardening.

2026-09-03 (pending release)taw-hub-companion 0.2.0: the plugin now self-updates from its GitHub releases — 'Update available' on every site, plus opt-out background auto-updates, so fleet-wide companion updates need no SSH or per-site zip upload
taw-hub-companionfeature

September 2026 — The companion keeps itself current

Every companion release used to mean building a plugin zip and uploading it to each fleet site by hand — the managed hosts have no shell and no proc_open, so the CLI update path didn't reach them.

taw-hub-companion 0.2.0 adds a self-updater (src/Update/Updater.php, self-contained — no taw/core, mirrors TAW\Core\Theme\ThemeUpdater). It feeds WordPress's plugin-update transient from the plugin's own GitHub releases:

  • Every site shows the standard "Update available" row; wp plugin update taw-hub-companion works.
  • Unless TAW_HUB_COMPANION_AUTO_UPDATE is false in wp-config.php, the plugin opts itself into WordPress's background auto-update cron — a tagged release reaches the whole fleet within a cron cycle (~12 h), no clicking.

Releases are now tagged (vX.Y.Z). A CI workflow builds the plugin zip (git archive, dev files stripped via .gitattributes, no vendor/) and publishes it as the release asset the updater points at; the tag must match both the Version: header and TAW_HUB_COMPANION_VERSION. A site on a pre-0.2.0 build has no updater and needs one manual bump to 0.2.0, then self-maintains.

The Hub-driven "push this update to the fleet now" route is a separate follow-up.

See TAW Hub Companion.

2026-09-03 (pending release)taw/core v1.23.0: new php bin/taw hub:enroll command — one-shot site enrolment into a TAW Hub fleet, with cryptographic verification of the Hub's signed reply
taw/corefeature

September 2026 — One-command fleet enrolment

Joining a site to a TAW Hub fleet used to end with a manual step: read the site's public key and key id, then register them on the Hub by hand. php bin/taw hub:enroll closes that gap.

It reads the identity the taw-hub-companion plugin generated on activation (taw_hub_companion_public_key / _key_id), the Hub URL (TAW_HUB_URL), and a one-time enrolment token minted on the Hub (--token, or TAW_HUB_ENROLMENT_TOKEN), and POSTs them to the Hub's POST /api/fleet/enroll (taw-hub ADR-0011).

That endpoint isn't signature-guarded — the Hub doesn't know the site yet, so the token is the credential (single-use, 30-minute TTL, hashed at rest). But every response is signed with the Hub's Ed25519 identity, and hub:enroll verifies that signature against TAW_HUB_PUBLIC_KEY before trusting the reply. A signed 409 site_already_enrolled counts as idempotent success (a dropped connection after the Hub consumed the token); a signature-verification failure is a hard stop — enrolment is reported as not confirmed.

--base-url sets the URL the Hub reaches the site at (home_url() is often wrong behind a proxy or Herd's :80), --dry-run prints the request without sending, --insecure skips TLS verification for a local self-signed Hub. The hub-connect skill now runs this automatically on taw/core ≥ v1.23.0.

See TAW Hub Companion.

2026-09-03 (pending release)taw-hub-companion 0.1.7: two new signed routes — GET /vulnerabilities (surfaces the site's own scanner findings from Defender or Wordfence, no paid feed) and GET /inventory/checksums (per-component file manifest for integrity + backdoor-on-update diffing)
taw-hub-companionfeature

September 2026 — Fleet vulnerabilities and file integrity

Two more slices of the fleet-security work, both on the companion side.

Vulnerabilities, without a paid feed

ADR-0013 slice 2 was going to have the Hub call a paid vulnerability API (WPScan). It doesn't need to: every fleet site already runs a security scanner — Defender Pro on the WPMU DEV sites, Wordfence elsewhere — matching core, plugins and themes against a vulnerability database continuously, with severity and CVSS.

GET /wp-json/taw-hub/v1/vulnerabilities — signed, read-only, DB-read-only. A per-scanner read adapter (src/Security/) reads the scanner's stored results (it never triggers a scan) and normalizes them; ScannerRegistry picks whichever scanner the site runs.

  • Defender / Defender Pro — reads the defender_scan / defender_scan_item tables: vulnerability items (one finding per bug, with CVSS), plugins closed on wp.org (→ removed), and abandoned plugins (→ abandoned). Verified against Defender 6.2.4. Checked first — it's the fleet standard.
  • Wordfence — reads the wfIssues table for known vulnerabilities, abandoned plugins, and directory removals; reports last_scan_at so the Hub can spot a stale scanner. Verified against Wordfence 8.2.x.

Both adapters degrade to an empty list on any schema change.

Each finding: { component_type, slug, installed_version, severity, cvss_score, kind, link, detected_at }, where kind separates a live vulnerability from the weaker abandoned / removed / outdated signals. Plain "update available" stays out — that's /inventory's job.

File integrity and backdoor-on-update diffing

GET /wp-json/taw-hub/v1/inventory/checksums — a per-component SHA-256 file manifest. Ground truth for the Hub to detect a webshell dropped into a plugin folder, diff one version of a component against the next on update (the "quiet backdoor on update" signal from the overview — new phone-home, new application passwords, a new drop-in), and dedupe fleet-wide analysis by (slug, version, tree_hash).

Summary mode (no slug) returns just tree_hash + file_count per active component — a cheap "did anything change" poll; detail mode (?slug=) adds the full relpath → sha256 map. Only executable / script file types are hashed; node_modules and .git are skipped; symlinks are not followed. tree_hash has a pinned canonical form (byte-sorted "<relpath>:<sha256> " lines, sha256'd) so the Hub can recompute it from a wordpress.org release zip and compare.

Both routes are subprocess-free and work on every host. The Hub-side ingest, storage, security.vuln.detected event, dashboards and version-diff analysis land under ADR-0013.

See TAW Hub Companion.

2026-09-02 (pending release)taw-hub-companion 0.1.3: new signed GET /inventory route — a security-focused SBOM the Hub polls for fleet vulnerability correlation and abandoned-plugin detection
taw-hub-companionfeature

September 2026 — Fleet inventory: /inventory on the companion

The first slice of a fleet-security initiative: giving the TAW Hub the visibility to defend a fleet against the WordPress plugin threats that AI-accelerated exploitation has made sharper — known-vuln plugins, quiet supply-chain backdoors, and abandoned plugins left installed for years.

taw-hub-companion 0.1.3 adds a signed, read-only, subprocess-free GET /wp-json/taw-hub/v1/inventory route — a software bill of materials for the site: every plugin, must-use plugin, drop-in and theme, with the metadata the Hub needs to correlate against vulnerability feeds, spot abandoned components, and flag pending updates.

The signal that isn't in wp plugin list: update_source per component — wordpress_org / external / disabled / unknown, i.e. who, if anyone, is watching this for security updates. unknown — in none of WordPress's update channels — is the abandoned-plugin tell.

Works on every host (like /health and /logs, it never spawns a subprocess). The response shape is frozen as the Hub's ADR-0013 contract and pinned by a schema_version; the companion mirrors the Hub's inventory-snapshot.schema.json into its own test fixtures and validates its output against it, so the two implementations can't silently drift. +11 tests (78 total).

The Hub-side half — snapshot storage, a daily poll, a flattened component projection, and an /inventory dashboard with a fleet pending-update digest and end-of-life-PHP flags — lands under the same ADR. Vulnerability-feed correlation and abandoned-component risk scoring are the next slices.

See TAW Hub Companion.

2026-09-02 (pending release)taw/core: MetaboxOrder::lockFromTemplate() now resolves page-{slug}.php and home.php templates, not just explicitly-selected ones and front-page.php
taw-corefix

September 2026 — Metabox order follows the full template hierarchy

MetaboxOrder::lockFromTemplate() derives a page's metabox order from the BlockRegistry::render() sequence in the template that will render it. It previously resolved only two cases: an explicitly-selected page template (_wp_page_template) and front-page.php for the static front page. Pages on WordPress's filename conventions — page-{slug}.php and home.php (the posts page) — write no template meta, so their edit screens fell back to raw metabox registration order.

Resolution now mirrors WordPress's own hierarchy for all four cases, tried highest-priority first: _wp_page_template → front-page.php → home.php → page-{slug}.php. The rules live in one shared method, Metabox::templateCandidatesForPost(), used by both MetaboxOrder and the screens template matching in Metabox, so the two can no longer drift apart.

Backward-compatible — pages that already ordered correctly are unaffected. See Locking Metabox Order.

2026-09-01taw-hub-companion 0.1.1–0.1.2: first live fleet enrolments (dev + production), TawRunner hardened for php-fpm and proc_open-disabled managed hosts
taw-hub-companionfix

September 2026 — First sites on the TAW Hub fleet

Two sites enrolled end to end — a local dev site and a production site (WPMU DEV managed hosting). Signed /health, /logs, and /framework/sync all verified over the wire, including over the public internet through a managed host's edge layer.

The rollout hardened TawRunner (which backs /framework/sync and /taw):

  • 0.1.1 — resolve a real CLI php. PHP_BINARY is php-fpm under the SAPI that serves the Hub's requests, so bin/taw never actually ran; now it resolves a CLI interpreter beside the SAPI binary / on PATH, override with taw_hub_companion_php_binary.
  • 0.1.2 — degrade cleanly where proc_open is disabled (most managed hosts). /framework/sync and /taw return 503 exec_unavailable; /health reports "exec_available": false; /health and /logs keep working (they never shell out). Error responses no longer leak stack traces or server paths.

See TAW Hub Companion.

2026-09-01 (pending release)taw/core: a new structured Logger replacing scattered error_log() calls, plus bin/taw log:tail; taw-hub-companion gains a signed /logs route
taw-coretaw-hub-companionfeature

September 2026 — Structured logging for the framework

taw/core gains TAW\Core\Log\Logger, an always-on structured log facade that replaces the hand-rolled error_log('[TAW …] …') calls the framework used internally (in Form, Turnstile, EmailConfig, Svg).

Every entry carries both a human message and a machine-stable, dot-namespaced code (subsystem.event — form.email_delivery_failed, mail.emailit_send_failed, svg.sanitizer_library_missing, …), plus a structured context array. The code is the part an AI agent or the TAW Hub filters on; the message is what a person reads.

Two sinks are active by default: PHP's error_log() (one readable line) and wp-content/taw-logs/taw.log.jsonl (size-rotated JSON Lines). Read it back with the new php bin/taw log:tail (--level, --code, --since, --json), or via TAW\Core\Log\LogReader. Level helpers are PSR-3 minus alert/emergency. Extend with the taw_core_log_sinks and taw_core_log_entry filters.

Theme code (blocks, inc/, custom on_submit callbacks) should use Logger instead of error_log() from now on.

taw-hub-companion adds a signed, read-only GET /wp-json/taw-hub/v1/logs route that serves this file to the Hub (filters: limit ≤ 500, level, code prefix, since), so fleet operators can see a site's errors without shell access.

See Logging and TAW Hub Companion → Routes.

2026-08-27 (pending release)taw/core: update-theme now preserves site-authored Claude/agent skills across a framework sync instead of deleting them
taw-coretaw-themefeature

August 2026 — Site-authored skills survive update-theme

.claude/skills/ and .agents/skills/ are framework-owned scaffold paths, so update-theme (and the weekly framework-sync.yml) used to sync them with rsync -a --delete — which silently destroyed any skill a client site had authored for its own workflows, since it wasn't in the canonical taw-theme repo.

Those two paths are now reconciled one skill folder at a time instead. A skill present in the canonical repo is refreshed to the canonical copy every sync, exactly as before. A skill folder that exists only in your project is resolved by the owner: key in its SKILL.md YAML frontmatter:

SKILL.md frontmatterWhat the sync does
owner: sitePreserved untouched, and named in the sync report
owner: tawDeleted — a framework skill that was retired upstream
no owner: keyPreserved, but flagged in the report for a human to resolve

Every framework skill in the canonical repo now carries owner: taw; this is also how you tell framework and site skills apart at a glance (grep -rl 'owner: site' .claude/skills).

Migrating an existing site skill: add owner: site to its SKILL.md frontmatter and commit it to your project repo. No manifest edit and no per-site config file — the marker travels with the skill. Until it's marked, the sync reports it under a "needs a human decision" warning rather than deleting it.

The skills-dir manifest type and the owner: marker are declared in taw/core's resources/update-manifest.json under skillsReconcile; TAW\CLI\SyncCommand implements the reconcile. See AI Integration.

2026-08-23 (pending release)taw/core: Form gains image/wysiwyg fields + an on_submit hook; new PagePassword API; taw-theme ships a password-gated client post-submission portal
taw-coretaw-themefeaturesecurity

August 2026 — Form image/wysiwyg fields, on_submit hook, and PagePassword

Form gains two new field types and a way to run custom logic after a submission. image renders a file input, uploads via media_handle_upload(), and validates the result as a genuine image server-side (wp_attachment_is_image() — the accept="image/*" attribute is a client-side hint only, not the real check). wysiwyg renders WordPress's own classic editor (wp_editor(), no media buttons) — free paste-from-Word cleanup via TinyMCE's built-in paste plugin, sanitized server-side with wp_kses_post(). A new 'on_submit' => callable config key runs after a submission is saved, for anything beyond "save it and maybe email it" — most commonly, creating a real post from the submitted data.

New TAW\Core\Auth\PagePassword gates a page template behind a single shared password, declared directly in the template before any output — not a wp-admin toggle. Fails closed (denies access) rather than gracefully degrading when misconfigured, since this is access control, not a supplementary check. See Page Password Protection and On Submit Callback.

taw-theme ships a working example of both together: page-client-submit.php, a password-gated portal where a client fills in a title, WYSIWYG body, category, and featured image, and gets a draft post created for later review — enabled by default, gated on a TAW_CLIENT_PORTAL_PASSWORD constant in that site's wp-config.php.

A dedicated security pass on both new features (prompted by explicit request, not incidentally) caught and fixed four real issues before release: neither the gate screen nor the unlocked page sent nocache_headers(), meaning a full-page cache/CDN could have served a cached, unlocked page to a visitor who never entered the password; the default unlock-cookie scope id was an unsalted md5($password), visible as a cookie name — now hash_hmac('sha256', $password, wp_salt('auth')); on_submit's catch caught any \Throwable and echoed its message straight to an anonymous submitter, risking internal-detail leakage on an unexpected error — narrowed to \RuntimeException only, with anything else logged server-side and a generic message shown instead; an image field's upload was left permanently orphaned in the Media Library if on_submit subsequently rejected the submission — now cleaned up automatically. Two remaining limitations are documented rather than fixed in this pass: PagePassword doesn't restrict WordPress's own REST API for the underlying post, and rate-limiting relies on a client-supplied X-Forwarded-For header unless a trusted reverse proxy sits in front of the site.

2026-08-20 (pending release)taw/core: Media Library Grid view now auto-appends new uploads to “All Files” again, instead of requiring a manual refresh
taw-corefix

August 2026 — Grid-view sidebar: fixed uploads not auto-appending

The Media Folders Grid-view sidebar's refreshGridQuery() calls WordPress core's own library._requery() to force a re-fetch after a folder switch, sort change, or drag-and-drop move — but core's _requery() always swaps in a brand-new Backbone Query collection, and only the original collection (wired up once, at Grid boot) is bound to auto-append newly-completed uploads from wp.Uploader.queue. Since sidebar initialization itself calls refreshGridQuery() on every Grid-view page load (to apply the remembered file sort), that binding was getting dropped almost immediately — so a file uploaded while viewing "All Files" never appeared until a hard page reload.

Fixed by re-attaching library.observe(wp.Uploader.queue) to the fresh collection every time _requery() replaces it, restoring live-append behavior after every folder/sort change, not just at initial page load.

2026-08-10 (pending release)taw/core: newly-uploaded images generate as AVIF/WebP automatically, using WordPress core's own conversion — no plugin
taw-corefeatureperformance

August 2026 — Modern image formats (AVIF/WebP) via WordPress core

Performance::configure(['modern_image_formats' => true]) (the default) hooks WordPress core's own image_editor_output_format filter — added in WP 5.8, not a plugin — so newly-uploaded JPEG/PNG images generate their subsizes as AVIF, falling back to WebP, checked at runtime via wp_image_editor_supports() rather than assumed. Does nothing on a host whose image library can't encode either format.

This is a replace, not a dual-format generation — the resulting files simply are AVIF/WebP, so Image::render() and everything else built on wp_get_attachment_image_src() picks this up automatically with zero template changes. Verified against a real upload on a live site, which also caught a real bug: WordPress core calls this filter with a null filename in some code paths, which fataled the original non-nullable signature — fixed, with a regression test locking it in. See Modern image formats.

2026-08-10 (pending release)taw/core: app.css now genuinely loads async, backed by hand-authored critical CSS for the header/hero
taw-coretaw-themeperformance

August 2026 — app.css loads async; critical.scss now carries real content

ViteLoader's CSS pipeline documentation always described app.css as loading asynchronously via the media="print" swap — the code didn't actually do that until now; it was synchronous and render-blocking. Fixed to match, using the same async pattern already used for non-critical block CSS.

This makes critical.scss load-bearing rather than decorative: an empty placeholder there now produces a real flash of unstyled HTML on every page, not just a marginally slower paint. The default theme scaffold's critical.scss now carries real, hand-authored layout-only CSS for the header and Hero/Button markup — enough geometry to prevent layout shift, without needing full color/typography fidelity. A scoped Tailwind @source for critical.scss was tried first and doesn't work in this build (two independent reasons, written up in full) — hand-authored is the practical answer for now. See critical.scss must contain real content.

2026-08-10 (pending release)taw/core: Vite's hashed build output gets long-lived cache headers automatically, with an nginx fallback notice
taw-coreperformance

August 2026 — Cache headers for Vite's build assets

Performance::configure(['build_asset_cache_htaccess' => true]) (the default) writes a .htaccess scoped to Vite's own {dist}/assets/ directory, setting Cache-Control: max-age=31536000, public, immutable for every file there — safe because everything Vite writes to that directory is content-hashed. Deliberately scoped by directory rather than matched by extension in the root .htaccess, since wp-content/uploads/ images share the same extensions but aren't hashed.

This only works on Apache. On a site detected running behind nginx, a dismissible wp-admin notice now shows a logged-in admin the exact nginx config block to paste in, with the site's real theme/build path filled in — discovered live: Local by Flywheel's own default site environment runs nginx, not Apache. See Cache headers for build assets.

2026-08-05 (pending release)taw/core: readonly Metabox fields now show a lock icon next to their label
taw-coreenhancement

August 2026 — Metabox: readonly fields get a lock icon

A follow-up to the readonly field option shipped earlier the same day: the muted, non-interactive styling alone wasn't a strong enough signal that a field is intentionally locked rather than just visually broken. Every readonly field's label now gets a small lock icon (dashicon, with a "Managed externally — read-only" tooltip) — wherever the label renders, including inside group and repeater sub-fields.

2026-08-05 (pending release)taw/core: Metabox fields support a readonly option for values an external process authoritatively owns
taw-corefeature

August 2026 — Metabox: readonly fields

Any Metabox (and Options Page) field now accepts 'readonly' => true, rendering it as non-interactive text instead of an editable <input>/<select>/<textarea> and skipping it entirely in save() — a forged $_POST key is ignored regardless of what a tampered-with rendered form contained. This closes a real gap: content synced in from an external source (e.g. a git-tracked .md-file pipeline) previously still rendered as an editable field in wp-admin, inviting edits that the next sync would silently overwrite.

readonly composes with group (propagates to every sub-field) and repeater (disables Add/Remove/reorder for the whole row list, not just sub-field values, and propagates to every sub-field in every row) rather than being a field type of its own. See Readonly fields for the full breakdown, including the one deliberate gap: VisualEditorEndpoint and the fields:set/seo:inject CLI commands are separate write paths that don't consult this flag yet.

2026-08-04 (pending release)taw/core: ViteLoader now marks its module scripts and stylesheets as off-limits to third-party JS/CSS optimizers
taw-corefeature

August 2026 — ViteLoader: optimizer-exclusion hardening

Vite's compiled output is already minified, hashed, and (for JS) split into ES modules that require exact, un-mangled execution order — a generic optimization plugin re-processing it can only make things worse. ViteLoader's module scripts, their modulepreload/preload <link> tags, and every Vite-extracted stylesheet now carry data-no-optimize="1" data-cfasync="false" data-no-defer="1" data-no-minify="1" — standard exclusion signals WP Rocket, Autoptimize, Perfmatters, and LiteSpeed Cache all document and honor.

This is defensive hardening, not a fix for the specific incident that motivated it: a production site hit a hard, silent failure when the host's JS optimization plugin (WPMUdev Hummingbird) CDN-rehosted the entry bundle onto a CDN with no CORS support — module scripts always require CORS, so the browser hard-blocked the fetch and the entire bundle (Alpine.js, all core init) never ran, with zero visible page errors. Hummingbird's CDN-rehosting feature rewrites the raw HTML buffer directly, bypassing WordPress's tag filters entirely, so these new markers don't help against it specifically — that class of tool still needs manual exclusion-list configuration in its own admin UI. See Assets and Vite integration for the full incident and a fast diagnostic.

2026-08-04 (pending release)taw/core: SeoMeta::OUTPUT_MODE_FIELD lets a site owner manually override unreliable SEO-plugin auto-detection
taw-corefeature

August 2026 — SEO output mode: a manual override for unreliable plugin detection

SeoMeta::isSeoPluginActive() detects Yoast/RankMath/SmartCrawl via version constants (WPSEO_VERSION, RANK_MATH_VERSION, WPMU_DEV_SITE_ID) so TAW's own <title>/meta/OG/JSON-LD output stands down rather than duplicating a real SEO plugin's own tags. That detection is inherently best-effort — not every install of every plugin defines a stable constant. This surfaced on a real production site: SmartCrawl (WPMU DEV) was active and rendering correct, complete SEO output, but WPMU_DEV_SITE_ID wasn't defined for that install, so TAW never detected it and kept rendering alongside it — duplicate og:title (one correct, one a stale "Index" placeholder), duplicate <link rel="canonical">, and duplicate JSON-LD <script> blocks, simultaneously.

Added: a new SEO Output setting on Schema's existing "SEO Schema" admin page (SeoMeta::OUTPUT_MODE_FIELD) — Automatic (today's detection-based default), Always On (ignore detection, TAW's output always renders), or Always Off (TAW stands down entirely, deferring to whatever's actually installed). The settings page itself now always registers, regardless of current detection state, so the override stays reachable even when detection is currently wrong in either direction.

2026-08-04 (pending release)taw/core: SeoMeta no longer prints a duplicate canonical tag, and a static front page's og:title/description no longer leak a raw internal post title
taw-corefix

August 2026 — SeoMeta: canonical dedup + static front-page identity fallback

Two related gaps in SeoMeta's <head> output, both only visible once you check the actual rendered HTML rather than trusting the code's own docblocks:

  1. Duplicate <link rel="canonical">. SeoMeta::renderHeadTags() prints its own canonical tag on wp_head (priority 1), but never removed WordPress core's default rel_canonical() output — every page shipped two identical canonical tags.
  2. og:title/twitter:title on a static front page could show a raw, possibly internal-only post_title. A static front page (a Page selected under Settings → Reading) is still is_singular(), so it never reached the dedicated is_front_page()/is_home() fallback branch that already existed for the dynamic home/blog-index case — the same gap WordPress core's own wp_get_document_title() already closes for the real <title> tag, but SeoMeta hadn't mirrored for OG/Twitter.

Fixed: remove_action('wp_head', 'rel_canonical') runs alongside SeoMeta's own hook registration, and singularContext() now falls back to get_bloginfo('name')/get_bloginfo('description') for a static front page when no per-post meta title/description is set — matching the treatment the dynamic front-page/home branch already had.

2026-08-03 (pending release)taw/core: block script.js CSS side-outputs are now enqueued in production
taw-corefix

August 2026 — BaseBlock: a script's own imported CSS now reaches production

A block's script.js is allowed to import '...css' — e.g. a third-party lightbox library's stylesheet. Vite's dev server injects that CSS automatically, so it always worked under npm run dev. A production build behaves differently: Rollup extracts that CSS into its own file and records it on the script's manifest entry (css: [...]), not the block's style.scss/style.css entry. BaseBlock::enqueueProdAssets() only ever read the latter, so the script's own CSS silently never reached the page in any real production build — while looking completely fine in local dev, the one environment most developers test in.

Fixed: enqueueProdAssets() now also reads $manifest[$js_key]['css'] and enqueues it, using the same critical/non-critical inline-vs-async logic already used for the block's dedicated stylesheet. See Assets and Vite integration for the updated per-block assets note.

2026-07-31 (v1.16.119)taw/core v1.16.119: ViteLoader's manifest cache can no longer serve stale asset hashes after a deploy
taw-corefix

July 2026 — ViteLoader: manifest cache invalidates on deploy, not on a timer

ViteLoader::getManifest() cached the parsed Vite manifest under a fixed key for 24 hours. On a site with a persistent object cache (Redis/Memcached), that meant a deploy which rotated asset filenames could leave the site serving the previous build's hashed JS/CSS URLs — both genuinely 404ing — for up to a day, unless something explicitly flushed the object cache afterward. This is exactly what happened on a real client site: two entry-point assets 404'd in production while the manifest on disk was already correct.

Fixed: the cache key now includes the manifest file's own mtime. A deploy necessarily rewrites manifest.json with a new mtime, so the previous build's cache entry is orphaned automatically — no deploy hook, no manual flush, no reliance on remembering. If a site is currently affected, one manual object-cache flush clears it immediately; going forward, this can't recur.

2026-07-31 (v1.16.118)taw/core v1.16.118: Emailit sends proper multipart/alternative (HTML + plain-text) emails
taw-corefix

July 2026 — Emailit: multipart/alternative emails, not HTML-only

EmailConfig::useEmailit() sent every HTML email (which is all of them, now that Form::sendWithTemplate() is active) with no plain-text alternative part — text was explicitly set to null and dropped before ever reaching the Emailit API. A missing plain-text part is one of several small factors that can compound with new-sender reputation issues on some spam filters.

Fixed: HTML sends now include a plain-text fallback, derived from the HTML via wp_strip_all_tags($message, true) (the whitespace-collapsing variant, since MJML's nested-table output otherwise leaves the plain-text version full of stray blank lines). Both html and text go to EmailService::send() together — Emailit's API accepts them as independent, non-exclusive params. Non-HTML sends are unaffected.

2026-07-30 (v1.16.117)taw/core v1.16.117: escape submitted field values before interpolating into HTML notification/confirmation emails
taw-coresecurityfix

July 2026 — Escaping fix: form notification/confirmation emails

Form::sendWithTemplate() built the {{all_fields}} block (and each individual {{field_id}} / {{client_name}} placeholder) by concatenating raw submitted values straight into the HTML email sent to both the site admin and the submitter themselves. A field value containing <, &, or a full tag broke the email's layout for both recipients — a real gap since this path emails the submitter their own data back.

Fixed: every value going into a {{...}} placeholder is now wrapped in esc_html() before it reaches the template. This applies uniformly, including inside attribute contexts (e.g. a mailto: href built from an email field) — HTML-entity encoding is correct there too, not just in element text. There's intentionally no raw/unescaped variant of these placeholders.

2026-07-30 (v1.16.116)taw/core v1.16.116: fix Emailit transport being a silent no-op since it was introduced
taw-corefix

July 2026 — Emailit transport: fixed a silent no-op

EmailConfig::useEmailit() has never actually sent mail through Emailit, since it was first documented in v1.16.110 — interceptForEmailit() referenced \Emailit\Client, a class that has never existed in the real emailit/emailit-php SDK (the actual client class is \Emailit\EmailitClient). Every site with Emailit configured silently fell back to plain wp_mail() on every send, with no error logged anywhere.

Fixed: both references now point at the correct \Emailit\EmailitClient class. No change to the public API — EmailConfig::useEmailit($apiKey, $from, $fromName) is called exactly the same way as before; sites already "configured" for Emailit will simply start actually using it once they update.

emailit/emailit-php is now also required as a dev dependency in taw/core itself (previously only composer suggested), so this integration has real regression test coverage against the actual installed SDK.

2026-07-30 (v1.16.115)taw/core v1.16.115: webhook payload gets page_url + a taw_form_webhook_payload filter for per-site customization
taw-corefeature

July 2026 — Webhook payload: page_url + a customization filter

Every form's webhook payload now includes page_url — the full URL of the page the form was actually submitted from, captured server-side at render time (not read from the request's Referer header, which browsers and privacy tools can strip). The same registered form is often embedded on more than one page; this is what lets a downstream automation (n8n, Zapier, Make) tell those submissions apart without needing a separate form_id per page.

A new taw_form_webhook_payload filter runs on the payload right before it's sent — and before the HMAC signature is computed over it — letting a specific site add or override anything the default shape doesn't cover, entirely from that site's own inc/customizations.php, with no taw-core changes needed:

add_filter('taw_form_webhook_payload', function (array $payload, string $formId, int $postId, array $data) {
    $path = wp_parse_url($payload['page_url'], PHP_URL_PATH) ?? '';

    if (str_contains($path, '/financiera/')) {
        $payload['destination'] = 'financiera-sheet';
    } elseif (str_contains($path, '/fideicomisos/')) {
        $payload['destination'] = 'fideicomisos-sheet';
    }

    return $payload;
}, 10, 4);

page_url is also stored on the submission's taw_submission CPT entry (visible in the admin detail view, next to Webhook Status and User IP) and shown in the Settings → Form Webhook page's payload example.

2026-07-30 (v1.16.112)taw/core v1.16.112: security audit — SVG upload sanitizer fail-open fix, ABSPATH guard hardening
taw-coresecurityfix

July 2026 — Security fixes from a full framework audit

A full pass across taw/core for anything that could let a site get compromised. One real, exploitable bug found and fixed; broader hardening applied everywhere it's safe to.

  • Fixed: Svg::sanitizeOnUpload() could leave an unsanitized file on disk. wp_handle_upload() moves an uploaded file to its final, publicly-servable location before this filter runs. Previously, if sanitization failed to positively confirm a file was safe — either enshrined/svg-sanitize returning false for malformed/unparseable XML, or (a second bug found in the same pass) throwing an uncaught LogicException for well-formed XML with no <svg> root — the original file was silently left in place, unsanitized, at its uploads URL. Any user with upload_files (Author and above by default) could exploit this for stored XSS. Now: any failure to positively confirm the file is clean deletes it and rejects the upload outright, and the exception path is caught so it converges to the same rejection instead of crashing the request.
  • Added if (!defined('ABSPATH')) { exit; } guards to the framework classes where it's safe to add them — direct defense-in-depth against a file being requested directly if a host doesn't block .php execution inside vendor/. Deliberately not added to bin/taw's CLI command classes, Framework.php, or the two Composer files-autoloaded bootstrap files (performance.php, utilities.php) — all four are used in contexts where WordPress is legitimately not yet bootstrapped, and guarding them breaks bin/taw outright (verified against a real, unintentional regression during this fix, caught before shipping).
2026-07-30 (v1.16.111)taw/core v1.16.111: per-form webhooks (previously one shared webhook per site)
taw-corefeature

July 2026 — Per-form webhooks

Every form previously shared one site-wide webhook URL/secret (Settings → Form Webhook), regardless of which form actually submitted. Each form can now target its own webhook, resolved in precedence order:

  1. An admin-configured override for that specific form — a new Per-Form Webhooks table on the same Settings → Form Webhook page, one row per form registered via Form::register().
  2. A code-level default set on the form itself, via a new 'webhook' => ['url' => ..., 'secret' => ...] config key.
  3. The existing Default Webhook field — now the site-wide fallback for any form with neither of the above, rather than the only option.

Fully backward compatible: any site with the old single webhook already configured keeps firing it for every form exactly as before, until a form is given its own override. No migration needed.

2026-07-30 (v1.16.110)taw/core v1.16.110: document + wire up the Emailit email transport for per-client sites
taw-coredocs

July 2026 — Emailit transport: documented and wired into the scaffold

TAW\Support\EmailConfig::useEmailit() already existed in taw/core but was never documented or wired into taw-theme's scaffold. Now that it's actually in use for paying clients:

  • New "Email" section in the taw/core README, and a matching section on the taw/core reference page here, documenting EmailConfig::useEmailit() — routes all wp_mail() calls (form submissions, password resets, WooCommerce, etc.) through Emailit's API, with automatic fallback to plain wp_mail() if unconfigured or the API call fails.
  • taw-theme's inc/customizations.php now has a commented-out wiring block, matching the style of this scaffold's other opt-in features — gated on a defined('EMAILIT_API_KEY') check, so it's a genuine no-op on every site that doesn't define the constant.
  • EMAILIT_API_KEY / EMAILIT_FROM_EMAIL / EMAILIT_FROM_NAME are site-specific secrets that belong in each client's own wp-config.php, never in the theme repo. The emailit/emailit-php SDK is only required on sites that opt in (composer require emailit/emailit-php), not a default dependency.

No behavior change to EmailConfig itself — this is documentation and scaffold wiring for an existing, already-working class.

2026-07-30 (v1.16.106)taw/core v1.16.106: Grid-view sidebar — folder and file sort controls
taw-corefeature

July 2026 — Grid-view sidebar: sort controls

Two independent sort controls, each remembered per-browser:

  • Folder tree — sort by name (A–Z/Z–A) or creation order (newest/oldest first), applied at every nesting level. Uses the terms REST endpoint's own orderby=id (term IDs increase monotonically with creation), since WordPress terms have no native creation-date field — no new REST endpoint needed.
  • File grid — sort by name, upload date, or file size (largest/smallest first). Applied directly as wp.media's existing Backbone query props (orderby/order/meta_key), the same bridge the folder filter already uses — no new AJAX filter needed for this half either.

File-size sorting needed one new piece: WordPress core only stores a file's size nested inside a serialized metadata array, which SQL can't ORDER BY. A new _taw_media_filesize postmeta value is now set on every upload, and backfilled once for pre-existing attachments the first time anyone sorts by size.

2026-07-30 (v1.16.101)taw/core v1.16.101: TAW Media — dedicated screen rebuilt as a full Alpine app
taw-corefeature

July 2026 — TAW Media: the dedicated screen becomes a full app

The dedicated Media → TAW Media admin screen (previously "Media → Folders") is rebuilt from an imperative vanilla-JS page into a full Alpine.js app, matching the Grid-view sidebar's look and adding real app capabilities the old screen never had:

  • Lucide icons throughout, a toolbar (New Folder / Rename / Delete, context-aware), an overflow menu (Expand all / Display folder IDs), and folder count badges — the same visual language as the Grid-view sidebar.
  • A breadcrumb above the attachment grid, and subfolders now show as clickable cards in the grid pane itself (not just the tree), matching the Grid-view sidebar's own folder cards.
  • Direct file upload: drag OS files straight onto the screen (or use the Upload Files button) to upload directly into the currently open folder, via wp/v2/media's own multipart upload support plus its taxonomy REST field, in the same request.
  • Multi-select + bulk actions: click attachments to select several, then bulk-delete or drag the selection onto a folder to move them all at once.
  • The scoping fix from the Grid-view sidebar (a folder only shows what's directly inside it, not descendants' files too) applies here as well.

The underlying TAW\Core\Media\MediaFolders class and the taw_media_folder taxonomy key are unchanged — this is a UI/JS rewrite of one admin screen, not an API or data-model change.

2026-07-30 (v1.16.96–v1.16.100)taw/core: Grid-view sidebar — folder scoping, breadcrumb, folder cards, live-refresh fixes
taw-corefixfeature

July 2026 — Grid-view sidebar: scoping, navigation, and live-refresh fixes

A run of fixes and refinements to the Grid-view sidebar, in order:

  • Fixed the Grid-view's folder click-filter always returning zero results — WP_Query::parse_tax_query() auto-detects any top-level query key matching a registered taxonomy's query_var and adds its own slug-based tax_query clause, AND-combined with the sidebar's own (numeric-ID-based) one, which never matched. Fixed by removing the raw key before the query runs.
  • Fixed a folder's Grid view also showing every subfolder's files mixed in — tax_query defaults to include_children: true for hierarchical taxonomies; now explicitly false, so a folder only shows what's directly inside it.
  • Replaced the initial subfolder "tiles" (which reused WP core's photo-thumbnail markup and had a clipped-icon rendering bug) with dedicated folder cards — their own markup, shown in a row above the photo grid rather than mixed into it — plus a breadcrumb above them for navigating back up.
  • Fixed the grid not refreshing live after moving a file while still viewing its (unchanged) source or destination folder — Backbone's props.set() only re-fetches on an actual value change, so the collection's own _requery() is now called directly instead.
  • Fixed folder-card count badges not updating after an in-view move — the card re-render's "already correct, skip" check compared folder IDs only, not their counts.
2026-07-30 (v1.16.95)taw/core v1.16.95: Media Folders drag-and-drop fixes — folder counts, drag-ghost rendering
taw-corefix

July 2026 — Grid-view sidebar: drag-and-drop fixes

Two fixes surfaced while testing the drag-and-drop refinement below:

  • Folder count badges always read 0, regardless of actual contents. WordPress core's default taxonomy count callback only counts attachments with post_status = 'publish' (or whose parent post is) — standalone Media Library uploads are inherit status with no parent, so they were never counted. registerTaxonomy() now sets 'update_count_callback' => '_update_generic_term_count', which counts relationships directly. The underlying filtering/query was never affected by this — only the badge. Existing sites need a one-time wp term recount taw_media_folder to catch up on assignments made before this shipped; new assignments count correctly automatically from here on.
  • Drag ghost visually bled into the next grid thumbnail. WordPress's Grid view lays out attachments with float: left + percentage widths, a layout where Chromium's automatic drag-ghost snapshot is known to leak into an adjacent floated sibling. Fixed by setting an explicit drag image (dataTransfer.setDragImage(), just the thumbnail <img>) instead of relying on the browser's automatic capture.
2026-07-29 (v1.16.94)taw/core v1.16.94: Media Folders Grid-view sidebar — drag-and-drop refinement
taw-corefeature

July 2026 — Grid-view sidebar: drag attachments into folders

Two refinements to the Grid-view sidebar's drag-and-drop, on top of the existing folder-to-folder re-nesting:

  • Grid thumbnails (single or multi-selected, via WordPress's own selection model) are now draggable straight onto a folder row to file them — a dragstart/drop-target bridge on top of WordPress core's own Backbone attachment views, reusing the same wp/v2/media REST call the dedicated Folders screen already used for this.
  • Dragging a folder row (or an attachment) no longer triggers WordPress core's own "drop files to upload" overlay — that overlay fires on any drag reaching it regardless of what's being dragged, so internal drags are now kept from bubbling up to it while real OS file drags are left completely alone.

Full reference: taw-core README § "Media Folders".

2026-07-29 (v1.16.93)taw/core v1.16.93: Media Folders Grid-view sidebar polish pass
taw-corefeature

July 2026 — Grid-view sidebar polish pass

A FileBird-inspired visual pass on the Grid-view sidebar: Lucide icons throughout, a toolbar (New Folder / Rename / Delete, context-aware and disabled until a real folder is selected), a collapsible sidebar (persisted via localStorage), an overflow menu (Expand all / Display folder IDs), per-folder expand/collapse chevrons, and folder count badges. Purely a UI layer on top of the existing tree/CRUD/live-filter behavior — no change to the REST routes or the ajax_query_attachments_args bridge underneath it.

Full reference: taw-core README § "Media Folders".

2026-07-29 (v1.16.88)taw/core v1.16.88: Media Folders Grid-view sidebar + vendored Alpine.js
taw-coretaw-themefeature

July 2026 — Media Folders Grid-view sidebar + vendored Alpine.js

TAW\Core\Media\MediaFolders gains a third admin surface: a FileBird-style sidebar bolted directly onto the default Media Library Grid view (upload.php's thumbnail grid), not just the dedicated Folders screen and the classic List view. Clicking a folder in the sidebar filters the grid live, via a new ajax_query_attachments_args filter plus a narrow JS bridge that reads/sets props on wp.media's existing Backbone query object — the same technique folder plugins like FileBird use, not a Backbone view override, so the existing Folders screen and List-view integration are untouched. The sidebar has the same full folder CRUD (create/rename/delete/drag-to-reparent) as the dedicated screen, and stays in sync with the List view's filter dropdown when switching between the two.

Alpine.js — used by every admin-side interactive widget in taw-core (Metabox fields, Options Page, the Icon picker, and now this sidebar) — is no longer loaded from a CDN. It's vendored as a static asset (assets/vendor/alpine.min.js, pinned to 3.15.12) and enqueued through a new shared TAW\Support\Alpine::enqueue() helper, since taw/core installs on arbitrary client sites where a CDN dependency for a required admin script isn't safe to assume.

Full reference: taw-core README § "Media Folders" and § "Dependencies".

2026-07-29 (v1.16.86)taw/core v1.16.86: Lucide icon field type + nestable Media Folders
taw-coretaw-themefeature

July 2026 — Icon System + Media Folders

Two new opt-in subsystems, both following the same enable()-before-Theme::boot() pattern VisualEditor established.

New icon Metabox/OptionsPage field type (TAW\Core\Icons\Lucide): the full Lucide icon set (~1,750 icons) is now vendored directly inside taw-core (resources/icons/lucide/, refreshed via the new php bin/taw icons:sync command) — the wp-admin picker never makes a network call. Stores a bare icon name; Lucide::render($name, [...]) renders it as inline SVG in templates and needs no enable() call of its own, same relationship Svg::render()/Svg::inline() have to Svg::register(). A new GET taw/v1/icons REST route (opt-in, only registered when enabled) powers the picker's search.

New nestable Media Library folders (TAW\Core\Media\MediaFolders): a single hierarchical taxonomy on attachments, with a dedicated Media → Folders admin screen (folder tree, drag-and-drop attachment organization, an "Unfiled" pseudo-folder) plus a filter dropdown/column/bulk-action on the classic Media Library list view. Built entirely on WordPress core's own taxonomy REST support (wp/v2/taw_media_folder, and a taw_media_folder param on wp/v2/media) — no custom REST endpoint needed. Deliberately does not patch the native Grid view's Backbone internals, to avoid the fragility that comes with hooking WordPress core's JS views across version updates. Opt-in at the taw/core level, but the taw-theme scaffold's inc/customizations.php calls MediaFolders::enable() by default, so every new taw-theme site has it active out of the box.

Full reference: taw-core README §§ "Icon System" and "Media Folders", and this site's Icon System / Media Folders sections.

2026-07-14 (4)taw/core v1.16.82: TAW-native SEO meta (Yoast-aware) + site-wide audit-seo
taw-coretaw-themeskillsfeature

July 2026 — SEO meta system + site-wide audits

TAW has never owned per-post SEO meta (title tag, meta description, social/OG image) natively — every real site either has an SEO plugin installed or nothing at all. New TAW\Core\Seo\SeoMeta (wired into Theme::boot()) fixes this without fighting whatever else might be installed: with no SEO plugin active, it registers its own lightweight metabox and renders the actual <head> tags itself; with Yoast active, TAW's own UI and output both stand down entirely and it reads/writes Yoast's own meta keys directly instead, so Yoast's own UI stays in sync; with a different plugin active (RankMath, SmartCrawl), TAW's output stands down but write support isn't attempted — seo:inject refuses any meta write with a clear reason rather than guessing at that plugin's key scheme.

seo:extract/seo:inject extended: every dump now includes a seo_meta object (title/description/social image, its current source, and the post's own already-uploaded candidate images for recommendation) beside the existing page-copy blocks. Both commands gain --all for a genuine site-wide audit — seo:extract --all extracts every published page/post in one run; seo:inject --all applies it with per-post independence, so one page failing validation doesn't block the rest.

The audit-seo skill now analyzes SEO meta alongside page copy (empty/generic meta title or description as a Red Flag, social image recommended from existing uploads or asked about via AskUserQuestion when none exist), and supports "audit SEO for the whole site" as a trigger.

Caught two real bugs live-testing before shipping: og_image_id: 0 (extraction's own "no image set" sentinel) was being rejected as an invalid attachment on an unedited round-trip; and the wp passthrough command (from the prior release) wasn't forwarding piped/redirected stdin to the child process, breaking wp eval-file --style usage — fixed via the standard isatty() check so a live interactive session never blocks on stdin that was never meant to be forwarded. Verified end-to-end: single-post and site-wide extract/inject round-trips, plus a deliberately-broken post confirming --all's per-post isolation (6/7 succeeded, the broken one specifically reported, command correctly exits non-zero for the partial run while still applying everything that passed).

2026-07-14 (3)taw/core v1.16.80–v1.16.81: auto-resolved Local by Flywheel MySQL socket (WpLoader + new wp passthrough)
taw-coretaw-themefixfeature

July 2026 — Local by Flywheel socket auto-detection

A recurring, easy-to-hit failure: every CLI command that boots WordPress (inspect, fields:get/fields:set, export:static, seo:extract/seo:inject) failed with "Error establishing a database connection" when run from an ordinary terminal under Local by Flywheel — DB_HOST in wp-config.php is just 'localhost', which resolves to the system default MySQL socket, but Local runs a separate MySQL instance per site on its own per-site Unix socket. Only Local's own "Open Site Shell" sets the environment variables that would make this resolve correctly.

WpLoader::autoConfigureLocalSocket() fixes this at the root instead of requiring a manually-typed -d mysqli.default_socket=... workaround every time: it reads Local's own sites.json (which maps every site's local path to a short site ID — the same ID that names its socket's containing directory), matches the theme directory against it, and points PHP's MySQL drivers at the real socket via ini_set() before wp-load.php ever connects. A pure no-op everywhere this doesn't apply (real hosting, CI, other local environments). Wired into all six commands that boot WordPress.

The same problem also affects the separate wp binary (WordPress's own official CLI) — a different process WpLoader's ini_set() fix can't reach. New php bin/taw wp <args> is a thin passthrough to the real wp binary (every argument forwarded exactly as given, unparsed) that resolves both the socket and --path automatically before shelling out. AGENTS.md's WP-CLI section now recommends it over a bare wp command.

Caught a real bug during live-testing: the socket-existence check used is_file(), which always returns false for a Unix domain socket (it only matches regular files) — silently failing every time even with everything else correct. Fixed to file_exists(), which doesn't discriminate by type. Verified end-to-end with zero manual flags: inspect, fields:get, seo:extract, and wp post list/wp option get/wp eval all confirmed working, plus error/exit-code passthrough on a deliberately-failing wp command.

2026-07-14 (2)taw/core v1.16.79: seo:extract/seo:inject CLI commands + audit-seo skill
taw-coretaw-themeskillsfeature

July 2026 — SEO/copy audit loop

New php bin/taw seo:extract <post_id> walks the live Metabox field registry (the same one TAW\Core\Metabox\SeoContentIntegration already walks to feed Yoast/SmartCrawl), keeps only non-empty text/textarea/wysiwyg content — including inside repeater rows, recursively — and writes a hierarchical JSON dump grouped by block. Image/URL/post_select/layout fields are excluded by design: this is a copy audit, not a general field dump.

New php bin/taw seo:inject <post_id> writes an edited copy of that JSON back, with real safety rails, not a thin update_post_meta() wrapper: every field is validated against the live registry before anything is written (all-or-nothing, never a partial write); only text-bearing fields are accepted, anything else is rejected in favor of fields:set; repeater rows are merged into the current live row by index rather than replaced wholesale, so a non-text sub-field on the same row is never lost; a row-count mismatch against live data (someone edited the post in the admin meanwhile) refuses rather than guessing alignment; core post data (post_title/post_content/post_status) is never touched. --dry-run previews the sanitized values before any real write.

New audit-seo Claude Code skill in taw-theme performs the analysis itself — keyword presence (H1/H2), copywriting/CTA quality, readability — and reports Red Flags/Polish Opportunities, applying approved rewrites only after explicit batch confirmation, following the same content-writing safety model as populate-content/fields:set.

Verified end-to-end against a real dev site: extract → edit → dry-run → real inject → confirmed via fields:get, plus both safety rails (unknown field, wrong field type) tested and confirmed to refuse cleanly without writing anything.

2026-07-14 (1)taw/core v1.16.77: static site export (export:static) + opt-in headless CORS
taw-coretaw-themeskillsfeature

July 2026 — Static site export + headless CORS

New php bin/taw export:static command (TAW\CLI\ExportStaticCommand) fetches every published page/post over HTTP against its own permalink, rewrites absolute site-URL references to root-relative paths (or an explicit --prod-url), and writes <dir>/<slug>/index.html plus the built Vite assets and wp-content/uploads/ into a self-contained bundle — deployable as-is to Cloudflare Pages, Vercel, or any static host. Forms and search deliberately stay dynamic (admin-ajax.php?action=taw_form_*, taw/v1/search-posts) rather than being frozen into the export.

New TAW\Core\Rest\Cors (wired into Theme::boot(), no-op unless TAW_HEADLESS_ORIGINS is set in wp-config.php — same opt-in pattern as Turnstile) opens up cross-origin access to both surfaces once the static bundle is served from a different domain: REST via WordPress core's own allowed_http_origins filter, admin-ajax.php via hand-rolled headers scoped to taw_form_* actions only. Always an explicit origin allowlist, never a wildcard.

New export-static Claude Code skill in taw-theme (.claude/skills/export-static/) walks through building assets, running the export, verifying the output, and reminding the user about the headless-CORS follow-up.

Two real bugs were caught live-testing against a real dev site before shipping: the command initially assumed Vite always builds to dist/ (this project's own vite.config.js uses public/build/ — now detects both, matching ViteLoader's own resolution order), and initially copied the Vite build flattened to the export root instead of its real theme-relative URL path (every CSS/JS reference would have 404'd once deployed — fixed to mirror the exact path the exported HTML references).

2026-07-13 (2)Docs fix: Get Started guides now cover git init, framework-sync, and project-init
taw-docsdocs

July 2026 — Get Started guides brought current with recent DX additions

The Quickstart and Setting up a new TAW project guides had fallen behind several recent framework additions. Both now cover: initializing git and pushing to a repository (a step that was missing entirely, despite being a prerequisite for framework-sync.yml to do anything), and the project-init skill as the recommended way to enable and verify the GitHub Actions PR permission plus walk through optional integrations.

setup-project.mdx additionally had a stale functions.php code example showing the pre-bootstrapFullSite() architecture (manual Theme::boot() + Theme::performance() calls wrapped in add_action('after_setup_theme', ...)) — replaced with the current two-line functions.php and an accurate description of what bootstrapFullSite() does, including the inc/ file split. Its "what was created" file tree was also missing .github/workflows/ and .claude/skills/, both of which ship from the first commit now.

A second pass caught two more real gaps in the new git-init step of both guides: the prerequisites list never mentioned needing git at all, and git remote add origin <your-repo-url> never explained where that URL comes from for a reader following the guide for the first time. Both fixed — git (plus an empty pre-created repo) is now a listed prerequisite, and a callout explains creating the repo first (on github.com, or via gh repo create --source=. --remote=origin --push as a one-step alternative).

2026-07-13 (1)taw/core v1.16.73: PHPUnit unit test suite (Brain Monkey, no real WP install)
taw-coretesting

July 2026 — PHPUnit unit test suite for taw-core

New tests/Unit/ suite (composer run test), using Brain Monkey to stub individual WordPress functions rather than requiring a real WordPress + MySQL install — fast, isolated tests of taw-core's own logic. Covers the message-resolution precedence every Forms validation rule shares (field-level override > form-level default > built-in default), RateLimiter's fixed-window logic, and Turnstile's configured/not-configured states plus verify()'s success/rejected/network-error/malformed-response paths with fail-closed behavior throughout.

This is a deliberate division of labor with taw-theme's bin/ci/smoke-test.php: that suite boots a real WordPress environment and exercises the full render path against a live theme (integration coverage); this suite covers taw-core's internal logic in isolation (unit coverage). Form::requiredMessage()/Form::emailMessage() were extracted out of Form::process() as small, independently-testable private methods (no behavior change) to make this possible without mocking the entire AJAX pipeline. Wired into taw-core's own CI alongside PHPStan.

2026-07-12 (6)Fix: update-theme no longer full-overwrites composer.json/package.json
taw-themeskillsfix

July 2026 — update-theme composer.json/package.json fix

A Tier 2 diff on composer.json/package.json was being treated the same as prose docs — full-file overwrite once approved. That's dangerous for these two files specifically: a real client project's own added dependencies are additive and will never exist in the canonical taw-theme scaffold, so a whole-file overwrite would silently delete every client-added package. Fixed: update-theme now edits only the genuinely framework-relevant lines by hand for these two files, and tells the user plainly when a diff is just structural noise from additive dependencies rather than asking them to re-decide the same non-issue on every run. Found via real usage on a client project.

2026-07-12 (5)New sync-remote skill: standard pull/compare/resolve/push git workflow
taw-themeskills

July 2026 — sync-remote skill

New Claude Code skill triggered explicitly ("sync with remote, please"): fetches and compares against origin, reconciles diverged history via a real merge (never rebase), stops for explicit human resolution on genuine conflicts rather than guessing a side, optionally runs phpstan before pushing, and always confirms the push separately — "sync" authorizes pulling and reconciling, not the push itself. Built after real cross-machine divergence on a client project (two independent commits both adding the same line to .gitignore), resolved cleanly by git's own merge algorithm — the case this skill generalizes.

2026-07-12 (4)Dynamic smoke test: real WordPress+MySQL block/form rendering in CI
taw-themeci

July 2026 — dynamic smoke test in CI

New bin/ci/smoke-test.php boots a real, CI-provisioned WordPress + MySQL install with the theme active, creates a real post, and renders every registered MetaBlock + displays every registered Form against it, failing on any runtime error. Complements the existing static checks (php -l, the getData() signature check, PHPStan) by catching the class of bug none of them can see: an undefined function call, a template referencing a variable getData() never returned, WP API misuse — anything that only surfaces once the render path actually executes.

Runs as a new parallel job in ci.yml on every push/PR, and inside framework-sync.yml's verification gate against the post-sync codebase before a weekly update PR is opened.

2026-07-12 (3)New project-init skill: post-scaffold client onboarding checklist
taw-themeskills

July 2026 — project-init skill

New Claude Code skill picking up right after composer create-project + the first push: verifies gh CLI authentication, checks and (with confirmation) enables the GitHub Actions "Allow GitHub Actions to create and approve pull requests" permission framework-sync.yml needs, then triggers a real manual run to confirm the whole pipeline actually works rather than assuming it does. Also walks through optional per-project integrations — Cloudflare Turnstile, transactional email, CSS Studio, Visual Editor — as explicit yes/no questions instead of leaving them to be discovered later. None of these are enabled without an explicit answer.

2026-07-12 (2)Automated framework-drift detection: php bin/taw sync + a weekly CI workflow
taw-coretaw-themeclici

July 2026 — automated framework-drift detection

New php bin/taw sync CLI command (taw/core v1.16.72) checks whether the installed taw/core version is behind the latest release, and whether a project's Tier 1/Tier 2 taw-theme scaffold paths differ from the canonical repo — without booting WordPress, so it can run in CI. --apply writes Tier 1 changes directly; Tier 2 is always report-only, same human-review requirement as the update-theme skill it shares logic with.

Both sync and update-theme now read a single shared manifest (taw/core's resources/update-manifest.json) for the Tier 1/Tier 2/never-touched path lists, instead of maintaining the same lists independently in two places.

A new .github/workflows/framework-sync.yml (Tier 1, so it propagates to every client project via update-theme) runs sync unattended on a weekly schedule: bumps taw/core, applies Tier 1, runs the same verification CI runs on every push, and opens a pull request with Tier 2 diffs for review — only if something actually changed and verification passed.

php bin/taw sync --json    # report only
php bin/taw sync --apply   # also write Tier 1 changes
2026-07-12 (1)Form-level default validation messages for translating an entire form's copy at once
taw-coreforms

July 2026 — form-level default validation messages

Adds a fallback tier between per-field {rule}_message overrides and the hardcoded English defaults: a top-level messages entry per rule (required, email, min_length, max_length, pattern, min, max). Lets a non-English site set its validation copy once per form instead of repeating a {rule}_message on every field. Precedence: field-level {rule}_message > form-level messages.{rule} > built-in English default.

Form::register([
    'id' => 'contact',
    'messages' => [
        'required'   => '%s es obligatorio.',
        'email'      => 'Correo electrónico no válido.',
        'min_length' => '%1$s debe tener al menos %2$d caracteres.',
    ],
    'fields' => [...],
]);
2026-07-10 (6)Per-field custom error messages for every Forms validation rule
taw-coreforms

July 2026 — per-field custom validation messages

Extends the {rule}_message convention (previously only pattern_message) to required, the built-in email format check, min_length, max_length, min, and max — each optional, falling back to the existing generic default (with the field's label interpolated) when not set.

['id' => 'name',  'type' => 'text',  'required' => true, 'required_message' => 'Please tell us your name.'],
['id' => 'email', 'type' => 'email', 'required' => true, 'email_message' => 'That doesn\'t look like a real email address.'],
['id' => 'age',   'type' => 'number', 'min' => 18, 'min_message' => 'You must be 18 or older.'],
2026-07-10 (5)Forms API: rate limiting, Cloudflare Turnstile, and richer validation rules
taw-coresecurity

July 2026 — Forms API security hardening

Three additions to the Forms API's security posture, taking it beyond its existing CSRF-nonce + honeypot baseline:

Rate limiting — on by default (5 attempts/60 seconds, per IP, per form), backed by WP transients, no external cache dependency. Checked before the nonce check, since a flooding script doesn't need a valid nonce to cause load.

Form::register([
    'id' => 'contact',
    'rate_limit' => ['max' => 3, 'window' => 120], // override, or false to disable
    'fields' => [...],
]);

Optional Cloudflare Turnstile bot verification — 'turnstile' => true, keyed via TAW_TURNSTILE_SITE_KEY/TAW_TURNSTILE_SECRET_KEY constants in wp-config.php (never a metabox/OptionsPage field — those are REST-readable by anyone with edit_posts). Fails closed on any network error; degrades gracefully (no widget, no check) rather than blocking submission when a form opts in but keys aren't configured, with a WP_DEBUG-only notice for developers.

Field validation rules beyond required: min_length, max_length, pattern (+ pattern_message), and min/max for number fields. Render as native HTML attributes for client-side UX; the authoritative check is always server-side.

Verified end-to-end against real forms on live sites before shipping: rate limiting via rapid requests at the live AJAX endpoint (confirmed blocked on the 6th attempt), Turnstile via Cloudflare's publicly documented test keys (always-pass, always-block, and the unconfigured fail-closed case), and every validation rule type via direct unit tests, including a malformed-pattern fail-safe.

2026-07-10 (4)populate-content skill, with a mandatory content-writing safety model
taw-themeskillssecurity

July 2026 — populate-content skill, and a content-writing safety model

New Claude Code skill: populate-content fills real field values on a real post from a document, list, or plain-language description the user provides — e.g. "fill in the team_members repeater on the About page with this list". It resolves the target post and field(s), maps source content to a field's shape (asking for confirmation on ambiguous mappings, e.g. repeater sub-fields that don't obviously correspond to the source document's structure), and writes via fields:set.

Because this is a skill that writes directly to a live database, it follows a mandatory safety model — documented once in AGENTS.md § "Content-writing safety model" and referenced by every skill that writes content, not duplicated per skill:

  • Always --dry-run before any real write.
  • Always confirm before overwriting a non-empty existing value — old vs. new shown side by side.
  • Always confirm before writing to a publish-status post, regardless of whether the field is currently empty.
  • Batch operations get one confirmation for the entire plan, shown up front — never expanded mid-batch without re-confirming.
  • Extra scrutiny on wysiwyg/'sanitize' => 'code' fields when the source content came from outside the conversation.
  • Never touches post_title/post_content/post_status — Metabox/OptionsPage meta only.
  • No wildcard/glob mass-edits — every affected post and field enumerated up front.

make-metablock (screenshot-sourced blocks) and figma-to-block (Figma-sourced blocks) now ask, once real content is available and a target post exists, whether to populate real extracted values, leave fields as template fallbacks, or fill with Lorem Ipsum placeholder content — delegating any real write to populate-content rather than writing meta directly themselves. build-page asks this once per page instead of once per section for a design-sourced brief, and passes the answer down so individual section builds don't re-ask.

2026-07-10 (3)fields:get/fields:set CLI commands for direct, sanitized field read/write
taw-coretaw-themecli

July 2026 — fields:get / fields:set

New taw/core v1.16.68 CLI commands give an agent (or a script) a safe, first-party way to read and write Metabox/OptionsPage field values directly, without going through wp-admin. This is the same read/write primitive VisualEditorEndpoint already uses for its REST-driven saves — Metabox::get_field_config() to resolve a field's type from the live registry, then the matching type-aware getter (get_repeater(), get_bool(), get_posts(), ...) or sanitizer (sanitizeValue(), sanitizeRepeaterRows()) — minus the Visual Editor's 'editor' => true gate, since CLI access is already a trusted, direct-DB-write context.

php bin/taw fields:get 42 hero_heading --json
php bin/taw fields:get 42 team_members --json                        # repeater comes back as a decoded array

php bin/taw fields:set 42 hero_heading "Welcome"
php bin/taw fields:set 42 team_members --file=/tmp/team.json         # --file avoids shell JSON-quoting for repeater/array values
php bin/taw fields:set 42 hero_heading "Welcome" --dry-run           # preview the sanitized result without writing

Verified end-to-end against a live site before shipping: XSS payloads correctly stripped by the real sanitizers on both scalar and repeater fields, unknown-field/nonexistent-post error paths return clean messages with the right exit code, and a real write/read-back/revert round-trip persisted correctly. The wp-load.php-locating logic (previously private to InspectCommand) was extracted into a shared TAW\CLI\WpLoader helper, since three commands now need it.

This is the "populate the content" half of a design-to-page pipeline — figma-to-block/make-metablock scaffold a block and its empty fields, fields:set fills them in, so "build this page from a Figma design" can mean the whole thing, not just an empty scaffold.

2026-07-10 (2)WP-CLI as a first-class agent tool, plus a real textdomain-timing bug fix
taw-coretaw-themewp-clibugfix

July 2026 — WP-CLI integration, and a real WordPress 6.7+ bug fix

WP-CLI documented as a first-class agent tool

bin/taw only covers framework scaffolding/introspection (blocks, fields, forms) — it has no visibility into actual site content. WordPress's own wp CLI fills that gap: posts, options, users, terms, transients, arbitrary PHP via wp eval/wp eval-file, an interactive wp shell. Documented in taw-theme.mdx and AGENTS.md/CLAUDE.md, including the Local by Flywheel connection quirk that makes a bare wp command fail with a DB connection error even though the site works fine in-browser (Local runs a per-site MySQL socket, not the system default wp-config.php assumes).

Real bug: textdomain loaded too early on every single request

Using WP-CLI to test the connection quirk above immediately surfaced a genuine, pre-existing bug: every request triggered WordPress 6.7+'s _load_textdomain_just_in_time doing_it_wrong notice. Root cause — Theme::bootstrapFullSite() required inc/options.php (whose OptionsPage/Metabox field configs call __('...', 'taw-theme') at file scope) synchronously, during functions.php's own load, which always runs before after_setup_theme even starts firing. WordPress's actual gate for this notice (verified directly in wp-includes/l10n.php) is whether after_setup_theme has started — not "before init" as the notice's own message text claims.

Fixed in taw/core v1.16.67: both the textdomain load and inc/options.php's require are now deferred to after_setup_theme, at priorities 1 and 5 respectively — ahead of inc/customizations.php's own callback and BlockLoader::loadAll() (both default priority 10). The scaffold's inc/customizations.php no longer needs its own load_theme_textdomain() call (removed — it was on init, running even later than register_nav_menus()'s translation calls, which never actually fixed the ordering).

This is exactly the class of bug static analysis can't catch — PHPStan checks types, not hook-firing order at runtime. It was only found by actually booting the site through wp eval and reading the debug output.

2026-07-10PHPStan static analysis, WordPress-aware, wired into CI across all three repos
taw-coretaw-themecistatic-analysis

July 2026 — PHPStan static analysis

taw-theme and taw/core now ship phpstan.neon (level 5) with WordPress core stubs via szepeviktor/phpstan-wordpress, so add_action, WP_Query, and other core WP symbols resolve correctly instead of erroring as unknown. Runs via composer run phpstan, and as a new CI step on every push/PR alongside the existing lint and getData() signature checks.

composer run phpstan

taw-theme analyzes Blocks/ and inc/ only; taw/core analyzes src/. Both repos started clean or near-clean — taw/core carries a small phpstan-baseline.neon (26 findings, mostly WP_Post dynamic-property access and a Symfony Console interface gap) captured as a migration aid, not a permanent suppression file.

If a block template trips a PHPStan false positive on a variable that's actually guaranteed present (e.g. isset($x) or $x ?? default on a value defaults()/getData() always supplies), that's expected — PHPStan can't trace TAW's extract()-based template variable injection statically. Verify the variable really is wired through render()/getData() before assuming it's a false positive; baseline it once confirmed, don't widen types or add defensive checks to silence it.

CI fix: excludePaths must mark build artifacts optional

phpstan.neon's excludePaths initially listed node_modules/ and public/build/ unconditionally. Both are gitignored build artifacts that don't exist on a fresh CI checkout (CI never runs npm install/npm run build), and PHPStan treats a missing excludePaths entry as a hard config error, not a no-op — it failed with Invalid entries in excludePaths, not the memory-limit crash it looked like at first. Fixed by marking both paths optional with the (?) suffix. A --memory-limit=-1 change landed alongside this while diagnosing (the parallel worker legitimately needs more headroom on GitHub's runners than a local dev machine), but the exclude-path fix was the actual break.

External skill references, not vendored wholesale

AGENTS.md/CLAUDE.md now point at specific skills from WordPress/agent-skills — wp-phpstan (source for the WP-aware PHPStan setup above), plus wp-performance, wp-wpcli-and-ops, wp-playground as on-demand reading. Explicitly steering away from wp-block-development and wp-block-themes: those teach native Gutenberg blocks and theme.json, both of which TAW replaces with its own MetaBlock/Block system and Vite pipeline — following them would fight this framework's conventions rather than extend it.

2026-07-09functions.php split, inspect CLI, and a real Vite dev-detection fix
taw-coretaw-themeclivitebugfix

July 2026 — framework bootstrap split, live introspection, dev-server detection fix

Theme::bootstrapFullSite() — functions.php is now 100% framework-owned

New one-call bootstrap (taw/core v1.16.63+): functions.php collapses to two lines (require autoload, call Theme::bootstrapFullSite(get_template_directory())). Everything that used to be hand-written there — theme supports, nav menus, performance tuning, VisualEditor::enable(), an explicit MetaboxOrder::lock() — moves to three theme-owned files, none of which framework updates ever touch: inc/options.php (pre-existing), inc/performance.php, and inc/customizations.php.

inc/customizations.php loads before Theme::boot() (fixed in v1.16.65). Flag-style opt-ins like VisualEditor::enable() are synchronous — boot() reads the flag immediately via VisualEditor::init(), which silently no-ops if enable() hasn't run yet. Hook-registration-only customizations (add_action calls for later events) work fine regardless of order.

VisualEditor is opt-in, not automatic — call VisualEditor::enable() in inc/customizations.php before Theme::boot() runs. (Earlier docs incorrectly said this was automatic — corrected.)

php bin/taw inspect — live introspection

New CLI command reports the site's actual current state: registered blocks and their real metabox field schemas, registered forms, the installed taw/core version, and whether MetaboxOrder is locked. Unlike other bin/taw commands, this one boots WordPress itself, since the data only exists once after_setup_theme/init have fired.

php bin/taw inspect          # human-readable summary
php bin/taw inspect --json   # machine-readable

CI smoke-test

New .github/workflows/ci.yml + bin/ci/check-getdata-signature.php — runs composer validate, php -l across the repo, and a check that every MetaBlock::getData() is declared exactly getData(int|false $postId): array on every push/PR. A narrower signature there is a PHP fatal that takes the entire site down, since every block auto-loads on every request.

update-theme — manifest-based sync, no git merge required

The update-theme AI skill (for syncing the shared taw-theme scaffold into a client site) no longer depends on shared git history at all. It copies a small, precisely-delimited set of framework-owned paths (functions.php, .claude/skills/, bin/, CI config) directly from a fresh checkout — no merge, no conflicts. A new client project can start from a single clean git init commit.

Vite dev-server detection — real bug fix

ViteLoader::isDevServerRunning() previously just checked whether anything was listening on port 5173 (fsockopen). Any unrelated process — another dev server, a Docker container, anything — could occupy that port for reasons that have nothing to do with the project, causing the theme to serve dead localhost:5173 asset URLs in production with no CSS/JS loading at all, even though npm run build succeeded and the manifest was completely correct. Found on a real client project where an unrelated Dockerized Vite instance for a different project happened to be bound to the same port.

Fixed in taw/core v1.16.66: isDevServerRunning() now verifies at the HTTP level — GET /@vite/client must return an actual 200 — not just that the port is open. The scaffold's vite.config.js also gained an optional hot-file convention (writes the dev server's real URL to public/build/hot) for resolving the correct host:port, and keeps server.strictPort: true deliberately (auto-moving ports would silently break hardcoded font URLs instead of failing loudly).

CI: composer GitHub auth + stale PHP version constraint

Two real bugs found fixing CI: (1) composer install needs the GitHub API to resolve taw/core's VCS repository, and unauthenticated API access caps at 60 requests/hour — Actions' shared runner IPs hit that constantly. Fixed by authenticating with the free GITHUB_TOKEN. (2) Once that was fixed, CI correctly surfaced that composer.json claimed PHP >=8.1 while symfony/console had already resolved to a version requiring >=8.2 — a real, pre-existing latent bug, not just a CI mismatch. Bumped the declared minimum PHP to 8.2 everywhere to match reality.

2026-06-16Docs update
docs

Docs — June 2026 (2)

New: Framework and Dump helpers

  • TAW\Helpers\Framework — documented the path/URL resolver helpers (path(), url(), themePath(), themeUrl()) that were present in the taw/core README but missing from the docs.
  • TAW\Helpers\Dump — documented the debug utility helpers (Dump::dd(), Dump::log()), also missing from the docs.

Blocks

  • getData(int|false) 404 note — added explanation that getData() receives int|false because get_the_ID() returns false on 404 pages, and that all MetaBlock convenience helpers handle false safely.
2026-06-11Docs update
docs

Docs — June 2026

Options page

  • OptionsPage field types expanded — per the authoritative taw/core README, OptionsPage now supports the same field types as Metabox (all types, including files, group, post_select, repeater, and datepicker). The previous explicit subset list has been replaced with the correct statement.
2026-06-10Docs update
docs

Docs — June 2026

Visual Editor

  • Visual Editor documented — the Visual Editor (TAW\Core\Editor\VisualEditor) is now opt-in and production-ready. Enable it by calling VisualEditor::enable() before Theme::boot() in functions.php. The new Visual Editor section covers automatic behaviour, template annotations (Editor::field(), Editor::attrs()), and the save endpoint.
  • Updated theme boot callout — removed the "not ready for production" warning from the Theme boot page; replaced with a note pointing to the Visual Editor API.

REST API

  • Visual editor endpoints documented — POST /taw/v1/visual-editor/save and GET /taw/v1/visual-editor/fields are now listed in the REST API endpoint table.

Options page

  • Removed files from supported field types — files is not supported in OptionsPage (only in Metabox). Corrected the supported-types list to match the authoritative README.
2026-06-05taw/core v1.15.52
stable

taw/core v1.15.52 — June 2026

Block system

  • MetaBlock::variations() static method — block variations are now declared by overriding public static function variations(): array and returning an array of string suffixes ('' = default variation). The previous constructor-based parent::__construct(['variations' => [...]]) approach is replaced.

Asset pipeline

  • ViteLoader::inlineCriticalCss(string $path) — inline critical CSS directly into <head> from any path relative to the Vite asset root. Use this to manage critical CSS loading from code rather than relying solely on the automatic critical.scss detection.
  • ViteLoader::preloadAssets(array $paths) — add modulepreload hints for JS chunks to improve first-load parse performance.

Theme boot

  • Theme::boot() accepts optional config array — pass ['performance' => [...]] directly to Theme::boot() as a shorthand for calling Theme::performance([...]) as a separate step. Both patterns remain supported.
2026-06-05taw/core v1.15 · TAW Theme update
stable

taw/core v1.15 · TAW Theme — June 2026

Asset pipeline

  • ViteLoader OOP API — TAW\Support\ViteLoader is now the authoritative way to interact with Vite assets. New static methods: ViteLoader::assetUrl(), ViteLoader::isDevServerRunning(), ViteLoader::enqueueAsset(), and ViteLoader::init().
  • Legacy procedural helpers removed from autoload — vite_asset_url() and vite_is_dev() are no longer in the Composer files autoload and are not available globally. Use ViteLoader instead.
  • SCSS block asset priority — when a block has both style.scss and style.css, SCSS now takes priority.

Form system

  • Form::register() + Form::display() — forms are now registered statically via Form::register() inside boot() and displayed in templates with Form::display('id'). The old new Form() + $form->render() pattern has been replaced.
  • New input field types: radio (with options and layout), checkbox_group (stored as comma-separated string), date (with min_date and max_date).
  • Structural field types: heading, divider, and html — cosmetic fields for layout and copy inside a form with no submission data.
  • AND / OR conditional logic — conditions now supports 'relation' => 'any' for OR logic. The old flat-array AND format is still fully supported.
  • Multi-step forms — replace the top-level fields key with steps. Each step has a title and its own fields array. Includes a numbered step indicator, per-step client-side validation, and automatic navigation to the failing step on server error.
  • SubmissionsHandler auto-wired — Theme::boot() now registers SubmissionsHandler automatically; manual instantiation is no longer needed.

Metabox engine

  • datepicker field type — jQuery UI date picker; stored as a date string (YYYY-MM-DD default). Supports date_format, min_date, and max_date options.
  • Repeater layout option — repeater rows can now render as tabbed_horizontal or tabbed_vertical in addition to the default accordion.
  • screens key (plural) — the Metabox config option is now screens (accepts an array of post types, page slugs, or page template filenames). The singular screen is no longer used.

Transactional email

  • Template path updated — pre-compiled HTML templates now live at mails/html/{name}.html. MJML source files remain at mails/{name}.mjml for dev-time compilation.
  • MailTester registration — register manually with (new \TAW\Core\Mail\MailTester())->register() in functions.php.

Block system

  • defaultData() method — UI Blocks now use defaultData() instead of defaults() to define prop fallbacks. (Correction, October 2026: this never shipped. UI Blocks define their prop fallbacks in defaults(), as TAW\Core\Block\Block requires.)
  • getData(int|false $postId) — MetaBlock::getData() now accepts int|false so blocks return safe empty values on 404 pages.
  • boot() for all blocks — BlockLoader now calls boot() on every block type (not just UI Blocks) during discovery.
  • MenuItem::target() — new method returning '_self' or '_blank'.
  • MenuItem::isActiveAncestor() — new method; isInActiveTrail() now means any of active/activeParent/activeAncestor is true.
  • MenuItem::wpPost() — new method returning the raw WP_Post menu item object.

TAW Theme

  • ThemeUpdater re-introduced via taw/core — TAW\Core\ThemeUpdater is available again for GitHub Releases-based auto-updates. Instantiate it in functions.php.
  • CSS Studio toggle — CSS Studio is now activated via WP Admin → TAW Settings → Developer Tools → Enable CSS Studio (previously auto-activated when the Vite dev server was running).
  • Image::render() signature — the $alt parameter has been removed. Pass options (including class, sizes, above_fold, attr) as the third argument directly.
  • Image::preloadTag() — renamed from preload_tag() to camelCase preloadTag().
2026-04-14taw/core v1.14 · TAW Theme update
stable

taw/core v1.14 · TAW Theme — April 2026

Metabox engine

  • files field type — new multi-file picker with drag-to-reorder. Stores an array of attachment IDs. Accepts limit to cap the number of selectable files.
  • Nested repeaters — repeater fields now fully support nesting: place a repeater inside another repeater's fields array for hierarchical data structures up to three levels deep. Serialization is pre-submit and handles re-entrant calls safely.
  • JSON encoding — all repeater data is now serialized with full Unicode support.
  • postMatchesTemplate — new method on Metabox for template matching; screen now accepts multiple post types (comma-separated or array).
  • Repeater template tag — internally switched from <script> to <template> for repeater row prototypes (better browser compatibility).
  • jQuery selector optimization — repeater element selectors are scoped for improved performance on pages with multiple repeaters.

Block system

  • Block variations — MetaBlock now accepts a variations array in its constructor to declare WordPress block variations. Asset handles are automatically namespaced per variation.
  • BlockLoader::boot() for plain blocks — when BlockLoader loads a UI Block (extending Block), it now calls the block's boot() method if one exists. Use it for one-time setup such as registering hooks.

REST API

  • post_select page handling — SearchEndpoints now properly validates the page post type and handles the WordPress special case automatically.

TAW Theme

  • ThemeUpdater removed — the ThemeUpdater class has been removed from TAW Theme. Use composer update taw/core to update the framework.
  • SearchEndpoints cleaned up — removed the theme-level override; the endpoint is now served exclusively by taw/core.
  • Hero block — added content (wysiwyg) field. Added a three-level nested_repeater field as a reference implementation for hierarchical repeater data.
  • CSS Studio — added as a dev-only dependency. Activated automatically when vite_is_dev() is true; never shipped to production.
  • Performance — Theme::performance() now schedules its work on after_setup_theme for better WordPress filter compatibility.
2026-03-19Initial release
stable

v1.0.0 — Initial release

TAW Theme and taw/core v1.0.0 are the first stable release of the framework. Everything below ships out of the box when you run composer create-project taw/theme.

Block system

  • Auto-discovery — drop a class and a template inside Blocks/ and TAW finds it automatically. No registration step, no functions.php edits required.
  • MetaBlock — page sections backed by metabox data. Extend TAW\Core\MetaBlock, define fields in registerMetaboxes(), return template vars from getData().
  • Block (UI Block) — stateless components that receive props at render time. Extend TAW\Core\Block and define a defaults() array. Missing props always fall back safely.
  • Block nesting — UI Blocks compose naturally inside MetaBlock templates via (new Button())->render([...]).
  • Subgroup organisation — blocks can live in nested folders (Blocks/sections/Hero/) for large projects.
  • BlockRegistry::queue() / ::render() — queue a block's assets before get_header(), render it anywhere in the template.

CLI scaffolding (bin/taw)

Powered by Symfony Console, shipped inside taw/core.

  • make:block Name --type=meta|ui — scaffold a MetaBlock or UI Block with the correct class stub and template.
  • --with-style — include a style.scss wired into the Vite pipeline.
  • --with-script — include a script.js loaded as an ES module in the footer.
  • --group=sections — place the block inside a subgroup folder.
  • --force — overwrite an existing block.
  • export:block Name — export any block as a portable ZIP file.
  • import:block path.zip — import a block from a ZIP into the Blocks/ directory.

Metabox engine

Config-driven metaboxes with no plugin dependencies. All field types render, validate, sanitize, and save through TAW\Core\Metabox\Metabox.

Supported field types: text, textarea, wysiwyg, url, number, range, select, checkbox, color, image, files, group, repeater, post_select

  • Conditional logic — show or hide fields based on other field values. Evaluated live in the admin with Alpine.js and server-side on save. Operators: ==, !=, contains, empty, !empty.
  • Tabbed layouts — group fields into tabs using the tabs key; each tab references field IDs from the fields array.
  • Responsive grid — control field width with the width option (e.g. '50' for two-column).
  • show_on callable — conditionally hide an entire metabox per post.
  • Retrieval helpers — Metabox::get(), ::get_bool(), ::get_image_url(), ::get_color(), ::get_posts(), ::get_repeater() cover every field type.

Options page

  • TAW\Core\OptionsPage — site-wide settings stored in wp_options using the same field config as metaboxes.
  • Supports tabbed layouts, validation, and all field types except repeater and group.
  • Retrieval via OptionsPage::get() and OptionsPage::get_image_url().

Forms

  • TAW\Core\Form\Form — config-driven frontend forms with CSRF (nonces), honeypot spam protection, field validation, and PRG redirect after success.
  • Supports text, email, textarea, select, and any standard HTML input type.
  • Sends dual emails (to-self and to-client) via Mailer when templates are configured; falls back to plain-text wp_mail() otherwise.
  • TAW\Core\Form\SubmissionsHandler — stores successful submissions as a taw_submission CPT in WP Admin.
  • Optional webhook forwarding (n8n, Zapier, Make, etc.) with HMAC-signed payloads. Configure endpoint and secret under Settings → Form Webhook.

Transactional email

  • TAW\Core\Mail\Mailer — fluent wrapper around wp_mail() with a to(), subject(), template(), setVariables(), send() chain.
  • TAW\Core\Mail\MailTemplate — resolves templates from mails/html/{name}.html (production) or mails/{name}.mjml (dev, compiled via spatie/mjml-php). Uses {{variable}} placeholder syntax.
  • TAW\Core\Mail\MailTester — admin page under Tools → Test Emails for sending test emails against any compiled template without a real form submission.
  • TAW\Core\Menu\Menu — typed wrapper around WordPress nav menus. Load by location slug with Menu::get('primary').
  • TAW\Core\Menu\MenuItem — exposes title(), url(), openInNewTab(), hasChildren(), children(), isActive(), isInActiveTrail(), classes(), and more. WP auto-classes are filtered out by default.

Asset pipeline

  • Per-block assets — style.scss (or style.css) and script.js inside any block folder are auto-detected and auto-enqueued only on pages that use that block.
  • Critical CSS — resources/scss/critical.scss is compiled and inlined in <head> to eliminate a render-blocking network round-trip for above-fold styles.
  • Async CSS — app.css / app.scss are loaded non-render-blocking via media="print" + onload swap.
  • ES module JS — block scripts and the main app.js are loaded as type="module".
  • Content-hashed filenames — all production assets are cache-busted automatically.
  • vite_asset_url($path) / vite_is_dev() — global helpers available anywhere in your templates.
  • Tailwind CSS v4 via @tailwindcss/vite. Alpine.js v3 for interactivity.

Performance

Theme::performance([...]) (forwarded to TAW\Core\Support\Performance) supports:

  • remove_bloat — strips unnecessary WordPress front-end scripts, styles, and features.
  • remove_emoji — disables WordPress emoji detection scripts.
  • remove_meta_tags — removes default WordPress generator and discovery meta tags.
  • remove_oembed — disables oEmbed support and associated scripts.
  • preconnect_origins — emits <link rel="preconnect"> hints for external origins.
  • preload_fonts — preloads self-hosted WOFF2 font files resolved via ViteLoader.
  • preload_images — emits <link rel="preload"> for above-fold images by attachment ID and size.

Image helper

  • TAW\Helpers\Image::render($id, $size, $alt, $options) — generates <img> with correct loading, fetchpriority, decoding, srcset, and sizes attributes. Pass ['above_fold' => true] for hero images.
  • TAW\Helpers\Image::preload_tag($id, $size) — generates a <link rel="preload"> tag for your single most important image.

SVG support

  • TAW\Helpers\Svg::register() — enables sanitized SVG uploads in WordPress (called automatically by Theme::boot()).
  • Svg::render($id, $alt, $options) — renders a sanitized <img> tag.
  • Svg::inline($id, $options) — outputs inline SVG markup for CSS/animation targeting.
  • Svg::url($id) — returns the URL for a stored SVG attachment.

REST API

  • GET /wp-json/taw/v1/search-posts — powers the post_select metabox field. Supports s, post_type, per_page, and exclude parameters. Requires edit_posts capability.

Theme updater

  • TAW\Core\Theme\ThemeUpdater — removed in a later release hooked into the WordPress update system to check a GitHub Releases URL for new versions. Shows the standard Update Available notice in admin. Updates are cached for 6 hours. Deprecated — see the v1.14 entry below.

Boot process

  • TAW\Core\Theme::boot() — single entrypoint that wires block loading, Vite assets, REST endpoints, and SVG support into WordPress hooks.
  • TAW\Core\Theme::performance([...]) — configures performance behavior after boot.