pokoblog / laravel
Laravel integration for the PokoBlog article API and publish webhook.
Requires
- php: >=8.2
- illuminate/cache: ^11.0 || ^12.0 || ^13.0
- illuminate/contracts: ^11.0 || ^12.0 || ^13.0
- illuminate/http: ^11.0 || ^12.0 || ^13.0
- illuminate/routing: ^11.0 || ^12.0 || ^13.0
- illuminate/support: ^11.0 || ^12.0 || ^13.0
- pokoblog/client: ^1.1
- symfony/http-kernel: ^7.0 || ^8.0
Requires (Dev)
- orchestra/testbench: ^9.0 || ^10.0 || ^11.0
- phpunit/phpunit: ^10.5 || ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A thin layer over pokoblog/client. It does not reimplement the
client; it adds the four things a plain PHP library cannot give you: config that
publishes, a webhook route in one line, a cache that uses the API's ETag, and
an event to listen for.
Installing
composer require pokoblog/laravel php artisan vendor:publish --tag=pokoblog-config
Requires PHP 8.2+ and Laravel 11 or 12. The provider and the PokoBlog facade
are auto-discovered.
POKOBLOG_URL=https://app.pokoblog.example POKOBLOG_TOKEN=your-embed-connector-token POKOBLOG_WEBHOOK_SECRET=whsec_…
Reading the blog
use PokoBlog\Laravel\Facades\PokoBlog; Route::get('/blog', function () { return view('blog.index', ['articles' => PokoBlog::page(20)->articles]); }); Route::get('/blog/{slug}', function (string $slug) { return view('blog.show', ['article' => PokoBlog::article($slug)]); });
{{-- blog/show.blade.php --}} <h1>{{ $article->title() }}</h1> {{-- `html` is PokoBlog's sanitized output. Unescaped on purpose; see below. --}} {!! $article->html !!}
{!! !!} rather than {{ }} because html is markup on purpose: it is the
output of PokoBlog's allowlist renderer and is the identical string PokoBlog
writes into a WordPress post. Escaping it shows the visitor its own tags.
markdown is not interchangeable here — it is the unsanitized source and
accepts raw HTML by design.
The facade resolves the cached client. Inject
PokoBlog\Laravel\CachedPokoBlog for the same thing where you would rather not
use a facade; PokoBlog::client() reaches the plain client underneath.
The caching is the point
Cache::remember('blog', 300, fn () => $poko->page()) is the obvious version and
it throws away what makes this API cheap: every five minutes it re-downloads the
whole list whether or not a word of it changed, and a blog is written to a few
times a week and read continuously.
This package uses the ETag instead. Two lifetimes, both in
config/pokoblog.php:
ttl(300s) — served with no HTTP request at all.hold(86400s) — how long the entry survives after that, so its tag is still there to revalidate with.
When a stale entry is refreshed, its tag goes out as If-None-Match and an
unchanged blog answers 304 with no body: a round trip and a header block
instead of an article list. hold shorter than ttl would throw the entry away
before it could ever be revalidated — that is refused in the constructor rather
than warned about, because its only symptom is a bandwidth bill nobody reads.
Receiving a publish
// routes/api.php Route::pokoblog('/webhooks/pokoblog');
That is the whole endpoint. The signature is verified before anything runs; a
call that fails gets a bare 403.
// app/Providers/AppServiceProvider.php use Illuminate\Support\Facades\Event; use PokoBlog\Laravel\Events\ArticlePublished; use PokoBlog\Laravel\Facades\PokoBlog; Event::listen(function (ArticlePublished $published) { // The blog stops serving yesterday's list this second, not at the TTL. PokoBlog::flush(); // $published->slug(), $published->event->article->title, … // $published->delivery() is stable across retries: use it as an // idempotency key. });
flush() is a version counter in the cache key, not cache tags. Tags exist on
Redis and Memcached and not on the file or database stores most small sites run,
so a tag-based flush would silently do nothing for most customers and be found
on the day somebody published a correction and the old copy stayed up.
Register it in routes/api.php
Or exclude the URI from CSRF. The web group verifies a CSRF token and PokoBlog
has none to send, so a route registered there answers 419 to every delivery —
which reads as an authentication problem and is not one.
// bootstrap/app.php ->withMiddleware(function (Middleware $middleware) { $middleware->validateCsrfTokens(except: ['webhooks/pokoblog']); })
Queue anything slow
The route answers 204 as soon as the event is dispatched, which is the
contract PokoBlog documents — it holds a customer's publish request open for at
most five seconds while it waits. A listener that resizes images should
implement ShouldQueue.
If a delivery is lost
PokoBlog tries three times over about ten seconds and then stops. Reconcile from
PokoBlog::articles() on a slow timer; the article list is the source of truth
and this event is the fast path, not a guarantee.
A 500 you might see once
If POKOBLOG_WEBHOOK_SECRET is not set, the first delivery raises a
LogicException naming that key rather than quietly refusing every call — from
PokoBlog's side an endpoint that rejects everything looks exactly like a
firewall problem, and you would debug the wrong end for a day.
Tests
clients/laravel/bin/test clients/laravel/bin/test --filter test_a_publish_drops_what_was_cached
Real Testbench, real routes, real cache stores; the transport is faked so nothing leaves the process. Uses containers when there is no local PHP.