GuidesCreate Your First Page

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.scss changes reflect instantly.
  • block.js changes 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:

  1. Calls Theme::boot() — which calls BlockLoader::loadAll() (discovers every block in Blocks/), ViteLoader::init() (prepares the asset pipeline), and Svg::register() (enables safe SVG uploads).
  2. Locks each page's metabox order to match its template's BlockRegistry::render() sequence, via MetaboxOrder::lockFromTemplate().
  3. Wires up CSS Studio's dev-mode config injection.
  4. Auto-loads three theme-owned files, only if they exist, none of which update-theme ever touches:
    • inc/options.phpOptionsPage field configuration
    • inc/performance.php — returns the config array passed to Theme::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=sections puts the block inside Blocks/sections/Hero/.
  • Export and reuse blocksphp bin/taw export:block Hero creates a ZIP you can import into any TAW project.
  • Add site-wide options — configure inc/options.php with an OptionsPage for settings like phone number, logo, and footer text.
  • Add a contact form — drop a Form config into any template for CSRF-protected, validated form handling.
  • Finish onboarding checks — if you skipped step 6 above, run the project-init skill (or its manual equivalent) before shipping any real feature work.