Skip to main content

easeo

Deterministic SEO metadata generation for content platforms.

PyPI npm Python Rust core License

Install

# Python
pip install easeo

# Node.js
npm install @easeo/core

Quick start

Python

from easeo import SEOConfig, SEOEntity, build_seo_payload

config = SEOConfig(
    canonical_host="example.com",
    public_base_url="https://example.com",
)

entity = SEOEntity(
    entity_type="post",
    title="Hello World",
    excerpt="An example post.",
)

payload = build_seo_payload(entity, "/blog/hello", config)
print(payload.render_html())

JavaScript / TypeScript

import { buildSeoPayload } from "@easeo/core";

const payload = buildSeoPayload(
  { entityType: "post", title: "Hello World", description: "An example post." },
  "/blog/hello",
  { canonicalHost: "example.com", publicBaseUrl: "https://example.com" }
);

console.log(payload.renderHtml());

Rust

use easeo_core::{SEOConfig, SEOEntity, EntityType, build_seo_payload};

let config = SEOConfig {
    canonical_host: "example.com".into(),
    public_base_url: "https://example.com".into(),
    ..Default::default()
};

let entity = SEOEntity {
    entity_type: EntityType::Post,
    title: Some("Hello World".into()),
    excerpt: Some("An example post.".into()),
    ..Default::default()
};

let payload = build_seo_payload(&entity, "/blog/hello", &config).unwrap();
assert_eq!(payload.canonical, "https://example.com/blog/hello");

Why easeo

Other SEO libraries do too much (scoring, keyword analysis, content rewriting), while easeo does one thing and does it well: generate deterministic SEO metadata from content entities.

Given the same inputs, easeo always produces the same output. No timestamps, no randomness, no environment reads, no hidden I/O. This makes SEO metadata something you can:

  • snapshot test: assert against expected payloads
  • hash: generate stable ETags
  • cache: safely memoize results
  • diff: detect changes across builds
  • validate in CI: commit your SEO intent as code
Content Entity
      |
      v
   easeo
      |
      +-- canonical URL
      +-- title & description
      +-- robots directives
      +-- Open Graph
      +-- Twitter Cards
      +-- JSON-LD
      |
      v
 Deterministic Payload

Architecture

  • Rust core (easeo-core): all logic, no I/O dependencies
  • Python bindings (easeo-python): via PyO3
  • Node bindings (easeo-node): via napi-rs
  • Framework integrations: Next.js, Astro, Vite, Nuxt, SvelteKit, React, FastAPI, Django, Flask, Zensical

The Rust core guarantees identical behavior across languages. Same entity, same route, same config, same payload everywhere.

Features

  • 11 entity types: home, post, page, video, taxonomy, search, product, organization, local_business, faq, other
  • URL normalization: HTTPS enforcement, lowercase paths, trailing slash control, duplicate slash collapse, tracking parameter removal
  • JSON-LD schemas: WebSite, WebPage, Article, Product, Organization, LocalBusiness, FAQPage, BreadcrumbList, plus custom schemas via registry
  • Open Graph: full og:type, title, description, image, locale, audio, video
  • Twitter Cards: summary_large_image by default, with image alt, site, creator
  • Robots directives: index/noindex, follow/nofollow, max-snippet, max-image-preview, max-video-preview
  • SEO contracts: machine-readable SEO intent for CI validation
  • Validation: built-in checks for title length, description, canonical format, OG image, robots directives
  • Hashing: SHA-256 payload hashing and ETag generation
  • HTML rendering: complete <head> snippet with meta tags, OG, Twitter, JSON-LD
  • Extension points: config-scoped hooks and schema registries

Framework integrations

Framework Package API
Next.js @easeo/next easeoMetadata() returns a Next.js Metadata object
Astro @easeo/astro Build-time integration with contract emission
Vite @easeo/vite transformIndexHtml hook
Nuxt @easeo/nuxt useEaseoSeo() composable
SvelteKit @easeo/sveltekit buildEaseoPayload() and <EaseoHead />
React @easeo/react <EaseoHead /> component

