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
| Name | Type | Description |
|---|---|---|
blogId | string | The 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
markdownis the canonical body. It already contains inline image references using the asset URLs below — render it with any standard CommonMark / GFM renderer.frontmattermirrors the YAML-style metadata the editor signed off on (e.g.tags,category). Keys and values are both strings.structuredContentis a structured outline of the article (typed asunknownso the schema can evolve). Use it if you want to render the outline as a TOC; ignore it if you only need the markdown.schemaJsonLdis 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 benullfor older releases — skip the tag when it is.assetsare uploaded media. PreferpublicUrl; fall back tosignedUrlfor buckets fronted by short-lived URLs. Filter byassetRoleto find the cover image (cover_imageis always at most one entry per release).relatedcontains 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 element | Where it comes from |
|---|---|
| Page title / H1 | version.renderedTitle |
| URL slug | version.renderedSlug |
<title> + meta tags | version.metaTitle, version.metaDescription |
| Cover image | version.assets entry with assetRole === "cover_image" |
| Article body | version.markdown (inline images already reference asset URLs) |
| Structured data | version.schemaJsonLd → <script type="application/ld+json"> |
| Tags / category | version.frontmatter |
| Publish date | releasedAt |
| Related posts module | related (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.