diff --git a/AGENTS.md b/AGENTS.md
index 5b463527d..1d82ae5dd 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -154,9 +154,11 @@ If you change CI bootstrap:
```bash
uv build --no-sources
-uv run --group docs pdoc -o docs/ --docformat google --logo "https://langfuse.com/langfuse_logo.svg" langfuse
+bash scripts/build_reference_docs.sh
```
+Build the reference through that script, not by calling pdoc directly -- it applies the `pdoc-templates/` overrides and the 404 page that the hosted site needs. See "SDK Reference" in `CONTRIBUTING.md`.
+
Releases are handled by GitHub Actions. Do not build an ad hoc local release flow into repository instructions.
## External Docs
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 45f1ed55d..0aaf910ed 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -125,18 +125,18 @@ The workflow will automatically:
Note: The generated SDK reference is currently work in progress.
-The SDK reference is generated via pdoc. The docs dependency group is installed on demand when you run the documentation commands.
+The SDK reference is generated via pdoc and published at [python.reference.langfuse.com](https://python.reference.langfuse.com). The docs dependency group is installed on demand when you run the documentation commands.
-To update the reference, run the following command:
+To build the reference into `docs/`, run:
```sh
-uv run --group docs pdoc -o docs/ --docformat google --logo "https://langfuse.com/langfuse_logo.svg" langfuse
+bash scripts/build_reference_docs.sh
```
-To run the reference locally, you can use the following command:
+To browse the reference locally with live reload, run pdoc's dev server:
```sh
-uv run --group docs pdoc --docformat google --logo "https://langfuse.com/langfuse_logo.svg" langfuse
+uv run --group docs pdoc --docformat google --logo "https://langfuse.com/langfuse_logo.svg" --template-directory pdoc-templates langfuse
```
## Credits
diff --git a/pdoc-templates/404.html b/pdoc-templates/404.html
new file mode 100644
index 000000000..39192a777
--- /dev/null
+++ b/pdoc-templates/404.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+ Page not found – Langfuse Python SDK API reference
+
+
+
+
+
+
Page not found
+
+ This page is not part of the Langfuse Python SDK API reference. The
+ symbol may have been renamed or removed in a later release.
+
+
+
+
diff --git a/pdoc-templates/index.html.jinja2 b/pdoc-templates/index.html.jinja2
new file mode 100644
index 000000000..6000a8fbe
--- /dev/null
+++ b/pdoc-templates/index.html.jinja2
@@ -0,0 +1,70 @@
+{#
+Root page of https://python.reference.langfuse.com.
+
+pdoc's default index is a bare `` stub with no
+title, no links and no canonical URL, because a single root module makes
+`langfuse.html` the entry point. Search engines read that stub as a
+soft-redirecting duplicate of `/index.html`. Setting `root_module_name` to
+false -- the escape hatch the default template documents -- renders the real
+module list instead, and the `content` block below gives that page something
+to say, since pdoc's own version leaves the main column empty.
+#}
+{% set canonical_base_url = env.get("PDOC_CANONICAL_BASE_URL", "https://python.reference.langfuse.com/").rstrip("/") ~ "/" %}
+{% set root_module_name = false %}
+{% extends "default/index.html.jinja2" %}
+
+{% block title %}Langfuse Python SDK API reference{% endblock %}
+
+{% block head %}
+
+
+{% endblock %}
+
+{% block content %}
+
+ {{ self.logo() }}
+ {% if search %}
+
+ {% endif %}
+
+
+
+
Langfuse Python SDK API reference
+
+ Generated reference for the langfuse package. It documents
+ every public symbol; the hand-written guides on
+ langfuse.com/docs are the better
+ starting point if you are setting Langfuse up for the first time.
+
+
Start here
+ {#
+ These keep the `.html` suffix even though the hosted site serves
+ clean URLs. pdoc generates its own sidebar links that way, and
+ extensionless paths 404 both under pdoc's dev server and when the
+ built output is served locally, so the one 308 hop in production is
+ the cheaper trade. Canonical URLs are a different case -- those must
+ not point at a redirect, which is why they drop the suffix.
+ #}
+
+
langfuse — the
+ Langfuse client, tracing decorators and context helpers.
+ Most code only needs this module.
+
langfuse.experiment
+ — running experiments over datasets and scoring the results.
+
langfuse.api — the
+ generated low-level API client and its request and response models.
Every module is listed in the sidebar, and the search box covers all of them.
+
+
+ {% if search %}
+ {% include "search.html.jinja2" %}
+ {% endif %}
+{% endblock %}
diff --git a/pdoc-templates/module.html.jinja2 b/pdoc-templates/module.html.jinja2
new file mode 100644
index 000000000..83eef8bd4
--- /dev/null
+++ b/pdoc-templates/module.html.jinja2
@@ -0,0 +1,15 @@
+{#
+Adds a self-referencing canonical URL to every module page.
+
+The `.html` suffix is deliberately dropped: Cloudflare Pages serves this site
+with clean URLs and 308-redirects `/langfuse.html` to `/langfuse`, so a
+canonical pointing at the `.html` path would point at a redirect.
+#}
+{% set canonical_base_url = env.get("PDOC_CANONICAL_BASE_URL", "https://python.reference.langfuse.com/").rstrip("/") ~ "/" %}
+{% extends "default/module.html.jinja2" %}
+
+{% block head %}
+ {{ super() }}
+
+{% endblock %}
diff --git a/scripts/build_reference_docs.sh b/scripts/build_reference_docs.sh
new file mode 100755
index 000000000..c94c46331
--- /dev/null
+++ b/scripts/build_reference_docs.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+# Builds the API reference published at https://python.reference.langfuse.com.
+#
+# Use this instead of calling pdoc directly: it applies the template overrides
+# in pdoc-templates/ and ships the 404 page, both of which the hosted site
+# needs. Set PDOC_CANONICAL_BASE_URL to build for a different origin.
+set -euo pipefail
+
+OUT_DIR="${1:-docs}"
+
+# Resolve a relative output path against the caller's working directory before
+# cd'ing to the repo root, so `build_reference_docs.sh out` from elsewhere does
+# not silently write to /out.
+case "$OUT_DIR" in
+ /*) ;;
+ *) OUT_DIR="$PWD/$OUT_DIR" ;;
+esac
+
+cd "$(dirname "$0")/.."
+
+uv run --group docs pdoc \
+ -o "$OUT_DIR" \
+ --docformat google \
+ --logo "https://langfuse.com/langfuse_logo.svg" \
+ --logo-link "https://langfuse.com" \
+ --template-directory pdoc-templates \
+ --edit-url "langfuse=https://github.com/langfuse/langfuse-python/blob/main/langfuse/" \
+ --no-show-source \
+ langfuse
+
+# Cloudflare Pages serves the closest index.html with a 200 for any unmatched
+# path unless the output contains a 404.html, which turns every stale or
+# mistyped URL into an indexable duplicate of the landing page.
+cp pdoc-templates/404.html "$OUT_DIR/404.html"
+
+echo "Reference docs written to $OUT_DIR/"