Python framework adapters

Framework Import
FastAPI from easeo.adapters.fastapi import EaseoSEO
Django from easeo.adapters.django import seo_head
Flask from easeo.adapters.flask import Easeo
Zensical from easeo.contrib.zensical import EaseoExtension

API

build_seo_payload(entity, route_path, config, overrides=None)

The core function. Takes an entity, route, and config. Returns a deterministic SEOPayload.

Parameter Type Description
entity SEOEntity Content entity with type, title, excerpt, images, etc.
route_path str Route path, e.g. "/blog/hello"
config SEOConfig Site-wide configuration (host, base URL, defaults)
overrides SEOOverrides | None Per-call overrides (highest precedence)

Returns: SEOPayload, a structured payload with all SEO fields.


SEOConfig

Site-wide configuration.

Field Type Description
canonical_host str Canonical hostname, e.g. "example.com" (host only, no scheme)
public_base_url str Full base URL for path resolution, e.g. "https://example.com"
url_policy URLPolicy | None URL normalization policy (HTTPS, lowercase, trailing slash, tracking params)
default_robots Robots | None Default robots directives for regular pages
search_robots Robots | None Robots directives for search-type pages (default: noindex,follow)
default_og_image SEOImage | None Fallback OG image
site_name str | None Site name (og:site_name, title template)
title_template str | None Template for titles, e.g. "{title} - My Site"
publisher_name str | None Organization/publisher name for schemas
publisher_logo str | None Publisher logo URL for schemas
locale str | None og:locale, e.g. "en_US"
locale_alternate list[str] | None Alternate locales
twitter_site str | None Twitter @handle for twitter:site
auto_generate_schema bool Auto-generate JSON-LD from entity type (default: True)
emit_warnings bool Collect validation warnings (default: False)
schema_type_map dict | None Override entity-type to schema.org type mapping
search_url_template str | None Search URL for the homepage WebSite SearchAction
hooks HookRegistry | None Config-scoped post-processing
schema_registry SchemaRegistry | None Config-scoped JSON-LD generators

SEOEntity

Content entity representation.

Field Type Description
entity_type str One of: home, post, page, video, taxonomy, search, product, organization, local_business, faq, other
title str | None Page title
excerpt str | None Short description
slug str | None Entity slug
body_html str | None Full content (used to derive a description snippet when no excerpt)
status str | None Publication status (non-published becomes noindex)
featured_image SEOImage | None Primary image
published_at str | None ISO date/datetime
updated_at str | None ISO date/datetime
author_name str | None Author display name
breadcrumbs list[Breadcrumb] | None Breadcrumb trail (appended as BreadcrumbList JSON-LD)
faq_items list[FAQItem] | None FAQ entries (FAQPage schema)
sku str | None Product SKU
price str | None Product price
price_currency str | None ISO currency code, e.g. "USD"
availability str | None Product availability
same_as list[str] | None sameAs URLs (Organization schema)
address str | None Address (LocalBusiness schema)

SEOPayload

The output. Structured, hashable, renderable.

Field Type Description
title str Resolved title
description str Resolved description
canonical str Normalized canonical URL
robots str Resolved robots directives, e.g. "index,follow"
og OGPayload Open Graph metadata
twitter TwitterPayload Twitter Card metadata
schema_jsonld dict | None JSON-LD structured data

Methods:

Method Description
render_html() Full <head> HTML snippet
render_opengraph() OG meta tags only
render_twitter() Twitter meta tags only
render_jsonld() JSON-LD <script> tag only (with < escaped to be script-safe)
to_dict() Payload as a canonical snake_case dictionary
to_json() Payload as a canonical JSON string
hash() SHA-256 hash of the payload
etag() HTTP ETag string (quoted)

Payloads are dict-compatible and comparable:

