Docs
Lovable
Publish articles and topical-map inner pages into your Lovable app. One prompt makes Lovable build the blog, page routes, SEO tags, and the endpoint.
How it works
Your Lovable app gets a blog section and a small receiving endpoint, both built by Lovable itself from a prompt we give you. When you click Publish on an article, blogr.ai sends it to that endpoint as a single JSON POST. Your app stores the post and serves it at /blog/{slug} with proper SEO meta tags — no plugin, no SDK, nothing from Lovable's roadmap needed.
Let Lovable build the blog
Copy this prompt into your Lovable app's chat. It creates the posts table, the blog listing and post pages with SEO meta tags and Article structured data, and the secured POST /api/articles endpoint. The full payload contract below is baked in.
I use blogr.ai to write and publish SEO articles for my website. Give this app a complete blog section that blogr.ai publishes into automatically, plus a way to receive the site pages blogr.ai designs (pricing, comparisons, services, locations) outside the blog. Use this app's existing backend (Lovable Cloud or Supabase), design system, and conventions.
1. Blog storage
- Create a blog_posts table (or reuse one if this app already has it) with columns for: blogr_id (number, unique), type ("post" or "page"), title, slug (unique among posts; two pages may share a slug, since a page is identified by its path), path (nullable, the site path of a page such as "/pricing/" or "/compare/acme-alternatives/", unique among pages), content_html, content_markdown, layout ("article" or "sections"), blocks (JSON), page_html, page_css, meta_title, meta_description, cover_image_url, cover_image_alt, categories, tags, published_at.
- Posts and pages are managed only through the endpoint below, so no admin UI is needed.
2. Public blog pages
- A blog index at /blog listing posts (type "post") newest first, showing each post's cover image, title, and meta description.
- A post page at /blog/{slug} that renders title as the page's <h1> and content_html below it as-is (it is already sanitized and contains no H1: the body starts with the introduction and uses H2s for sections). Set the page title from meta_title (fall back to title), the description meta tag from meta_description, the cover image as the og:image, and add Article JSON-LD structured data. These SEO tags must be present in the served HTML.
- Link the blog index from the site's navigation or footer.
3. Site pages
- A row with type "page" is a site page, not a blog post. Serve it at its "path" exactly, outside /blog ("/pricing/" at /pricing, "/compare/acme-alternatives/" at /compare/acme-alternatives), rendering title as the <h1> and content_html below it, with the same SEO tags as a post but WebPage JSON-LD instead of Article. Never list pages on the blog index.
- If a hand-built route already exists at a page's path, keep the hand-built route and skip rendering the page there.
Section layouts:
- "layout" is "article" for every post and for pages written as one body: render "content_html" as described. It is "sections" for a page built as a designed landing page (pricing, comparison, service pages). Then "blocks" holds its sections, "page_html" the same sections rendered to HTML, and "page_css" the stylesheet for that HTML; all three are null otherwise. Older deliveries may not carry these four fields at all: treat a missing "layout" as "article" and missing "blocks", "page_html", and "page_css" as null, so the same code handles both.
- Render a "sections" page from "blocks.sections", in order, with this app's own components and design system, under the title as the page's only <h1>. Section headings are <h2>, item titles <h3>. Keep "content_html" only as the fallback for search and feeds.
- "blocks" looks like {"version": 1, "sections": [{"type": "hero", "heading": "Pricing", "body": "Plans for every team size.", "items": [], "table": {"columns": [], "rows": []}, "cta": {"label": "Start now", "href": "https://example.com/signup/"}, "secondary_cta": null, "image": null}]}. Every section has those same fields. "body" fields (on the section and on its items) are Markdown; every other field is plain text. "items" entries have title, body, value, and source_url. "cta" and "secondary_cta" are {label, href} or null; "image" is {url, alt} or null.
- By type: hero is the page lead (heading is a short kicker above the lead, never a second <h1>; body is the lead paragraph; cta and secondary_cta are buttons). features is a card grid of items. steps is a numbered sequence of items. stats shows each item's value as a big number, title as its label, body as a note, and links source_url. comparison is a table whose first column holds the criteria and whose second column is this business. pricing is plan cards (title the plan, value the price, body what it includes) with the section's cta under them. proof is quotes (body the quote, title the person, value their role). faq is an accordion (title the question, body the answer). text is a heading with Markdown prose. cta is a closing band with heading, body, and the cta button. Any section may carry an image: show it below the section's content. Skip a section type you do not recognize.
- If building components for this is not practical, output "page_html" as-is below the <h1> and add "page_css" to that page: every rule in it is scoped under .blogr-page, so it never affects the rest of the site.
4. Make every page reachable
A page nothing links to is one visitors never find and search engines rank poorly, so no page may be reachable only by typing its URL.
- Directory pages: the parent path of every page must answer (/compare/ for /compare/acme-alternatives/). When a page with type "page" sits at that path, it is the hub: render it, then list the pages under it below its body, each as its title linking to its path with its meta description. When no page sits there yet, serve a generated index at that path listing the pages under it, titled from the path segment ("Compare"), with its own title and description meta tags. A hub that arrives later takes over the generated index.
- Navigation: add a footer section linking each top-level directory once ("Compare" to /compare/, "Services" to /services/), and single pages such as /pricing/ directly. Build it from the stored rows, so a newly delivered page shows up without a code change.
- Breadcrumbs: show them on every page and post, built from the path (Home / Compare / Acme alternatives, or Home / Blog / post title), each crumb linking to the directory page above, with BreadcrumbList JSON-LD.
- Sitemap: serve /sitemap.xml from the backend (an edge function if the app has no server) listing the home page, the blog index, every post, every page, and every directory page, with published_at as the last-modified date. Reference it from robots.txt.
- Links inside content_html point at other posts and pages on this site: render them as they are. blogr.ai re-delivers an older article when it adds a link to a page that went live later, so a repeat delivery must replace the stored body.
5. The receiving endpoint
- A backend endpoint at POST /api/articles that blogr.ai calls with a JSON body.
- Every request carries my access token in an "Authorization: Bearer" header. Store the expected token as a backend secret named BLOGR_ACCESS_TOKEN. Ask me for the value; I will copy it from the Lovable card in blogr.ai. Respond 401 when the header is missing or wrong. Never hardcode the token.
- Test deliveries carry "test": true with a sample article. Real deliveries carry "test": false.
The JSON body:
{
"event": "article.published (a blog post) or page.published (a site page)",
"test": false,
"article": {
"id": 123,
"type": "post or page",
"title": "Article title",
"slug": "article-title",
"path": "null for a post; the site path for a page, e.g. /compare/acme-alternatives/",
"content_markdown": "Markdown body, no H1...",
"content_html": "<p>HTML body, no H1...</p>",
"layout": "article, or sections for a page built as a designed layout",
"blocks": null,
"page_html": null,
"page_css": null,
"seo": {
"title": "SEO title, or null",
"meta_description": "Meta description, or null"
},
"og_image_url": "https://... cover image, or null",
"og_image_alt": "Image alt text, or null",
"target_keyword": "keyword, or null",
"categories": ["Category"],
"tags": ["tag"],
"published_at": "2026-01-01T00:00:00+00:00"
},
"website": {
"domain": "example.com"
}
}
What the endpoint must do:
1. Check the bearer token and respond 401 on a mismatch.
2. Respond 200 to test deliveries ("test": true) without saving anything.
3. For real deliveries, upsert the row by "article.id" (stored as blogr_id) so a repeat delivery updates the existing row instead of duplicating it. When no row has that id yet, match a post on "article.slug" and a page on "article.path", never a page on its slug: a page's slug is only the last segment of its path, so /services/seo/ and /industries/seo/ share it. Store "article.type", "article.path", "article.layout", "article.blocks", "article.page_html", and "article.page_css".
4. Respond 200 with a JSON body of {"url": "..."}, which blogr.ai saves as the live URL. For a post that is https://<this app's public domain>/blog/<slug>; for a page it is https://<this app's public domain><path>.
5. Respond fast (blogr.ai times out after 15 seconds), and only respond non-2xx when something really failed.
When you are done, tell me the full public URL of the POST /api/articles endpoint, so I can paste it into the Lovable card in blogr.ai, and the URL of the sitemap.
Connect your Lovable app
- In your dashboard, open Setup → Publishing and choose Lovable. We generate your access token there.
- Copy the prompt above into Lovable. When Lovable asks for the BLOGR_ACCESS_TOKEN secret, paste the access token from the Lovable card.
- Lovable tells you the endpoint URL when it's done — something like https://your-app.lovable.app/api/articles. Paste it into the endpoint field. Your app must be published, since blogr.ai can only deliver to public https URLs.
- Click Send test to deliver a sample article, then Save.
A website publishes to one destination: connecting Lovable replaces any other integration on that website.
The delivery request
Every delivery — test or real — is an HTTP POST with these headers:
POST /api/articles HTTP/1.1
Host: your-app.lovable.app
Authorization: Bearer <your access token>
Content-Type: application/json
Accept: application/json
- Redirects are not followed — the endpoint URL must respond directly.
- Deliveries time out after 15 seconds. The endpoint should store the post and respond right away.
- Any 2xx response counts as delivered. Anything else — or no response — marks the article as failed to publish.
The payload
The request body is one JSON object — the same contract as our Webhooks integration:
{
"event": "article.published",
"test": false,
"article": {
"id": 812,
"type": "post",
"title": "How to Choose a Laravel Hosting Provider",
"slug": "how-to-choose-a-laravel-hosting-provider",
"path": null,
"content_markdown": "Picking a host…\n\n## What to compare first…",
"content_html": "<p>Picking a host…</p>\n<h2>What to compare first</h2>…",
"seo": {
"title": "How to Choose a Laravel Hosting Provider (2026 Guide)",
"meta_description": "Compare Laravel hosting providers on speed, pricing, and deploys."
},
"og_image_url": "https://blogr.ai/storage/articles/laravel-hosting-9f2c41ab.png",
"og_image_alt": "How to Choose a Laravel Hosting Provider (2026 Guide) — laravel hosting",
"target_keyword": "laravel hosting",
"categories": ["Hosting", "Laravel"],
"tags": ["laravel hosting"],
"published_at": "2026-07-08T09:30:00+00:00"
},
"website": {
"domain": "example.com"
}
}
content_html is sanitized and ready to render as-is. It is the body only, with no H1: the prompt tells Lovable to render title as the page's <h1> above it. article.id is stable across retries — the endpoint Lovable builds upserts on it, so a repeat delivery updates the post instead of duplicating it. The field-by-field reference on the Webhooks page applies to Lovable deliveries unchanged.
Inner pages from your topical map (pricing, comparisons, services, locations) arrive the same way with event page.published, type "page", and path set to where the page belongs on your site, such as /compare/acme-alternatives/. The prompt above tells Lovable to serve those rows at their path, outside /blog. If you built your app with an earlier version of the prompt, paste the current prompt into Lovable again before publishing pages, or they would be stored as blog posts.
Pages built as designed landing pages carry layout "sections" with their sections in blocks, and the prompt tells Lovable to build them with your app's own components. See Page layouts below for the fields. Apps built from an earlier prompt keep showing those pages as plain HTML until you paste the current prompt again.
Page layouts
Some inner pages are built as designed landing pages rather than one body of text: a hero with buttons, feature cards, numbered steps, a comparison table, an FAQ, and a closing call to action. Those arrive with article.layout set to sections. Every post, and every page written as one body, says article, and the three fields below are null. Content sent before layouts existed carries none of these fields: treat a missing layout as article.
| Field | Meaning |
|---|---|
| article.layout | article or sections. |
| article.blocks | The sections as data, in page order: {"version": 1, "sections": [...]}. Build them with your own components for a page that matches your design. |
| article.page_html | The same sections rendered to HTML inside one .blogr-page element, with no H1. Output it under your own <h1> title. |
| article.page_css | The stylesheet that designs page_html. Every rule is scoped under .blogr-page, so adding it never restyles the rest of your site. Fonts and text color come from your site. |
Every section has the same fields: type, heading, body, items (each with title, body, value, and source_url), table (columns and rows), cta and secondary_cta (a label and href, or null), and image (a url and alt, or null). The body fields are Markdown; the rest are plain text. The types are hero, features, steps, stats, comparison, pricing, proof, faq, text, and cta. Skip a type you do not recognize, since more may be added.
article.content_html stays filled in for sections pages too: it is the same page as plain HTML, for search, feeds, or a receiver that ignores layouts.
Test deliveries
The Send test button on the setup screen delivers a fake article with the exact shape shown above, flagged with "test": true. The endpoint Lovable builds responds 200 to test deliveries without saving anything, so nothing test-shaped ever shows up on your blog.
Responding to a delivery
Respond with any 2xx status code to confirm the delivery. When the JSON response includes a url, blogr.ai records it as the article's published URL and links to it from your workspace:
{
"url": "https://your-app.lovable.app/blog/how-to-choose-a-laravel-hosting-provider"
}
Failed deliveries and retries
If your app is unreachable or responds with a non-2xx status, the article is marked "Failed to publish" in your workspace with the reason on the article row — for example "Your Lovable app responded 500". Nothing is lost: paste the error into Lovable's chat to fix the endpoint, then click Retry publish to deliver the article again.
Securing the endpoint
- The endpoint verifies the Authorization header on every request against the BLOGR_ACCESS_TOKEN secret and rejects anything else with a 401.
- The token is generated for you and stored encrypted — treat it like a password. Rotate it anytime by entering a new value in Setup → Publishing and updating the secret in Lovable.
- Keep the token in Lovable's secrets manager, never in the app's frontend code — frontend code is visible to every visitor.