PHPackages                             smking/laravel - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. smking/laravel

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

smking/laravel
==============

Laravel package for smking AEO — auto-inject JSON-LD, FAQ, and AI summary into Laravel responses so AI crawlers (ChatGPT, Perplexity, Google AI) can cite your pages.

v0.19.1(1w ago)0362↓30%[5 issues](https://github.com/sillyleo/smking-laravel/issues)MITPHPPHP ^8.1

Since Apr 27Pushed 1w agoCompare

[ Source](https://github.com/sillyleo/smking-laravel)[ Packagist](https://packagist.org/packages/smking/laravel)[ Docs](https://github.com/sillyleo/smking-laravel)[ RSS](/packages/smking-laravel/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (7)Dependencies (12)Versions (42)Used By (0)

smking/laravel
==============

[](#smkinglaravel)

AI-native SEO (AEO) for Laravel. Auto-inject JSON-LD, FAQ, and AI summaries into your pages so ChatGPT, Perplexity, and Google AI can cite them.

Install
-------

[](#install)

**Don't follow this README to install.** Your smking dashboard generates a per-site install prompt with the real `SMKING_API_KEY`, `SMKING_BASE_URL`, and (if you use CMS) `SMKING_WEBHOOK_SECRET` baked in, plus the exact `composer require`, `vendor:publish`, and `php artisan smking:doctor` commands. The prompt is the source of truth and stays in sync with the SDK version.

Two ways to get it:

```
# Option 1 — one-shot wizard (composer require + .env + doctor)
npx @soloworks/smking-wizard

# Option 2 — copy the prompt manually from your smking dashboard's
# install panel into your editor / coding agent.
```

Once `php artisan smking:doctor` is green, the middleware auto-registers and every HTML GET response picks up:

- **AEO** — JSON-LD, FAQ/summary blocks (for ChatGPT, Perplexity, Google AI)
- **SEO** — ``, `og:*`, `twitter:*`, `` (for Google snippet + social shares)
- **Markdown for Agents** (v0.4.0+) — agents requesting `Accept: text/markdown` get a structured markdown rendition. Boosts your Cloudflare Agent Readiness score.
- **Markdown alternate Link header** (v0.5.0+) — every HTML response advertises the markdown rendition via `Link: ; rel="alternate"; type="text/markdown"`.

smking is the source of truth for SEO/AEO meta. Any existing ``, ``, `og:*`, or `` in your layout is stripped and replaced with smking's version (v0.3.0+). To keep a tag under your control, disable it via `config('smking.inject.{tag}', false)` or render it yourself with the `` Blade component.

Manual usage
------------

[](#manual-usage)

Disable auto-injection and render where you want:

```
// config/smking.php
'auto_inject' => false,
```

```
{{-- 1. Body content (JSON-LD + FAQ + summary) --}}

{{-- 2. SEO meta inside  with fallback to your own page data --}}

{{-- 3. Facade for full control --}}
@php($aeo = \Smking::forPath('/products/'.$product->slug))
@if ($aeo->isReady())
    {!! json_encode($aeo->jsonLd) !!}
    {{ $aeo->seo?->title ?? $product->name }}
@endif
```

The `` component mirrors `getSmkingMetadata()` from `@smking/next` — call it inside `` and it emits exactly the SEO tags the API has values for, falling back to the `fallback-*` props otherwise. Use it when you want SEO meta in your Blade layout but body injection from the middleware.

Mount the runtime once (v0.17.1+)
---------------------------------

[](#mount-the-runtime-once-v0171)

Add `` once in your root Blade layout `` — it emits a `` to the saas-served CSS and a `` to the bundled Web Component runtime IIFE. Browser caches both per saas-controlled stale-while-revalidate headers, so the cost amortises across every `` instance on the page.

```
{{-- resources/views/layouts/app.blade.php --}}

    {{ config('app.name') }}

    @yield('content')

```

Optional `:base-url="..."` attribute overrides `config('smking.base_url')`. Trailing slash is trimmed.

Without ``, `` content still renders but the Tailwind utility classes from the dashboard's cva variants resolve to dead strings — the page reaches the browser unstyled. The wizard installer auto-adds this mount; if you're upgrading manually from &lt; v0.17.1, add the one line above.

CMS rendering (optional, v0.11.0+)
----------------------------------

[](#cms-rendering-optional-v0110)

The base install above only wires AEO. If you author content in the smking dashboard's CMS and want to render it on your Laravel site, use the `` Blade component.

The SDK **does not have a "CMS root" config** — you choose any URL prefix (`/blog`, `/knowledge`, `/shop/articles`) and wire your own route. The component takes a `slug` prop, fetches the published page from `${SMKING_BASE_URL}/api/v1/public/page?slug=…`, and renders the Tiptap ProseMirror JSON as `…` server-side via `ueberdosis/tiptap-php`.

### Flat slug

[](#flat-slug)

```
// routes/web.php
Route::get('/blog/{slug}', function (string $slug) {
    return view('blog-page', ['slug' => $slug]);
});
```

```
{{-- resources/views/blog-page.blade.php --}}

```

This handles `/blog/hello` but **not** `/blog/seo/intro` — Laravel's default route param rejects `/`.

### Nested slugs (`/blog/123`, `/blog/seo/intro`)

[](#nested-slugs-blog123-blogseointro)

smking CMS slugs can be nested. Use a catch-all parameter with the `.*` regex constraint:

```
// routes/web.php
Route::get('/blog/{path}', function (string $path) {
    return view('blog-page', ['slug' => $path]);
})->where('path', '.*');  // ← required; without this Laravel rejects nested paths
```

The same `` Blade view works for both flat and nested. Slug is purely what your route resolver passes in — single-segment (`hello`) or multi-segment (`blog/seo/intro`) both match dashboard slug format.

### Cache invalidation

[](#cache-invalidation)

CMS responses cache server-side for 5 minutes. When you publish, rename, or archive a page in the dashboard, smking SaaS POSTs a signed webhook to `https:///api/smking/webhook` (auto-mounted by the SDK in v0.11+). The handler verifies HMAC against `SMKING_WEBHOOK_SECRET` and evicts the matching cache so visitors see the new content on the next request.

If `SMKING_WEBHOOK_SECRET` is unset, the webhook route returns 503 and cache invalidation falls back to TTL-based expiry.

Config (config/smking.php)
--------------------------

[](#config-configsmkingphp)

KeyDefaultNotes`api_key``env('SMKING_API_KEY')`Publishable key from the dashboard`base_url`*(required, no default)*Set `SMKING_BASE_URL` to your smking deployment origin`auto_inject``true`Register middleware globally`inject_in_tests``false`When false (v0.7.3+ default), middleware short-circuits under `php artisan test` / Pest so feature tests don't time out against an unreachable backend. Set `SMKING_INJECT_IN_TESTS=true` in `.env.testing` for genuine integration tests`only` / `except`see filePath filters (Laravel wildcard)`inject.*`all `true`Toggle json\_ld / meta\_description / faq / summary / seo\_title / og\_title / og\_description / og\_image / canonical / markdown`inject.visibility``sr_only`Body-fragment visibility: `sr_only` (default, visually hidden), `visible` (raw, v0.5.x behavior), `noscript``cache.ttl``3600`Seconds; `0` disables`timeout``3`HTTP timeout in secondsHow it works
------------

[](#how-it-works)

1. Middleware runs after your response is built.
2. For each HTML `GET` 200, it calls `POST /api/v1/public/aeo` with the request path.
3. If smking has ready content, structured data + SEO meta go into ``; FAQ + summary go before ``.
4. **Always override** (v0.3.0+): every enabled SEO tag (``, `og:*`, `canonical`, `meta description`) gets written by smking. Any matching host markup is stripped first (attribute-order-insensitive) so the document only ever has one of each. To keep a tag under your control, set `config('smking.inject.{tag}', false)` or render it yourself.
5. Unknown paths are registered for background crawling — next request will serve content.
6. Responses are cached per path in Laravel's cache. Pending/error states fail open.
7. **Agent content negotiation** (v0.4.0+): when `Accept: text/markdown` is preferred over `text/html` (q-value-aware), the middleware fetches `/api/v1/public/md` and replaces the body with markdown. `Vary: Accept` is added so caches stay consistent. First-time misses fall through to HTML and trigger the same background crawl.
8. **Agent discovery** (v0.5.0+): every HTML response advertises the markdown alternate via `Link: ; rel="alternate"; type="text/markdown"` (RFC 8288). Appended to any existing Link headers; idempotent if you already wired your own.
9. **Visually-hidden body fragments by default** (v0.6.0+): auto-injected `summaryHtml` / `faqHtml` are wrapped in an inline-style sr-only `` so they don't pollute SPA layouts where `` injection lands outside `#app`. Microdata stays in the DOM (Googlebot reads it); JSON-LD in `` is the primary AEO signal. Switch with `SMKING_INJECT_VISIBILITY=visible` if you want the v0.5.x behavior. The `` Blade component is unaffected — explicit placement is always rendered as you wrote it.

Gradual rollout / A/B comparison
--------------------------------

[](#gradual-rollout--ab-comparison)

`config('smking.only')` is a strict whitelist — when non-empty, the middleware only runs on paths that match. Use it to roll out smking gradually, or to A/B-compare smking-enabled paths against untouched ones.

### Soft launch one URL

[](#soft-launch-one-url)

```
// config/smking.php
'only' => ['products/widget'],
```

Now only `https://your-site.com/products/widget` gets smking-injected meta + JSON-LD. Every other page is untouched. Measure impact for a week before expanding.

### Expand to one section

[](#expand-to-one-section)

```
'only' => ['products/*'],
```

All product pages enabled, rest of site untouched. Continue measuring against control pages (homepage, blog, etc.).

### A/B comparison

[](#ab-comparison)

```
'only' => [
    'products/widget',   // Variant A — smking enabled
    'products/gizmo',    // Variant B — smking enabled
    // 'products/sprocket' — Control: NOT in `only`, no smking
],
```

Compare AEO score / search ranking / AI-citation share across the three pages over your measurement window.

### Full rollout

[](#full-rollout)

```
'only' => [],   // empty == every HTML page (default behavior)
```

`only` patterns use Laravel's `Request::is()` syntax, identical to `except`. Combine both — `only` is checked first (must match), then `except` (must not match) — so you can whitelist `products/*` and blacklist `products/draft-*` simultaneously.

Outage Runbook
--------------

[](#outage-runbook)

When the smking SaaS is down or unreachable, the SDK fails open — your pages still render normally, just without smking-injected content. Three knobs you may want to know about:

### 0. Layered protection — single-flight + circuit breaker (v0.7.0+)

[](#0-layered-protection--single-flight--circuit-breaker-v070)

Two complementary defenses run on every cache miss:

**Single-flight cache lock** — When a path is uncached and traffic spikes, only ONE PHP-FPM worker calls smking upstream; others fail open immediately (return un-injected page). Per-path protection. Uses `Cache::lock()` (redis / memcached / database / array drivers; graceful fallback for stores without lock support).

**Per-surface circuit breaker** — Once any path hits a 5xx / transport error, a flag is set for `circuit_breaker_ttl` seconds (default 60). While the flag is present, every path on THAT surface short-circuits without touching the upstream. Protects against high-cardinality outage events (catalog spray, full-site crawler) where per-path cache wouldn't help — the second URL in the burst doesn't know the first one just failed. Auto half-open: when the flag expires the next request hits upstream; success closes the breaker, another failure trips it again. Disable with `SMKING_CIRCUIT_BREAKER=false` if your customer cache layer can't store namespace flags reliably.

Two independent breakers exist (since v0.7.0 round-4):

- HTML AEO injection (`/api/v1/public/aeo`, every page render) — `smking:circuit:aeo:{ns}`
- Markdown for agents (`/api/v1/public/md`, agent-only `Accept: text/markdown` clients) — `smking:circuit:md:{ns}`

A markdown outage no longer suppresses HTML injection: the agent surface is optional, and an issue isolated there should never affect the customer-facing render path. Both surfaces still rotate together when `(api_key, base_url)` changes.

### 1. Cache absorbs most outages automatically (v0.7.0+, refined in v0.10.0)

[](#1-cache-absorbs-most-outages-automatically-v070-refined-in-v0100)

Four-tier cache TTL with adaptive backoff on errors:

StatusTTLBehavior`ready`1 hourCustomer's cached AEO content keeps serving`not_found` (4xx)60 secBackend audit catching up; first-launch products visible within ~1 min after crawl/generate completes`pending` (202)15 secSaaS explicit "in-progress" signal — short cushion against hot-launch polling`server_error` (5xx, DNS, TCP, timeout)**30s → 5min → 30min → 24hr**Adaptive backoff per consecutive failure (v0.10.0+)The `server_error` ladder is keyed by consecutive-failure count for each cache key independently. A first failure (typical of an install-time typo or transient network blip) caches for 30 seconds — auto-recovers without operator intervention once the underlying issue is fixed. Steady-state outage protection kicks in by failure #4 at the full 24hr fallback, preserving the FPM-pool-saturation defense that made the flat 24hr TTL necessary in v0.7.0.

A successful `ready` response resets the counter, so the next outage starts at 30s again. Customize the ladder via `cache.server_error_backoff` in `config/smking.php`, or set to `[]` to disable backoff and restore the pre-v0.10.0 flat 24hr behavior.

### 2. Tighten timeouts further if you're at scale

[](#2-tighten-timeouts-further-if-youre-at-scale)

Default since v0.7.0: `connect_timeout=1s`, `timeout=1.5s`. For million-PV sites where every millisecond counts:

```
SMKING_CONNECT_TIMEOUT=0.5
SMKING_HTTP_TIMEOUT=1
```

**Conversely, low-traffic / stage / newly-launched sites have the opposite problem** (v0.19.1+): they don't keep the upstream serverless function warm, so the first AEO hit eats a 3-5s cold start that blows the 1.5s read timeout → `server_error` stuck in the backoff cache (and the page never gets registered for crawling). AEO discovery now retries **once** on a first-attempt timeout using a generous cold-start timeout — the first attempt wakes the function, the retry lands warm. Warm sites never retry, so there's zero steady-state impact.

```
SMKING_COLD_START_RETRY=true          # default; set false for single-shot discovery
SMKING_COLD_START_TIMEOUT=8           # read timeout (s) for the retry attempt
SMKING_COLD_START_CONNECT_TIMEOUT=3   # connect timeout (s) for the retry attempt
```

The retry only fires on a thrown request (DNS / TCP / timeout) — a transport success carrying a 5xx isn't retried (the SaaS is reporting its own breakage). Applies to the AEO HTML surface; the markdown-for-agents surface keeps single-shot semantics.

### 3. Kill switch when SaaS is in trouble

[](#3-kill-switch-when-saas-is-in-trouble)

Set in `.env` and clear config cache:

```
SMKING_AUTO_INJECT=false
```

Middleware still emits `X-Smking-Status` headers (so `curl -I` install verification works) but doesn't try to fetch any content. Reverts to original page entirely.

### 4. Recovering after SaaS comes back

[](#4-recovering-after-saas-comes-back)

Per-path:

```
php artisan smking:cache:purge /products/widget
```

This forgets both `smking:aeo:*` and `smking:md:*` cache for that path AND clears the per-surface circuit breakers so the next request actually re-fetches (no waiting for breaker TTL). Use this whenever you've fixed something upstream and want immediate recovery on a specific path.

Per WC product:

```
php artisan smking:cache:purge --product-id=42
```

Clears the AEO-surface entry for that product plus the AEO-surface circuit breaker.

For wholesale recovery (clears the whole app cache):

```
php artisan cache:clear
```

### 5. Inspecting circuit-breaker state (v0.7.1+)

[](#5-inspecting-circuit-breaker-state-v071)

When AEO content stops appearing in production, the breaker may be silently short-circuiting upstream calls. Check it without touching the cache:

```
php artisan smking:circuit:status
```

Sample output (any surface open → exit code 1 for scripted health checks):

```
smking circuit breaker status:

  aeo (HTML AEO injection): closed
       key: smking:circuit:aeo:abc123
  md  (markdown for agents): OPEN — re-check after configured TTL (60s)
       key: smking:circuit:md:abc123

Recovery: wait for TTL, or `php artisan smking:cache:purge ` / `--product-id=N` to force-clear.

```

The breaker also logs trip + close events through the configured `LoggerInterface`:

```
[warning] smking: circuit breaker tripped for aeo surface
  context: {"surface":"aeo","ttl_seconds":60,"key":"smking:circuit:aeo:..."}

[info] smking: circuit closed for aeo surface
  context: {"surface":"aeo"}

```

The trip log is rate-limited to one line per outage window (a million-request burst produces one log, not a million). The close log fires once on the first successful upstream call after recovery — implemented via an atomic tombstone pull, so concurrent recovery requests log at most once.

Wire your usual log → metric path (Datadog Logs / Sentry / etc.) to alert on either string when you want a paging trigger instead of a polling status check.

Upgrading
---------

[](#upgrading)

This package is in `v0.x`. Per Composer's caret convention for pre-1.0 packages, **every minor bump (0.5 → 0.6, 0.6 → 0.7) is treated as breaking** — the constraint `"smking/laravel": "^0.6"` resolves to `>=0.6.0