payload["title"]
payload.get("title")
"title" in payload
payload == other_payload
payload == payload.to_dict()

Custom JSON-LD schemas

Replace the schema for one page with overrides:

from easeo import SEOOverrides, build_seo_payload

payload = build_seo_payload(
    entity,
    "/podcast/ep-1",
    config,
    SEOOverrides(schema_jsonld={
        "@context": "https://schema.org",
        "@type": "Podcast",
        "name": "My Podcast",
    }),
)

Register a generator for a whole schema type on the config:

from easeo import SchemaRegistry

registry = SchemaRegistry()

@registry.register("Article")
def podcast_episode(entity, config, canonical, title, description, og_image):
    return {"@context": "https://schema.org", "@type": "Podcast", "name": title}

config = SEOConfig(..., schema_registry=registry)

Extension points

Hooks post-process the payload. They are config-scoped, so the builder stays a pure function of its inputs:

from easeo import HookRegistry

hooks = HookRegistry()

@hooks.hook("post_process")
def add_generator(payload, entity, config):
    payload["generator"] = "easeo"
    return payload

config = SEOConfig(..., hooks=hooks)

The JavaScript equivalents are HookRegistry and SchemaRegistry on @easeo/core, attached through hooks and schemaRegistry on the config.


SEOEntityBuilder

Fluent builder for complex entities.

from easeo import SEOEntityBuilder

entity = (
    SEOEntityBuilder("post")
    .title("Hello World")
    .excerpt("An example post.")
    .featured_image("https://example.com/hero.jpg", width=1200, height=630)
    .breadcrumb("Home", "/")
    .breadcrumb("Blog", "/blog")
    .build()
)

Factories

Convenience constructors for common content types.

from easeo import from_blog_post, from_product, from_faq

entity = from_blog_post(title="Hello", body_html="<p>Body</p>", author="Jane")
product = from_product(name="Widget", sku="W-1", price=9.99)
faq = from_faq(questions=[{"question": "Q?", "answer": "A."}])

URLPolicy

Controls canonical URL normalization.

from easeo import URLPolicy

policy = URLPolicy(
    enforce_https=True,
    lowercase_paths=True,
    trailing_slash="never",
    collapse_duplicate_slashes=True,
    strip_tracking_params=True,
    allowed_query_params=["page", "q"],
)

build_seo_payload_dict(entity, route_path, config, overrides=None)

Returns the payload as a plain dictionary. Useful for JSON serialization.

data = build_seo_payload_dict(entity, "/blog/hello", config)

build_seo_payload_async(entity, route_path, config, overrides=None)

Async wrapper that offloads the build to a thread pool. The Rust core releases the GIL, so this genuinely runs off the event loop.

from easeo import build_seo_payload_async

payload = await build_seo_payload_async(entity, "/blog/hello", config)

Fallback resolution

easeo resolves SEO fields through an explicit precedence chain:

SEOOverrides        highest priority
      |
SEOEntity
      |
SEOConfig
      |
hardcoded defaults  lowest priority

For example:

config.default_og_image
      |
entity.featured_image
      |
overrides.og_image

This avoids both "everything is global" and "everything is magically inferred".

Errors

All errors inherit from EaseoError, which inherits from ValueError in Python and Error in JavaScript. Python code that catches ValueError keeps working.

Type Raised when
InvalidUrlError A URL is malformed or a URL policy is invalid
ConfigurationError A config value fails validation
EntityError An entity or overrides value fails validation
SchemaError JSON-LD construction fails
ContractError Contract generation fails

Documentation

Full documentation lives in docs/ and builds with Zensical. The docs site uses the easeo Zensical extension, so easeo must be importable by the interpreter that runs Zensical. uv handles this in one command:

uv run zensical serve     # live preview on http://localhost:8000
uv run zensical build     # writes site/

Or with a manual environment:

pip install -e ".[zensical]"
zensical serve

Topics include a tutorial, concepts, guides, an API reference, per-framework integrations, and recipes.

