G Growreplies 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, 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:

  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,
])

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.