Skip to content

Developer docs / Lovable

Connect BlazeHive to Lovable

BlazeHive delivers every page it generates to your site as a signed HTTP POST. Copy the prompt below into Lovable — it builds a Supabase Edge Function to receive pages, a table to store them, and server-rendered routes to publish them.

1. Give your Lovable agent this prompt

Copy it and paste it straight into the Lovable chat. It is self-contained — the endpoint, signature verification, how to store the pages in whatever database you already use, and how to render them so search engines can actually read them. Your agent does not need to open any links.

655 lines · everything the agent needs, no links to follow

Lovable apps are client-rendered React by default, which search engines cannot read — and unreadable pages defeat the whole point. The prompt instructs the agent to server-render these routes instead, and to tell you if it cannot rather than quietly shipping something invisible to Google.
The full prompt
# 1. Task context

You are a senior full-stack engineer working inside this Lovable app. You are
adding a BlazeHive integration to it.

BlazeHive is an SEO content engine: it researches and writes pages
automatically, then delivers each finished page to my site as a signed HTTP
POST. Your job is to receive those deliveries, store them, and serve them as
crawlable, server-rendered pages on this site — so that the content actually
ranks.

Judge your own work by that last part. A receiver that accepts webhooks
perfectly but produces pages Google cannot read is a failed integration.

# 2. Background — the BlazeHive webhook contract

Everything in this section is the complete public documentation for the
contract: the events, the full payload, the page types and their URLs, the
landing-page block model with a real example payload, signature verification
with working code, the rendering rules, the archive and sitemap conventions, and
retry semantics. Treat it as the spec. Read it before you write anything.

Two things in it do not apply to you:

- Its **Setup** section describes what I do in the BlazeHive dashboard. That is my job, not yours.
- Where it shows an example endpoint path, ignore it and use the path specified in section 3.

---

# BlazeHive Webhook Integration (contract v2)

Receive every page BlazeHive generates as a signed HTTP POST to any endpoint you own — your own server, Zapier, Make, or n8n.

## Overview

- **Signed** — HMAC-SHA256 with a timestamp, Stripe-style. No shared-secret-in-plaintext.
- **Reliable** — automatic retries on transient failures, with a stable delivery id for deduplication.
- **Versioned** — every payload carries `version: 2`; breaking changes bump it.

## Setup

1. In app.blazehive.io, go to Integrations → Webhook.
2. Paste your endpoint URL. It must be HTTPS, publicly reachable (private/internal addresses are blocked), and must not redirect.
3. Optionally set a secret (any long random string). When set, every request is signed. Strongly recommended for production.
4. Click "Test connection" — BlazeHive sends a `test` event and expects a 2xx response.

## Events

| Event | When |
| --- | --- |
| `publish.live` | A page went live. Create it — or update it if you've seen its id before. |
| `publish.draft` | Same payload as publish.live, but the page should be stored unpublished. |
| `unpublish` | A page was taken down in BlazeHive. Remove or unpublish it on your side. |
| `test` | Sent by the "Test connection" button. Minimal payload — just acknowledge with a 2xx. |

### Sample publish.live

```json
{
  "event": "publish.live",
  "version": 2,
  "id": "b3d7a2f1-4c8e-4a1d-9f2b-7e6d5c4b3a2f",
  "title": "Best Project Management Tools in 2026",
  "keyword": "best project management tools",
  "slug": "best-project-management-tools",
  "type": "listicle",
  "url_path": "/listicles/best-project-management-tools",
  "content": "# Best Project Management Tools in 2026\n\nChoosing the right tool...",
  "meta_title": "Best Project Management Tools in 2026",
  "meta_description": "Compare the 10 best project management tools for small teams.",
  "schema_json": { "@context": "https://schema.org", "@type": "Article" },
  "completed_at": "2026-07-24T14:32:00.000Z"
}
```

### Sample unpublish

```json
{
  "event": "unpublish",
  "version": 2,
  "id": "b3d7a2f1-4c8e-4a1d-9f2b-7e6d5c4b3a2f",
  "slug": "best-project-management-tools",
  "type": "listicle",
  "url_path": "/listicles/best-project-management-tools",
  "external_id": "rec_abc123xyz"
}
```

## Payload reference (publish.live / publish.draft)

