Publishing integrations
Every blog post you generate can be published anywhere — your website, WordPress, Zapier automations, RSS readers, or any custom frontend. Here's how each integration works.
Site-specific snippets with your site ID pre-filled are available under Sites → Integrations.
Security model
All public API endpoints are open — no token or authentication required. Your published posts are accessed using your site_id. Since the content is intentionally public (blog posts), no secret is needed.
All endpoints return Access-Control-Allow-Origin: * so they can be called from any browser or server without CORS issues.
Your site_idis a UUID that identifies your site's content. It isn't a secret — your blog posts are meant to be public.
Site-specific snippets with your real site ID pre-filled are available under Sites → Settings → Integrations — open it here →
Versioning
The public API and widget are versioned so existing integrations never break when new versions are released.
- API — current version is
v1. All endpoints are prefixed with/api/v1/public/. - Widget — current version is
v1. Usewidget.v1.jsto pin to this version.widget.jsalways points to the latest and may change.
widget.v1.js, /api/v1/public/) in production. The unversioned widget.js alias is useful for quick tests but may introduce breaking changes on updates.JS widget
~4 KBA single widget.v1.js script handles two modes: a card grid of posts (data-blog-widget) and a full article renderer (data-blog-post). Drop the script on any HTML page — no build step, no dependencies.
Post listing — card grid
Renders a responsive card grid of your latest posts. The API is open — no token or authentication required.
<div
data-blog-widget
data-site-id="YOUR_SITE_ID"
data-api-url="https://airun.blog"
data-post-url="https://yoursite.com/blog/{slug}"
data-limit="6"
data-theme="light"
></div>
<script src="https://airun.blog/widget.v1.js" defer></script>| Attribute | Required | Description |
|---|---|---|
| data-site-id | yes | Your site UUID. Find it under Sites → Settings → Integrations. |
| data-api-url | no | API origin (no trailing slash). Defaults to the same origin the widget script is served from. |
| data-post-url | no | URL template for card links — {slug} is replaced per post. E.g. https://yoursite.com/blog/{slug}. |
| data-theme | no | light (default) or dark. |
| data-limit | no | Number of posts to display (default 6). |
| data-lang | no | Force a specific language code, e.g. es. Defaults to navigator.language. |
| data-lang-switch | no | Add this attribute (no value needed) to show a language switcher above the grid. |
| data-languages | no | Comma-separated language codes shown in the switcher, e.g. en,es,fr. Used with data-lang-switch. |
Use Sites → Integrations → Widget → Preview to see your posts rendered in the dashboard without any local setup.
Blog post page — article renderer
Renders a full article on your post page. Place this on the page that matches your data-post-url template (e.g. /blog/[slug]). The widget reads the slug from the last URL path segment automatically.
<div data-blog-post data-site-id="YOUR_SITE_ID" data-api-url="https://airun.blog" data-theme="light" ></div> <script src="https://airun.blog/widget.v1.js" defer></script>
- Slug is read from
window.location.pathnameautomatically — no extra config for standard/blog/[slug]URLs. - Use
data-slug="my-override"if the slug is not the last URL segment. - Automatically injects
document.title,<link rel="canonical">, JSON-LD,og:title,og:description,og:image, and Twitter card tags into<head>. - When the post has multiple translations, a language switcher is shown automatically at the bottom. Add
data-lang-switch="false"to hide it.
SEO injection mode
Control how the article renderer interacts with existing <head> tags via data-seo-mode:
| Value | Behaviour |
|---|---|
| rewrite (default) | Creates or overwrites all OG, Twitter, title, canonical, and JSON-LD tags. |
| merge | Only sets tags that don't already exist — safe for sites with their own base SEO setup. |
| none | Skips all <head> injection entirely. Render only, no SEO changes. |
JSON API
v1Use the REST API to build any custom integration — Next.js, PHP, Vue, static site generators, or anything that can make HTTP requests. All endpoints are public (no auth required) and return Access-Control-Allow-Origin: *.
Recommended over the widget — server-side rendered pages don't require JavaScript, so search engine crawlers index your content reliably.
What you need for a good blog + SEO
Create two pages on your site:
- Blog index (e.g.
/blog) — fetch the posts list and render cards/links using the List posts endpoint. - Article page (e.g.
/blog/[slug]) — fetch the full post and render it using the Get full post endpoint.
On each page, populate <title>, <meta name="description">, and OG / Twitter meta tags using meta_title, description, and hero_image_url from the API response.
Structured data is generated for you. The schema_orgfield is a ready-made JSON-LD block — no hand-authoring needed. It's a schema.org Article by default and automatically becomes a FAQPage when the post answers FAQ-style questions, which can earn rich FAQ results in Google. Just drop it into a single <script type="application/ld+json"> tag.
Sitemap — serve the sitemap from your own domain (e.g. https://yoursite.com/sitemap.xml) and submit that URL to Google Search Console → Sitemaps. Google indexes sitemaps best when they live on the same hostname as the URLs they list. See RSS & Sitemap below for a one-line Next.js rewrite that proxies our upstream endpoint to /sitemap.xml on your domain.
Then add a Sitemap: line to your robots.txt so crawlers can discover it automatically: Sitemap: https://yoursite.com/sitemap.xml.
RSS (recommended) — expose the feed on your own domain too (e.g. https://yoursite.com/feed.xml) and advertise it in your <head> with <link rel="alternate" type="application/rss+xml"> so readers, Feedly, and email tools can subscribe. See RSS & Sitemap for the proxy setup.
1. List posts
/api/v1/public/sites/{site_id}/postsReturns a paginated list of published posts for a site.
| Param | Type | Required | Description |
|---|---|---|---|
| limit | integer | no | Number of posts to return (default 10, max 100). |
| offset | integer | no | Pagination offset (default 0). |
| lang | string | no | BCP-47 language code (e.g. es). Returns translated titles and descriptions when available. |
curl "https://airun.blog/api/v1/public/sites/YOUR_SITE_ID/posts?limit=10&offset=0"
Response
{
"items": [
{
"slug": "10-tips-for-remote-work-a1b2c3",
"title": "10 Tips for Remote Work",
"description": "Working from home effectively requires discipline and the right setup...",
"hero_image_url": "https://images.airun.blog/hero/remote-work.jpg",
"created_at": "2024-05-15T10:30:00Z"
}
],
"total": 42,
"languages": ["en", "es"],
"author_name": "Jane Smith",
"author_picture_url": "https://images.airun.blog/authors/jane.jpg"
}Cache: 5 minutes with stale-while-revalidate. Suitable for ISR with revalidate: 300.
2. Get full post
/api/v1/public/sites/{site_id}/posts/{slug}Returns the full post body including HTML, markdown, and SEO fields.
| Param | Type | Description |
|---|---|---|
| lang | string | BCP-47 code. When provided and a translation exists, title / content_html / description are replaced with the translated version. Falls back to primary language if translation unavailable. |
curl "https://airun.blog/api/v1/public/sites/YOUR_SITE_ID/posts/my-post-slug"
Response schema
| Field | Type | Description |
|---|---|---|
| slug | string | URL-safe identifier. |
| title | string | Post headline. |
| meta_title | string | SEO <title> tag (≤60 chars, keyword-optimised). Falls back to title. |
| description | string | Auto-generated 160-char summary. |
| content_html | string | Full article as sanitised HTML. Includes the title <h1> and hero image — no need to render title or hero_image_url separately above the article. |
| content_markdown | string | Full article as Markdown. |
| hero_image_url | string | null | Featured image URL. |
| canonical_url | string | Canonical URL (set via Post URL template in Integrations). |
| schema_org | object | Auto-generated JSON-LD — schema.org Article (or FAQPage when the post answers FAQ-style questions), ready for <script type='application/ld+json'>. |
| site_name | string | Your site's display name. |
| author_name | string | Author display name (set in Sites → Settings). |
| author_picture_url | string | null | Author avatar URL. |
| created_at | ISO 8601 | Publication timestamp. |
| primary_language | string | Language the original post was generated in. |
| available_languages | string[] | All language codes with completed translations, including primary. |
| is_translated | boolean | true when the response contains a translation (lang param was matched). |
Example response
{
"slug": "10-tips-for-remote-work-a1b2c3",
"title": "10 Tips for Remote Work",
"meta_title": "10 Remote Work Tips to Stay Productive",
"description": "Working from home effectively requires discipline and the right setup...",
"content_html": "<h2>Create a dedicated workspace</h2><p>...</p>",
"content_markdown": "## Create a dedicated workspace\n\n...",
"hero_image_url": "https://images.airun.blog/hero/remote-work.jpg",
"canonical_url": "https://yoursite.com/blog/10-tips-for-remote-work-a1b2c3",
// Article by default; becomes a FAQPage when the post answers FAQ-style questions
"schema_org": {
"@context": "https://schema.org",
"@type": "Article",
"headline": "10 Tips for Remote Work",
"description": "Working from home effectively requires discipline...",
"inLanguage": "en",
"wordCount": 1240,
"datePublished": "2024-05-15T10:30:00Z",
"dateModified": "2024-05-15T10:30:00Z",
"image": "https://images.airun.blog/hero/remote-work.jpg",
"author": { "@type": "Person", "name": "Jane Smith" },
"publisher": { "@type": "Organization", "name": "My Blog" }
},
"site_name": "My Blog",
"author_name": "Jane Smith",
"author_picture_url": "https://images.airun.blog/authors/jane.jpg",
"created_at": "2024-05-15T10:30:00Z",
"primary_language": "en",
"available_languages": ["en", "es"],
"is_translated": false
}No duplicate title/hero: content_html already contains the article <h1> and hero <img> — rendering title or hero_image_url above the article body will duplicate them.
Cache: 1 hour with stale-while-revalidate. Set Post URL template in Sites → Integrations to populate canonical_url and sitemap URLs.
3. SEO metadata only
/api/v1/public/sites/{site_id}/posts/{slug}/metaLightweight endpoint with only the SEO fields — no HTML body. Use in edge middleware or SSR to inject <head> tags without fetching the full post.
curl "https://airun.blog/api/v1/public/sites/YOUR_SITE_ID/posts/my-post-slug/meta"
Response
{
"title": "10 Tips for Remote Work",
"meta_title": "10 Remote Work Tips to Stay Productive",
"description": "Working from home effectively...",
"hero_image_url": "https://images.airun.blog/hero/remote-work.jpg",
"canonical_url": "https://yoursite.com/blog/10-tips-for-remote-work-a1b2c3",
"schema_org": { "@context": "https://schema.org", "@type": "Article", "..." }
}Use meta_title for the <title> and og:title, and schema_org for the JSON-LD block.
Accepts the same ?lang= query parameter as the full-post endpoint.
4. RSS feed
/api/v1/public/sites/{site_id}/feed.xmlReturns an RSS 2.0 feed (Content-Type: application/rss+xml). Up to 20 most recent posts. Cache: 1 hour. RSS is single-language, so each language is its own feed: the default feed is your primary language, and ?lang=es returns a per-language feed containing only posts available in that language (no untranslated fallback items). The default feed advertises every available language via <atom:link rel="alternate" hreflang> so readers can discover them.
5. Sitemap
/api/v1/public/sites/{site_id}/sitemap.xmlReturns an XML sitemap with up to 1 000 posts. Cache: 1 hour. The URLs inside the sitemap already point at your domain (via your Post URL template), so the recommended setup is to serve the sitemap from your domain too — see below.
Serve via your domain (recommended)
Add a rewrite so that https://yoursite.com/sitemap.xml proxies our upstream endpoint. Submit that URL — not the airun.blog one — to Google Search Console. Proxying /feed.xml is optional; RSS readers and automation tools (Zapier, Make, Feedly, etc.) work fine pointing directly at the airun.blog URL.
Next.js (next.config.js)
module.exports = {
async rewrites() {
return [
{
source: "/sitemap.xml",
destination:
"https://airun.blog/api/v1/public/sites/YOUR_SITE_ID/sitemap.xml",
},
// optional — only if you want /feed.xml on your domain too
{
source: "/feed.xml",
destination:
"https://airun.blog/api/v1/public/sites/YOUR_SITE_ID/feed.xml",
},
];
},
};robots.txt
User-agent: * Allow: / Sitemap: https://yoursite.com/sitemap.xml
Submit the URL on your own domain to Google Search Console → Sitemaps (https://yoursite.com/sitemap.xml), not the airun.blog upstream. The sitemap and the URLs inside it should share a hostname.
Error responses
| Status | Meaning |
|---|---|
| 404 Not Found | The site_id or slug does not exist, or the post is not published. |
| 200 with primary content | For the post and listing endpoints, when ?lang= is requested but no translation exists, the primary-language content is returned (no 404). The RSS feed instead omits posts not available in the requested language. |
| 429 Too Many Requests | Rate limit exceeded. Retry after the Retry-After header. |
Next.js example
// app/blog/page.tsx — listing with ISR
export const revalidate = 300;
export default async function BlogPage() {
const res = await fetch(
"https://airun.blog/api/v1/public/sites/SITE_ID/posts?limit=10",
{ next: { revalidate: 300 } }
);
const { items } = await res.json();
return (
<ul>
{items.map((p) => (
<li key={p.slug}><a href={`/blog/${p.slug}`}>{p.title}</a></li>
))}
</ul>
);
}
// app/blog/[slug]/page.tsx — post with JSON-LD + canonical
export async function generateStaticParams() {
const res = await fetch(
"https://airun.blog/api/v1/public/sites/SITE_ID/posts?limit=100"
);
const { items } = await res.json();
return items.map((p) => ({ slug: p.slug }));
}
export default async function PostPage({ params }) {
const { slug } = await params;
const res = await fetch(
`https://airun.blog/api/v1/public/sites/SITE_ID/posts/${slug}`
);
const post = await res.json();
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(post.schema_org) }}
/>
<link rel="canonical" href={post.canonical_url} />
{/* content_html already includes the <h1> title and hero image */}
<article dangerouslySetInnerHTML={{ __html: post.content_html }} />
</>
);
}https://yoursite.com/blog/{slug}). This populates canonical_url in API responses and generates correct sitemap URLs.Multilingual
Every post can be generated in any language and automatically translated into multiple target languages. Translations are available through the same public API endpoints via a lang query parameter.
Fetching a specific language
GET /api/v1/public/sites/{site_id}/posts/{slug}?lang=esWhen a ?lang= code is provided, the response fields (title, content_html, description) are replaced with the translated version if a completed translation exists. If no translation is available for the requested language, the original content is returned as a fallback.
GET /api/v1/public/sites/{site_id}/posts?lang=frThe list endpoint also accepts ?lang= — post titles and descriptions are substituted with their translations when available.
Response fields
The single-post endpoint always includes these language fields:
{
"slug": "my-post-a1b2c3d4",
"title": "Mi artículo en español",
"content_html": "...",
"primary_language": "en",
"is_translated": true,
"available_languages": ["en", "es", "fr"],
...
}primary_language— the language the original article was generated in.available_languages— all language codes ready to fetch, including the primary. Use this to render a language switcher.is_translated—truewhen the returned content is a translation (i.e. a?lang=was requested and matched).
Widget language settings
The widget auto-detects the visitor's language from navigator.language and requests the matching translation automatically. You can override this with data-lang:
<!-- Always render in Spanish --> <div data-blog-post data-site-id="YOUR_SITE_ID" data-api-url="https://airun.blog" data-lang="es" ></div>
When a post has available_languages with 2 or more entries, the article renderer automatically shows a language switcher at the bottom of the post. Clicking a language reloads the article in that language and updates all SEO tags (og:title, og:description, og:locale, and hreflang alternate links).
Next.js multi-locale example
// app/[locale]/blog/[slug]/page.tsx
export async function generateStaticParams() {
const res = await fetch(
"https://airun.blog/api/v1/public/sites/SITE_ID/posts?limit=100"
);
const { items } = await res.json();
const locales = ["en", "es", "fr"];
return locales.flatMap((locale) =>
items.map((p) => ({ locale, slug: p.slug }))
);
}
export default async function PostPage({ params }) {
const { locale, slug } = await params;
const res = await fetch(
`https://airun.blog/api/v1/public/sites/SITE_ID/posts/${slug}?lang=${locale}`
);
const post = await res.json();
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(post.schema_org) }}
/>
{/* content_html already includes the <h1> title and hero image */}
<article dangerouslySetInnerHTML={{ __html: post.content_html }} />
</>
);
}RSS feed & sitemap
Every site gets an RSS 2.0 feed and a sitemap XML file. Submit the sitemap to Google Search Console to help Google discover your posts.
RSS feed
GET /api/v1/public/sites/{site_id}/feed.xmlAdd to WordPress (Settings → Reading → RSS), Feedly, Mailchimp RSS campaigns, or any feed reader. Post links use your configured URL template if set, otherwise fall back to the hosted URL.
Sitemap XML
GET /api/v1/public/sites/{site_id}/sitemap.xmlSubmit this URL to Google Search Console → Sitemaps. Refreshes every hour.
WordPress auto-publish
Connect your WordPress site and every new post will be automatically published via the WordPress REST API.
- In WordPress, go to Users → Profile → Application Passwords and create a new application password.
- In Sites → Integrations → WordPress, enter your WordPress URL, username, and the application password (not your login password).
- Save. New posts generated for this site will publish to WordPress automatically.
status: "publish". If you want to review before publishing, set the post status to "draft" in WordPress before connecting.Webhook
Register a webhook URL and we'll POST a JSON payload every time a new post is saved. Works with Zapier, Make.com, n8n, Slack incoming webhooks, and any custom endpoint.
Payload
{
"slug": "my-post-a1b2c3d4",
"title": "My Post Title",
"description": "First 160 characters of plain text...",
"content_html": "<h2>...</h2><p>...</p>",
"hero_image_url": "https://images.airun.blog/image.jpg",
"created_at": "2024-01-15T10:30:00Z",
"translations": [
{
"language": "es",
"title": "Mi artículo en español",
"description": "Primeros 160 caracteres...",
"content_html": "<p>...</p>"
}
]
}Webhooks are fire-and-forget (10 s timeout, no retries). Set the URL in Sites → Integrations → Webhook.
Zapier example
Create a Zap: Webhooks by Zapier → Catch Hook, copy the URL into your site's Webhook field, then connect it to any Zapier action — post to Slack, tweet, send email, etc.
Make.com / n8n example
Use a Webhook trigger node, paste the generated URL into your site's Webhook field, then chain any downstream actions. The payload fields map directly to node outputs.