> For the complete documentation index, see [llms.txt](https://docs.overflowcms.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.overflowcms.com/seo-engine.md).

# SEO Engine

## SEO Engine

The embed script (Starter/Pro) renders your content client-side — great for app-like lists, but a crawler that doesn't execute JavaScript only sees an empty template until it does. SEO Engine solves that by rendering your content into your own page design **on the server**, so the very first response already contains your real content: title, meta description, canonical URL, Open Graph/Twitter cards, and JSON-LD structured data.

### How it works

1. **Capture** — OverflowCMS fetches one of your real, published Webflow pages (the one with your `data-oc-list` widget already on it) and stores it as a template.
2. **Render** — on each request, your published items are read from your collection and injected into that captured template, in the same place your embed script would put them client-side — same binding rules, same result, just done before the response leaves the server.
3. **Meta & structured data** — an item-detail page gets its own `<title>`, meta description, canonical URL, Open Graph/Twitter tags, and `Article` + `BreadcrumbList` JSON-LD, built from that item's own fields. A listing page keeps whatever title/description you already set on it in Webflow — it's a real page you designed, so nothing overrides it.
4. **Sitemap & RSS** — `sitemap.xml` and `rss.xml` are regenerated automatically every time you publish or unpublish an item in the connected collection.

### Which routing option do I need?

There are two ways to get traffic from your domain to OverflowCMS, and they solve different problems — picking the right one up front saves a re-do later.

|                                | **Custom domain**                                                  | **Reverse proxy Worker**                                                                     |
| ------------------------------ | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| What it covers                 | The **entire hostname** you register                               | Only the **path** you route (e.g. `/blog/*`)                                                 |
| Best for                       | A dedicated subdomain just for OverflowCMS (`blog.yourdomain.com`) | Carving one path out of a domain that already serves other content                           |
| Setup                          | Add one CNAME record. SSL and routing are automatic.               | Deploy a small Worker in your own Cloudflare account, add a Worker Route scoped to your path |
| Requires domain on Cloudflare? | No — works with any DNS provider                                   | Yes — Worker Routes only exist within a Cloudflare zone you control                          |

**The one thing to get right: a custom domain is whole-hostname, not path-scoped.** If you register `yourdomain.com` (your apex domain) as a custom domain, *every* path on it routes through OverflowCMS — not just `/blog`. Since OverflowCMS only has a captured template for your blog, every other page (your homepage, contact page, anything else) would break instead of falling through to wherever it's actually hosted today.

So:

* **Want a dedicated blog subdomain**, like `blog.yourdomain.com`, entirely served by OverflowCMS? Use **custom domain** — it's the simplest option, one DNS record, nothing to deploy or maintain.
* **Want to keep your existing site on `yourdomain.com` and only serve one path** (e.g. `yourdomain.com/blog/*`) through OverflowCMS, with everything else untouched? Use the **reverse proxy Worker** — Worker Routes are path-scoped, so you can target exactly `/blog/*` and nothing else. This is also your only option if your domain isn't on Cloudflare at all.

### Tutorial: build a real blog setup

This walks through everything needed to get a real collection serving real, crawlable pages on your own domain — capture, routing, the list/item-only choice, cover images, and verification, in the order you'd actually do them.

{% stepper %}
{% step %}
**Decide your URL structure**

There are two common shapes, depending on how your Webflow site is built. Pick whichever matches your site — you can change this later by re-capturing.

{% tabs %}
{% tab title="One design for everything" %}
Your list page (e.g. `/blog`) and every item page (`/blog/:slug`) share the same captured design — the common case if you built a single Webflow page with a list widget on it and don't have a separate native item template.

Capture that one page. `/blog` renders the list; `/blog/:slug` renders each item using the same template, with that item's fields bound in instead of the list.
{% endtab %}

{% tab title="Native Collection List + Page" %}
If your Webflow site uses Webflow's own CMS collection pages, you likely already have **two different URLs**: a Collection List page (e.g. `/blog`) and a separate Collection Page item template registered at its own base (e.g. also `/blog`, with items at `/blog/:slug` — Webflow's native convention nests items under the list's own path, so this is usually the same base either way).

In this shape, capture the **item template** page as your design (not the list page) — that's the page a crawler actually lands on for every individual post, so it's the one that needs real content and meta tags server-rendered. Your list page keeps working exactly as it already does (Webflow's native rendering, or the embed script if you've added one) — SEO Engine doesn't need to touch it.

Since the base path itself isn't a real "list of everything" page in this shape, turn off the list render for it in step 2 below — otherwise visiting the bare base URL would render every published item through the item template, which looks broken.
{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}
**Capture your page design**

From your site's dashboard, open the **SEO Engine** tab and fill in:

* **Page URL** — the real, published URL of the page to use as the template (the one with your `data-oc-list` widget and embed script already on it).
* **Public URL** — the URL this content should be addressed at once it's live (usually the same as Page URL).
* **Collection** — which collection this page's list widget is bound to.
* **This URL shows a list of items** — leave checked for the "one design for everything" shape above. Uncheck it for the native Collection List/Collection Page shape, so the base URL 404s instead of rendering every item through the item template.

You don't need to keep re-running this by hand: a captured page is refreshed automatically every night, and for an instant refresh right after you publish in Webflow, your dashboard's SEO Engine tab shows a webhook URL you can paste into Webflow's **Site Settings → Integrations → Webhooks** (event `Site Publish`). Both only ever refresh the page at the URL you already captured — see Known limitations below.
{% endstep %}

{% step %}
**Point your domain at it**

A request for your path has to reach OverflowCMS instead of — or in front of — Webflow. If you're not sure which of the two options below fits your situation, see **Which routing option do I need?** above first — the short version: a dedicated subdomain wants Option 1, carving one path out of an existing domain wants Option 2.

{% tabs %}
{% tab title="Option 1 — custom domain" %}
No Worker of your own to deploy or maintain — register your domain directly and OverflowCMS issues its own SSL certificate for it. Covers the **entire hostname** you register, so this is the right fit for a dedicated subdomain (`blog.yourdomain.com`), not for carving a single path out of a domain that also serves other content — see the decision guide above.

```
POST /sites/:siteId/ssr/custom-hostname
{ "hostname": "blog.yourdomain.com" }
```

The response includes a `cnameTarget`. Add **one CNAME record** at your DNS provider pointing your hostname at it — that's the only DNS record you need. Then poll status:

```
GET /sites/:siteId/ssr/custom-hostname
```

`sslStatus` moves from `pending_validation`/`pending_issuance` to `active` once your CNAME propagates — SSL verification happens automatically from there, no separate step. This can take anywhere from a couple of minutes to a few hours. Once `active`, `https://blog.yourdomain.com` serves your content directly — no proxy Worker involved. `DELETE /sites/:siteId/ssr/custom-hostname` detaches it later.

{% hint style="info" %}
Option 1 depends on a one-time, account-level Cloudflare for SaaS setup on OverflowCMS's side. If `POST .../ssr/custom-hostname` returns `not_configured`, it isn't enabled yet on your account — use Option 2 in the meantime.
{% endhint %}
{% endtab %}

{% tab title="Option 2 — reverse proxy Worker" %}
A small Worker you deploy in your own Cloudflare account. Works if your domain's DNS is on Cloudflare. Unlike Option 1, this is **path-scoped** — the Worker Route you add only covers the exact path you give it, so it's the right fit for keeping your existing site on `yourdomain.com` and carving out just `/blog/*`. Webflow officially documents this pattern — no Enterprise plan required.

```javascript
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const upstream = new URL(url.pathname + url.search, 'https://ssr.overflowcms.com');

    const upstreamRequest = new Request(upstream, {
      method: request.method,
      headers: {
        ...Object.fromEntries(request.headers),
        'x-overflow-token': env.OVERFLOW_TOKEN,
      },
    });

    return fetch(upstreamRequest);
  },
};
```

Deploy this as a Worker, set `OVERFLOW_TOKEN` to your site's public CMS token (Integrate tab), then add **one Worker Route** with a trailing wildcard: `yourdomain.com/blog/*`.

{% hint style="warning" %}
**Always reference your list page with a trailing slash** — `https://yourdomain.com/blog/`, not `https://yourdomain.com/blog`. Cloudflare's `/*` wildcard reliably matches the trailing-slash form and every item page (`yourdomain.com/blog/:slug`), but not the bare path with nothing after it.

You'd expect a second, exact-match route (`yourdomain.com/blog`, no wildcard) to cover the no-trailing-slash form, and in principle it should — but we've seen exact-match Worker Routes silently fail to take effect on some Cloudflare accounts, for reasons still unclear. Don't rely on one. Wildcard-plus-trailing-slash is the only combination we've seen work reliably every time.
{% endhint %}
{% endtab %}
{% endtabs %}

{% hint style="info" %}
If your domain's DNS is on Cloudflare, double-check the record is **proxied** (orange cloud), not "DNS only" (grey cloud) — Worker Routes only intercept traffic that passes through Cloudflare's proxy. A grey-clouded record sends every request straight to your origin, bypassing OverflowCMS entirely, with no error to tip you off — it just quietly keeps serving your old, unrendered page.
{% endhint %}
{% endstep %}

{% step %}
**Add cover images**

If your collection has an **Image** field (e.g. `cover`), any item with a value set there renders with a real `src`, responsive `srcset`, and `alt` text on both the server-rendered page and the client-side embed — same binding, same field, no extra setup. Upload images per item from the item editor. See **Images & Media** under Webflow Integration for the full field reference and responsive-image details.
{% endstep %}

{% step %}
**Verify**

Visit your list page and an item page in a browser — both should render your real design with real content. Then check with a tool that doesn't execute JavaScript — `curl`, or Google's Rich Results Test — rather than just the browser. "View Source" (or `curl`) should show the actual title, meta description, canonical link, and JSON-LD `<script>` blocks already in the HTML, not injected later by JS. That's the actual test: it confirms what a crawler sees, not what appears after client-side JS runs.
{% endstep %}
{% endstepper %}

### Known limitations (of the current design)

* **A custom domain (Option 1) covers a whole hostname, not a path.** It's not a way to carve one path out of a domain that also serves other content — see the decision guide above. Use Option 2 for that.
* **One captured page per site.** If your listing and item pages use genuinely different Webflow designs (beyond the native Collection List/Collection Page split covered above), both currently render from whichever page you captured — a real gap, not a bug, tracked for a future multi-template capture.
* **Re-capture only refreshes the existing page, on the existing URL.** The nightly automatic sweep and the webhook both re-fetch whatever URL was last captured — a real redesign at a *new* URL, or changing which collection a page is bound to, still needs a fresh manual capture from the dashboard.
* **Search and related-items widgets stay client-side**, even on a routed page — a static server render has no live query or "current item" context for those. The plain list widget is what renders with real content server-side.
* **Exact-match Worker Routes (Option 2) can silently fail to route traffic** on some Cloudflare accounts — see the warning in the routing step above. Use a wildcard route and a trailing slash on your list page URL instead.

### Plan tiers

SEO Engine is its own top plan tier — see Plans & Billing for pricing and what else it includes (unlimited sites, everything in Pro).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.overflowcms.com/seo-engine.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