| Field | Type | Description |
| --- | --- | --- |
| `event` | string | "publish.live", "publish.draft", "unpublish", or "test". |
| `version` | number | Contract version. Currently 2 — bumped only on breaking changes. |
| `id` | string (uuid) | Stable BlazeHive page id. Use it as your idempotency / upsert key. |
| `title` | string | The page title as plain text, for your own convenience — store it, list it, use it in `<title>`. Do NOT render it as a heading on the page; `content` already contains the H1. |
| `keyword` | string | The exact search keyword the page targets. |
| `slug` | string | URL-safe slug, unique per page. |
| `type` | string | Page format: "landing", "faq", "alternatives", "comparison", or "listicle". ("vs" may also arrive — head-to-head pages are served at the comparison path, so no extra route is needed.) |
| `url_path` | string | Recommended path on your site, e.g. "/faq/{slug}" or "/solutions/{slug}". |
| `content` | string | **The page.** Markdown, complete and self-contained — its H1 is the page heading. Render this. |
| `meta_title` | string \| null | SEO <title> for the page. |
| `meta_description` | string \| null | SEO meta description. |
| `schema_json` | object \| null | JSON-LD structured data, ready to embed in a <script type="application/ld+json"> tag. |
| `completed_at` | string (ISO) | When BlazeHive finished generating the page. |

### Page types and their URLs

`url_path` is built for you — serve the page there rather than inventing your own scheme. New types may be added over time, so route on `url_path` generically.

| type | url_path | Archive | What it is |
| --- | --- | --- | --- |
| `landing` | `/solutions/{slug}` | `/solutions/` | Designed, conversion-focused landing page. The only type carrying rich_html + blocks. |
| `faq` | `/faq/{slug}` | `/faq/` | One question answered per page. |
| `alternatives` | `/alternatives/{slug}` | `/alternatives/` | "Alternatives to X" page. |
| `comparison` | `/comparisons/{slug}` | `/comparisons/` | Multi-product comparison, and head-to-head "X vs Y" pages. |
| `listicle` | `/listicles/{slug}` | `/listicles/` | Ranked list article. |

> **`content` is markdown — render it with any markdown library** (marked, markdown-it, remark, python-markdown). It is plain CommonMark plus tables: headings, paragraphs, lists, links, images, tables, blockquotes and code fences. Nothing custom, no front-matter, no shortcodes. Style the output with your own prose container. `rich_html` on landing pages is the opposite — it ships its own scoped stylesheet and needs no styling from you.
>
> Images inside `content` point at absolute BlazeHive CDN URLs and are ~1700px wide — give them `max-width: 100%; height: auto` in your prose styles so they cannot overflow your layout.

> **Title vs. content:** `content` is the whole page and already contains its own H1 — render it and nothing else. `title` is metadata for YOU: save it alongside the page so you can show it in an index, a card, a breadcrumb or the `<title>` tag. Printing `title` as a heading above `content` gives the page two H1s.
>
> Note the H1 is not the first line: pages open with a short `## TL;DR` summary above it, on purpose — that block is what AI Overviews and assistants quote. Render the markdown in the order it arrives.

`unpublish` events carry `id`, `slug`, `type`, `url_path`, and `external_id` (always null for webhook integrations).

## Landing page blocks (type: "landing" only)

| Field | Type | Description |
| --- | --- | --- |
| `blocks` | array | The structured layout: hero, split, centered, benefits, faq, and cta blocks in render order. |
| `images` | object | Image manifest keyed by imageId → { src, alt, w, h }. All URLs are absolute and publicly served. |
| `rich_html` | string | An HTML fragment — a <style> tag followed by <div class="bh-landing">…</div>. Drop it straight into your page body. Not a full document: it has no <html>, <head>, or <body> of its own. |
| `schema_version` | number | Version of the block model. Render `content` instead when it is newer than what you support. |
| `doc_version` | number | Monotonic per-page revision — cache-bust your rendered output when it changes. |

Three ways to consume a landing page, in ascending effort: render `rich_html` as-is; render `content` into your own template; or map `blocks` + `images` onto your own components.

### The block model

Every block carries a `t` discriminator naming its type. Blocks are already in render order — do not re-sort them.

