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, a description, the logo when one
exists, and
sameAsprofile links whenseo.social_profilesis configured. Those links are opt-in on purpose: asameAspointing at a URL nobody verified is worse than none. -
SoftwareApplication — home only.
Its
offersarray is built from the LIVE plan rows the pricing page renders, so the rich result and the page can never quote different prices. An install with no active plans falls back to a single free offer rather than an invented number. -
BreadcrumbList — any page that passes
a
breadcrumboverride. The marketing reference pages build theirs from the registry's parent chain. -
FAQPage — any page that passes
faq_items, not just the home page. The home page sources them from the marketing FAQ admins edit at Settings → Marketing, and a reference page with an FAQ section earns the same markup. Edits flow through to the structured data automatically — Google's rich-result picker renders Q+A directly under the search snippet for matching queries.
-
Organization — every page. Brand
name, canonical URL, a description, the logo when one
exists, and
- /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,
])
Which host, and which pages
Every absolute URL on the SEO surface — each sitemap <loc>,
every hreflang alternate, the Sitemap: line in robots.txt — is
built from APP_URL, never from the host the request arrived on.
One web-server vhost usually serves the primary domain and its country-code
siblings, and the sitemap is cached for an hour under a key that carries no
host; building it from the request once let a single crawler fetch via a
sibling domain put that domain into every entry served to everyone. A
request for /sitemap.xml or /robots.txt on a
country-code host is redirected to the same path on the primary host.
Only the customer-facing documentation groups are listed in the sitemap:
Get started, Improve your agent, Build your agent, Embed the widget, Run
your workspace, WordPress & WooCommerce and API reference. The internal
groups — Platform admin, Troubleshooting, Architecture and Operate — stay
reachable by URL (support links to them) but are excluded from the sitemap
and render noindex,nofollow. The flag is the group's
public key in DocumentationNav::tree(); a group
without one is treated as public.
Page language for social crawlers
Every page emits og:locale, and a marketing page also emits
one og:locale:alternate per other marketed language. The
values are language_TERRITORY pairs
(en_US, nl_NL) because a bare language code is
silently ignored by the crawlers that read the tag. The mapping lives in
LocaleResolver::ogLocaleFor(), and the alternates are derived
from the same hreflang set as the <link> tags, so the
two can never disagree.
The documentation site
These pages are Blade and never pass through the Inertia root, so
SeoMeta does not reach them. Their head tags are emitted by
documentation/layout.blade.php instead: a canonical, the
Open Graph and Twitter pair, and a description derived from the page's
own first paragraph. Deriving it beats hand-writing one per page, and it
cannot go stale relative to the page it describes.
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). Since #632 both trees also carry a hero
node, section titles, a closing cta with the website-address
capture, and — for how-it-works — a boolean done_by_you per
step (a string marker would be machine-translated away) plus the measured
instant.stats; for integrations, an editable
native list. A row saved before that keeps the shipped copy
for every node it lacks, and every string in both trees now reaches the
Translation Manager.