Skip to content

Static Site Extensions

Who is this for? Developers adding custom output files (RSS feeds, alternate sitemaps, custom JSON exports) to the static-site export, or transforming the export process itself.

TL;DR: Register a callable with StaticSiteExtensionRegistry to inject custom handlers into the static-site generation pipeline. Your handler receives the Site and SiteDomain being exported, plus a callback to mark URLs as processed.


You have custom content that needs to be generated and deployed as part of the static-site export—for example:

  • An RSS feed for blog posts
  • An alternate sitemap format (JSON, XML variants)
  • Custom JSON metadata for client-side consumption
  • Transformed or filtered versions of existing pages

Using the registry is preferred over post-export shell scripts because:

  • Your custom handler integrates into the progress tracking (checkpoint callback counts your output correctly)
  • The generated files are included in the export job tracking
  • You have direct access to the database models (Site, SiteDomain) without extra queries

The registry is a singleton accessed via StaticSiteExtensionRegistry::instance(). Handlers are registered early (typically in a service provider’s boot() method) before the static-site generation job runs.

When StaticSiteGenerator->process() executes (invoked by the StaticSiteExportAction or related pipeline):

  1. Prepare phase: StaticSiteGenerator::processExtensionHandlers() iterates over all registered handlers.
  2. Invocation: Each handler callable is invoked with ($site, $siteDomain, $checkpoint).
  3. Progress: Your handler calls the checkpoint callback for each URL/file it processes, updating progress.
  4. Totals: The count of items your handler processes is added to the total URL count for accurate progress reporting.

File references:

  • Registry definition: packages/core/src/Support/StaticSite/StaticSiteExtensionRegistry.php
  • Consumer/invocation: packages/core/src/Support/StaticSite/StaticSiteGenerator.php, lines 74–88 (the processExtensionHandlers() method)
MethodReturnsPurpose
instance(): selfStaticSiteExtensionRegistryReturns the singleton instance
register(string $key, callable $extension): voidvoidRegisters a handler (no-op if key already exists)
has(string $key): boolboolChecks if a key is registered
all(): arrayarray<string, callable>Returns all registered handlers
reset(): voidvoidClears all handlers (testing only)

Handlers are callables with signature:

callable(Site $site, SiteDomain $siteDomain, Closure $checkpoint): void

Where:

  • $site: The Site model being exported (access properties like id, slug, domain, etc.)
  • $siteDomain: The SiteDomain (language/domain pair) being exported
  • $checkpoint: A closure fn (string $url): void that you call for each URL/file your handler processes. Increments the job progress counter and invokes any registered progress callback.

Important: The checkpoint callback accepts only a URL/file path as a string (for progress tracking); it does not return a value.

<?php
declare(strict_types=1);
namespace App\Providers;
use Capell\Core\Models\Site;
use Capell\Core\Models\SiteDomain;
use Capell\Core\Support\StaticSite\StaticSiteExtensionRegistry;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
StaticSiteExtensionRegistry::instance()->register('feed.xml', function (
Site $site,
SiteDomain $siteDomain,
\Closure $checkpoint,
): void {
// Fetch posts for this site/language
$posts = $site->pages()
->where('type_slug', 'post')
->where('language_id', $siteDomain->language_id)
->get();
// Build XML feed
$feedXml = '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL;
$feedXml .= '<rss version="2.0">' . PHP_EOL;
$feedXml .= '<channel>' . PHP_EOL;
foreach ($posts as $post) {
$feedXml .= sprintf(
' <item><title>%s</title><link>%s</link></item>' . PHP_EOL,
htmlspecialchars($post->title, ENT_XML1),
$post->url,
);
}
$feedXml .= '</channel>' . PHP_EOL;
$feedXml .= '</rss>';
// Write to export directory or storage
$path = storage_path("exports/{$site->slug}/feed.xml");
file_put_contents($path, $feedXml);
// Mark as processed
$checkpoint('feed.xml');
});
}
}
  • Key naming: Choose keys that don’t collide with built-in exports (e.g., avoid keys that resolve to the same filename as generated page URLs). Consider prefixing with custom: or your app name.
  • Synchronous execution: Handlers run in the main request/job context. Long-running operations block the entire export job. For expensive operations, consider dispatching to a separate queue or deferring to a post-export step.
  • Directory/file handling: The registry does not manage filesystem operations. Your handler must ensure the export directory exists and has write permissions. Consider using Laravel’s Storage facade or explicitly creating directories.
  • Checkpoint is required for progress tracking: If you generate multiple files or URLs, call $checkpoint() for each one so that job progress and totals are accurate. A single call to $checkpoint() counts as one processed item.
  • No return value: Handlers must return void. The registry does not capture or use return values.