| t | Fields | Notes |
| --- | --- | --- |
| `hero` | `h1, intro[], cta{label,url}, imageId` | Always the first block. One per page. |
| `split` | `h2, body[], bullets[]?, imageId, side` | Image beside copy. side alternates "right", "left", "right", … |
| `centered` | `h2, body[]` | Full-width centred prose. No image. |
| `benefits` | `h2, items[{title, body}]` | A card grid. |
| `faq` | `h2, items[{q, a}]` | Question/answer pairs. Good source for FAQPage structured data. |
| `cta` | `h2, body?, cta{label,url}` | Always the last block. |

> **Prose fields carry limited markdown.** Any `intro`, `body`, or `bullets` string may contain `**bold**` and `[text](https://…)` links — nothing else. HTML-escape the string FIRST, then convert those two patterns, and accept http(s) link targets only.

### A real publish.live for a landing page

Generated by the same renderer that serves production deliveries — long strings elided with ``.

```json
{
  "event": "publish.live",
  "version": 2,
  "id": "b3d7a2f1-4c8e-4a1d-9f2b-7e6d5c4b3a2f",
  "title": "Mobile dog grooming in Austin, at your curb",
  "keyword": "mobile dog grooming in austin",
  "slug": "mobile-dog-grooming-in-austin",
  "type": "landing",
  "url_path": "/solutions/mobile-dog-grooming-in-austin",
  "content": "# Mobile dog grooming in Austin, at your curb\n\nRufflands does mobile dog grooming…",
  "meta_title": "Mobile Dog Grooming in Austin | Rufflands",
  "meta_description": "Rufflands does mobile dog grooming in Austin — van-based, one dog at a time, no cage drying.",
  "schema_json": null,
  "completed_at": "2026-07-28T09:14:00.000Z",
  "blocks": [
    {
      "t": "hero",
      "h1": "Mobile dog grooming in Austin, at your curb",
      "intro": [
        "Rufflands does mobile dog grooming in Austin, parked right outside your door.",
        "One dog at a time, start to finish, in a van with its own water and power."
      ],
      "cta": {
        "label": "Book a groom",
        "url": "https://example.com/book"
      },
      "imageId": "hero"
    },
    {
      "t": "centered",
      "h2": "Why the van beats the salon",
      "body": [
        "A salon groom means a crate, a car ride, and three hours of barking.",
        "The van takes 90 minutes and your dog never leaves the street."
      ]
    },
    {
      "t": "split",
      "h2": "What a Rufflands groom includes",
      "body": [
        "Every appointment is a full groom, not a bath with add-ons stacked on top."
      ],
      "bullets": [
        "Warm hydrobath and hand dry",
        "Breed-standard clip or scissor finish",
        "Nails, ears, and pad trim"
      ],
      "imageId": "included",
      "side": "right"
    },
    {
      "t": "split",
      "h2": "Booked around your day",
      "body": [
        "Pick a two-hour window and we text when the van is ten minutes out, see [service areas](https://example.com/areas)."
      ],
      "bullets": [
        "Same-week slots",
        "Text-ahead arrival",
        "Card on file, no cash"
      ],
      "imageId": "base-value-props",
      "side": "left"
    },
    {
      "t": "split",
      "h2": "Groomers who stay",
      "body": [
        "Every Rufflands groomer is salary-paid and certified — the same hands each visit."
      ],
      "imageId": "base-credibility",
      "side": "right"
    },
    {
      "t": "benefits",
      "h2": "Built for anxious dogs",
      "items": [
        {
          "title": "No cage drying",
          "body": "Hand drying only, so nothing is left in a hot box."
        },
        {
          "title": "One dog at a time",
          "body": "No pack noise, no waiting, no stacked appointments."
        },
        {
          "title": "Same groomer",
          "body": "Your dog sees a familiar face every visit."
        }
      ]
    },
    {
      "t": "centered",
      "h2": "Serving central and south Austin",
      "body": [
        "Travis Heights, Zilker, Hyde Park, Mueller, and everything inside the loop."
      ]
    },
    {
      "t": "faq",
      "h2": "Frequently asked questions",
      "items": [
        {
          "q": "Do you need my driveway?",
          "a": "Street parking is fine. The van runs on its own power and water."
        },
        {
          "q": "How long does a groom take?",
          "a": "About 90 minutes for most breeds, longer for a heavy double coat."
        }
      ]
    },
    {
      "t": "cta",
      "h2": "Your dog's next groom, without the car ride",
      "body": "Same-week appointments across Austin.",
      "cta": {
        "label": "Book a groom",
        "url": "https://example.com/book"
      }
    }
  ],
  "images": {
    "hero": {
      "src": "https://cdn.blazehive.io/demo/page/hero.webp",
      "alt": "Grooming van parked on an Austin street",
      "w": 1712,
      "h": 1063
    },
    "included": {
      "src": "https://cdn.blazehive.io/demo/page/included.webp",
      "alt": "Groomer hand drying a terrier",
      "w": 1712,
      "h": 1057
    },
    "base-value-props": {
      "src": "https://cdn.blazehive.io/demo/_base/value-props.webp",
      "alt": "Booking window on a phone",
      "w": 1712,
      "h": 1060
    },
    "base-credibility": {
      "src": "https://cdn.blazehive.io/demo/_base/credibility.webp",
      "alt": "Certified groomer at work",
      "w": 1712,
      "h": 1060
    }
  },
  "rich_html": "<style>.bh-landing,.bh-landing *{box-sizing:border-box…</style>\n<div class=\"bh-landing\">…</div>",
  "schema_version": 1,
  "doc_version": 3
}
```

