Get a released blog

GET /api/consumer/blogs/{blogId}

This is the everything endpoint: one call returns the complete payload needed to render a finished blog post page end to end — markdown body, frontmatter, SEO title and description, JSON-LD schema, every uploaded media asset (cover image included), and a set of topically-related released posts for an interlinking module. No follow-up requests are required.

Path parameters

NameTypeDescription
blogIdstringThe blog identifier returned as id from GET /api/consumer/blogs.

Headers

Same as the list endpoint: pass the workspace API key via x-api-key or Authorization: Bearer.

Response

200 OK with the released blog detail. 404 Not Found if the ID does not match a released blog in your workspace — either the ID is wrong, the blog was never released, or it belongs to a different workspace.

Response shape

type GetReleasedBlogResponse = {
  id: string;             // same as the blogId you requested
  releaseNotes: string | null;
  releasedAt: string;     // ISO 8601 timestamp
  version: {
    id: string;
    versionNumber: number;
    renderedTitle: string;
    renderedSlug: string;
    metaTitle: string | null;
    metaDescription: string | null;
    markdown: string;     // full article body
    frontmatter: Record<string, string> | null;
    structuredContent: unknown | null; // outline JSON, when available
    reviewSummary: string | null;
    schemaJsonLd: unknown | null;      // JSON-LD structured data for the post
    updatedAt: string;
    assets: Array<{
      id: string;
      assetRole: "cover_image" | "inline_image" | "diagram" | "illustration";
      publicUrl: string | null;
      signedUrl: string | null;
      altText: string | null;
      caption: string | null;
      width: number | null;
      height: number | null;
      metadata: unknown;
    }>;
  };
  related: Array<{
    id: string;           // blogId of the related post — pass it back to this endpoint
    releasedAt: string;
    version: {            // same summary shape as the list endpoint
      id: string;
      versionNumber: number;
      renderedTitle: string;
      renderedSlug: string;
      metaTitle: string | null;
      metaDescription: string | null;
      reviewSummary: string | null;
      updatedAt: string;
      assets: Array<{
        id: string;
        assetRole: "cover_image" | "inline_image" | "diagram" | "illustration";
        publicUrl: string | null;
        signedUrl: string | null;
        altText: string | null;
        caption: string | null;
        width: number | null;
        height: number | null;
      }>;
    };
  }>;
};

Field notes

  • markdown is the canonical body. It already contains inline image references using the asset URLs below — render it with any standard CommonMark / GFM renderer.
  • frontmatter mirrors the YAML-style metadata the editor signed off on (e.g. tags, category). Keys and values are both strings.
  • structuredContent is a structured outline of the article (typed as unknown so the schema can evolve). Use it if you want to render the outline as a TOC; ignore it if you only need the markdown.
  • schemaJsonLd is ready-to-embed JSON-LD structured data for the post. Serialise it into a <script type="application/ld+json"> tag in your page head to get rich results in search. It can be null for older releases — skip the tag when it is.
  • assets are uploaded media. Prefer publicUrl; fall back to signedUrl for buckets fronted by short-lived URLs. Filter by assetRole to find the cover image (cover_image is always at most one entry per release).
  • related contains up to 3 released posts from the same workspace, ranked by topical relevance (title, meta, and target-keyword overlap) — never just “latest N”. Each item uses the same summary shape as the list endpoint, including assets, so you can render a related-posts module with cover images and no extra requests. Only genuinely related posts are returned; the array is empty when there are none, so simply hide the section in that case.

Markdown features used

The markdown field uses GitHub-Flavored Markdown (GFM) in addition to CommonMark. Pick a renderer that supports GFM (e.g. react-markdown with remark-gfm, markdown-it with the gfm preset, or any renderer GitHub itself uses). You may encounter:

  • Tables for comparisons and criteria.
  • Task lists (- [ ], - [x]) for checklists.
  • Strikethrough (~~text~~).
  • Footnotes ([^1]) for inline citations.
  • Fenced code blocks with language tags (```ts, ```bash, etc.). Pair your renderer with a syntax highlighter (Prism, Shiki, Highlight.js) if you want highlighted code.
  • Callout-style blockquotes that lead with a bold label (> **Note:** …, > **Tip:** …, > **Warning:** …, > **Example:** …). These render as plain styled blockquotes on any CommonMark renderer; if you want stronger per-type styling (icon, background colour) you can detect the leading <strong> text in the blockquote and theme accordingly.

A vanilla CommonMark renderer will degrade gracefully — tables fall back to plain text rows, task lists become regular bullets, footnotes appear inline. For the cleanest reading experience, use GFM.

Example

curl -sS https://api.essel.ai/api/consumer/blogs/ck1234567890 
  -H "x-api-key: $ESSEL_API_KEY"
{
  "id": "ck1234567890",
  "releaseNotes": "First public release after editor sign-off.",
  "releasedAt": "2026-05-08T09:14:00.000Z",
  "version": {
    "id": "cv0987654321",
    "versionNumber": 1,
    "renderedTitle": "How ATS optimisation actually works in 2026",
    "renderedSlug": "ats-optimisation-2026",
    "metaTitle": "ATS optimisation in 2026",
    "metaDescription": "What today's ATS systems really score, and the practical levers that move the needle.",
    "markdown": "# How ATS optimisation actually works in 2026\n\nMost candidates...",
    "frontmatter": {
      "category": "career",
      "readingTime": "8 min"
    },
    "structuredContent": null,
    "reviewSummary": "Quality recommendation: ship.",
    "schemaJsonLd": {
      "@context": "https://schema.org",
      "@type": "BlogPosting",
      "headline": "How ATS optimisation actually works in 2026",
      "datePublished": "2026-05-08T09:14:00.000Z"
    },
    "updatedAt": "2026-05-08T09:13:42.000Z",
    "assets": [
      {
        "id": "ca555",
        "assetRole": "cover_image",
        "publicUrl": "https://content-pilot.s3.ap-south-1.amazonaws.com/ck1234567890/cover.png",
        "signedUrl": null,
        "altText": "Recruiter screen showing ATS scoring",
        "caption": null,
        "width": 1600,
        "height": 900,
        "metadata": null
      }
    ]
  },
  "related": [
    {
      "id": "ck2222222222",
      "releasedAt": "2026-04-30T11:02:00.000Z",
      "version": {
        "id": "cv1111111111",
        "versionNumber": 2,
        "renderedTitle": "Resume keywords that still matter in 2026",
        "renderedSlug": "resume-keywords-2026",
        "metaTitle": "Resume keywords in 2026",
        "metaDescription": "Which keywords ATS systems still weight, and which are noise.",
        "reviewSummary": "Quality recommendation: ship.",
        "updatedAt": "2026-04-30T11:01:12.000Z",
        "assets": [
          {
            "id": "ca777",
            "assetRole": "cover_image",
            "publicUrl": "https://content-pilot.s3.ap-south-1.amazonaws.com/ck2222222222/cover.png",
            "signedUrl": null,
            "altText": "Keyword highlighting on a resume",
            "caption": null,
            "width": 1600,
            "height": 900
          }
        ]
      }
    }
  ]
}

TypeScript helper

async function fetchReleasedBlog(blogId: string) {
  const response = await fetch(
    `https://api.essel.ai/api/consumer/blogs/${encodeURIComponent(blogId)}`,
    { headers: { "x-api-key": process.env.ESSEL_API_KEY! } },
  );

  if (response.status === 404) return null;
  if (!response.ok) throw new Error(`Failed: ${response.status}`);

  return (await response.json()) as GetReleasedBlogResponse;
}

Rendering a complete post page

Everything a finished blog page needs comes from this one response:

Page elementWhere it comes from
Page title / H1version.renderedTitle
URL slugversion.renderedSlug
<title> + meta tagsversion.metaTitle, version.metaDescription
Cover imageversion.assets entry with assetRole === "cover_image"
Article bodyversion.markdown (inline images already reference asset URLs)
Structured dataversion.schemaJsonLd<script type="application/ld+json">
Tags / categoryversion.frontmatter
Publish datereleasedAt
Related posts modulerelated (titles, slugs, and cover images included)

Updates and republishing

When an editor releases a new version of the same blog, the response under the same blogId updates in place — version.versionNumber increments and releasedAt advances. Treat the response as the current released version; use version.versionNumber and version.updatedAt to invalidate any cached copy on your side.

Errors

See Errors. On this endpoint specifically:

  • 401 Unauthorized — missing or invalid API key.
  • 404 Not Found — the ID is not a released blog in your workspace.
  • 500 Internal Server Error — retry with backoff.