Ghost's Content API
A front end built on Ghost's Content API can read a Khalid blog instead. It keeps its code and Ghost's own client, @tryghost/content-api, and changes two things: the blog's address and the key.
This API only reads. It answers at /ghost/api/content/ on the blog's address, the same path Ghost uses. It is off until you switch it on.
Switch it on
Add this line to site.toml, above the first table:
ghost_content_api = true
Restart khalid serve. While the line is missing or false, every address under /ghost/api/content/ answers 404, as any unknown address does.
Make a content key for the front end:
khalid keys create --kind content --name "Ghost front end"
Khalid shows the key once. It is safe in a web page, as a Ghost content key is.
Point the front end at Khalid
import GhostContentAPI from "@tryghost/content-api";
const api = new GhostContentAPI({
url: "https://www.example.com/blog",
key: "the key Khalid showed",
version: "v5.0",
});
const posts = await api.posts.browse({ limit: 5, include: "tags,authors" });
url is the blog's address with its mount path and no slash at the end. Ghost's client adds /ghost/api/content/ itself. The API takes Ghost's Accept-Version header for v5, which the client sends. Khalid's own Content API, at /api/content/v1/, does not change.
What it answers
| Resource | Browse | Read |
|---|---|---|
| Posts | posts/ |
posts/<id>/, posts/slug/<slug>/ |
| Pages | pages/ |
pages/<id>/, pages/slug/<slug>/ |
| Tags | tags/ |
tags/<id>/, tags/slug/<slug>/ |
| Authors | authors/ |
authors/<id>/, authors/slug/<slug>/ |
| Settings | settings/ |
A list takes limit (a number, or all), page, order, filter, include=tags,authors on posts and pages, fields, and formats. Posts come with html; ask formats=plaintext for the text as well.
Answers use Ghost's wrapper, such as {"posts": [...], "meta": {"pagination": {...}}}, and Ghost's field names. Each carries an ETag, so a front end or a cache can ask again cheaply and get 304 when nothing changed.
One language. A Ghost site has one language, so this API answers in the site's default language only. Posts, tags and authors in the other languages stay out, and so does a translation that shares its slug with the original.
Filters
The common part of Ghost's filter language works:
- the fields
tag,author,featured,slug,id,status,visibility,published_at,primary_tag.slugandprimary_author.slug, withtags.slugandauthors.slugas other names fortagandauthor; +for and,,for or, and brackets for groups;-for not, as inprimary_tag.slug:-news;- lists, as in
tag:[news,guides]; >,<,>=and<=onpublished_at.
A filter that uses anything else answers Ghost's 400 error, with a message that names the part this API does not answer.
How Khalid's posts look in Ghost's fields
| Ghost field | From Khalid |
|---|---|
id, slug, title, url |
The post's own |
html, plaintext |
The post's body as its page shows it |
feature_image, meta_title, meta_description |
The post's page head: its share image, title and description |
custom_excerpt |
The post's description |
excerpt |
The description, or the start of the text |
published_at, created_at, updated_at |
The post's date and its updated date |
reading_time |
Words over 275, rounded, as Ghost counts |
featured |
featured: true in the front matter |
tags, primary_tag |
The post's tags; the first is the primary tag |
authors, primary_author |
The post's authors, with profile_image; the first is the primary author |
A members-only post comes with visibility: "members", access: false and an empty html, as Ghost answers a key with no signed-in member.
Fields Khalid has no meaning for carry Ghost's neutral values: comments is false, and code injection, og_*, twitter_*, canonical_url, custom_template, email_subject and image captions are null.
Settings
settings/ gives the blog's name as title, the [seo.titles] tagline as description and meta_description, the publisher's logo as logo, the share image as cover_image, the X account as twitter, the default language as lang and locale, the header menu as navigation, the footer menu as secondary_navigation, and the blog's address as url. The time zone is Etc/UTC, since Khalid keeps times in UTC.
Keys and errors
The key goes in ?key=, as Ghost's client sends it. A missing or wrong key answers Ghost's 401, UnauthorizedError. An address with nothing at it answers Ghost's 404, NotFoundError. A request this API cannot read answers Ghost's 400, BadRequestError. Each error is in Ghost's shape: {"errors": [{"message": "...", "type": "..."}]}. A content key has the same request limits here as on Khalid's Content API (see docs/standards/api.md), counted apart.
Not covered
- Ghost's Handlebars themes. Khalid's templates draw the pages.
- Ghost's Admin API. Khalid's Admin API keeps its own shape.
- Members, tiers, offers and newsletters through this path.
- The look of Ghost's editor cards (
kg-*classes) insidehtml.
Each is a separate piece of work. Khalid's own CI runs Ghost's client, at a pinned version, against the showcase site on every change to this API, so a break shows at once. just ghost-client runs the same check on your machine.
Written in the Khalid repository. Edit this page on GitHub.