## Rendering the pages

Receiving the payload is half the job — the SEO value only exists once the page is server-rendered at a crawlable URL. This route handles every page type:

```js
// Express — one route renders every BlazeHive page type.
// npm i marked   (any markdown renderer works — markdown-it, remark, …)
import { marked } from "marked";

app.get("/solutions/:slug", async (req, res) => {
  const page = await db.pages.findOne({ slug: req.params.slug });
  if (!page) return res.status(404).send("Not found");

  // Landing pages carry rich_html; every other type renders `content` (markdown).
  // rich_html is a FRAGMENT (<style> + <div>) — drop it into the body as-is.
  // NOTE: no <h1> of our own — `content` already opens with the page heading.
  const body = page.rich_html
    ? page.rich_html
    : `<article class="prose">${marked.parse(page.content)}</article>`;

  res.send(`<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>${esc(page.meta_title || page.title)}</title>
  <meta name="description" content="${esc(page.meta_description || "")}">
  <link rel="canonical" href="https://yoursite.com${page.url_path}">
  <meta property="og:title" content="${esc(page.meta_title || page.title)}">
  <meta property="og:description" content="${esc(page.meta_description || "")}">
  <meta property="og:url" content="https://yoursite.com${page.url_path}">
  ${page.schema_json
    ? `<script type="application/ld+json">${JSON.stringify(page.schema_json)}</script>`
    : ""}
</head>
<body>${body}</body>
</html>`);
});
```

### What has to be right

- **Server-render it.** A client-side fetch that injects content after load leaves the page effectively empty for crawlers.
- **Never iframe `rich_html`.** It is a fragment meant to sit in your body; inside an iframe it carries no SEO weight for the parent page.
- **Do not escape rendered markdown or `rich_html`.** Markdown output and `rich_html` are already safe HTML. Use `dangerouslySetInnerHTML` (React), `v-html` (Vue), or the raw filter (Liquid/Jinja).
- **Never add an `<h1>` of your own.** `content` already contains the page heading, and `rich_html` has one too — printing `title` above either gives the page two H1s. Keep `title` for indexes, cards, breadcrumbs and the `<title>` tag.
- **Use `images[id].w` / `.h`** as width/height attributes to avoid layout shift.
- **Add each page to your sitemap** and ping it on publish.

> **The stylesheet will not collide with your site.** Every rule in `rich_html` is scoped under `.bh-landing`, including its reset.

## Archives & sitemaps

Individual pages are only half the structure. Two things make the set crawlable, and the official WordPress plugin sets both up automatically — mirror them on any custom integration.

### An archive page per type

Serve a listing page at each type's prefix — `/comparisons/`, `/solutions/`, `/alternatives/` and so on (the Archive column above) — linking every published page of that type. This is the internal-link hub: without it, each generated page is an orphan reachable only from the sitemap, which is a much weaker crawl signal and wastes most of the internal-linking value.

Order newest-first, paginate past ~50, and link each archive from somewhere permanent — a footer column is enough.

### One sitemap per type, behind an index

Split the sitemap by page type rather than shipping one flat file. Each stays small and its `lastmod` values stay meaningful, so a change to one type does not invalidate the crawl budget for the rest.

```xml
<!-- /sitemap-index.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap><loc>https://yoursite.com/sitemap-landing.xml</loc></sitemap>
  <sitemap><loc>https://yoursite.com/sitemap-faq.xml</loc></sitemap>
  <sitemap><loc>https://yoursite.com/sitemap-vs.xml</loc></sitemap>
  <sitemap><loc>https://yoursite.com/sitemap-alternatives.xml</loc></sitemap>
  <sitemap><loc>https://yoursite.com/sitemap-comparison.xml</loc></sitemap>
  <sitemap><loc>https://yoursite.com/sitemap-listicle.xml</loc></sitemap>
</sitemapindex>

<!-- /sitemap-vs.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://yoursite.com/comparisons/</loc></url>
  <url>
    <loc>https://yoursite.com/comparisons/notion-vs-asana</loc>
    <lastmod>2026-07-28</lastmod>
  </url>
</urlset>
```

- `/sitemap-{type}.xml` — one per page type, listing that type's archive plus every published page of it.
- `/sitemap-index.xml` — a `<sitemapindex>` pointing at each child sitemap.
- `/sitemap.xml` — redirect it to the index; that is the path crawlers try first.

> **Generate both at request time** from your stored pages, rather than writing static files on publish. A page that arrives at 3am then needs no deploy to become discoverable.

## Request headers

```http
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: BlazeHive-Webhook/2
X-Blazehive-Event: publish.live
X-Blazehive-Delivery: 7c2e6a90-1f4b-4c3d-9e8a-2b5d6f7a8c9d
X-Blazehive-Timestamp: 1753366321
X-Blazehive-Signature: t=1753366321,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```

| Header | Meaning |
| --- | --- |
| `X-Blazehive-Event` | Event name — route without parsing the body. |
| `X-Blazehive-Delivery` | Unique id per delivery, stable across retries — your deduplication key. |
| `X-Blazehive-Timestamp` | Unix seconds when the delivery was signed. |
| `X-Blazehive-Signature` | Only when a secret is configured — see Verifying signatures. |

## Verifying signatures

The signature header is `t=<timestamp>,v1=<hex digest>`, where v1 is HMAC-SHA256 over the string `"<timestamp>.<raw body>"` keyed with your secret. Verifying the timestamp inside the signed string blocks replay attacks.

```js
const crypto = require("crypto");

// IMPORTANT: verify against the RAW request body bytes.
// Parsing and re-stringifying the JSON changes the bytes and breaks the HMAC.
function verifyWebhook(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((p) => p.split("="))
  );
  const { t, v1 } = parts;
  if (!t || !v1) return false;

  // Replay guard: reject deliveries older than 5 minutes.
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");

  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

// Express example — capture the raw body before JSON parsing:
app.post("/webhooks/blazehive",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const ok = verifyWebhook(
      req.body.toString("utf8"),
      req.get("X-Blazehive-Signature") || "",
      process.env.BLAZEHIVE_WEBHOOK_SECRET
    );
    if (!ok) return res.status(401).end();

    const payload = JSON.parse(req.body);
    // ... upsert by payload.id, then:
    res.status(200).json({ ok: true });
  });
```

## Delivery & retries

- Respond with a 2xx within 10 seconds. Do slow work asynchronously after acknowledging.
- Network failures, timeouts, and 429/502/503/504 responses are retried automatically — up to 3 attempts with a short backoff, all sharing one `X-Blazehive-Delivery` id.
- Any other non-2xx response fails the delivery immediately (retry manually from the dashboard).
- Redirects are never followed — a 3xx response is treated as an error. Point BlazeHive at the final URL.
- **Idempotency:** upsert by `id` — republished pages arrive as another `publish.live` with the same `id`.

# 3. What to build

## 3.1 Endpoint

Create a Supabase Edge Function named `blazehive-webhook` that accepts POST.

Read the raw request body as text (`await req.text()`) and verify the signature
against that exact string BEFORE calling `JSON.parse` on it. Parsing first and
re-serializing will break verification. Return 401 if it does not match, and
respond 2xx within 10 seconds. Working verification code is in section 2, under
"Verifying signatures" — in Deno, use `node:crypto`
(`import crypto from "node:crypto"`) or the Web Crypto API; the logic is
identical.

