Changelog
Release history for every TAW piece: taw/core and its taw command, TAW Theme, TAW Gutenberg, taw-fleet and the TAW Companion.
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
httporhttps. Anhttp://link to the site's own domain used to come back ashttps://, 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.
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 inspectand 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.
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.
Nin the dashboard adds one, and the details' new NOTES section shows the newest three. In a terminal,taw-fleet notes <site>shows them andtaw-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 withadd_notewhen 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.
October 2026: open at login
- taw-fleet v1.33.0:
taw-fleet config open-at-login onopens the dashboard in its own window each time you log in to your Mac;offstops it. It sets the newopen_at_loginsetting 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.
October 2026: tags and grouping in popups
- taw-fleet v1.32.0:
Topens 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#isOption+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.
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 pressenter, or press a number;spaceticks several when it allows more than one; the last option types your own answer, andescleaves it unanswered. Before, a yes sent Claude no answer at all.
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 --decisionsand Claude'spublish_contenttake 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”).
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.
October 2026: tags and groups
- taw-fleet v1.31.0: give each site tags (
retainer,es,client:acme) with#in the dashboard ortaw-fleet tag. They show on the site's card, the filter finds them (#retainer,tag:client:), andtaw-fleet list --taglists 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 withset_tags. See Tags and groups.
October 2026: a JSON field that can't break the page
- taw/core v1.99.0: a new
jsonfield 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.
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 updatealso brings taw-theme 1.12.75'stests/TestCase.phpto 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.
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.95and more.mx -is:liveorcore:<1.95 has:commentsnarrow a big fleet in a few keys. The footer counts the matches and names anything it didn't understand. See Filter.
October 2026: block tests on taw/core 1.98
- TAW Theme v1.12.75: the theme's
tests/TestCase.phpstubsget_post()instubBlockConstructor(), 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 whoseTestCase.phpis unedited gets it fromtaw update; a site that edited it can add the same stub.
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+nstarts a new chat (the old one stays in Claude Code's history), andctrl+cin the chat only stops Claude: it no longer quits the dashboard. See Chat with Claude.
October 2026: publish content to production
- taw-fleet v1.29.0:
ppublishes 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, andxundoes the last publish, keeping anything edited since. In a terminal,taw-fleet publish <site>(--no-reviewprints 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.jsonsays"publish": trueunderextra.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 thecontent:*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).
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 bysingle-{type}.php: addMetaboxOrder::lockFromTemplate('<post type>');toinc/customizations.php. A client site's role pages asked for it. The themes pick it up with their next taw/core lock.
October 2026: the launch report
- taw-fleet v1.28.0: once a site is live,
Ichecks 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 linklinks a site to its BugSmash project. In the chat, thenew-siteskill 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.
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/,.envfiles or a key is refused. Sites with their own deploy keep it. - taw-fleet v1.27.0:
Ion 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, theadd-productionskill 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.
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.
Idoes 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 readyprints the checklist in a terminal; Claude can read it and create the repository too, asking first. See Set up a site.
October 2026: a new site from a link
- 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-sitefleet skill. It writes a brief for you to approve, creates the Local site, builds it with the theme'sbuild-siteskill (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.
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.
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 aCLAUDE.mdmade 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).alists them under Fleet skills, and they run in the fleet's chat.taw-fleet initwrites the folder, and the dashboard keeps it current. See The fleet folder.
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.escleaves it working;taw-fleet chat --promptprints what Claude is told. See Chat with Claude.
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 mergeand the MCP merge tool, which don't wait for the deploy, end with the same sentence as taw/core's update report.
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.
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.
October 2026: shared links show their picture again
- 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.
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 syncthen 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 yourghlogin), for 5,000 requests an hour. If you see the rate-limit message, rungh auth loginonce. - taw-theme v1.12.68 and taw-gutenberg v0.3.70 lock taw/core 1.94.1.
October 2026: doctor watches the pre-commit check
- taw-fleet v1.22.1: on themes with taw/core 1.94 or newer,
doctorreportsgit.no-hookwhen the pre-commit check that refuses dumps,.envfiles and keys isn't on, andgit.hook-ownwhen the clone has git hooks of its own, so the check needs adding by hand.list --jsonshows each theme'spre_commit_hook. - The report's terminal summary now counts framework updates, maintenance and the site's own changes separately.
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'suorU, orcomposer 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-verifyskips 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.
October 2026: a monthly report for each client
- taw-fleet v1.22.0:
min the dashboard, ortaw-fleet report <site>, makes a site's monthly report: availability fromwatch, 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,
watchmakes last month's report for each production site and tells you.report_language(en,es, also per site) andreport_fromset the language and the name. See Monthly reports.
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 exportinto 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. zin the dashboard, ortaw-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.
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 andtaw make:block --helpsay what the critical CSS needs: the header positioned exactly like the full stylesheet, each template's top section, andprotected bool $critical = true;on blocks right under the banner. - Fixed: a
$criticalblock queued withBlockRegistry::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.
October 2026: the fleet tells you, even with the dashboard closed
- taw-fleet v1.20.0:
taw-fleet watch installchecks 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-notifierfor that.taw-fleet --site <site>does the same from a terminal. watch statusshows whether it's on, the last check and what it told lately.watch testsends a notification so you can see it arrive. See Notifications.
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 astyle_loader_tagfilter, 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.
October 2026: AI apps that ask before they act
- taw-fleet v1.19.0:
taw-fleet mcpnow serves the dashboard's actions too:update_site,update_all,merge_pr,site_branch,start_site,stop_site,sync_applyandpull_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
tawtool 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,Din the dashboard) andtaw-fleet comments <site> --prompt(the promptXhands Claude). Both are also MCP pieces: thesite_branchtool and theresolve_commentsprompt.
October 2026: the fleet, for AI apps
- taw-fleet v1.18.0:
taw-fleet mcpserves the TAW sites on your Mac over the Model Context Protocol. Add it once withclaude mcp add taw-fleet -- taw-fleet mcp, or in any app's MCP settings. - Tools:
list_sites,show_site,doctor,pull_requests,comments,production, andtawfor 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 toolslists them. - Read-only for now. Writes that ask first, as the dashboard does, come next. See AI Integration.
October 2026: one command, taw
- The docs, the themes' agent docs and skills, and taw/core's help now write commands as
taw ….tawcomes with taw-fleet 1.17.0 and runs the theme's ownvendor/bin/tawwith the site's PHP, from the theme or from anywhere withtaw @<theme>. See The taw command, rewritten around it. - Nothing changes in a theme:
vendor/bin/tawis the same command wheretawisn't installed, like CI. - taw/core v1.93.2, taw-theme v1.12.64 (skills and README) and taw-gutenberg v0.3.66.
October 2026: one taw command
- taw-fleet v1.17.0 installs a second command,
taw. It runs the theme's ownvendor/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,tawlists every theme by its short name. - The right PHP: on a Local site,
tawuses 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 withoutvendor/is told to runcomposer install. TAW_DEBUG=1 taw …prints the exact command it runs. CI keeps callingvendor/bin/taw, which is the same command.- Install:
brew upgrade taw-fleetinstallstawtoo.taw-fleet self-updateputs it next totaw-fleet.
October 2026: merges you can follow
- taw-fleet v1.16.0:
Mno 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.
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-latestmoves 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.
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*.wpressbackup, or a file that looks like one), anything under.sync/(content snapshots,remote.env), a.envorwp-config.phpfile, 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
.gitignoreline. 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.
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, anddoctorcodegit.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.Mwith a delivery remote: whentaw.json'supdate.remoteisn'torigin,Malso bringsorigin'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:
uis always available; block themes' update question no longer mentions framework files;/blockfinds block themes; an unknowniconsvalue is an error; help texts point to the theme'sresolve-commentsskill.
October 2026: update the whole fleet, no agent needed
- taw-fleet v1.15.0:
U(ortaw-fleet update --all) updates every client theme that needs it throughvendor/bin/taw update, one after another, each ending in its own pull request. One screen shows a line per theme;entershows a theme's steps,oits pull request,rits 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 anddoctorfollowtaw.json'supdate.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 nerdswitches to Nerd Font icons. - See Dashboard and keys.
October 2026: updates reach the repository that deploys
- taw/core v1.92.0: a new
taw.jsonsetting,"remote"(default"origin").vendor/bin/taw updatepushes 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 whileoriginis 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.
October 2026: watch an update happen
- taw-fleet v1.14.1: "Update this site" gets its own screen: the versions and the
taw.jsonpolicy 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 (lfor the full log). - When it ends, a result panel:
oopens the pull request,rthe report, and the "For you" steps or the error are shown in full.escgoes back to the list while it runs.
October 2026: update a site from the dashboard
- taw-fleet v1.14.0:
u(ortaw-fleet update <site>) runs the theme'svendor/bin/taw updatewith the site's PHP and Local's Composer, after one question worded from itstaw.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, putscomposer.lockback, 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 updateexits with 1. - See Updating a site.
October 2026: the update report starts from what git has
- taw/core v1.91.2:
vendor/bin/taw updatereads the installed taw/core version fromcomposer.lock, and fromvendor/only when there's no lock. That version drives the report's “from → to”, thepatchrange and a pin, so the update is right even whenvendor/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.
October 2026: updates that respect the site's choices
- taw/core v1.91.1: migrations follow
taw.jsonwherever they run: invendor/bin/taw update, in the Composer hook and invendor/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-docsmigration anddocs:sync --applyreplace only copies identical to a version the scaffolds shipped. An edited copy stays, with the steps to move your notes;docs:sync --apply --forcereplaces it anyway. In 1.91.0 they replaced edited copies too: if that happened to a theme, its notes are in git history. sync --applynow only reports the agent docs and the configs;docs:syncandconfigs:syncwrite them. The weekly framework-sync workflow runsvendor/bin/taw upgrade --applyaftersync.vendor/bin/taw updateprints its progress as it goes (on stderr with--json), keeps quoted parts of--composerwhole, so Local's paths with spaces work, and--plan --jsonprints JSON.
October 2026: the docs, built around the tools
- New navigation: Start here, then the
tawcommand, 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(), notdefaultData()), the Vite dev-server port notes, the companion's version constraint, and links to pages that don't exist. Commands are writtenvendor/bin/tawthroughout. - Removed: the documentation template's sample pages (
authentication,notice,components).
October 2026: a site updates like a package
- taw/core v1.91.0:
vendor/bin/taw updateupdates 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.jsonis 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 policyexplains it in words.- When an update stops, nothing is pushed.
.taw/update-report.mdsays 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 updateruns 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.
October 2026: the taw command lives in taw/core
- taw/core v1.90.0 ships the
tawcommand asvendor/bin/taw, so new commands arrive withcomposer update taw/core. Add your own withTAW\CLI\CommandRegistry. See The taw command. - taw-theme v1.12.57 and taw-gutenberg v0.3.59:
bin/tawnow hands over to it, sophp bin/tawkeeps working. - This is the first step toward updates that need no questions: a TAW update becomes a plain
composer update.
October 2026: menus that explain themselves
- taw-fleet v1.13.0:
:(all actions) anda(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.
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) andperf-audit(GTMetrix and PageSpeed).bin/taw syncinstalls them in classic themes, and the newphp bin/taw skills:sync --applyinstalls 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
ato start Claude Code in a theme folder on one of its skills, andXnow runsresolve-commentsthere too.taw-fleet wp-remotewrites to the live site as a bot user whose password stays in the Keychain, anddoctorreports skills a theme hasn't installed.
October 2026 — starts and stops that don't hang
- taw-fleet v1.11.0:
X,Uandhandoff --launchopened 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.
October 2026 — act on client comments from the dashboard
- taw-fleet v1.10.0:
Fopens a site's BugSmash review page, andXhands its open comments to Claude Code in the TAW umbrella with thetaw-resolve-commentsskill. 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
classicorblock. - 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.
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.
October 2026 — client feedback in the morning check
- taw-fleet v1.9.0: sites with a BugSmash project (
bugsmash_projectin 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,doctorfindings (comments.open,comments.unreachable,comments.no-key), andtaw-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.
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.
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 pullgets 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.
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.
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.
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-termsbring them back. - Change-sets from
content:diffcarry 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.
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.
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.
October 2026 — references and links that survive a move
- 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.
October 2026 — refresh every site at once
- taw-fleet v1.8.0:
ctrl+rreads 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 refreshdoes the same on the command line.
October 2026 — content from code into the database
- taw/core v1.80.0: metaboxes and options pages take a
defaultskey: what a field reads, and shows in the editor, while nothing is stored. It replacesgetMeta(...) ?: '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:defaultsruns as the first administrator. taw-theme v1.12.49, taw-gutenberg v0.3.47: taw/core 1.80.1.
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.
October 2026 — check every theme, find any action
- taw-fleet v1.7.0:
Y(ortaw-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.
October 2026 — production content into Local
- taw-fleet v1.6.0:
C(ortaw-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
@accountin the dashboard (/otherfilters to them). - TAW companion v0.4.0: a signed, read-only
GET /contentroute with the site's published content snapshot. All four live sites run it.
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).Mmerges 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.doctorwarns about failed deploys, failing or conflicting pull requests, and commits not deployed.
October 2026 — one key to start working
- taw-fleet v1.4.0:
wstarts 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
viteafter a theme's name while its dev server runs;/vitefilters to those.
- The dashboard shows
- taw-fleet v1.3.1: production sites come first in the dashboard;
/livefilters to them.
October 2026 — update every theme at once
- taw-fleet v1.3.0:
Uupdates every client TAW theme that needs it in one Claude Code session.- One subagent per theme follows the
update-themeskill's batch mode. - You decide docs changes, sites to start and pushes once, at the end.
unow moves taw/core's dependencies too, and says so when the version didn't move.
- One subagent per theme follows the
- 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.
October 2026 — safer theme updates
- taw/core v1.78.0:
php bin/taw synccomparescomposer.jsonandpackage.jsonwith 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-manifestswrites the suggestions.
- taw-theme v1.12.45: the
update-themeskill 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.
October 2026 — taw-fleet v1.2.0
- Its own window.
taw-fleetopens the dashboard in a new terminal window that it fills, and gives the current terminal back; quitting closes the window.--window=false, orwindow = falsein the config, keeps it in the current terminal. - Update with agent.
Aopens 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.uoffers 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".
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 liveshows 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;
doctoraddslive.*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.
- a Composer requirement, the fleet key in
- taw/core v1.77.0:
hub:installandhub:enrollare retired (hidden stubs that explain what replaced them);enshrined/svg-sanitize1.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/tawdrops the hub commands, and thehub-connectskill is gone.
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:
updaterunscomposer update taw/coreand lists the UPGRADING.md sections to check;synccompares a theme with the taw-theme scaffold;handoffgives the whole update to an agent with a prompt that has the site's context and your rules.
- Sites:
start,stopandrestartgo through the Local app;wpruns wp-cli with the site's PHP and database;createmakes a new Local site with taw-theme or taw-gutenberg, activated and committed.
- Checks:
doctorlists what needs attention, with stable codes for scripts. - Updating taw-fleet:
brew upgrade taw-fleet, ortaw-fleet self-updatewithout Homebrew.
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-printclass on the details column; - the cover's caption from a new Cover credit field;
- a
sponsoredrel on the Buy button; - an
#awardsanchor.
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()servestaw/v1/canon-law/*from a Code installed withbin/taw canon-law:install <edition> <.sqlite|.json>. On hosts withoutpdo_sqlite, it installs into MySQL. bin/tawgainscanon-law:installandcanon-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.
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.
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'smetadata.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.
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.
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.
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.
September 2026 — Expressions in links and images
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.
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_functionsfilter, and they work everywhere expressions do. - The TAW data popup's Fields tab now lists Visitor and date values (logged in, role, today, now).
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. Alsocoalesce()(the first value that isn't empty) andempty(). - 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.
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\Performanceis now loaded by the class autoloader instead of on every request. Nothing changes for themes: the same hooks register, andTAW_PERFORMANCE_AUTOLOADstill works.
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.
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).
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.
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.lastin 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.
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_linkfield) 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.
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
todayor-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.
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.
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, withformat,upper,lower,defaultandtruncate. - 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.
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/fieldBlock 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.
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
.potranslated taw-core text. php bin/taw wp db …works under Local, andbin/taw wptells you when the Local site is stopped.bin/taw synckeeps 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.mdhas 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.
September 2026 — The link field
Tagged taw/core v1.59.0, TAW Gutenberg v0.3.13 and taw-theme v1.12.16.
linkis 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/contactwork, andrequiredmeans 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.
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')andTaw::options('site')read fields the same way asTaw::post(). See Reading fields.
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(), andechoescaped 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.
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-coretext domain. Before, most used the theme'staw-themedomain, and the form and password-gate messages used atawdomain 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.potranslates 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.
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 atGET /wp-json/taw/v1/options/<page id>, to anyone. Use it only for pages with nothing private on them.- Pages without
restexpose nothing, as before. See Options pages over REST.
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.
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.
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()orMetabox::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.
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:importruns 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.
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 andfields:get/fields:setlook 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:setand the importer wrote group sub-fields to the wrong meta key (_taw_{sub}instead of_taw_{group}_{sub}).
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) whenLucide::enable()has been called. Before, onlyTheme::boot()did, soiconfields 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.
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 (asettingsschema file withfieldsetUi), or per install (TAW_DATA_UIinwp-config.php). - Every field type works, including groups, repeaters (with nested ones) and conditions. Values are stored exactly as the metabox stores them.
wysiwygfields 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,validatecallbacks and read-only fields on block-editor saves. In the editor, saving is locked while a required field is empty.
- It's off by default. Switch it on per fieldset (
- Fix: repeater values written outside the metabox form (REST, the visual editor,
fields:set,content:import) no longer lose nested repeaters, orfilesand multiplepost_selectvalues, inside rows. - Fix: REST writes of
gradient_textandhubspot_formare cleaned the way the metabox cleans them (for example, an empty HubSpot region becomesna1). - Both themes only update their taw/core lock. The panel stays off in both, and their hooks are unchanged.
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.
themeBlocksis 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
guidedand above can still insert them. - Levels that allow every block are unchanged, and it still applies when
TAW_EDITING_PRESETsets the level. - See Editing policies.
- They're added to every allow list, the curated one included, so clients at
- 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.
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()andurl(), plusblock(), which loads a block.json'sfile: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/hotfile now lives inAssets\DevServer, and the classicViteLoaderuses 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()andphpReload(). 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-projectbuilds them for you. See TAW Gutenberg.
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 intaw-schema/editing.json(new JSON kindediting), withSchema::editing()in PHP, or per post type withPostType::editing().schema:validatechecks the file. - Per install:
TAW_EDITING_PRESETinwp-config.phppicks the level.TAW_EDITING_BYPASS_USERSnames who stays unlocked, even when clients are Administrators.TAW_EDITING_OFFturns 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
contentOnlylock, 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 theopenpreset. taw-theme doesn't, and its hooks are unchanged.
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.
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) ortaw-gutenberg(block theme).TAW_STARTER=theme|gutenbergskips the question, andTAW_STARTER_VERSIONpins 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.
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, orwp-content/). PHP definitions outrank every file. An invalid file is skipped with a notice. See Data layer. php bin/taw schema:validatechecks those files without WordPress, pointing at each problem (/fields/2/type).resources/schema/taw-schema-1.0.jsongives 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:validateinbin/taw. The README andstyle.cssno longer spell out “TAW”, and both now state the real PHP requirement (8.2+).
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.
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 callingboot(), defineTAW_PERFORMANCE_AUTOLOADastrueinwp-config.php. Framework::url()resolves package assets from a child theme too.dump()/dd()no longer clash with other libraries.ext-pdo_sqliteis now optional (only the RAG and corpus features use it).
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.
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.
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.
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.
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 plainheadingfield plus a single always-trailinghighlightfield (which can't express a heading like "How can we help?", where the highlighted run isn't last). Render withMetabox::renderGradientText($segments, $highlightClass).hubspot_form—{portal_id, form_id, region}, rendered via newTAW\Core\Integrations\Hubspot::render()/isConfigured(), for a HubSpot embed with a fallback to the site's nativeFormblock when unconfigured.
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;
});
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.
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 "). esc_attr() left that " 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.
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.
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.
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.
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 realsqlite::memory:connection attempt, since a loaded extension isn't always functional on every host build.TAW\Core\Corpus\Bible\BibleReaderInterfaceextracted (deferred in the original design until a second implementation existed to shape it against — that point has arrived);BibleReaderimplements it with zero internal changes, so every host wherepdo_sqlitealready works sees no behavior change at all.MysqlBibleReader— an independent, MySQL-backed mirror of the same read contract:books()/chapter()via$wpdbjoins,searchVerses()/searchNotes()via MySQL boolean-modeFULLTEXT(+word1 +word2, translating the same AND-of-words semantics the SQLite search fix already established) with hand-built<mark>excerpts (MySQL has nosnippet()).bin/taw corpus:export <sqlite-path> <json-path>— dumps a corpus to portable JSON on a machine that haspdo_sqlite, for installing on a target host that doesn't.bin/taw corpus:install— now content-sniffs.sqlitevs. portable JSON and routes to the matching backend; a.sqlitesource on a host withoutpdo_sqlitenow fails with a clear message pointing atcorpus:export, instead of a barePDOException.BibleEndpointresolves 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).
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 asLucide::enable()/MediaFolders::enable()/BibleEndpoint::enable(). Call it ininc/customizations.phpbeforeTheme::boot().taw-theme'sBlocks/Chatbotwidget now checksRagSettings::isEnabled()in bothheader.php(asset enqueue) andfooter.php(render) — previously it always rendered and would havePOSTed to a REST route that no longer registers on a site that hasn't opted in.inc/customizations.phpgets a commented-outRagSettings::enable();line, matching the Lucide/iconfield type's posture (off by default, one line to turn on).
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\Storagesilently killed pre-boot CLI usage. Both carriedif (!defined('ABSPATH')) exit;, butCorpusInstallCommand::execute()callsProtectedSqlite::looksLikeSqliteFile()beforerequire $wpLoad— so the guard fired and exited the whole process with zero output. The exact pitfall this repo's ownCLAUDE.mdalready documents forContent\*(v1.25.1). Fixed by dropping the guard from both, matchingContent\*'s precedent — neither class has a WordPress dependency of its own. A subprocess regression test (Storage\PreBootAutoloadTest, mirroringContent\PreBootAutoloadTest) now runs with noABSPATHdefined at all, the one test shape that can actually catch this.corpus:installwas never wired intobin/taw. The command class shipped intaw-corev1.29.0'ssrc/CLI/, but canonicaltaw-theme'sbin/tawnever got the matching registration (comparecontent: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, sosearchVerses('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.
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'staw-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. Notfinal— 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 throughapply_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.
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.
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
.sqlitefile — 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.sqlitefile 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-contententry, 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 oldlookup_bible/lookup_catechism/search_unstructured_archivetrio. Itsknowledge_baseenum 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/chatrequest/response shape. Markdown replies render viamarked, sanitized withDOMPurify.
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.
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) pluscomment_status/ping_status. On import the author resolves by login, then email, then falls back to the importing user with a warning. --with-usersexports ausers[]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-commentsexports comments on the exported posts withparent_refthreading; import remaps them to their post by slug, rebuildscomment_parent, and recomputes counts.--with-settingsexports a second, opt-in option allowlist for environment settings —permalink_structure,timezone_string,sticky_posts(as slugs),date_format, and more — plus ataw_content_export_settings_optionsfilter. These are import-gated too:content:importskips 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--migrateimport is complete.- Slug-less drafts no longer duplicate on re-import.
draft/pending/auto-draftposts are excluded by default;--include-draftsre-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\Metaboxattached is now included even whenpublic => false. Sites that added a manualtaw_content_export_post_typesfilter just for a content CPT (for example aneventschedule) can drop it. --all-mediaalso exports attachments not referenced by any post/field; media records now carrytitle/description, set on the sideloaded attachment.- Import ordering is deterministic and dependency-first: users → terms → media → posts → comments → settings.
content:diffdiffs the newusersandcommentssections.- The
Tools → TAW Dataexport screen gains checkboxes for the new scopes (passwords / comments behind an "Advanced" disclosure).GET /wp-json/taw/v1/content/exportstays content-only — no user or settings export over REST. - Downstream: the
taw-themescaffoldbin/tawalready registerscontent:*. Any site keeping anowner: sitecontent-sync skill should update itspull/pushsections for the new--with-*/--migrateflags.
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:importprinted nothing (exit 0, no plan table, no "no changes" line).ContentImportCommandreferences aTAW\Core\Content\*class beforerequire wp-load.php; that autoloaded the class file, whoseif (!defined('ABSPATH')) exit;guard fired (WordPress not booted yet) and silently killed the process. The guard is removed from the sixContent\*classes thecontent:*commands load before boot — they are pure class definitions, likeTAW\Helpers\Framework/TAW\CLI\WpLoader, which already omit it. This also fixescontent:diff, which was silent for the same reason.page_on_front/page_for_postsbroke 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 intopage_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) andfeatured_media(filename) were audited — already resolved on write;featured_mediais 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 throughFieldCodec::decodeplus 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).
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.
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=authorsauthor selector still works for Editors (who lacklist_users). /wp/v2/users/mealways stays available — the mobile apps, Jetpack, and the block editor need the authenticated self-lookup, and it already returns401without 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.
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-companionworks. - Unless
TAW_HUB_COMPANION_AUTO_UPDATEisfalseinwp-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.
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.
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_itemtables:vulnerabilityitems (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
wfIssuestable for known vulnerabilities, abandoned plugins, and directory removals; reportslast_scan_atso 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.
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.
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.
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_BINARYisphp-fpmunder the SAPI that serves the Hub's requests, sobin/tawnever actually ran; now it resolves a CLI interpreter beside the SAPI binary / onPATH, override withtaw_hub_companion_php_binary. - 0.1.2 — degrade cleanly where
proc_openis disabled (most managed hosts)./framework/syncand/tawreturn503 exec_unavailable;/healthreports"exec_available": false;/healthand/logskeep working (they never shell out). Error responses no longer leak stack traces or server paths.
See TAW Hub Companion.
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.
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 frontmatter | What the sync does |
|---|---|
owner: site | Preserved untouched, and named in the sync report |
owner: taw | Deleted — a framework skill that was retired upstream |
no owner: key | Preserved, 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
- Duplicate
<link rel="canonical">.SeoMeta::renderHeadTags()prints its own canonical tag onwp_head(priority 1), but never removed WordPress core's defaultrel_canonical()output — every page shipped two identical canonical tags. og:title/twitter:titleon a static front page could show a raw, possibly internal-onlypost_title. A static front page (aPageselected under Settings → Reading) is stillis_singular(), so it never reached the dedicatedis_front_page()/is_home()fallback branch that already existed for the dynamic home/blog-index case — the same gap WordPress core's ownwp_get_document_title()already closes for the real<title>tag, butSeoMetahadn'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.
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.
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.
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.
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.
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.
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.
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 — eitherenshrined/svg-sanitizereturningfalsefor malformed/unparseable XML, or (a second bug found in the same pass) throwing an uncaughtLogicExceptionfor well-formed XML with no<svg>root — the original file was silently left in place, unsanitized, at its uploads URL. Any user withupload_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.phpexecution insidevendor/. Deliberately not added tobin/taw's CLI command classes,Framework.php, or the two Composerfiles-autoloaded bootstrap files (performance.php,utilities.php) — all four are used in contexts where WordPress is legitimately not yet bootstrapped, and guarding them breaksbin/tawoutright (verified against a real, unintentional regression during this fix, caught before shipping).
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:
- 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(). - A code-level default set on the form itself, via a new
'webhook' => ['url' => ..., 'secret' => ...]config key. - 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.
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/coreREADME, and a matching section on thetaw/corereference page here, documentingEmailConfig::useEmailit()— routes allwp_mail()calls (form submissions, password resets, WooCommerce, etc.) through Emailit's API, with automatic fallback to plainwp_mail()if unconfigured or the API call fails. taw-theme'sinc/customizations.phpnow has a commented-out wiring block, matching the style of this scaffold's other opt-in features — gated on adefined('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_NAMEare site-specific secrets that belong in each client's ownwp-config.php, never in the theme repo. Theemailit/emailit-phpSDK 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.
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.
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.
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'squery_varand 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_querydefaults toinclude_children: truefor hierarchical taxonomies; now explicitlyfalse, 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.
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 areinheritstatus 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-timewp term recount taw_media_folderto 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.
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 samewp/v2/mediaREST 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".
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".
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".
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.
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).
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.
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.
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).
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).
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.
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.
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.
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.
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.
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
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' => [...],
]);
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.'],
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.
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-runbefore 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.
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.
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.
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.
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.
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 thetaw/coreREADME 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 thatgetData()receivesint|falsebecauseget_the_ID()returnsfalseon 404 pages, and that all MetaBlock convenience helpers handlefalsesafely.
Docs — June 2026
Options page
- OptionsPage field types expanded — per the authoritative
taw/coreREADME,OptionsPagenow supports the same field types asMetabox(all types, includingfiles,group,post_select,repeater, anddatepicker). The previous explicit subset list has been replaced with the correct statement.
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 callingVisualEditor::enable()beforeTheme::boot()infunctions.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/saveandGET /taw/v1/visual-editor/fieldsare now listed in the REST API endpoint table.
Options page
- Removed
filesfrom supported field types —filesis not supported inOptionsPage(only inMetabox). Corrected the supported-types list to match the authoritative README.
taw/core v1.15.52 — June 2026
Block system
MetaBlock::variations()static method — block variations are now declared by overridingpublic static function variations(): arrayand returning an array of string suffixes (''= default variation). The previous constructor-basedparent::__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 automaticcritical.scssdetection.ViteLoader::preloadAssets(array $paths)— addmodulepreloadhints for JS chunks to improve first-load parse performance.
Theme boot
Theme::boot()accepts optional config array — pass['performance' => [...]]directly toTheme::boot()as a shorthand for callingTheme::performance([...])as a separate step. Both patterns remain supported.
taw/core v1.15 · TAW Theme — June 2026
Asset pipeline
ViteLoaderOOP API —TAW\Support\ViteLoaderis now the authoritative way to interact with Vite assets. New static methods:ViteLoader::assetUrl(),ViteLoader::isDevServerRunning(),ViteLoader::enqueueAsset(), andViteLoader::init().- Legacy procedural helpers removed from autoload —
vite_asset_url()andvite_is_dev()are no longer in the Composerfilesautoload and are not available globally. UseViteLoaderinstead. - SCSS block asset priority — when a block has both
style.scssandstyle.css, SCSS now takes priority.
Form system
Form::register()+Form::display()— forms are now registered statically viaForm::register()insideboot()and displayed in templates withForm::display('id'). The oldnew Form()+$form->render()pattern has been replaced.- New input field types:
radio(withoptionsandlayout),checkbox_group(stored as comma-separated string),date(withmin_dateandmax_date). - Structural field types:
heading,divider, andhtml— cosmetic fields for layout and copy inside a form with no submission data. - AND / OR conditional logic —
conditionsnow supports'relation' => 'any'for OR logic. The old flat-array AND format is still fully supported. - Multi-step forms — replace the top-level
fieldskey withsteps. Each step has atitleand its ownfieldsarray. Includes a numbered step indicator, per-step client-side validation, and automatic navigation to the failing step on server error. SubmissionsHandlerauto-wired —Theme::boot()now registersSubmissionsHandlerautomatically; manual instantiation is no longer needed.
Metabox engine
datepickerfield type — jQuery UI date picker; stored as a date string (YYYY-MM-DDdefault). Supportsdate_format,min_date, andmax_dateoptions.- Repeater
layoutoption — repeater rows can now render astabbed_horizontalortabbed_verticalin addition to the default accordion. screenskey (plural) — the Metabox config option is nowscreens(accepts an array of post types, page slugs, or page template filenames). The singularscreenis no longer used.
Transactional email
- Template path updated — pre-compiled HTML templates now live at
mails/html/{name}.html. MJML source files remain atmails/{name}.mjmlfor dev-time compilation. MailTesterregistration — register manually with(new \TAW\Core\Mail\MailTester())->register()infunctions.php.
Block system
defaultData()method — UI Blocks now usedefaultData()instead ofdefaults()to define prop fallbacks. (Correction, October 2026: this never shipped. UI Blocks define their prop fallbacks indefaults(), asTAW\Core\Block\Blockrequires.)getData(int|false $postId)—MetaBlock::getData()now acceptsint|falseso blocks return safe empty values on 404 pages.boot()for all blocks —BlockLoadernow callsboot()on every block type (not just UI Blocks) during discovery.
Navigation menus
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 rawWP_Postmenu item object.
TAW Theme
ThemeUpdaterre-introduced viataw/core—TAW\Core\ThemeUpdateris available again for GitHub Releases-based auto-updates. Instantiate it infunctions.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$altparameter has been removed. Pass options (includingclass,sizes,above_fold,attr) as the third argument directly.Image::preloadTag()— renamed frompreload_tag()to camelCasepreloadTag().
taw/core v1.14 · TAW Theme — April 2026
Metabox engine
filesfield type — new multi-file picker with drag-to-reorder. Stores an array of attachment IDs. Acceptslimitto cap the number of selectable files.- Nested repeaters — repeater fields now fully support nesting: place a
repeaterinside another repeater'sfieldsarray 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 onMetaboxfor template matching;screennow 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 —
MetaBlocknow accepts avariationsarray in its constructor to declare WordPress block variations. Asset handles are automatically namespaced per variation. BlockLoader::boot()for plain blocks — whenBlockLoaderloads a UI Block (extendingBlock), it now calls the block'sboot()method if one exists. Use it for one-time setup such as registering hooks.
REST API
post_selectpage handling —SearchEndpointsnow properly validates thepagepost type and handles the WordPress special case automatically.
TAW Theme
ThemeUpdaterremoved — theThemeUpdaterclass has been removed from TAW Theme. Usecomposer update taw/coreto update the framework.SearchEndpointscleaned up — removed the theme-level override; the endpoint is now served exclusively bytaw/core.- Hero block — added
content(wysiwyg) field. Added a three-levelnested_repeaterfield as a reference implementation for hierarchical repeater data. - CSS Studio — added as a dev-only dependency. Activated automatically when
vite_is_dev()istrue; never shipped to production. - Performance —
Theme::performance()now schedules its work onafter_setup_themefor better WordPress filter compatibility.
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, nofunctions.phpedits required. - MetaBlock — page sections backed by metabox data. Extend
TAW\Core\MetaBlock, define fields inregisterMetaboxes(), return template vars fromgetData(). - Block (UI Block) — stateless components that receive props at render time. Extend
TAW\Core\Blockand define adefaults()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 beforeget_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 astyle.scsswired into the Vite pipeline.--with-script— include ascript.jsloaded 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 theBlocks/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
tabskey; each tab references field IDs from thefieldsarray. - Responsive grid — control field width with the
widthoption (e.g.'50'for two-column). show_oncallable — 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 inwp_optionsusing the same field config as metaboxes.- Supports tabbed layouts, validation, and all field types except
repeaterandgroup. - Retrieval via
OptionsPage::get()andOptionsPage::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
Mailerwhen templates are configured; falls back to plain-textwp_mail()otherwise. TAW\Core\Form\SubmissionsHandler— stores successful submissions as ataw_submissionCPT 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 aroundwp_mail()with ato(),subject(),template(),setVariables(),send()chain.TAW\Core\Mail\MailTemplate— resolves templates frommails/html/{name}.html(production) ormails/{name}.mjml(dev, compiled viaspatie/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.
Navigation menus
TAW\Core\Menu\Menu— typed wrapper around WordPress nav menus. Load by location slug withMenu::get('primary').TAW\Core\Menu\MenuItem— exposestitle(),url(),openInNewTab(),hasChildren(),children(),isActive(),isInActiveTrail(),classes(), and more. WP auto-classes are filtered out by default.
Asset pipeline
- Per-block assets —
style.scss(orstyle.css) andscript.jsinside any block folder are auto-detected and auto-enqueued only on pages that use that block. - Critical CSS —
resources/scss/critical.scssis compiled and inlined in<head>to eliminate a render-blocking network round-trip for above-fold styles. - Async CSS —
app.css/app.scssare loaded non-render-blocking viamedia="print"+onloadswap. - ES module JS — block scripts and the main
app.jsare loaded astype="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 viaViteLoader.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 correctloading,fetchpriority,decoding,srcset, andsizesattributes. 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 byTheme::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 thepost_selectmetabox field. Supportss,post_type,per_page, andexcludeparameters. Requiresedit_postscapability.
Theme updater
TAW\Core\Theme\ThemeUpdater—removed in a later releasehooked 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.