Search by

pokoblog / laravel

rbouma

Laravel integration for the PokoBlog article API and publish webhook.

v1.2.0 2026-09-06 14:40 UTC

This package is auto-updated.

Last update: 2026-09-06 15:26:50 UTC


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.