Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ buzz/
├── resources/assets/
│ ├── css/app.css # @theme static tokens (colour only — see below) + the few hand-authored rules
│ └── fonts/ # Self-hosted Playfair Display + Source Serif 4 (OFL, Google Fonts, latin subset)
└── app/Providers/AssetServiceProvider.php
└── app/
├── Cms/Bindings/ArticleBinding.php # Block Bindings source: the byline's reading time
└── Providers/AssetServiceProvider.php
```

One rule decides where a file goes: **the theme root holds what WordPress reads itself**
Expand All @@ -68,6 +70,12 @@ docblock (`Title`, `Slug`, `Categories`…). Most of Buzz's patterns are, since
— the colophon's year, here — is a Blade view in `resources/views/patterns/*.blade.php`, registered
by Pollora.

The byline's reading time ("4 min read", in the article and in the index) is a plain
`core/paragraph` bound to the theme's own [Block Bindings](https://pollora.dev/blocks/block-bindings/)
source, `app/Cms/Bindings/ArticleBinding.php` (`%theme_name%/article`, fields `reading_time` and
`word_count`): a PHP method computes it from the post's words, in a query loop for each post, and
the editor shows it too. Add a field to the class, and it is offered in the block's "Attributes" panel.

WordPress caches the list of a theme's `patterns/` files: a new file shows up once that cache is
cleared (`wp eval 'wp_get_theme()->delete_pattern_cache();'`), or at once with
`WP_DEVELOPMENT_MODE=theme`.
Expand Down
52 changes: 52 additions & 0 deletions app/Cms/Bindings/ArticleBinding.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace %theme_namespace%\Cms\Bindings;

use Pollora\Attributes\BlockBinding;
use Pollora\Attributes\BlockBinding\BindingField;
use Pollora\BlockBinding\Domain\Models\BindingContext;

/**
* What the byline says about an article, computed from the post itself.
*
* Bound in the article and index patterns:
* "metadata": {"bindings": {"content": {"source": "%theme_name%/article", "args": {"field": "reading_time"}}}}
*/
#[BlockBinding('%theme_name%/article', label: 'Article')]
final class ArticleBinding
{
/**
* Words read in a minute, for the reading time.
*/
private const int WORDS_PER_MINUTE = 230;

#[BindingField(label: 'Reading time')]
public function readingTime(BindingContext $context): ?string
{
$words = $this->wordCount($context);

if ($words === null) {
return null;
}

$minutes = max(1, (int) ceil($words / self::WORDS_PER_MINUTE));

return sprintf(_n('%d min read', '%d min read', $minutes, '%theme_name%'), $minutes);
}

#[BindingField(label: 'Word count')]
public function wordCount(BindingContext $context): ?int
{
$post = $context->post();

if ($post === null) {
return null;
}

$text = wp_strip_all_tags(strip_shortcodes((string) $post->post_content));

return count(preg_split('/\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY) ?: []);
}
}
18 changes: 17 additions & 1 deletion bin/tests/e2e/buzz.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,18 @@ test.beforeAll(() => {

const category = wp('term', 'create', 'category', `${tag}-cat`, '--porcelain');
ids.post = create('--post_type=post', `--post_title=Headline ${tag}`, `--post_content=<!-- wp:paragraph --><p>Body ${tag}</p><!-- /wp:paragraph -->`, `--post_category=${category}`);
ids.long = create('--post_type=post', `--post_title=Long read ${tag}`, `--post_content=<!-- wp:paragraph --><p>${'word '.repeat(700)}</p><!-- /wp:paragraph -->`);
ids.page = create('--post_type=page', `--post_title=Page ${tag}`, `--post_content=<!-- wp:paragraph --><p>Page body ${tag}</p><!-- /wp:paragraph -->`);
ids.category = category;

urls.post = link(ids.post);
urls.long = link(ids.long);
urls.page = link(ids.page);
urls.category = new URL(wp('term', 'get', 'category', category, '--field=url')).pathname;
});

test.afterAll(() => {
for (const key of ['post', 'page']) {
for (const key of ['post', 'long', 'page']) {
if (ids[key]) {
wp('post', 'delete', ids[key], '--force');
}
Expand Down Expand Up @@ -79,6 +81,20 @@ test('a post renders its title and body', async ({ page }) => {
await expect(page.locator('main')).toContainText(`Body ${tag}`);
});

test('the byline says how long a post takes to read, from its own words', async ({ page }) => {
// 700 words at 230 a minute: the theme's own binding source, buzz/article.
await visit(page, urls.long);
await expect(page.locator('main')).toContainText('4 min read');

await visit(page, urls.post);
await expect(page.locator('main')).toContainText('1 min read');

// In the query loop, each item reads its own post.
await visit(page, '/');
await expect(page.locator('main .buzz-index-list > li').filter({ hasText: `Long read ${tag}` })).toContainText('4 min read');
await expect(page.locator('main .buzz-index-list > li').filter({ hasText: `Headline ${tag}` })).toContainText('1 min read');
});

test('a page renders its title and body', async ({ page }) => {
await visit(page, urls.page);

Expand Down
8 changes: 8 additions & 0 deletions patterns/article.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@
<!-- /wp:paragraph -->

<!-- wp:post-date /-->

<!-- wp:paragraph {"fontSize":"caption"} -->
<p class="has-caption-font-size">&middot;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"%theme_name%/article","args":{"field":"reading_time"}}}},"fontSize":"caption"} -->
<p class="has-caption-font-size">Reading time</p>
<!-- /wp:paragraph -->
</div>
<!-- /wp:group -->
</div>
Expand Down
8 changes: 8 additions & 0 deletions patterns/index-list.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@
<!-- /wp:paragraph -->

<!-- wp:post-date /-->

<!-- wp:paragraph {"fontSize":"caption"} -->
<p class="has-caption-font-size">&middot;</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"%theme_name%/article","args":{"field":"reading_time"}}}},"fontSize":"caption"} -->
<p class="has-caption-font-size">Reading time</p>
<!-- /wp:paragraph -->
</div>
<!-- /wp:group -->

Expand Down
Loading