Reading a Jant feed
Jant's Atom feed carries enough to redraw the site's own timeline: what kind of post each entry is, the text as the timeline cuts it, the attachments with their dimensions, and the shape of a thread. Hand this page to whoever writes the consumer — an app, a directory, a script, an AI given the feed and asked to render it.
It says what arrives and how to read it; feed addresses and settings are in Feeds. The elements and attributes on this page change only in a major release. New ones can appear in any release, so ignore an element or attribute you don't recognize.
Namespaces
| Prefix | URI | Carries |
|---|---|---|
| — | http://www.w3.org/2005/Atom |
Everything standard |
jant |
https://jant.me/ns |
Post ID and format, threads, Discover |
media |
http://search.yahoo.com/mrss/ |
Attachment metadata |
Both extensions are declared only when the feed emits something in them — which for jant is every served feed, since each carries <jant:discover>. The URIs are fixed identifiers, not addresses to fetch, and the prefixes bound to them are arbitrary — match on the URI.
An Atom reader that knows neither extension still gets a working feed. Everything Jant adds is either an element in its own namespace or an attribute on a Media RSS element.
One entry
<entry>
<title></title>
<link href="https://ex.com/roasting-again" rel="alternate"/>
<id>https://ex.com/roasting-again</id>
<jant:id>pst_01jpyx3m7gw4w3h7m4bknq0v1d</jant:id>
<published>2026-03-19T09:00:00.000Z</published>
<updated>2026-03-19T09:00:00.000Z</updated>
<jant:format>note</jant:format>
<category term="coffee" label="Coffee" jant:page="https://ex.com/coffee"/>
<media:content url="https://ex.com/m/beans.jpg" type="image/jpeg" medium="image"
width="1600" height="1200">
<media:description type="plain">Roasted beans cooling on a tray</media:description>
</media:content>
<summary type="html"><![CDATA[<p>Roasting again.</p>]]></summary>
<content type="html"><![CDATA[<p>Roasting again.</p>
<div data-post-media>…</div>]]></content>
</entry>
| Element | Count | Read it for |
|---|---|---|
id |
1 | The post's permalink. Always — even on a Link post, whose alternate points elsewhere |
jant:id |
1 | The post's ID. Unlike id it survives a slug rename or a new domain: key on it to recognise a post |
title |
1 | The title. Empty, not absent, on untitled Notes and every Quote |
link[@rel="alternate"] |
1 | Where the entry points: the external URL on a Link post, the permalink otherwise |
link[@rel="related"] |
0–1 | Link posts only: the permalink, since alternate was spent on the external URL |
link[@rel="enclosure"] |
0–n | Attachments a plain Atom parser can fetch. Images are excluded — the content already shows them |
published / updated |
1 | Timestamps. A curated feed may date an entry by the curation rather than the post |
jant:format |
1 | note, link, or quote — the entry's, which is the root's in a thread |
jant:thread |
0–1 | The entry is a thread. Absent means a lone post |
jant:truncated |
0–1 | The summary's text was cut. Offer a "read more". A row's truncated says which post. Never on a Quote |
category |
0–n | Collections. @term is the slug, @label the title, @jant:page the absolute URL |
media:thumbnail |
0–1 | A Link post's preview image, for a card or grid |
media:content |
0–n | One per attachment, with dimensions and duration |
summary |
0–1 | The post's text, as the timeline renders it |
content |
1 | The whole post page |
A Quote's attribution is not its title — it is inside the text, as a <figure> with a <figcaption>. A star rating is inside the text too, as ★★★★☆ 4/5.
Two renderings
The site draws every post twice: cut down in the timeline, whole on its own page. The entry carries both.
<summary>is the post's text as the timeline shows it — cut at the same place, a Quote's quotation and attribution, the rating, a thread folded down. No media, no Link preview image.<content>is the whole post page, attachments included.
Both are HTML in CDATA, with every URL absolute. Render them; do not strip the markup.
<summary> is present whenever the entry has any text, including when that text is the whole post. Missing says one thing: this post has no text — a photo with no caption. So summary ?? "" is the whole rule, and truncation is jant:truncated, never a comparison against <content>.
A Quote arrives whole: neither its quoted text nor its commentary is cut, because the site does not cut them either. So a Quote entry never carries jant:truncated, and its <summary> and <content> hold the same text.
To draw a timeline you need <summary>, not <content>.
Threads
A thread arrives as one entry — root and replies together, so a reader does not fill with fragments. <content> carries the whole chain; <summary> folds it exactly the way the site's timeline does: the root, the first two replies, a gap link standing for the run between, then the last three replies with the newest as the hero. Up to six posts, so a thread that fits arrives whole and hides nothing.
<jant:thread posts="4" hidden="2" gap="https://ex.com/r1" latest="https://ex.com/r3">
<jant:post href="https://ex.com/dialing-in" format="note" published="2026-03-14T09:00:00.000Z"/>
<jant:post href="https://ex.com/r1" format="link" published="2026-03-15T09:00:00.000Z"
title="The AeroPress guide" url="https://other.example/aeropress"
thumbnail="https://ex.com/m/prev.jpg"/>
<jant:post href="https://ex.com/r2" format="quote" published="2026-03-16T09:00:00.000Z"/>
<jant:post href="https://ex.com/r3" format="note" published="2026-03-17T09:00:00.000Z"
title="Got it" truncated="true"/>
</jant:thread>
| Attribute | Means |
|---|---|
posts |
Posts in the thread, root included |
hidden |
How many the summary folds away |
gap |
The first post the fold hides. Only when hidden is above zero |
latest |
The newest reply — the one the summary shows in full |
One row per post, in thread order. gap, latest, and every
media:content/@jant:post name a href from this list.
| Row attribute | Count | Notes |
|---|---|---|
href |
1 | The post's permalink |
format |
1 | note, link, or quote, for this post |
published |
1 | This post's own timestamp |
title |
0–1 | Absent when the post has none, and on every Quote |
url |
0–1 | Link posts only: where it points |
thumbnail |
0–1 | Link posts only: the preview image |
folded |
0–1 | "true" when the fold hid this post; absent means the summary renders it |
truncated |
0–1 | "true" when the summary cut this post's block |
Read a reply here, not from the entry. Every entry-level field describes
the root: <jant:format> says note for a thread whose newest reply is a
Quote, and <title> and link[@rel="alternate"] say nothing about a titled
reply or a Link reply. The root carries a full row too, so you can walk the
rows without special-casing it.
A row carries what Atom would put on that post's entry — links, title, date —
plus format and truncated. Never the body or an excerpt: those are in
<content>.
Read folded rather than looking for the post in the summary. A rendered
post with no text — a photo with no caption — contributes no block and no tail
meta, so its absence from the summary says nothing about whether it was hidden.
Treating it as folded drops its attachments and the post disappears.
truncated on a row means the summary cut that post's block. Any post the
fold renders can carry it. On a post behind the gap its absence means "not cut
here", not "this post arrived whole". The entry's <jant:truncated/> is the same answer for the whole card,
and the only form a lone post has.
Inside both text constructs, each post's block ends with that post's own dated permalink:
<p>Dialing in a new bag.</p>
<p><small><a href="https://ex.com/dialing-in" class="u-url"><time class="dt-published"
datetime="2026-03-14T09:00:00.000Z">Mar 14, 2026</time></a></small></p>
<hr/>
<p><small><a href="https://ex.com/r1">2 more posts</a></small></p>
Every marker ends the block before it and names the post that wrote it, the root's included. That is how you split one field into several posts. The markup is microformats2 (u-url, dt-published), so an mf2 parser gets it without a special case, and the <a> wrapping a <time datetime> still identifies it when class is stripped.
A post with no text contributes no block and leaves no marker. A lone post has no marker at all — <published> already dates it.
A reply's title, and a Link reply's source line, sit in a <header> inside
its block — the root's are in the entry's own <title> and
link[@rel="alternate"] instead. Render it as it stands, or drop that one
element and redraw from the row, which also has the thumbnail the text cannot
show.
Do not parse <hr/>. It is drawn between posts, but it also separates a Quote from the author's commentary, and an author can type one.
Attachments
Three surfaces, three jobs.
<content> shows them, each post's own inside a <div data-post-media> beside that post's text. It is the only place an attachment sits in the running order of a thread.
<media:content> describes them: type, medium, fileSize, width, height, duration, a media:title holding the filename, a media:description holding alt text or a text file's excerpt, and a nested media:thumbnail when the file has a still that is not the file itself: a video's poster, or a picture's resized rendering.
@jant:postnames the post carrying the file. In a thread every attachment has it, the root's included; a lone post's entry has none, because its one post is the entry.- Files from folded posts arrive too. An entry's media is the whole thread's. When a file's
jant:postnames a row that is neither the root norlatest, it belongs to a post behind the gap link — drawing it is your call. @jant:pageis where a click should land, written only when it differs from@url— text attachments, whose file a browser downloads. Readjant:page ?? url.mediumis Media RSS's fixed vocabulary, so anything that is not a picture or playable isdocument.
<link rel="enclosure"> is what a plain Atom parser reads. Images are left out — the content already shows them full size — while audio, video and documents keep theirs, the way a podcast feed encloses its audio and not its show-note images.
Do not match <media:content> to <content> by URL: a text attachment's link points at its rendered page, not at the file.
Rendering a timeline
permalink = entry/id
title = entry/title // no heading when empty
text = entry/summary ?? "" // HTML; render it
media = entry/media:content[] // document order
tags = entry/category[]
readMore = entry/jant:truncated exists
thread = entry/jant:thread // absent on a lone post
For a thread drawn as separate cards, split text on the tail markers and match each block's files by jant:post. Each block's permalink also names a <jant:post> row, which is where that post's own format lives. Files left over belong to the posts the fold hid; showing them is your call. hidden is a number, so the gap line can be written in your reader's own language.
Layout is yours. No feed format can express the site's justified media strip — compute it from the dimensions on media:content, where a picture or clip is rowHeight * aspectRatio wide, everything else is a 3:4 card, and only pictures and clips set the row height.
What the feed does not carry
- A rating as a number. It is
★★★★☆ 4/5inside the text. - A text attachment's character count. Media RSS has no slot for it and
fileSizeis bytes. - Attachments interleaved inside a summary.
<summary>is text alone. If you place a gallery between two posts' paragraphs, either decide that position yourself or read<content>, where eachdata-post-mediaalready sits in order. - Translated strings.
▶ Watch videoand2 more postsare English in every feed. - Anything a self-hosted export promises. A site exported to a static theme writes its own feed, and that one carries less. Read the served feed when you need this contract.