What you paste into BlazeHive
| Field in BlazeHive | Where it comes from | Looks like |
|---|---|---|
| Webhook URL | The Webhook URL: line in the agent's last message | https://<project>.supabase.co/functions/v1/blazehive-webhook |
| Signing Secret | The Secret: line in the agent's last message | Long random string |
Fields are under Integrations → Lovable in BlazeHive.
1. Give your Lovable agent this prompt
Copy it into the Lovable chat. The prompt covers the endpoint, signature verification, storing pages in the database you already use, and server-rendering them so search engines can read them.
654 lines. The agent does not need to open any links.
Show 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
Use the webhook integration when your stack has no native BlazeHive integration. On every publish, BlazeHive POSTs a JSON payload to your URL with the Markdown content, SEO metadata and JSON-LD structured data. Landing pages also include a block layout and a `rich_html` fragment that carries its own styles.
- **Signed**: HMAC-SHA256 over a timestamp and the raw body. The secret itself is never sent.
- **Retried**: network errors, timeouts and 429, 502, 503 and 504 responses are retried, with one delivery id for deduplication.
- **Versioned**: every payload carries `version: 2`. Breaking changes raise it.
## Setup
To have a coding agent (Cursor, Claude Code, Copilot) build the receiver, give it the agent prompt from https://www.blazehive.io/docs/webhooks/#setup first. It covers the endpoint, signature verification, storage and server-rendered pages for Laravel, Express, Django, Next.js, Rails or any other stack. It finishes by printing the URL and secret you paste in steps 2 and 3.
1. In app.blazehive.io, go to Integrations → Webhook.
2. Paste your endpoint URL into **Webhook URL**. It must be HTTPS and publicly reachable (private and internal addresses are blocked), and it must not redirect.
3. Paste a long random string into **Signing Secret**. It is required, and every request is signed with it.
4. Click "Save & 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 already have a page with this id. |
| `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 when you click "Save & Test Connection", or "Test" on a saved integration. Respond 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. It changes 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. Store it and use it in lists or the <title> tag. Don't render it as a heading: 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 use the comparison path, so you need no extra route.) |
| `url_path` | string | Recommended path on your site, e.g. "/faq/{slug}" or "/solutions/{slug}". |
| `content` | string | The page, as complete Markdown. 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 at that path. New types may be added later, so route on `url_path` instead of hard-coding these prefixes.
| type | url_path | Archive | What it is |
| --- | --- | --- | --- |
| `landing` | `/solutions/{slug}` | `/solutions/` | Landing page. The only type with rich_html and 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. No front matter, no shortcodes. Style the output with your own prose container, and give images `max-width: 100%; height: auto`. They are absolute BlazeHive CDN URLs, about 1700px wide. `rich_html` on landing pages is different: it includes its own scoped stylesheet.
> **Title vs. content:** `content` is the whole page and already contains its H1. Render it and nothing else. `title` is metadata: use it in an index, a card, a breadcrumb or the `<title>` tag. Printing `title` above `content` gives the page two H1s. The H1 is not the first line: pages open with a short `## TL;DR` summary on purpose, because 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>. Put it in 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 | Per-page revision number that only goes up. Clear your cached output when it changes. |
You can render a landing page three ways, from least to most work: output `rich_html` as-is, render `content` into your own template, or map `blocks` and `images` onto your own components.
### The block model
Every block has a `t` field 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 centered 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, and 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
Built by the same code that sends real deliveries. Long strings are shortened 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
Search engines only see a page once your server renders it at a crawlable URL. The route below 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.
// 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 empty for crawlers.
- **Never put `rich_html` in an iframe.** It is a fragment for your page body. Inside an iframe it carries no SEO weight for the parent page.
- **Do not escape rendered markdown or `rich_html`.** Both 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 so does `rich_html`. 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 does not collide with your site.** Every rule in `rich_html` is scoped under `.bh-landing`, including its reset.
## Archives & sitemaps
Two more things make the pages crawlable. The WordPress plugin sets up both. Add them to any custom integration.
### An archive page per type
Serve a listing page at each type's prefix (the Archive column above), for example `/comparisons/` or `/solutions/`, and link every published page of that type from it. Without it, each page is reachable only from the sitemap.
Order newest first, paginate past about 50 pages, and link each archive from a permanent place, such as a footer column.
### One sitemap per type, behind an index
Split the sitemap by page type instead of shipping one flat file. Each file stays small and its `lastmod` values stay accurate.
```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-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-comparison.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. It lists that type's archive and every published page of that type.
- `/sitemap-index.xml`: a `<sitemapindex>` that points at each child sitemap.
- `/sitemap.xml`: redirect it to the index. Crawlers try this path first.
> **Generate both at request time** from your stored pages, not as static files written on publish. New pages then show up without a deploy.
## 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, so you can route without parsing the body. |
| `X-Blazehive-Delivery` | Unique per delivery and the same across retries. Use it to deduplicate. |
| `X-Blazehive-Timestamp` | Unix seconds when the delivery was signed. |
| `X-Blazehive-Signature` | The HMAC signature. 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 and 504 responses are retried automatically: up to 3 attempts with a short backoff, all with the same `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 URLs 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. Webhook URL: <url>
Secret: <secret> 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. Turn JWT verification off for the function. BlazeHive authenticates with the HMAC signature, not a Supabase token.
https://<project>.supabase.co/functions/v1/blazehive-webhook 3. Paste the signing secret
When the agent finishes, it prints a Secret: line and saves the same value in your Edge Function secrets as BLAZEHIVE_WEBHOOK_SECRET. Paste it into the Signing Secret field in BlazeHive. The two must match. If they differ, every delivery is rejected with a 401.
Secret name
BLAZEHIVE_WEBHOOK_SECRET 4. Test the connection
Click Save & Test Connection in BlazeHive. It sends a test event and expects a 2xx back. When the test passes, the connection is set up. Choose when pages are sent under After you connect.
After you connect
Your Lovable connection shows a status: Active, Error or Not tested. Use Test to run the check again, Edit to change a value, and Disconnect to remove it.
Choose how pages go out from the Auto-publish dropdown on the dashboard:
- Off: nothing is sent until you publish a page yourself.
- Auto (Draft): pages arrive as drafts.
- Auto (Live): pages go live.
What you end up with
- BlazeHive sends pages to your site signed, and retries deliveries that fail on network errors or timeouts.
- Pages are stored in your database and served from your domain.
- 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.
The webhook reference lists every payload field, the landing-page block model and the rendering rules.