diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 560cb5f..37ea4d7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,6 +18,41 @@ permissions: contents: read jobs: + # The template as a user receives it, generated under another slug: no + # placeholder left, no concrete name left, PHP that parses. No site needed. + contract: + name: Contract + runs-on: ubuntu-latest + + if: github.repository == 'Pollora/theme-buzz' + + steps: + - name: Checkout the theme + uses: actions/checkout@v6 + + - name: Generate the theme under another slug + run: php bin/ci/install.php --source="$PWD" --themes="$RUNNER_TEMP" --slug=my-journal + + - name: Every placeholder is substituted + run: | + if grep -rn '%theme_[a-z_]*%' "$RUNNER_TEMP/my-journal"; then + echo "::error::a placeholder the scaffolder does not substitute is left (see bin/replacements.php)" + exit 1 + fi + + # The slug is the only concrete name in the template: a buzz/ left in a + # pattern slug or a handle collides with another theme generated from it. + - name: No concrete theme name is left + run: | + # README.md documents the template itself, and names it on purpose. + if grep -rn --exclude=README.md 'buzz/\|Theme\\Buzz' "$RUNNER_TEMP/my-journal"; then + echo "::error::a concrete theme name is left; use %theme_name% / %theme_namespace%" + exit 1 + fi + + - name: PHP parses + run: find "$RUNNER_TEMP/my-journal" -name '*.php' -print0 | xargs -0 -n1 php -l > /dev/null + browser: name: Browser (framework ${{ matrix.framework }}) runs-on: ubuntu-latest @@ -124,14 +159,13 @@ jobs: --no-interaction || true ddev exec wp core is-installed - # The commit under test, copied as it is: Buzz carries no scaffolding - # placeholders. The directory name matters — the Vite build folder is - # named after it. + # The commit under test, with its placeholders substituted as + # pollora:make:theme would (bin/ci/install.php). The directory name + # matters — the Vite build folder is named after it. - name: Install and build the theme working-directory: site run: | - cp -r ../theme themes/buzz - rm -rf themes/buzz/.git themes/buzz/bin themes/buzz/.github + php ../theme/bin/ci/install.php --source="$PWD/../theme" --themes="$PWD/themes" --slug=buzz ddev exec bash -c 'cd themes/buzz && npm install --no-audit --no-fund && npm run build' ddev exec wp theme activate buzz ddev exec wp rewrite flush diff --git a/README.md b/README.md index cf85a6b..a8e0645 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,21 @@ A typographic, journal/magazine [Pollora](https://pollora.dev) theme, built in W Site Editing** (block theme) mode: real `templates/*.html` and `parts/*.html`, editable in the Site Editor, instead of Pollora's usual Blade-based template hierarchy. +## Use it + +Buzz is one of the templates of `pollora:make:theme`: + +```bash +php artisan pollora:make:theme my-journal # then choose "Magazine" +# or, without the prompt +php artisan pollora:make:theme my-journal --repository=Pollora/theme-buzz +``` + +This repository is that template: its files carry `%theme_name%`, `%theme_namespace%`… which the +command substitutes, so the pattern slugs (`my-journal/masthead`), the namespace +(`Theme\MyJournal`) and the `style.css` header are the new theme's own. The design's vocabulary +— the `buzz-*` classes, the `buzz_card` image size — keeps its name. + ## Why a block theme, in a Blade-first framework Pollora normally resolves every page through its own Blade template hierarchy, and its guidance is @@ -77,16 +92,23 @@ edge everywhere) are the design system itself, authored by hand. ## Local development +Develop on a theme generated from this template, never in this repository — its placeholders do +not run: + ```bash -npm install -npm run dev # HMR -npm run build # Writes public/build/theme//… +php artisan pollora:make:theme buzz --repository=Pollora/theme-buzz # in a test project +cd themes/buzz && npm install && npm run dev +# … then package the changes back into this repository: +./bin/package-theme.sh /path/to/project/themes/buzz +git diff # review: only buzz/, Theme\Buzz and the style.css header become placeholders ``` -The build's output folder name is derived from the theme's own directory name on disk — so build -from the path WordPress actually serves the theme from (`wp-content/themes/`), not from a -symlink or a clone under a different name: Node resolves `__dirname` to the real path, and a -mismatch there breaks every asset and font URL silently. +The build's output folder is named after the theme's directory on disk, so build from where +WordPress serves the theme (`themes/`), not from a symlink: Node resolves `__dirname` to the +real path, and a mismatch breaks every asset and font URL silently. + +`bin/` is dropped by the scaffolder. It holds the packaging script, `bin/ci/install.php` (the +commit under test, substituted as `make:theme` would — what CI installs) and the browser tests. ## Status diff --git a/app/Providers/AssetServiceProvider.php b/app/Providers/AssetServiceProvider.php index 4aba057..87551a1 100644 --- a/app/Providers/AssetServiceProvider.php +++ b/app/Providers/AssetServiceProvider.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Theme\Buzz\Providers; +namespace %theme_namespace%\Providers; use Illuminate\Support\ServiceProvider; use Pollora\Support\Facades\Asset; @@ -19,7 +19,7 @@ public function register(): void {} */ public function boot(): void { - Asset::add('buzz/script', 'app.js') + Asset::add('%theme_name%/script', 'app.js') ->container('theme') ->toFrontend() ->loadInFooter() diff --git a/bin/ci/install.php b/bin/ci/install.php new file mode 100755 index 0000000..b6d0647 --- /dev/null +++ b/bin/ci/install.php @@ -0,0 +1,65 @@ +#!/usr/bin/env php + --themes=/themes [--slug=buzz] + * + * This repository is a template: `style.css` carries `%theme_name%`, the + * provider `%theme_namespace%`, every pattern slug `%theme_name%/…`. A user only + * ever sees them substituted. So CI copies the commit under test, drops what the + * scaffolder drops (bin/, .github/, .git/) and substitutes the placeholders with + * the same table — measuring the branch, as a user would receive it. + * + * It installs. It asserts nothing: the browser tests do that. + */ + +require_once dirname(__DIR__).'/replacements.php'; + +$options = getopt('', ['source:', 'themes:', 'slug::']); + +if (! isset($options['source'], $options['themes'])) { + fwrite(STDERR, "Usage: php bin/ci/install.php --source= --themes=/themes [--slug=buzz]\n"); + exit(2); +} + +$source = rtrim((string) $options['source'], '/'); +$slug = (string) ($options['slug'] ?? 'buzz'); +$target = rtrim((string) $options['themes'], '/').'/'.$slug; +$replacements = scaffolderReplacements($slug); +$dropped = ['.git', '.github', 'bin', 'node_modules']; +$binary = ['woff2', 'woff', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'ico']; + +if (is_dir($target)) { + fwrite(STDERR, "{$target} already exists.\n"); + exit(1); +} + +$files = new RecursiveIteratorIterator( + new RecursiveCallbackFilterIterator( + new RecursiveDirectoryIterator($source, FilesystemIterator::SKIP_DOTS), + fn (SplFileInfo $file): bool => ! in_array($file->getFilename(), $dropped, true), + ), +); + +foreach ($files as $file) { + $relative = substr($file->getPathname(), strlen($source) + 1); + $destination = $target.'/'.$relative; + + if (! is_dir(dirname($destination))) { + mkdir(dirname($destination), 0775, true); + } + + $contents = (string) file_get_contents($file->getPathname()); + + if (! in_array(strtolower($file->getExtension()), $binary, true)) { + $contents = strtr($contents, $replacements); + } + + file_put_contents($destination, $contents); +} + +echo "Installed {$slug} in {$target}\n"; diff --git a/bin/package-theme.sh b/bin/package-theme.sh new file mode 100755 index 0000000..156e672 --- /dev/null +++ b/bin/package-theme.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -euo pipefail + +# Package a Buzz theme developed on a site back into this template. +# +# Usage: ./bin/package-theme.sh /themes/buzz +# +# Develop on a theme generated from this template under the slug "buzz" +# (`php artisan pollora:make:theme buzz --repository=Pollora/theme-buzz`), then +# run this: it copies the theme back and turns the slug and namespace into the +# placeholders the scaffolder substitutes. Only the anchored forms are +# replaced — `buzz/` (pattern slugs, handles), `Theme\Buzz` and the style.css +# header — so the design's own vocabulary, the `buzz-*` classes and the +# `buzz_card` image size, is left alone. + +SOURCE="${1:?Usage: $0 /themes/buzz}" +TARGET="$(cd "$(dirname "$0")/.." && pwd)" + +[ -d "$SOURCE" ] || { echo "Not a directory: $SOURCE"; exit 1; } + +rsync -a --delete \ + --exclude='.git' --exclude='.github' --exclude='bin/' \ + --exclude='node_modules' --exclude='package-lock.json' \ + "$SOURCE/" "$TARGET/" + +find "$TARGET" -type f \ + -not -path "*/.git/*" -not -path "*/bin/*" -not -path "*/.github/*" \ + -not -path "*/node_modules/*" -not -name "README.md" \ + -not -name "*.woff2" -not -name "*.png" -not -name "*.jpg" \ + | while read -r file; do + sed -i \ + -e 's|Theme\\Buzz|%theme_namespace%|g' \ + -e 's|buzz/|%theme_name%/|g' \ + -e "s|'name' => 'buzz'|'name' => '%theme_name%'|g" \ + -e "s|'label' => 'buzz'|'label' => '%theme_name%'|g" \ + "$file" + done + +# The theme header, in style.css only. +sed -i \ + -e 's|^Theme Name: .*|Theme Name: %theme_name%|' \ + -e 's|^Theme URI: .*|Theme URI: %theme_uri%|' \ + -e 's|^Description: .*|Description: %theme_description%|' \ + -e 's|^Author: .*|Author: %theme_author%|' \ + -e 's|^Author URI: .*|Author URI: %theme_author_uri%|' \ + -e 's|^Version: .*|Version: %theme_version%|' \ + "$TARGET/style.css" + +cd "$TARGET" +git status --short +echo "Review with git diff, then commit." diff --git a/bin/replacements.php b/bin/replacements.php new file mode 100644 index 0000000..6d81e48 --- /dev/null +++ b/bin/replacements.php @@ -0,0 +1,28 @@ + placeholder => a plausible substituted value + */ +function scaffolderReplacements(string $slug = 'my-theme'): array +{ + $studly = str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $slug))); + + return [ + '%theme_name%' => $slug, + '%theme_camel%' => lcfirst($studly), + '%theme_namespace%' => 'Theme\\'.$studly, + '%theme_author%' => 'Pollora', + '%theme_author_uri%' => 'https://pollora.dev', + '%theme_uri%' => 'https://pollora.dev', + '%theme_description%' => 'Theme under test', + '%theme_version%' => '1.0.0', + ]; +} diff --git a/config/config.php b/config/config.php index d9b472b..664533e 100644 --- a/config/config.php +++ b/config/config.php @@ -14,7 +14,7 @@ | */ - 'name' => 'buzz', + 'name' => '%theme_name%', /* |-------------------------------------------------------------------------- diff --git a/config/gutenberg.php b/config/gutenberg.php index fdc8a3c..083048b 100644 --- a/config/gutenberg.php +++ b/config/gutenberg.php @@ -17,8 +17,8 @@ | */ 'patterns' => [ - 'buzz/patterns' => [ - 'label' => 'Buzz', + '%theme_name%/patterns' => [ + 'label' => '%theme_name%', ], ], ], diff --git a/parts/footer.html b/parts/footer.html index f056d4e..b622f82 100644 --- a/parts/footer.html +++ b/parts/footer.html @@ -1 +1 @@ - + diff --git a/parts/header.html b/parts/header.html index 4a729cf..0e4f7f0 100644 --- a/parts/header.html +++ b/parts/header.html @@ -1 +1 @@ - + diff --git a/patterns/article.php b/patterns/article.php index 943e2d5..c8a9328 100644 --- a/patterns/article.php +++ b/patterns/article.php @@ -1,8 +1,8 @@ diff --git a/patterns/design-system.php b/patterns/design-system.php index e60c674..80ea858 100644 --- a/patterns/design-system.php +++ b/patterns/design-system.php @@ -1,8 +1,8 @@ diff --git a/patterns/index-list.php b/patterns/index-list.php index 1d981e0..1f79c4d 100644 --- a/patterns/index-list.php +++ b/patterns/index-list.php @@ -1,8 +1,8 @@ diff --git a/patterns/masthead.php b/patterns/masthead.php index c3bab7c..80a3553 100644 --- a/patterns/masthead.php +++ b/patterns/masthead.php @@ -1,8 +1,8 @@ diff --git a/patterns/page-body.php b/patterns/page-body.php index d220b58..1a00408 100644 --- a/patterns/page-body.php +++ b/patterns/page-body.php @@ -1,8 +1,8 @@ diff --git a/resources/views/patterns/colophon.blade.php b/resources/views/patterns/colophon.blade.php index 7226ab1..92ff810 100644 --- a/resources/views/patterns/colophon.blade.php +++ b/resources/views/patterns/colophon.blade.php @@ -1,7 +1,7 @@ {{-- Title: Colophon - Slug: buzz/colophon - Categories: buzz/patterns + Slug: %theme_name%/colophon + Categories: %theme_name%/patterns Block Types: core/template-part/footer Inserter: false --}} diff --git a/style.css b/style.css index 56e8ea0..c9636d9 100644 --- a/style.css +++ b/style.css @@ -1,10 +1,10 @@ /* -Theme Name: Buzz -Theme URI: https://pollora.dev -Description: A typographic, journal/magazine Full Site Editing theme. -Author: Pollora -Author URI: https://pollora.dev -Version: 1.0.0 +Theme Name: %theme_name% +Theme URI: %theme_uri% +Description: %theme_description% +Author: %theme_author% +Author URI: %theme_author_uri% +Version: %theme_version% License: MIT License URI: https://opensource.org/licenses/MIT Tags: full-site-editing, block-patterns, blog, entertainment diff --git a/templates/404.html b/templates/404.html index e534d01..57bd763 100644 --- a/templates/404.html +++ b/templates/404.html @@ -1,5 +1,5 @@ - + diff --git a/templates/archive.html b/templates/archive.html index 602cd53..9cda3e4 100644 --- a/templates/archive.html +++ b/templates/archive.html @@ -1,5 +1,5 @@ - + diff --git a/templates/index.html b/templates/index.html index 602cd53..9cda3e4 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,5 +1,5 @@ - + diff --git a/templates/page.html b/templates/page.html index 3cc3a0a..0e5811f 100644 --- a/templates/page.html +++ b/templates/page.html @@ -1,5 +1,5 @@ - + diff --git a/templates/search.html b/templates/search.html index 602cd53..9cda3e4 100644 --- a/templates/search.html +++ b/templates/search.html @@ -1,5 +1,5 @@ - + diff --git a/templates/single.html b/templates/single.html index 72ec5e8..1a430a4 100644 --- a/templates/single.html +++ b/templates/single.html @@ -1,5 +1,5 @@ - +