Setting up a new TAW project
A complete walkthrough for going from zero to a running TAW Theme with your first block, from Composer install through to production build.
What you'll have at the end
- A fresh TAW Theme installed in WordPress
- Vite running with hot module reloading
- A working MetaBlock with metabox fields
- A production-ready build
This guide covers the same ground as the Quickstart but goes a level deeper on each step, explaining why things work the way they do.
1. Create the theme
TAW Theme is installed via composer create-project. The --repository flag tells Composer where to find the private package.
cd wp-content/themes
composer create-project taw/theme my-theme \
--repository='{"type":"vcs","url":"https://github.com/Relmaur/taw-theme"}'
cd my-theme
What was created:
my-theme/
├── .claude/skills/ ← Claude Code skills (make-metablock, project-init, update-theme, …)
├── .github/workflows/ ← CI (lint, phpstan, smoke test) + weekly framework-sync, from commit 1
├── Blocks/ ← your blocks go here
├── bin/taw ← CLI entry point
├── inc/
│ ├── customizations.php ← theme supports, nav menus, any other site-specific hooks
│ ├── options.php ← theme options page config
│ └── performance.php ← performance config (returns the array for Theme::performance())
├── resources/
│ ├── css/app.css ← Tailwind v4 directives
│ ├── scss/ ← global SCSS + critical CSS
│ ├── fonts/ ← self-hosted WOFF2 files
│ └── js/app.js ← Alpine.js + global JS
├── vendor/taw/core/ ← framework internals (managed by Composer)
├── functions.php ← 100% framework-owned, never hand-edit — see step 7
├── vite.config.js
├── composer.json
└── package.json
2. Initialize git and push to your own repository
git init
git add -A
git commit -m "Initial commit"
git remote add origin <your-repo-url>
git push -u origin main
<your-repo-url> assumes an empty repository already exists for this project — create one on github.com first, or use gh repo create <name> --private --source=. --remote=origin --push to create it, add the remote, and push in one step (in which case skip the last two lines above).
Why this doesn't need --keep-vcs or a shared git history with taw-theme. Earlier versions of this framework synced updates via git merge, which needed a common ancestor commit. update-theme now copies a small, precisely delimited set of framework-owned paths directly from a fresh checkout of the canonical taw-theme repo, regardless of this project's own git history — so a single clean commit here is all you need, forever.
3. Install dependencies
composer install # PHP deps — pulls taw/core and other packages
npm install # Frontend toolchain — Vite, Tailwind, Alpine
Why two install steps? PHP and JS have separate dependency graphs. Composer manages PHP classes. npm manages the build tool and frontend libraries. Both are required.
4. Start the dev server
npm run dev
Vite starts on http://localhost:5173 and watches for changes. While the dev server is running:
- Tailwind CSS classes update without a full reload.
- Block
style.scsschanges reflect instantly. block.jschanges hot-reload.
Leave this terminal open while you develop.
5. Activate the theme
Go to WordPress Admin → Appearance → Themes, find your theme, and click Activate.
Visit the frontend — you should see the TAW layout loading assets from the Vite dev server (check the browser network tab: scripts and styles come from localhost:5173).
6. Set up framework-sync and optional integrations
Your new project ships with .github/workflows/framework-sync.yml from the first commit — a weekly check that bumps taw/core, applies framework-owned scaffold updates, runs the same verification CI runs on every push, and opens a pull request if anything changed.
If you're working with Claude Code, run the project-init skill now. It checks gh CLI authentication, enables the GitHub setting below with confirmation, triggers a real run to verify the pipeline actually works, and walks through optional per-project integrations — Cloudflare Turnstile, transactional email, CSS Studio, Visual Editor — as explicit yes/no questions, only setting up what you actually want.
Doing it by hand instead just needs one manual setting enabled on the new repo:
Settings → Actions → General → "Allow GitHub Actions to create and approve pull requests"
This is off by default on every new GitHub repo. Without it, the workflow still runs on schedule, but its PR-opening step silently fails — nothing breaks loudly, it just never surfaces the update.
7. Review functions.php
Open functions.php. It's intentionally tiny and 100% framework-owned — never hand-edit it, since update-theme overwrites it unconditionally on every sync:
<?php
require_once get_template_directory() . '/vendor/autoload.php';
TAW\Core\Theme\Theme::bootstrapFullSite(get_template_directory());
That's the whole file. bootstrapFullSite() does everything a theme used to have to spell out by hand:
- Calls
Theme::boot()— which callsBlockLoader::loadAll()(discovers every block inBlocks/),ViteLoader::init()(prepares the asset pipeline), andSvg::register()(enables safe SVG uploads). - Locks each page's metabox order to match its template's
BlockRegistry::render()sequence, viaMetaboxOrder::lockFromTemplate(). - Wires up CSS Studio's dev-mode config injection.
- Auto-loads three theme-owned files, only if they exist, none of which
update-themeever touches:inc/options.php—OptionsPagefield configurationinc/performance.php— returns the config array passed toTheme::performance()inc/customizations.php— theme supports, nav menu registration, and any other site-specific hooks
Site-specific setup that used to go directly in functions.php — theme supports, nav menus, Svg::register(), an explicit MetaboxOrder::lock() call — goes in inc/customizations.php instead. This is what makes update-theme a plain file copy instead of a merge: the boundary between "framework" and "this project's site" is a fact about which file something is in, not something computed from a diff.
8. Scaffold your first block
Use the CLI to generate the files:
php bin/taw make:block Hero --type=meta --with-style
composer dump-autoload
Always run composer dump-autoload after adding a new block. Without it, PHP cannot find the new class and the block is silently skipped.
Open Blocks/Hero/Hero.php and add your fields:
protected function registerMetaboxes(): void
{
new Metabox([
'id' => 'taw_hero',
'title' => 'Hero Section',
'screen' => 'page',
'fields' => [
['id' => 'heading', 'label' => 'Heading', 'type' => 'text', 'required' => true],
['id' => 'image', 'label' => 'Image', 'type' => 'image'],
],
'tabs' => [
['label' => 'Content', 'fields' => ['heading']],
['label' => 'Media', 'fields' => ['image']],
],
]);
}
protected function getData(int $postId): array
{
return [
'heading' => $this->getMeta($postId, 'heading'),
'image_url' => $this->getImageUrl($postId, 'image', 'large'),
];
}
Open Blocks/Hero/index.php and write the template:
<?php if (empty($heading)) return; ?>
<section class="hero">
<h1><?php echo esc_html($heading); ?></h1>
<?php if ($image_url): ?>
<img src="<?php echo esc_url($image_url); ?>" alt="">
<?php endif; ?>
</section>
9. Render the block
In your WordPress template (e.g. front-page.php):
<?php
use TAW\Core\BlockRegistry;
BlockRegistry::queue('hero');
get_header();
?>
<?php BlockRegistry::render('hero'); ?>
<?php get_footer(); ?>
Go to a page in the WordPress editor — the Hero Section metabox appears below the editor. Fill it in, publish, and visit the page.
10. Production build
When you're ready to deploy:
npm run build
Vite outputs content-hashed files to public/build/. WordPress loads these when the dev server is not running. Critical CSS is inlined in <head>, the main CSS loads asynchronously, and JS loads as an ES module.
What's next
- Add more blocks — repeat the scaffold + dump-autoload cycle for each new section.
- Organise into groups —
--group=sectionsputs the block insideBlocks/sections/Hero/. - Export and reuse blocks —
php bin/taw export:block Herocreates a ZIP you can import into any TAW project. - Add site-wide options — configure
inc/options.phpwith anOptionsPagefor settings like phone number, logo, and footer text. - Add a contact form — drop a
Formconfig into any template for CSRF-protected, validated form handling. - Finish onboarding checks — if you skipped step 6 above, run the
project-initskill (or its manual equivalent) before shipping any real feature work.