laravel maintained by picsmage
PicsMage for Laravel
The official Laravel SDK for synchronizing a site image catalog and serving only last-known-ready, immutable PicsMage Open Graph image URLs. Page rendering reads Laravel Cache only: it never calls PicsMage, queries a database, dispatches a job, or waits for image generation.
Requires PHP 8.4 and Laravel 13. The package is licensed under MIT.
Install
composer require picsmage/laravel
php artisan vendor:publish --tag=picsmage-config
Configure the site-scoped credential through the host's secret store:
PICSMAGE_URL=https://picsmage.com
PICSMAGE_SITE_ID=01...
PICSMAGE_API_KEY=pm_live_...
PICSMAGE_CDN_HOST=cdn.picsmage.com
PICSMAGE_CACHE_STORE=file
PICSMAGE_MANIFEST_REFRESH_ENABLED=true
PICSMAGE_MANIFEST_REFRESH_INTERVAL=5
Do not print, commit, or log the API key.
Register the catalog
Create one application-owned catalog. It owns only external IDs and current page metadata; PicsMage owns generation, retry, quota, readiness, and object/CDN reconciliation.
<?php
namespace App\Seo;
use PicsMage\Laravel\Contracts\ImageCatalog;
use PicsMage\Laravel\Data\ImageDefinition;
final class OgImageCatalog implements ImageCatalog
{
public function definitions(): iterable
{
yield new ImageDefinition(
externalId: 'blog:index',
canonicalUrl: 'https://example.com/blog',
title: 'Example blog',
description: 'Useful updates from Example.',
);
// Yield the rest of the site's definitions.
}
}
Register it in the application's service provider:
use App\Seo\OgImageCatalog;
use PicsMage\Laravel\Facades\PicsMage;
public function boot(): void
{
PicsMage::catalog(OgImageCatalog::class);
}
Deploy and prewarm
Validate without network access:
php artisan picsmage:catalog:sync --dry-run
Synchronize the entire catalog in one atomic request, wait on one catalog-level status resource, then refresh the ready manifest:
php artisan picsmage:prewarm --wait --timeout=900
The package optionally registers picsmage:manifest:refresh every five minutes with onOneServer() and withoutOverlapping(). PicsMage uses its own PICSMAGE_CACHE_STORE instead of inheriting the host default, so a database-backed application cache does not add a query to page rendering. The default file store is suitable for workers on one Forge server because Laravel's storage directory is shared across releases; use a shared Redis store for multiple application servers. The selected store must support atomic locks. The host must run Laravel's normal scheduler:
* * * * * cd /path-to-app && php artisan schedule:run >> /dev/null 2>&1
Other commands:
php artisan picsmage:catalog:sync
php artisan picsmage:manifest:refresh
An empty full replacement requires the explicit --allow-empty option.
Render OG metadata
Pass the current definition metadata directly. This computes its source digest without loading the registered catalog, so domain catalogs may safely use this method while the registered catalog enumerates those same sources.
$image = PicsMage::ogImage(
externalId: 'blog:index',
title: $title,
description: $description,
canonicalUrl: $canonicalUrl,
fallback: asset('images/og-fallback.webp'),
alt: 'Example social preview',
);
return [
'title' => $title,
'description' => $description,
...$image->toSeoArray(),
];
toSeoArray() returns:
[
'image_url' => 'https://cdn.picsmage.com/.../{fingerprint}.jpg',
'image_alt' => 'Example social preview',
'image_type' => 'image/jpeg',
'image_width' => 1200,
'image_height' => 630,
]
Generated entries are always JPEG. Fallback URLs report PNG, WebP, or SVG MIME from their path, defaulting to JPEG.
An already-constructed definition can be resolved with no catalog lookup:
$url = PicsMage::urlFor($definition, $fallback);
PicsMage::url($externalId, $fallback) is available as a convenience when loading the registered catalog at runtime is safe. Recursive catalog resolution fails closed to the fallback.
The SDK returns the fallback for a cold or malformed cache, an unknown ID, a changed source digest, an invalid URL/fingerprint/dimension/media type, or a cache read exception. A refresh transport failure, rate limit, or server error leaves the last-known-good manifest untouched. A valid 200 replaces the whole map, so absence is a tombstone; 304 retains it.
Testing
$fake = PicsMage::fake()
->ready('blog:index', 'https://cdn.example.test/ready.jpg')
->missing('guide:intro');
// Exercise application SEO code...
$fake->assertCatalogDefinitionCount(2)
->assertCatalogContains('blog:index')
->assertResolved('blog:index')
->assertFallbackUsed('guide:intro');
Public HTTP contract expected from PicsMage
The SDK expects these authenticated, site-scoped endpoints:
GET|PUT /api/v1/sites/{site}/catalogGET /api/v1/sites/{site}/catalog/revisions/{catalog_revision}GET /api/v1/sites/{site}/ready-manifest
Catalog replacement uses a strong ETag precondition, an Idempotency-Key, and a SHA-256 revision over canonical JSON { "schema_version": 1, "entries": [...] }. The ready manifest supports strong ETag/If-None-Match, returns 200 or 304, and contains only 1200×630 JPEG entries whose HTTPS CDN basename is exactly {fingerprint}.jpg with no credentials, port, query, or fragment.
The package retries only connection failures, 5xx responses, and explicitly retryable 429 rate_limited responses, with explicit connect/total timeouts and at most three attempts. Authentication, validation, quota, conflict, and precondition errors fail immediately. Laravel events expose only safe site/revision/count/error-code data or a hashed external ID; the package never logs credentials or page content.
Package development
composer install
composer validate --strict
composer check-platform-reqs
composer audit --locked
composer check