Run BlogRun BlogAIDocs
Dashboard

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. Use widget.v1.js to pin to this version. widget.js always points to the latest and may change.
Always use versioned URLs (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 KB

A 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>
AttributeRequiredDescription
data-site-idyesYour site UUID. Find it under Sites → Settings → Integrations.
data-api-urlnoAPI origin (no trailing slash). Defaults to the same origin the widget script is served from.
data-post-urlnoURL template for card links — {slug} is replaced per post. E.g. https://yoursite.com/blog/{slug}.
data-themenolight (default) or dark.
data-limitnoNumber of posts to display (default 6).
data-langnoForce a specific language code, e.g. es. Defaults to navigator.language.
data-lang-switchnoAdd this attribute (no value needed) to show a language switcher above the grid.
data-languagesnoComma-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.pathname automatically — 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:

ValueBehaviour
rewrite (default)Creates or overwrites all OG, Twitter, title, canonical, and JSON-LD tags.
mergeOnly sets tags that don't already exist — safe for sites with their own base SEO setup.
noneSkips all <head> injection entirely. Render only, no SEO changes.

JSON API

v1

Use 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

GET/api/v1/public/sites/{site_id}/posts

Returns a paginated list of published posts for a site.

ParamTypeRequiredDescription
limitintegernoNumber of posts to return (default 10, max 100).
offsetintegernoPagination offset (default 0).
langstringnoBCP-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

GET/api/v1/public/sites/{site_id}/posts/{slug}

Returns the full post body including HTML, markdown, and SEO fields.

ParamTypeDescription
langstringBCP-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

FieldTypeDescription
slugstringURL-safe identifier.
titlestringPost headline.
meta_titlestringSEO <title> tag (≤60 chars, keyword-optimised). Falls back to title.
descriptionstringAuto-generated 160-char summary.
content_htmlstringFull 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_markdownstringFull article as Markdown.
hero_image_urlstring | nullFeatured image URL.
canonical_urlstringCanonical URL (set via Post URL template in Integrations).
schema_orgobjectAuto-generated JSON-LD — schema.org Article (or FAQPage when the post answers FAQ-style questions), ready for <script type='application/ld+json'>.
site_namestringYour site's display name.
author_namestringAuthor display name (set in Sites → Settings).
author_picture_urlstring | nullAuthor avatar URL.
created_atISO 8601Publication timestamp.
primary_languagestringLanguage the original post was generated in.
available_languagesstring[]All language codes with completed translations, including primary.
is_translatedbooleantrue 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

GET/api/v1/public/sites/{site_id}/posts/{slug}/meta

Lightweight 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

GET/api/v1/public/sites/{site_id}/feed.xml

Returns 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

GET/api/v1/public/sites/{site_id}/sitemap.xml

Returns 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

StatusMeaning
404 Not FoundThe site_id or slug does not exist, or the post is not published.
200 with primary contentFor 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 RequestsRate 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 }} />
    </>
  );
}
Set Post URL template in Sites → Integrations → API tab to your blog URL pattern (e.g. 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=es

When 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=fr

The 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_translatedtrue when 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.xml

Add 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.xml

Submit 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.

  1. In WordPress, go to Users → Profile → Application Passwords and create a new application password.
  2. In Sites → Integrations → WordPress, enter your WordPress URL, username, and the application password (not your login password).
  3. Save. New posts generated for this site will publish to WordPress automatically.
Posts are published with 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.

Questions? Reach us at [email protected]Go to Dashboard