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. |
Editable homepage SEO content block
The homepage carries an optional, admin-authored SEO
content block rendered below the main content, above the footer
— a place to add a keyword-rich, well-structured SEO section without
touching template code. Edit it under
/settings/marketing → the SEO block tab:
- Show toggle — off by default, so the homepage is unchanged until you opt in. When off, nothing renders.
- Heading (optional) — a section title.
- Body — Markdown:
#/##/###for H1/H2/H3,[text](https://…)for links,-for lists, plus bold/italic. Works across all themes (harvest / aurora / prism).
The layout adapts to the Markdown's structure so longer content reads as a native landing section rather than an article pasted below the page:
- A heading that opens the body (
#or##) is promoted to the section's display heading when the Heading field is empty. - Two or more subsections (
##headings — or###when no##is used) render as a wide, centered section: the heading and any opening paragraphs appear centered up top, and each subsection becomes a card in a responsive grid (two columns, three when the count fills them). Deeper headings stay inside their card. - Plain Markdown without subsections keeps a single readable column.
The body is rendered to HTML server-side through
SafeMarkdown — the same hardened converter the knowledge
base uses. Raw HTML tags and javascript: links are
stripped, so the block can't introduce script onto the public
homepage even though only super-admins can edit it. The stored value
keeps your raw Markdown, so the editor round-trips cleanly.
This is distinct from the per-page meta title / description (still set
by SeoMeta): the block adds visible, indexable body
content, which is what most "add an SEO section" asks are really
after.
og:title, and twitter:title (all
the same SeoMeta title) are translatable there too; the
homepage title and description ship with Dutch and German values.
Editable /how-it-works and /integrations pages
The /how-it-works and /integrations marketing
pages are admin-editable — no code change to adjust copy or SEO. Edit
them under /settings/marketing in the How it works
page and Integrations page sections:
- How it works — the step walkthrough (number, title, duration, description, and bullet points per step) plus the page's meta title and meta description.
- Integrations — the data-source cards and the roadmap cards (name + tagline, plus an icon name for data sources), plus the page meta. The native integration cards have their own editor.
Both pages fall back to the shipped defaults for any field you leave
blank, so an empty meta title keeps the built-in
SeoMeta default. The content lives in
app_settings.how_it_works_content /
integrations_content; the public React pages already read
these props, so edits show up immediately on save (after a deploy of
the built assets).