seoslug
Deterministic SEO payload generation for content platforms.
Quick start
from seoslug import SEOConfig, URLPolicy, SEOEntity, build_seo_payload
config = SEOConfig(
canonical_host="blog.example.com",
public_base_url="https://blog.example.com",
url_policy=URLPolicy(
enforce_https=True,
lowercase_paths=True,
trailing_slash="never",
),
)
entity = SEOEntity(
entity_type="post",
title="Introducing v2",
excerpt="A fully deterministic SEO payload library.",
)
payload = build_seo_payload(entity, "/blog/introducing-v2", config)
That is it. payload is a SEOPayload dataclass with title, description, canonical,
robots, og, twitter, and schema_jsonld, ready to inject into your HTML.
Render the full <head> in one call:
payload.render_html()
# '<title>Introducing v2</title>\n<meta name="description" ...'
Or access individual fields:
payload.title # "Introducing v2"
payload.canonical # "https://blog.example.com/blog/introducing-v2"
payload.og.image # None (set default_og_image in config)
payload.to_dict() # plain dict for JSON serialization
{
"title": "Introducing v2",
"canonical": "https://blog.example.com/blog/introducing-v2",
"robots": "index,follow",
"og": {
"type": "article",
"title": "Introducing v2",
"url": "https://blog.example.com/blog/introducing-v2"
},
"twitter": {
"card": "summary_large_image",
"title": "Introducing v2"
},
"schema_jsonld": {
"@context": "https://schema.org",
"@type": "Article",
"name": "Introducing v2",
"url": "https://blog.example.com/blog/introducing-v2"
}
}
Render the same payload twice: identical HTML, identical hash. That is the foundation of everything that follows.
Benchmarks
10,000 payloads in 276 ms (28 µs/payload)
Every payload includes: canonical URL, Open Graph (10+ fields), Twitter Cards (6+ fields), robots directive, and JSON-LD structured data. That is 10,000 complete SEO metadata objects in under 300 ms.
Comparison: manual vs seoslug
| Task | Manual | seoslug |
|---|---|---|
| Canonical URL | Construct by hand | build_seo_payload(entity, path, config) |
| Open Graph tags | 10+ <meta> tags |
payload.og or payload.render_html() |
| Twitter Cards | 6+ <meta> tags |
payload.twitter or payload.render_html() |
| JSON-LD schema | Write and maintain schema.org JSON | Auto-generated |
| BreadcrumbList | Manual BreadcrumbList JSON-LD |
Breadcrumb(name, url) auto-generates |
| URL normalization | HTTPS, trailing slash, lowercase logic | URLPolicy handles it |
| HTML excerpt | Strip tags, decode entities, truncate | Built-in html_to_text() |
| Validation | Manual audit of title/description length | emit_warnings=True in config |
| HTML rendering | Jinja2 template for every tag | payload.render_html() |
| Content-based ETag | Manual hash | payload.etag() |
| Testing | Manual snapshot fixtures | Deterministic: assert payload == dict |
Why determinism matters
p1 = build_seo_payload(entity, path, config)
p2 = build_seo_payload(entity, path, config)
assert p1 == p2 # Always True
Most SEO tools produce different output for identical input: random cache busters, timestamps, dictionary key order instability. seoslug does none of that. Every call is a pure function with zero side effects.
- Commit SEO output to Git and validate it in CI. If SEO changes, your build fails.
- Cache forever. Identical inputs always produce identical metadata. No invalidation logic needed.
- Diff staging vs production to instantly reveal configuration drift.
- Track SEO evolution right alongside your code.
SEO becomes another deterministic build artifact: testable, cacheable, and CI-verifiable.
Features
| Category | What seoslug handles |
|---|---|
| URL normalization | HTTPS enforcement, trailing slash policy, lowercase paths, duplicate-slash collapse, tracking-parameter stripping (detrack or built-in regex) |
| Open Graph | og:title, og:description, og:image (+ width/height/alt), og:type, og:url, og:site_name, og:locale/locale:alternate, og:audio, og:video |
| Twitter Cards | twitter:card, twitter:title, twitter:description, twitter:image (+ alt), twitter:site, twitter:creator |
| JSON-LD schemas | Article, WebPage, VideoObject, CollectionPage, SearchResultsPage, Product, Organization, LocalBusiness, FAQPage, BreadcrumbList |
| Schema registry | Register custom schema generators for any schema.org type |
| Robots directives | index/noindex, follow/nofollow, max-snippet, max-image-preview - via string or structured Robots dataclass |
| Breadcrumbs | Breadcrumb(name, url) auto-generates BreadcrumbList JSON-LD |
| Validation warnings | Title length, description length, canonical URL scheme, OG image URL, robots format |
| Dependency model | Default: seoslug[fast] (lxml + detrack). Minimal: seoslug[light] (pure Python). |
Framework support
| Framework | Integration |
|---|---|
| FastAPI / Starlette / Litestar | Call build_seo_payload in your route handler. Or use build_seo_payload_async to offload to a thread pool. |
| Django / Flask / Quart | Call in your view function. Pass the dataclass to your template context. |
| Static site generators (Pelican, MkDocs, Astro) | Run at build time. Commit the JSON output to Git. |
| Custom CMS | Use build_seo_payload_dict for a plain dict if you prefer. |
Installation
pip install "seoslug[fast]"
This is the recommended install. It includes lxml (C-optimized HTML extraction) and detrack (tracking-parameter stripping).
For a minimal footprint with pure-Python fallbacks:
pip install "seoslug[light]"
Identical output. Only performance differs for very large HTML bodies.
At a glance
# Structured OG images
entity = SEOEntity(
entity_type="post",
featured_image=OGImage(url="https://ex.com/img.jpg", width=1200, height=630, alt="Hero"),
)
# Structured robots
config = SEOConfig(..., default_robots=Robots(index=True, follow=False, max_snippet=-1))
# Breadcrumbs
entity = SEOEntity(
entity_type="page",
breadcrumbs=[Breadcrumb(name="Home", url="/"), Breadcrumb(name="Blog", url="/blog")],
)
# Schema registry
registry = SchemaRegistry()
registry.register("Podcast", lambda e, c, can, t, d, og: {"@type": "Podcast", "name": t})
# Factory shortcuts
entity = from_blog_post(title="Hello", body_html="<p>World</p>", author="Jane")
# Fluent builder
entity = SEOEntityBuilder().entity_type("product").title("Widget").sku("W-001").build()
# Async support
payload = await build_seo_payload_async(entity, "/post", config)
# Dict wrapper
d = build_seo_payload_dict(entity, "/post", config)
# Render HTML
head_html = payload.render_html()
# Content hash / ETag
fingerprint = payload.hash()
etag_value = payload.etag()
Documentation
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file seoslug-2.0.1.tar.gz.
File metadata
- Download URL: seoslug-2.0.1.tar.gz
- Upload date:
- Size: 39.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ea0d1dbbcb302c08e2a1981d43bbbfa26fa8e2cde8a20e95e06c79ddc0a5d45a
|
|
| MD5 |
5d6322a87f185b18ac18a0a1eaf66f06
|
|
| BLAKE2b-256 |
1c1f1c66bf1f6d1d1761f925d849e6a39ebb34c3bfb230a56b5edec0cc312831
|
Provenance
The following attestation bundles were made for seoslug-2.0.1.tar.gz:
Publisher:
publishing.yml on emiliano-go/seoslug
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
seoslug-2.0.1.tar.gz -
Subject digest:
ea0d1dbbcb302c08e2a1981d43bbbfa26fa8e2cde8a20e95e06c79ddc0a5d45a - Sigstore transparency entry: 2052043357
- Sigstore integration time:
-
Permalink:
emiliano-go/seoslug@9741cbf97e90a8408f19bf4bbbf54b75cf26a9e8 -
Branch / Tag:
refs/tags/v2.0.1 - Owner: https://github.com/emiliano-go
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publishing.yml@9741cbf97e90a8408f19bf4bbbf54b75cf26a9e8 -
Trigger Event:
release
-
Statement type:
File details
Details for the file seoslug-2.0.1-py3-none-any.whl.
File metadata
- Download URL: seoslug-2.0.1-py3-none-any.whl
- Upload date:
- Size: 26.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7de6100216e4f21c589fe927603bd050d761dae19a96c18155b8dd80a2412742
|
|
| MD5 |
20ea59ec8d523e5c52b26cc590373256
|
|
| BLAKE2b-256 |
5709eead94b67234c286f849acac1bd01c9efa9708d41d58a490243b5cc4ccc9
|
Provenance
The following attestation bundles were made for seoslug-2.0.1-py3-none-any.whl:
Publisher:
publishing.yml on emiliano-go/seoslug
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
seoslug-2.0.1-py3-none-any.whl -
Subject digest:
7de6100216e4f21c589fe927603bd050d761dae19a96c18155b8dd80a2412742 - Sigstore transparency entry: 2052043864
- Sigstore integration time:
-
Permalink:
emiliano-go/seoslug@9741cbf97e90a8408f19bf4bbbf54b75cf26a9e8 -
Branch / Tag:
refs/tags/v2.0.1 - Owner: https://github.com/emiliano-go
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publishing.yml@9741cbf97e90a8408f19bf4bbbf54b75cf26a9e8 -
Trigger Event:
release
-
Statement type: