Sitemap and JSON-LD
The SEO plugin ships two indexability primitives: a sitemap helper that builds /sitemap.xml from published content, and a JSON-LD block emitted per page by SeoTags. Together they let search engines discover and richly render every public page.
Sitemap
Mamenein\FilamentSeo\Sitemap::xml(array $urls): string is a static helper that takes a list of ['loc' => string, 'lastmod' => string|null] rows and returns a valid <urlset> document. The plugin does not register the route itself — the clone wires it on its public controller:
// routes/web.php
Route::get('/sitemap.xml', [PublicPageController::class, 'sitemap']);
The controller enumerates published Posts and Docs, mapping each to its public URL and updated_at date:
$urls = collect([['loc' => url('/'), 'lastmod' => null]])
->merge(Post::query()->published()->get()->map(fn (Post $post) => [
'loc' => url($post->publicPath()),
'lastmod' => $post->updated_at?->toDateString(),
]))
->merge(Doc::query()->published()->get()->map(fn (Doc $doc) => [
'loc' => url($doc->publicPath()),
'lastmod' => $doc->updated_at?->toDateString(),
]))
->values()->all();
return response(Sitemap::xml($urls), 200, [
'Content-Type' => 'application/xml; charset=UTF-8',
]);
lastmod is optional; the helper omits the element when null. loc is escaped with e().
JSON-LD
SeoTags::toHtml() renders the meta tags view and embeds a <script type="application/ld+json"> block. The schema type emitted is WebPage — verified in SeoTags.php:
$jsonLd = json_encode([
'@context' => 'https://schema.org',
'@type' => 'WebPage',
'name' => $this->title,
'description' => $this->description,
'url' => $this->canonical,
'image' => $this->ogImage,
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
SeoTags::for($model) builds the tags from the model's morph SEO row ($model->seo), falling back to the model's title attribute and url()->current() for the canonical. Example output for a published post:
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "Shipping to Magic Containers",
"description": "Stateless deploy notes for FrankenPHP on bunny.net.",
"url": "https://edge.qcentic.com/blog/shipping-to-magic-containers",
"image": "https://cdn.qcentic.com/media/42/og.png"
}
Nested SEO on the Content API
When posts or docs are created through /api/v1, the Content API accepts a nested seo payload and stores it on the morph via $model->seo()->updateOrCreate([], $values). Accepted keys: title, description, canonical_url, og_image_url. API-published pages are therefore fully indexed by both the sitemap and JSON-LD — no panel visit required. See the Content API page.
flowchart LR
A[Published Post/Doc] --> B[Sitemap helper]
B --> C[/sitemap.xml/]
C --> D[Search engine]
E[SeoTags::for model] --> F[JSON-LD block]
F --> D
See also: