Content Publishing API (Blog & Pages)
Publish and update blog posts and custom marketing pages programmatically — including the "new-style" AEO fields: FAQs, citations, key takeaways, and custom JSON-LD schema. Two APIs share one authentication scheme:
| API | Base | Creates |
|---|---|---|
| Blog API | /blog/api/ |
Blog posts (rendered at /blog/<slug>/) |
| Pages API | /api/pages/ |
Custom pages (rendered at /<slug>/) |
Full field-by-field reference lives in the repo at
docs/BLOG_API.mdanddocs/PAGES_API.md. This page is the in-product quick start.
Authentication
Both APIs use the same key, sent on every request:
X-API-Key: <your-raw-key>
(Staff may instead use Authorization: Bearer <session-key> or a logged-in session.)
Get a key: Django Admin → Blog API Keys → select or add a row → Actions → Generate new API key → Go. The raw key is shown once — copy it immediately; it is stored only as a hash and never shown again. The same key works for both APIs.
The new-style content fields
These four fields turn a plain post/page into AEO-optimized, machine-readable content.
They work on both APIs (the Pages API supports faqs and schema_json; the Blog
API supports all four).
| Field | Shape | Renders as |
|---|---|---|
faqs |
array of {"question": "...", "answer": "..."} |
Visible FAQ accordion + schema.org FAQPage JSON-LD |
citations |
array of {title, url, author, publisher, date_published, type} |
"Sources & References" block + JSON-LD citation[] |
key_takeaways |
string, one takeaway per line | A styled "Quick Answer" box near the top |
schema_json |
raw JSON-LD string | Injected verbatim (overrides the auto-generated schema) |
Notes that apply to all of them:
faqs: rows missing eitherquestionoranswer(or non-object rows) are silently dropped. The visible accordion and theFAQPageJSON-LD are built from the same list, so they never drift. (FAQ rich results are limited by Google to authoritative government/health sites — treatFAQPageas semantic metadata for search and AI answer engines, not a guaranteed rich-result feature.)citations: a row with neithertitlenorurlis dropped; stored rows are normalized to the full key set.- On update, providing
faqsorcitationsreplaces the whole list (not append). - You do not need to hand-write FAQ or citation JSON into
schema_json— those are generated automatically fromfaqs/citations.
Publish a blog post
curl -X POST https://<host>/blog/api/posts/create/ \
-H "X-API-Key: <key>" -H "Content-Type: application/json" \
-d '{
"title": "CELPIP vs IELTS for Canadian PR",
"author_username": "admin",
"content_format": "html",
"content": "<section><h2>Which test?</h2><p>...</p></section>",
"is_published": true,
"categories": ["immigration"],
"tags": ["celpip", "ielts"],
"meta_description": "CELPIP vs IELTS for Express Entry — accepted tests, scoring, and which to pick.",
"key_takeaways": "Both are accepted for Express Entry\nCELPIP is computer-delivered and Canada-only\nIELTS General Training is available worldwide",
"faqs": [
{"question": "Is CELPIP accepted for Express Entry?", "answer": "Yes. IRCC accepts CELPIP General for Express Entry and most PR streams."},
{"question": "Which is easier, CELPIP or IELTS?", "answer": "Neither is objectively easier; CELPIP is fully computer-based, IELTS offers paper and computer formats."}
],
"citations": [
{"title": "Language testing — IRCC", "url": "https://www.canada.ca/...", "publisher": "IRCC", "date_published": "2026", "type": "WebPage"}
]
}'
Update (send only what changes; faqs/citations/categories replace the whole list):
curl -X PUT https://<host>/blog/api/posts/celpip-vs-ielts-for-canadian-pr/ \
-H "X-API-Key: <key>" -H "Content-Type: application/json" \
-d '{"faqs": [{"question": "How long are scores valid?", "answer": "Two years for both tests."}]}'
Blog fields (create): title (required), author_username (required for API-key
auth), slug, content, content_format (richtext | html), is_published,
featured, published_at, categories, tags, meta_description, meta_keywords,
schema_json, citations, faqs, key_takeaways, show_in_footer, image_alt,
image_title, image_caption.
Publish a custom page
Custom pages render at the site root (/<slug>/). Reserved slugs (e.g. admin,
blog, docs) are rejected.
curl -X POST https://<host>/api/pages/ \
-H "X-API-Key: <key>" -H "Content-Type: application/json" \
-d '{
"slug": "best-ielts-platform-canada",
"title": "Best IELTS Platform in Canada",
"html_content": "<h1>Best IELTS platform for Canadian institutes</h1><p>...</p>",
"is_active": true,
"seo_title": "Best IELTS Platform in Canada (2026)",
"seo_description": "How to pick an IELTS/CELPIP platform for a Canadian coaching centre.",
"faqs": [
{"question": "Does it support CLB score mapping?", "answer": "Yes — CLB predictions are built in for CELPIP and PTE Core."}
]
}'
Pages fields (create): slug (required), title (required), html_content,
is_active, seo_title, seo_description, seo_keywords, og_title,
og_description, og_image, schema_json, faqs.
Editing without the API
Both content types are also editable in the staff GUI, where the same FAQ / citations editors are available:
- Blog posts —
/blog/manage/(or Django Admin → Blog Posts). - Custom pages — the custom-pages dashboard (Django Admin → language-tests admin → Custom Pages), which now includes the FAQ editor card.
Endpoint summary
| API | Method & path | Purpose |
|---|---|---|
| Blog | POST /blog/api/posts/create/ |
Create a post |
| Blog | GET /blog/api/posts/ |
List posts |
| Blog | GET /blog/api/posts/<slug>/ |
Get a post |
| Blog | PUT /blog/api/posts/<slug>/ |
Update a post |
| Blog | DELETE /blog/api/posts/<slug>/delete/ |
Delete a post |
| Pages | GET /api/pages/ |
List pages (?is_active=true) |
| Pages | POST /api/pages/ |
Create a page |
| Pages | GET / PUT / DELETE /api/pages/<slug>/ |
Get / update / delete a page |
