Operate
SEO surface
Every public route emits a per-page SEO block — title,
description, canonical URL, Open Graph card, Twitter card, and
JSON-LD structured data — without any per-page work in the
React layer. Buyers running a white-label install inherit the
same machinery; replace the brand name in
/settings/branding and the meta block follows.
What ships out of the box
-
Per-route titles + descriptions. Defined in
App\Support\SeoMeta::ROUTE_DEFAULTS. Tokens like{brand}are interpolated at render time so a renamed install never leaks the source product name into the meta block. -
Canonical URLs. Every page declares its
canonical via
<link rel="canonical">, anchored onconfig('app.url')+ the route path. Search engines never have to guess which variant is the master. -
Open Graph + Twitter cards. Shared social
previews cover
og:type,og:site_name,og:title,og:description,og:url,og:image, plus thetwitter:card="summary_large_image"companion. The OG image defaults topublic/og-image.png; drop your own in there to override. -
JSON-LD structured data. Three blocks layer
on top of the standard meta:
- Organization — every page. Brand name, canonical URL, and (when present) the logo.
- SoftwareApplication — home only. Lets Google's rich-result picker render us as an app with an offer + free-tier price.
- FAQPage — home only, sourced from the marketing FAQ admins edit at Settings → Marketing. Edits flow through to the structured data automatically — Google's rich-result picker renders Q+A directly under the search snippet for matching queries.
- /sitemap.xml — every marketing page, every documentation slug, and every published changelog version. Cached for 1 hour at the controller level so a fresh changelog entry shows up within the cache window.
-
/robots.txt — allows public surfaces;
disallows
/admin,/app,/api/,/settings, and the auth flows; references the sitemap so crawlers find it on first sniff.
Adding SEO to a new route
Two places to touch when adding a new public page:
-
Register a default in
SeoMeta::ROUTE_DEFAULTSwithtitle/description/path. Use{brand}for any place the install's name should appear. -
In the controller, pass the SEO payload to Inertia:
return Inertia::render('your/page', [ // ... your props 'seo' => SeoMeta::for('your-route-key'), ]);
The Inertia root layout
(resources/views/app.blade.php) reads
props.seo and emits the meta block automatically.
No per-page Blade work needed.
Need to vary the description per row (e.g. a per-version changelog page)? Pass overrides:
'seo' => SeoMeta::for('changelog.show', [
'title' => "v1.1.0 — what's new in {brand}",
'description' => "Released " . $entry->released_at_human . " — " . $entry->summary,
])
Customising the Open Graph image
Drop a 1200×630 PNG (or JPG) at public/og-image.png.
Buyers running their own install can swap the file directly via
SFTP or the deploy host's file manager. The
SeoMeta resolver checks the file exists at render
time; if it doesn't, the meta block silently omits
og:image and twitter:image rather than
pointing at a 404.
Sitemap cache
The sitemap is cached for 1 hour under
seo:sitemap.xml. To force a refresh after publishing
a long-awaited changelog entry:
php artisan tinker --execute 'cache()->forget("seo:sitemap.xml");'
Crawler traffic on a busy install would otherwise be O(crawl rate) database hits; the cache flattens it to one rebuild per hour per node.
Excluded routes
| Route prefix | Why excluded |
|---|---|
/admin/* |
Platform-admin surface. Tenant-aware data. |
/app/* |
Workspace dashboard. Auth-required, workspace-scoped. |
/api/* |
API surface — JSON, not for crawlers. |
/login, /register, password reset |
Auth flows — no SEO value, nothing for a crawler to index. |
/settings/* |
Auth-required user / platform settings. |