A starter kit for nonfiction WordPress themes: nf at the command line, nonfiction/theme in PHP, Timber and Twig for rendering, Composer for PHP packages, and Vite for front-end assets.
This repository is meant to be copied at the start of a client project. It gives you a working local WordPress environment, a small block library, starter content, predictable formatting, and a theme structure that stays pleasant as the site grows.
The main idea is simple: use nf from the repository root. Raw Docker, Composer, npm, and WP-CLI commands are still available when you need them, but nf is the friendly front door.
Enter the development shell:
nix developRefresh PHP and JavaScript dependencies:
nf theme updateStart WordPress:
nf env upSeed the starter site:
nf theme seedShow the local URLs, ports, paths, and environment metadata:
nf env showWatch theme assets while you work:
nf theme watchThat is the happy path. After those commands, you should have a local WordPress site running this theme, sample pages in the menu, and built assets rebuilding as you edit files.
This starter is intentionally small, but it is not empty. It includes enough shape to begin real work without forcing every project into the same final design.
- A Composer-managed WordPress theme in
theme/ - Timber 2 and Twig templates for page rendering
- The shared
nonfiction/themeapp layer for imports, assets, post helpers, menus, blocks, and view resolution - Vite entry points for head assets, body assets, block editor assets, editor UI assets, and admin assets
- A compact custom block set for banners, callouts, grids, cards, and accordions
- An example
articlecustom post type with archive, single, and block wiring - A seed script that creates useful QA content instead of a blank install
- Nix, treefmt, PHP-CS-Fixer, Prettier, ESLint, PHPactor, Composer, Node, and Docker client tooling in one dev shell
The result is a starter that teaches the project architecture while giving you useful seams to cut, rename, and extend.
Most project work happens in theme/app.
theme/functions.php Theme bootstrap, imports, Timber context, Twig extensions
theme/app/ Views, custom post types, custom blocks, and app assets
theme/app/views/ Global Twig templates and native post/page helpers
theme/app/blocks/ Reusable custom blocks
theme/app/<cpt>/ Custom post type modules, views, blocks, scripts, styles
theme/config/ WordPress configuration hooks and admin tweaks
theme/src/ Project-local PHP classes loaded by Composer
theme/dist/ Built Vite assets
theme/vendor/ Composer dependencies
theme/functions.php is the bootstrap. It initializes Nonfiction\Theme\App, imports modules, registers view paths, enqueues the Vite manifest, registers menus, adds theme support, and fills the global Timber context.
The import rules are deliberately predictable:
App::import([
'app/views/*.php',
'app/*/index.php',
'app/blocks/*/index.php',
'app/*/blocks/*/index.php',
'config/*.php',
]);Those paths give the starter its rhythm:
- Put reusable blocks in
theme/app/blocks/<block>/. - Put custom post types in
theme/app/<cpt>/index.php. - Put custom post type views in
theme/app/<cpt>/views/. - Put custom post type-specific blocks in
theme/app/<cpt>/blocks/<block>/. - Put native WordPress post and page helpers in
theme/app/views/, not intheme/app/post/ortheme/app/page/. - Put shared layouts, partials, and broad Twig templates in
theme/app/views/. - Put project-local PHP classes in
theme/src/only when they are real reusable PHP extensions.
When in doubt, colocate the files that change together. A block can own its block.json, PHP registration, editor JavaScript, frontend script, styles, and view markup. A custom post type can own its registration, templates, blocks, scripts, and styles.
nf env up starts the Docker-backed local WordPress environment defined by nf and nf.json.
Useful environment commands:
nf env up
nf env down
nf env reset
nf env show
nf env logs
nf env shell
nf env wp -- <args>Use WP-CLI through nf env wp --:
nf env wp -- theme list
nf env wp -- plugin list
nf env wp -- post-type listConfigured plugins live in nf.json under wordpress.plugins. The list is bootstrap intent: it tells new environments which plugins this project expects, but it is not a full plugin lifecycle policy.
Plugin helpers:
nf plugin list
nf plugin status
nf plugin diff
nf plugin install --dry-run
nf plugin install --yesSnapshot helpers:
nf env snapshot list
nf env snapshot add before-big-change
nf env snapshot use before-big-change --yes
nf env snapshot prune --dry-runRun the seed task after a fresh environment:
nf theme seedThe seed script is safe to run again. It looks for existing content before creating new content.
It does the useful setup work people otherwise forget:
- Sets permalinks to
/%postname%/ - Removes Hello Dolly if it is installed
- Creates Home, About, Services, Contact, Resources, and child resource pages
- Creates a Block Examples page with the starter blocks already placed
- Creates and assigns a
Primarymenu to theprimarytheme location - Sets Home as the static front page
- Creates a starter Article if the
articlecustom post type is available - Flushes rewrite rules
Good QA routes after seeding:
/
/about/
/services/
/block-examples/
/resources/
/resources/resource-one/
/resources/resource-two/
/contact/
/articles/
/articles/starter-article/
/search/starter/
/not-a-real-page/
Use those pages as quick checks while changing navigation, templates, blocks, archives, search, and 404 behavior.
Twig templates live under the view paths registered in theme/functions.php:
App::views([
'app/views',
'app',
]);That means a template can be global, module-local, or block-local depending on where it belongs.
Examples:
theme/app/views/base.twig
theme/app/views/page.twig
theme/app/views/partial/header.twig
theme/app/article/views/archive.twig
theme/app/article/views/single.twig
The global Timber context currently includes common values such as site, menus, image path helpers, the search query, current year, side navigation data, the current post, and current post collection.
Useful menus in context:
{{ menu_primary }}
{{ menu_utility }}
{{ menu_footer }}
{{ menu_social }}Useful Twig helpers added by the theme:
{{ title|titleize }}
{{ label|humanize }}
{{ price|currency }}
{{ number|padded }}
{{ edit_post_link() }}Keep presentation in Twig and pass real data from PHP. If a template starts doing business logic, move that logic into the post class, block registration, helper function, or module PHP file that owns it.
Reusable starter blocks live in theme/app/blocks/.
Current generic blocks:
nf/banner: a full-width page introduction with a background image, heading, and supporting textnf/aside: a callout block with a heading and nested contentnf/grid: a responsive container for repeated contentnf/card: a card intended for use insidenf/gridnf/accordion: a grouped set of expandable itemsnf/accordion-item: a single item insidenf/accordion
The article module also includes nf/article-archive under theme/app/article/blocks/article-archive/. That location is intentional: it belongs to the Article feature, not to every theme.
A typical block directory looks like this:
theme/app/blocks/banner/
block.json
index.php
index.js
script.js
style.css
Use block.json for metadata, index.php for registration and rendering setup, index.js for editor registration, script.js for frontend behavior, and style.css for styles. Keep the block self-contained unless it truly shares behavior with other blocks.
Before client work begins, decide which starter blocks are useful primitives and which ones are just examples. Delete what the project does not need. Move feature-specific blocks under theme/app/<cpt>/blocks/.
Vite builds a WordPress-oriented manifest into theme/dist/manifest.json, and App::enqueue('dist/manifest.json') loads those assets from PHP.
Entry points live in theme/vite.config.js:
const entryPoints = {
head: "app/head.js",
body: "app/body.js",
blocks: "app/blocks.js",
editor: "app/editor.js",
admin: "config/admin.js",
};Use the watch task while developing:
nf theme watchBuild assets once:
nf theme buildThe Vite config supports JSX in theme app JavaScript and aliases @nf to theme/app/nf.js, which provides a small wrapper around WordPress block registration.
The Nix dev shell exposes the project tooling:
nftreefmt- PHP 8.3
- Composer
- PHP-CS-Fixer
- PHPStan
- PHPactor
- Node 24
- Docker client
- Git
Format configured files from the repository root:
treefmtnix fmt uses the same treefmt wrapper.
Treefmt runs Alejandra for Nix, PHP-CS-Fixer for authored theme and local plugin PHP, and Prettier for Markdown plus theme/plugin CSS, HTML, JavaScript, JSON, Twig, YAML, and YML files. The Prettier wrapper runs from theme/ so it can use theme/.prettierrc.json, theme/.prettierignore, and the project-local Twig plugin.
If Prettier is missing, install or refresh theme node dependencies:
nf theme npmRun PHP, CSS, and JavaScript checks:
nf theme checkRun individual checks directly when debugging a specific tool:
composer --working-dir=theme check:php-style
npm --prefix theme run lintRun an asset build:
nf theme buildThe current nf.json keeps custom theme tasks intentionally lean: build, check, composer, npm, seed, update, and watch. Older wrapper tasks such as format, lint, release, and test are not configured; use nf theme check for project checks.
nf.json is the project manifest. It records the project slug, WordPress theme path, theme slug, environment settings, configured plugins, artifact path, remotes, and custom theme tasks.
List the current theme tasks:
nf theme tasksCurrent custom theme tasks:
nf theme build # Build Vite assets
nf theme check # Run PHP, CSS, and JavaScript checks
nf theme composer # Update Composer dependencies and optimized autoload
nf theme npm # Refresh npm development dependencies
nf theme seed # Seed starter WordPress content
nf theme update # Update Composer and npm dependencies
nf theme watch # Watch Vite assetsBuilt-in theme commands:
nf theme tasks
nf theme package --dry-run
nf theme package
nf theme deploy <remote> --dry-run
nf theme deploy <remote>
nf theme rollback <remote> --dry-runnf theme package zips the files that already exist. It does not run Composer, npm, or an asset build first. Build and check the project before packaging.
Environment commands:
nf env up
nf env down
nf env reset
nf env show
nf env logs
nf env shell
nf env wp -- <args>
nf env snapshot list
nf plugin list
nf plugin status
nf plugin install --dry-runRemote commands are configured per project. This starter ships with no remotes.
nf remote list
nf remote add production
nf theme deploy production --dry-runUse --dry-run before any deploy or destructive sync. Treat remotes as project-specific infrastructure, not starter defaults.
When you copy this starter for a client, make the project yours before adding features.
Update these values first:
nf.jsonproject.slugnf.jsonwordpress.theme_slugnf.jsonartifact.paththeme/style.cssTheme Name,Text Domain,Description, andVersiontheme/package.jsonname,version, and repository URLtheme/composer.jsonnameanddescription- README title and project description
- Any block names, labels, namespaces, or example content that should become client-specific
- Any configured plugins in
nf.jsonthat do not belong in the project
The theme slug matters. nf theme package uses wordpress.theme_slug as the zip root directory even though source files live in wordpress.theme_path.
After renaming, run the first checks:
nf theme check
nf theme build
nf theme package --dry-runThen start trimming. A good starter theme is useful because it is easy to delete from. Remove example blocks, views, plugins, post types, routes, and content that are not part of the client project.
Before packaging, make sure dependencies are current, assets are built, and checks pass:
nf theme update
nf theme check
nf theme buildPreview the package:
nf theme package --dry-runCreate the package:
nf theme packageWhen a project has a remote configured, preview deployment first:
nf theme deploy production --dry-runDeploy only after the dry run matches what you expect:
nf theme deploy productionThis starter is a path, not a cage. Follow the structure while it helps, then simplify it for the project in front of you.
Keep reusable framework behavior in nonfiction/theme. Keep project behavior in this theme. Keep templates readable. Keep blocks close to their assets. Keep custom post type features together. Keep the command surface boring enough that the next developer can trust it.
That is the whole trick: a theme starter should make the first day faster without making the hundredth day heavier.
