B Blengi docs

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 on config('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 the twitter:card="summary_large_image" companion. The OG image defaults to public/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 sameAs profile links when seo.social_profiles is configured. Those links are opt-in on purpose: a sameAs pointing at a URL nobody verified is worse than none.
    • SoftwareApplication — home only. Its offers array 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 breadcrumb override. 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.
  • /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:

  1. Register a default in SeoMeta::ROUTE_DEFAULTS with title / description / path. Use {brand} for any place the install's name should appear.
  2. 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 prefixWhy 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.

The SEO block is translatable per language
Both the block's heading and its full Markdown body are surfaced in Settings → Translations, so you can author a keyword-rich Dutch or German version of the section — the body is one entry you translate as a whole, and the localized Markdown re-renders into the same card layout. The per-route page title, 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.