To reproduce the exact artifact that CI and Cloudflare Pages deploy (easeo installed editable, llms-full.txt regenerated, then the build):

bash scripts/build_docs.sh     # output in site/

Cloudflare Pages settings: build command bash scripts/build_docs.sh, output directory site, environment variable PYTHON_VERSION=3.12 (a Rust toolchain is required). See docs/about/deploying-docs.md for details.

Development

Prerequisites

  • Rust (stable)
  • Python 3.10+
  • Node.js 18+

Build

# Rust core
cargo build --release

# Python bindings
pip install maturin
maturin develop -m crates/easeo-python/Cargo.toml

# Node bindings
cd packages/core
napi build --platform --release --manifest-path ../../crates/easeo-node/Cargo.toml
cp ../../crates/easeo-node/*.node .

Test

# Rust tests
cargo test --workspace

# Python tests
pytest tests/python/

# JavaScript tests
node --test tests/javascript/*.cjs

# Cross-language conformance
python tests/conformance/test_conformance.py

License

MIT

Metadata

Release files for easeo 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for easeo 0.1.0
File
easeo-0.1.0-cp310-abi3-win_amd64.whl CPython 3.10 abi3 Windows x86-64 Details
easeo-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.10 abi3 Linux glibc 2.17+ x86-64 Details
easeo-0.1.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.10 abi3 Linux glibc 2.17+ ARM64 Details
easeo-0.1.0-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details
easeo-0.1.0-cp310-abi3-macosx_10_12_x86_64.whl CPython 3.10 abi3 macOS 10.12+ x86-64 Details

Total release size: 3.5 MB

Release files / easeo-0.1.0-cp310-abi3-win_amd64.whl

Download URL easeo-0.1.0-cp310-abi3-win_amd64.whl
Size 599.1 kB
Tags CPython 3.10 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
496d088de70cce5edd03d95e581506e9663f957ed75ed21382d9549715e45f5a
BLAKE2b-256 checksum
How to use checksums
bc17e28ae12f1c938be2d969f8a10a5ff83a3874319fc51da67600c078584c62
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release files / easeo-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL easeo-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 772.4 kB
Tags CPython 3.10 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
c10178a040ef10d9d77977d9de1b87e71684e5ea80c8039ccb25309020487fac
BLAKE2b-256 checksum
How to use checksums
4dfeceaed7aba8db6c326a00d27670dfac8b52b7163841898acaef0b9554f19b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release files / easeo-0.1.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL easeo-0.1.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 757.7 kB
Tags CPython 3.10 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
40835cc59452d2cfc0ceab803557ea4cee474b9cca7ff40e9d4bd40a61d86780
BLAKE2b-256 checksum
How to use checksums
8973e4f643778405d1b5f18fb880dda4fb545c0693dd9c1922f0ad4614821834
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release files / easeo-0.1.0-cp310-abi3-macosx_11_0_arm64.whl

Download URL easeo-0.1.0-cp310-abi3-macosx_11_0_arm64.whl
Size 699.6 kB
Tags CPython 3.10 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
10e1626245c566bb4df0872e29beae3eadba116338128e674755b827e31c5e5a
BLAKE2b-256 checksum
How to use checksums
154159fef3e0411b4a3c67daf0d6bcb70b11f8cebd7fbf4d58eb65850a73d86a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release files / easeo-0.1.0-cp310-abi3-macosx_10_12_x86_64.whl

Download URL easeo-0.1.0-cp310-abi3-macosx_10_12_x86_64.whl
Size 709.1 kB
Tags CPython 3.10 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
b172e16242a0b5e8a7801fa4e335f01b388e655a983cf93a5baab76d33902169
BLAKE2b-256 checksum
How to use checksums
f0c89c382a306c2f1aa944265cd0874c262a8da8313e8240a008fb688daf023e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release history Release notifications | RSS feed

0.2.0

5 release files

0.1.1

5 release files

This release

0.1.0 This release

5 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page