A custom, production-oriented WordPress theme built with Timber and Twig, with a
complete Docker-based local environment, Composer dependency management, GitHub Actions
validation and a release pipeline that produces an installable lions-theme.zip.
The visual language is inspired by lionsclubs.org (blue /
yellow / navy palette, bold Helvetica-style typography, white content panels, angled photo
edges, statistic tiles, substantial navy footer) and implemented independently. See
docs/design.md.
.
├── .github/workflows/ ci.yml (validation + Docker smoke test + package), release.yml (tag -> GitHub Release)
├── bin/ build.sh, twig-lint.php, validate-theme.sh, wp-install.sh (WP-CLI setup + demo content),
│ placeholder-image.php (generates the demo featured images)
├── docker/wordpress/ Dockerfile: WordPress 7.1 / PHP 8.3 / Apache + Composer + WP-CLI;
│ entrypoint.sh keeps wp-content/uploads writable by www-data
├── docs/ design.md, architecture.md, deployment.md
├── theme/ the WordPress theme (mounted into the container as lions-theme)
│ ├── functions.php loads Composer + boots Lions\Theme\Theme
│ ├── src/ PHP: Theme, Site, Controllers/, Components/, Functions/, Content/
│ ├── templates/ page-level Twig templates (base.twig, front-page.twig, page.twig, ...)
│ ├── views/ components/, partials/, sections/
│ ├── assets/ css/ (design tokens + modules), js/main.js, images/ (icons, placeholders, logo)
│ ├── languages/ lions-theme.pot
│ ├── theme.json editor palette / font sizes (settings only)
│ └── composer.json timber/timber ^2.5 + dev tools (PHPCS/WPCS, PHPStan)
├── docker-compose.yml wordpress + db (MySQL 8.4), named volumes, theme bind mount
├── Makefile make up / install / lint / test / build / ...
├── AGENTS.md rules for AI coding agents (and humans)
└── .env.example environment template (copy to .env)
| Layer | Choice |
|---|---|
| CMS | WordPress 7.1 (image wordpress:7.1-php8.3-apache) |
| PHP | 8.3 (Docker, CI and Composer platform config all pin 8.3; the theme requires >= 8.2) |
| Templating | Timber 2.5 + Twig 3 |
| Dependencies | Composer (PSR-4 autoloading, Lions\Theme\ -> theme/src/) |
| Frontend | Plain CSS with custom properties, one small vanilla JS file; no build step, no framework |
| Local env | Docker Compose (WordPress + MySQL 8.4), WP-CLI inside the container |
| Quality | PHP_CodeSniffer + WordPress Coding Standards, PHPStan (level 6, WordPress stubs), Twig linter, structure check |
| CI/CD | GitHub Actions: CI on push/PR, release on 1.2.3 tags (no v prefix) |
- Docker Desktop (or Docker Engine) with Compose v2
- GNU Make
- Git
PHP and Composer are not required on the host; they run inside the container.
(If you have them locally, bin/build.sh and bin/twig-lint.php also work natively.)
git clone <this repository> lions-theme
cd lions-theme
cp .env.example .env # adjust ports/credentials if needed
make up # builds the image, starts WordPress + MySQL, runs composer install
make install # completes the WordPress installation and seeds demo contentmake install uses WP-CLI to create the admin user from .env (default admin / admin),
activate the theme, create pages, menus, categories, sample stories and Customizer defaults.
It is idempotent. If you prefer the browser installer, skip make install, open the site and
activate Lions International under Appearance > Themes.
- Site: http://localhost:8080
- Admin: http://localhost:8080/wp-admin/ (credentials from
.env) - MySQL is not exposed on the host; use
make db-shell.
Change the port with WORDPRESS_PORT in .env.
theme/ ──bind mount──▶ /var/www/html/wp-content/themes/lions-theme (inside the container)
Edit files in theme/, refresh the browser. Nothing is copied or compiled.
- PHP prepares data.
theme/src/Controllers/*build the Timber context for each WordPress template (front-page.php,page.php, ...). Feature classes intheme/src/Functions/*register hooks (assets, menus, Customizer, SEO, ...). - Twig renders.
theme/templates/*.twigextendbase.twigand composeviews/sections,views/componentsandviews/partials. - Design tokens live in
theme/assets/css/tokens.css; components reference them. - Editors manage pages/posts with the block editor, navigation under Appearance > Menus, and organisation data (Join/Donate URLs, social profiles, contact, legal text, logo) under Appearance > Customize > Lions International.
WP_DEBUG is on in development; PHP errors go to wp-content/debug.log inside the
container (make logs or make shell).
Architecture details: docs/architecture.md.
Dependencies are declared in theme/composer.json; composer.lock is committed, vendor/ is not.
make composer # composer install (inside the container)
make composer ARGS="update timber/timber"
make composer ARGS="require vendor/package"make lint # composer validate, php -l, PHPCS (WordPress Coding Standards), Twig syntax
make test # PHPStan (level 6), theme structure validation, docker compose config
make cs-fix # phpcbf auto-fixesCI (.github/workflows/ci.yml) runs the same checks on every push to main and every pull
request, plus a Docker Compose smoke test that boots WordPress, seeds content and requests
the homepage, a page, the stories index, search and a 404, and a job that builds the package.
make build # -> build/lions-theme.zipThe package contains only what WordPress needs, including production Composer dependencies
(vendor/ with Timber and Twig, no dev tools). Upload it via Appearance > Themes > Add New >
Upload Theme or unzip into wp-content/themes/. Rationale and deployment strategies:
docs/deployment.md.
Deploying to a real server (production is https://lions-vallendar.org, WordPress root
/srv/www/wp_lions). The server needs an installed WordPress, PHP 8.2+ with the GD
extension (the seeder generates the demo featured image) and WP-CLI.
Debian's packaged wp-cli lags upstream; install the phar, the same way
docker/wordpress/Dockerfile does:
sudo apt update
sudo apt install -y php-cli php-mysql php-xml php-mbstring php-curl php-zip php-gd curl
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
php wp-cli.phar --info # sanity check before installing
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
wp --infoOptionally verify the download first:
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar.sha512
sha512sum -c wp-cli.phar.sha512The repository is not needed on the server - the ZIP is self-contained (it carries
vendor/), and the two seeding scripts are copied alongside it. Build locally:
make build
scp build/lions-theme.zip bin/wp-install.sh bin/placeholder-image.php user@server:/tmp/Then on the server, as the user owning the WordPress directory (typically www-data,
hence sudo -u www-data; as root, WP-CLI additionally needs --allow-root):
cd /srv/www/wp_lions
# must print https://lions-vallendar.org - every seeded permalink inherits it
sudo -u www-data wp option get home
sudo -u www-data wp theme install /tmp/lions-theme.zip --force --activate
sudo -u www-data env \
WP_PATH=/srv/www/wp_lions \
WORDPRESS_SITE_TITLE="Lions Vallendar" \
WORDPRESS_LOCALE=de_DE \
bash /tmp/wp-install.shwp-install.sh reads the site URL from the existing home option and skips
wp core install when WordPress is already set up, so it only creates pages, categories,
demo stories, both menus and the Customizer defaults. It is idempotent - existing content
is never overwritten - so it can be re-run after a theme update. Useful variables:
| Variable | Default | Purpose |
|---|---|---|
WP_PATH |
/var/www/html |
WordPress root (the wp-config.php dir) |
WORDPRESS_URL |
http://localhost:8080 |
Only used when WordPress is not yet installed |
WORDPRESS_SITE_TITLE |
Lions Vallendar |
Site title, re-applied on every run |
WORDPRESS_LOCALE |
de_DE |
Installs and activates core translations |
CONTACT_EMAIL |
info@lion-example.com |
Address used in Impressum, Datenschutz and the Customizer |
Afterwards, in wp-admin: re-save Settings > Permalinks if the host has to write rewrite rules, and replace the placeholder phone number under Appearance > Customize > Lions International.
Migration alternatives (seed locally, then move the database) and CI-based deployment:
docs/deployment.md.
Cut the release on GitHub: Releases > Draft a new release, enter a new tag such as
1.0.4, target main, write the notes, Publish release. That is the whole process -
there is nothing to bump, commit or push first.
Publishing creates the tag, which starts .github/workflows/release.yml. It stamps the tag
into theme/style.css and Theme::VERSION (via bin/set-version.sh), runs all validation,
builds lions-theme.zip and attaches it to the release you just published - your notes are
kept as written. The ZIP shows up under Assets when the run finishes, after about a minute.
Tags are plain versions (1.0.4), without a v prefix; the workflow rejects v-prefixed
tags. The tag is the version.
One thing the release does not do is compile translations: WordPress reads
theme/languages/*.mo, and the ZIP contains those files exactly as committed. After editing
a .po, run make i18n and commit the .po and .mo together - otherwise the release
ships the previous translation.
The same thing from the terminal, if you prefer - here the workflow creates the release too:
git tag 1.0.4 && git push origin 1.0.4Because the version is stamped during the release run, the one committed in style.css
only reflects the last manual edit and may lag behind the newest tag. To build a correctly
versioned ZIP from a working tree:
make set-version VERSION=1.0.4
make buildRead AGENTS.md. It states the non-negotiables (Timber/Twig split, no page
builders, centralised tokens, accessibility, validation) and where each kind of change goes.
theme/assets/images/logo.svg is the Lions Clubs International emblem, a registered trademark.
Use it only in accordance with the organisation's brand guidelines. Photography is
represented by SVG placeholders in theme/assets/images/placeholders/; replace them with
licensed images (same aspect ratios) before launch.