Server-Side SDKs (Node, Python, PHP)
The client <script> applies SEOJuice’s optimizations in the browser. That’s fine for most sites — Google renders JavaScript. Reach for server-side injection when a strict Content-Security-Policy or an ad-blocker is dropping the client script, or when you want the optimizations in the raw HTML so every crawler sees them without running any JS.
The Node, Python, and PHP SDKs all do the same job: fetch your page’s suggestions and rewrite the HTML on the server, before it leaves your app. Every one of them fails open. A fetch error, a timeout, a malformed response — you get your original HTML back, untouched. Injection failures return your original HTML; they never surface as a 5xx.
What it injects
The same transforms across every SDK, matching the SEOJuice edge Worker:
<head>tags — title, meta description, meta keywords, and Open Graph tags. Added only when the page doesn’t already have them; your own tags win.- JSON-LD structured data — a valid
application/ld+jsonblock. - Internal links — real
<a>elements in the body copy, with your configured link class, first occurrence only, never inside an existing link or a heading. CJK sentence punctuation is handled. - Image alt-text, content refreshes, h1 (rewrites an existing
<h1>), and broken-link fixes. - A
<!-- seojuice: … -->manifest comment and thewindow.seojuiceSSRflag.
To confirm it ran, look for window.seojuiceSSR === true in the page — or the <!-- seojuice: … --> comment in View Source.
Node
npm install seojuice
On Next.js it’s one file — see the Next.js integration for the createSeoMiddleware drop-in.
On Express, or anything else, fetch then inject in your response path:
import { fetchSuggestions, injectSEO } from "seojuice/injection";
app.use(async (req, res, next) => { const html = await renderYourPage(req); // your existing rendered HTML
let suggestions = null; try { suggestions = await fetchSuggestions(`https://yoursite.com${req.originalUrl}`); } catch { // fetch failed or timed out — fall through and serve the original HTML }
res.type("html").send(injectSEO({ html, suggestions }));});injectSEO returns a string and never throws — but fetchSuggestions does, on a non-2xx response or a timeout, so wrap it in try/catch as above. An empty payload injects nothing but the SSR flag: injectSEO still stamps window.seojuiceSSR and the manifest comment into the HTML, it just adds no tags, links, or rewrites on top.
Python
pip install seojuice
Django — add one middleware, high enough in the list that it sees the finished response:
MIDDLEWARE = [ "seojuice.injection.django.SEOJuiceDjangoMiddleware", # ... your other middleware]
# optionalSEOJUICE_INJECTION_ENABLED = True # default TrueSEOJUICE_INJECTION_TIMEOUT = 5.0 # seconds, default 5.0It only touches text/html responses, keys off request.build_absolute_uri(), and returns the response unchanged when there are no suggestions.
FastAPI / Starlette — same idea, ASGI-style:
from fastapi import FastAPIfrom seojuice.injection.asgi import SEOJuiceASGIMiddleware
app = FastAPI()app.add_middleware(SEOJuiceASGIMiddleware, timeout=3.0)It buffers text/html responses, injects, and fixes up Content-Length. Everything else streams through untouched. As with Django, add it early enough in the middleware stack that it sees the fully-rendered response — middleware added later in ASGI apps wraps closer to the final response, so ordering matters the same way.
PHP
composer require seojuice/seojuice
Fetch with the Smart Client, inject into your rendered HTML:
use SEOJuice\SEOJuice;use SEOJuice\Injection\SeoInjector;
$client = new SEOJuice('your-api-key');
$data = $client->smart()->suggestions('https://yoursite.com/blog/post'); // array$html = (new SeoInjector())->inject($renderedHtml, $data);
echo $html;inject() fails open — empty or invalid $data returns the original HTML.
Laravel — put it in middleware so every HTML response gets it, registered late enough in the pipeline (app/Http/Kernel.php) that it runs after the response body is fully rendered:
public function handle($request, Closure $next){ $response = $next($request);
$contentType = $response->headers->get('Content-Type', ''); if (str_contains($contentType, 'text/html')) { $data = app(SEOJuice::class)->smart()->suggestions($request->url()); $response->setContent( (new SeoInjector())->inject($response->getContent(), $data) ); }
return $response;}Register SEOJuice as a singleton in a service provider so the client is reused across requests:
$this->app->singleton(SEOJuice::class, fn () => new SEOJuice( config('services.seojuice.api_key'),));This singleton binding supersedes the inline new SEOJuice('your-api-key') constructor shown above — use app(SEOJuice::class) (as the middleware does) instead of constructing the client directly once it’s registered.
Content-Security-Policy
Server-side injection needs no CSP changes — it never runs in the browser. If you keep the client snippet alongside it, allowlist SEOJuice so the browser fetch isn’t blocked:
script-src https://cdn.seojuice.ioconnect-src https://smart.seojuice.io https://cdn.seojuice.io
Reacting to changes
Fetching suggestions on every request works, but if you’d rather react to events — a change getting approved, a report finishing — instead of polling, subscribe an endpoint to Webhooks. It covers the event catalog and how to verify delivery signatures.