Generate the signing secret yourself — at least 32 random bytes, hex or base64,
from a real CSPRNG — and set it as the Edge Function secret
`BLAZEHIVE_WEBHOOK_SECRET`. You will print it back to me at the end so I can
paste it into BlazeHive.

This function must be public — disable JWT verification for it, since BlazeHive
authenticates with the HMAC signature, not a Supabase token.

## 3.2 Store the pages

Persist what you receive. **Use whatever storage this project already uses, and
update the database yourself if it needs new columns, tables, or collections to
hold these fields** — add a database if there isn't one. You know this codebase;
model it however fits. I am not prescribing a schema.

However you store it, these have to hold:

- Upsert on `id`. Republished pages arrive again with the same `id` and must update in place, never create a duplicate.
- You must be able to look a page up by its `url_path` (or `slug`) when serving a request — index accordingly.
- Track published state: `publish.live` → published, `publish.draft` → stored but not published, `unpublish` → unpublished or deleted.
- `schema_json`, `blocks`, and `images` are JSON. Store them as JSON if your database supports it, rather than as stringified text.
- Keep `doc_version` if present, and use it to invalidate any cached render.

## 3.3 Render the pages

Serve each published page at its `url_path`. **The HTML must be complete in the
server response.** This is the entire point: a page assembled client-side after
load is invisible to search crawlers, and the content is worthless for SEO.

Section 2 spells out the head tags, the `rich_html` vs `content` branch,
and the escaping rules. Two things worth being explicit about:

- On a landing page you get `rich_html`, `blocks`, and `content`. These are three representations of the SAME content — pick one and render only that, or you will publish the page twice over. `rich_html` is the recommended default: it carries its own scoped CSS and needs no styling work from you.
- `content` on every other page type is markdown. Render it with any markdown library and wrap the output in your own prose container, styled like the rest of your site.

## 3.4 Archives and sitemaps

Build the archive pages and the per-type sitemaps described under
"Archives & sitemaps" in section 2. Generate both from your stored pages at
request time, so a page that arrives overnight is discoverable without a deploy.

## 3.5 Lovable specifics — read this carefully

This app is a client-rendered React SPA by default. **That is not good enough for
these pages.** If the page HTML is assembled in the browser after load, search
engines see an empty shell and the entire integration is pointless — see example
4.1 for exactly what that failure looks like.

So the page routes must return complete HTML from the server. Do this with a
second Supabase Edge Function that:
- matches the incoming path against `url_path` in your stored pages,
- returns a full HTML document (head tags + body) with `Content-Type: text/html`,
- returns 404 for unknown paths.

Then route the public page paths to that function at the hosting layer so
`https://mysite.com/solutions/some-slug` is served by it directly.

If you cannot make server-rendered HTML work, STOP and tell me explicitly rather
than shipping a client-rendered version — I need to know, because it changes the
whole approach.

Keep the existing design system and layout components for the surrounding page
chrome. Do not restyle the app.

## 3.6 Rules — do not break these

1. Verify the signature over the **raw request body bytes**, before any JSON parsing. Parse-then-restringify changes the bytes and fails every time.
2. Reject an invalid signature with **401**. Never process an unverified payload.
3. Always respond **2xx within 10 seconds**. Do slow work after responding.
4. **Upsert by `id`**, never blind-insert. Duplicate pages are worse than no pages.
5. **Never escape rendered markdown or `rich_html`.** Both are already safe HTML; escaping again ships visible tags to users.
6. **Never put `rich_html` in an `<iframe>`.** It is a fragment meant to live in your page; in an iframe it carries no SEO weight.
7. **Never render two representations of the same landing page.** Pick one of `rich_html`, `blocks`, or `content`.
8. **Never add an `<h1>` of your own** — every representation already carries the page heading. `title` is metadata, not a heading.
9. Serve pages at the **`url_path` given to you**. Do not invent a different URL scheme.
10. Do not restructure this app, change its styling, or refactor unrelated code. Add what is needed and nothing else.
11. Do not commit the signing secret to the repo. It lives in the secret store only.
12. If something in this prompt conflicts with what you find in the codebase, stop and tell me rather than guessing.


# 4. Examples

## 4.1 What "server-rendered" means

**GOOD**`curl https://yoursite.com/comparisons/notion-vs-asana` returns the page:

