Lightweight WordPress starter theme built with Timber 2, Twig and Vite. It keeps a familiar WordPress request lifecycle while providing controllers, Twig views, Carbon Fields examples and a small asset pipeline.
- WordPress 6.9 or later
- PHP 8.2–8.5
- Composer 2
- Node.js 18 or later
wp-theme is a theme, not a full WordPress distribution: WordPress core, its database and web-server configuration are supplied by the host project.
-
Place the repository at
wp-content/themes/wp-themein an existing WordPress project. -
From the theme directory, install the locked PHP and JavaScript dependencies:
composer install npm ci npm run build
Composer installs the two declared WordPress plugins into
wp-content/plugins. Activate only the plugins needed by the site in wp-admin. Yoast is optional: the theme calls its breadcrumbs only when the plugin is active. All-in-One WP Migration is a development/operations tool and does not need to be active on the public site. -
In wp-admin, activate wp-theme in Appearance → Themes.
-
Create a menu and assign it to the
Main menulocation. Create pages, then select one of the supplied page templates where appropriate.
npm run dev # rebuild assets while source files change
npm run lint # ESLint + Stylelint
npm run fix # fix lintable JS and SCSS issues
composer test # format-check PHP with PintVite outputs production files to resources/assets/dist. Commit or deploy those built files together with the theme; never deploy node_modules.
The theme uses WordPress conditionals for routing. resources/index.php loads routes/base.php; a matched route calls a controller and renders its Twig view through Core\Route.
| Path | Purpose |
|---|---|
routes/base.php |
Maps WordPress request types to controllers and views |
app/Controllers/ |
Builds the data supplied to Twig templates |
resources/views/ |
Twig layouts, components and page views |
resources/views/template-*.php |
WordPress page-template entry points |
app/CustomFields/ |
Carbon Fields examples for pages, options and blocks |
app/Handlers/AjaxHandlers/ |
Optional AJAX handlers used by the included examples |
resources/assets/src/ |
SCSS, JavaScript and source images |
resources/assets/dist/ |
Built frontend assets |
Use a controller when a view needs queries, transformation, reusable view data or several related fields.
- Add a method to
app/Controllers/PageController.phpand put its data in$this->data. - Add a Twig view under
resources/views/pages/. - Create a
resources/views/template-*.phppage template that callsRoute::load()with the controller method and Twig view. - Create a page in wp-admin and select that template.
Create a partial in resources/views/components/ and include it from a page or layout with Twig’s {% include %} tag. Keep data preparation in a controller or timber/context filter rather than querying WordPress from the view.
The existing files in app/CustomFields/ are intentionally kept as working examples. Add a field group there and use Carbon Fields getters in the corresponding controller or Twig view. Carbon Fields is booted in resources/functions.php.
For a small amount of presentation-only data, a controller is optional. Timber exposes WordPress and Carbon Fields functions through fn(), so a Twig view can read fields directly:
{# A simple field on the current page or post #}
{% set subtitle = fn('carbon_get_post_meta', post.ID, 'sub_title') %}
{% if subtitle %}
<p class="subtitle">{{ subtitle }}</p>
{% endif %}
{# A theme option #}
<span>{{ fn('carbon_get_theme_option', 'site_name') }}</span>
{# A complex field #}
{% for item in fn('carbon_get_post_meta', post.ID, 'tech_items') %}
<article>
<h2>{{ item.tech_title }}</h2>
{% for child in item.tech_sub_items %}
<p>{{ child.tech_title }}</p>
{% endfor %}
</article>
{% endfor %}Twig escapes output by default. Use |raw only for trusted rich-text content that an administrator entered, for example {{ fn('carbon_get_post_meta', post.ID, 'new_short_text')|raw }}. Keep data in a controller when its retrieval or preparation becomes non-trivial; this is about code clarity, not speed.
get_post_meta() and get_post_field() are standard WordPress APIs and are appropriate in Twig through fn(). There is no WordPress function named post_get_meta().
{# Prefer Timber's object API for the usual post properties #}
<h1>{{ post.title }}</h1>
{# A native custom meta value; true returns a single value #}
{% set badge = fn('get_post_meta', post.ID, 'badge', true) %}
{% if badge %}
<span class="badge">{{ badge }}</span>
{% endif %}
{# A column from wp_posts, not post meta #}
{% set excerpt = fn('get_post_field', 'post_excerpt', post.ID) %}
{% if excerpt %}
<p>{{ excerpt }}</p>
{% endif %}Use get_post_field() for core wp_posts columns such as post_title, post_excerpt and post_date; use get_post_meta() for simple custom metadata. For Carbon Fields, prefer carbon_get_post_meta() — especially for complex, association, image and rich-text fields — because it returns the field value in the format Carbon Fields expects.
The full WooCommerce implementation, including PHP overrides and Twig storefront views, is maintained in the feature/woocommerce-integration branch. Use that branch for a store project; this default branch remains a framework-agnostic starter.
Build and install dependencies before deployment:
composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
npm ci
npm run buildDeploy the theme including vendor/ and resources/assets/dist/; exclude node_modules/. Production must use a PHP version in the declared range and be smoke-tested on its target runtime.
wp-theme is a focused alternative for teams that want Timber + Twig + Vite without adopting a full Laravel/Acorn stack. It favors conventional WordPress routing, readable project structure and optional integrations over a large framework surface.
composer validate --strict --no-check-publish
composer audit --locked
composer test
npm run lint
npm run build