Skip to content

FastAPI integration

In FastAPI, you can use seoslug inside your route handlers. Create a dependency that loads your SEO config and entity. Call build_seo_payload before rendering your template.

Basic example

from fastapi import FastAPI, Depends
from fastapi.responses import HTMLResponse
from seoslug import SEOConfig, URLPolicy, SEOEntity, build_seo_payload

app = FastAPI()

def get_seo_config():
    return SEOConfig(
        canonical_host="blog.example.com",
        public_base_url="https://blog.example.com",
        url_policy=URLPolicy(),
    )

@app.get("/posts/{slug}", response_class=HTMLResponse)
async def get_post(slug: str, config: SEOConfig = Depends(get_seo_config)):
    entity = SEOEntity(
        entity_type="post",
        title="My Post",
        excerpt="A short description",
        status="published",
    )
    payload = build_seo_payload(entity, f"/posts/{slug}", config)
    # Render template with payload
    return HTMLResponse(f"""
    <head>
        <title>{payload['title']}</title>
        <meta name="description" content="{payload['description']}">
        <link rel="canonical" href="{payload['canonical']}">
    </head>
    """)

Caching with ETags

Use seoslug's deterministic output for ETag based caching. Compare the hash of the payload against the If-None-Match header.

import hashlib
from fastapi import Request, Response

@app.get("/posts/{slug}")
async def get_post_cached(request: Request, slug: str, config=Depends(get_seo_config)):
    entity = SEOEntity(entity_type="post", title="My Post")
    payload = build_seo_payload(entity, f"/posts/{slug}", config)
    etag = hashlib.sha256(str(payload).encode()).hexdigest()[:16]

    if request.headers.get("if-none-match") == etag:
        return Response(status_code=304)

    return Response(
        content=render_template(payload),
        headers={"ETag": etag},
    )

Jinja templates

Use Jinja2Templates for full HTML rendering.

from fastapi.templating import Jinja2Templates

templates = Jinja2Templates(directory="templates")

@app.get("/posts/{slug}")
async def get_post(request: Request, slug: str, config=Depends(get_seo_config)):
    entity = SEOEntity(entity_type="post", title="My Post")
    payload = build_seo_payload(entity, f"/posts/{slug}", config)
    return templates.TemplateResponse("post.html", {
        "request": request,
        "payload": payload,
    })