```html
<!doctype html>
<html lang="en">
<head>
  <title>Notion vs Asana: which fits your team? | Rufflands</title>
  <meta name="description" content="A side-by-side look at how the two tools handle projects.">
  <link rel="canonical" href="https://yoursite.com/comparisons/notion-vs-asana">
  <script type="application/ld+json">{"@context":"https://schema.org","@type":"Article"}</script>
</head>
<body>
  <article class="prose">
    <h1>Notion vs Asana: which fits your team?</h1>
    <p>Both tools promise to replace your project tracker...</p>
  </article>
</body>
</html>
```

**BAD** — the same `curl` returns a shell, with the content fetched by JS after
load:

```html
<!doctype html>
<html><head><title>Rufflands</title></head>
<body><div id="root"></div><script src="/assets/index.js"></script></body>
</html>
```

The second one is exactly what a crawler sees. It contains no page, so as far as
search is concerned the page does not exist. If your result looks like this, the
integration has failed no matter how well the webhook works.

## 4.2 The report you finish with

**GOOD** — real, working values I can paste straight into BlazeHive:

```
Webhook URL: https://ktvhqmxbzlpwnrdaeugs.supabase.co/functions/v1/blazehive-webhook
Secret: 9f2b7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f
```

**BAD** — placeholders, which are useless to me:

```
Webhook URL: https://<project>.supabase.co/functions/v1/blazehive-webhookSecret: <your secret here>
```

# 5. Your task right now

Build the integration described in section 3, following the contract in
section 2 and the rules in 3.6.

Then validate it yourself. Do not report back until all of these pass:

1. **Every page type renders.** Feed your endpoint a fixture `publish.live` for each of `landing`, `faq`, `alternatives`, `vs`, `comparison` and `listicle`, then load each one. The `landing` fixture must include `rich_html` so you exercise that branch.
2. **The HTML is in the server response.** View source (or `curl`) each page — the body copy must be there, not injected after load. Compare against example 4.1.
3. **Head tags are present** on each page: `<title>`, meta description, canonical, and the JSON-LD script when `schema_json` was not null.
4. **Archives work.** Each type's archive URL lists its pages.
5. **Sitemaps work.** `/sitemap-index.xml` and every `/sitemap-{type}.xml` return valid XML, and `/sitemap.xml` redirects to the index.
6. **Re-delivery is idempotent.** Send the same `publish.live` twice — the second must update the existing page, not create a duplicate.
7. **Bad signatures are rejected.** A request with a tampered signature must get a 401.
8. **`unpublish` works.** The page stops being served and drops out of the archive and sitemap.

Then print exactly this block, with the real values filled in, as the last thing
you output:

Webhook URL: <the full public https URL of your endpoint>
Secret: <the value you stored in BLAZEHIVE_WEBHOOK_SECRET>

I copy those two straight into BlazeHive, so they must be the actual working
values — never placeholders or examples, as shown in 4.2. The URL must be
HTTPS, publicly reachable, and must NOT redirect: BlazeHive treats a 3xx as a
failure, so if the apex domain redirects to `www`, print the `www` form.

If any check above failed, say so plainly instead of printing the block.

2. Paste the endpoint URL into BlazeHive

The agent creates a Supabase Edge Function and gives you its URL. In BlazeHive go to Integrations → Lovable and paste it into Webhook URL. Make sure the function is public — JWT verification off — because BlazeHive authenticates with the HMAC signature, not a Supabase token.

https://<project>.supabase.co/functions/v1/blazehive-webhook

3. Set a signing secret

Generate any long random string. Paste it into the Signing Secret field in BlazeHive and into your Edge Function secrets as BLAZEHIVE_WEBHOOK_SECRET. The two must match — if they differ, every delivery is rejected with a 401.

4. Test the connection

Hit Test connection in BlazeHive. It sends a test event and expects a 2xx back. Once that passes you are done — every page BlazeHive generates from then on lands on your site automatically.

What you end up with

  • Every page BlazeHive generates is delivered to your site automatically, signed and retried on failure.
  • Pages are stored in your own database and served from your own domain — you own the content and the URLs.
  • Each page is server-rendered with its title, meta description, canonical, and structured data, and added to your sitemap.
  • Republished pages update in place instead of creating duplicates.

Want the underlying contract — every payload field, the landing-page block model, and the rendering rules? See the webhook reference.