// The document `content` union member: Ghost post HTML → at.markpub.markdown. // site.standard.document's content union is open (`closed: false`), so the // member needs no lexicon registration — but a shared $type is a shared // contract: at.markpub.markdown (markpub.at, AramZS) is a community lexicon // for markdown-in-a-union that standard.site viewers understand. The markdown // here is *derived* from Ghost's HTML (Ghost emits html/lexical/plaintext, // never markdown), which is why `renderingRules` is omitted: nothing ever // renders our pages from it. // Design: docs/superpowers/specs/2026-07-06-markpub-content-design.md import TurndownService from 'turndown'; import { gfm } from 'turndown-plugin-gfm'; export const MARKPUB_MARKDOWN_TYPE = 'at.markpub.markdown'; const turndown = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced', }); turndown.use(gfm); // Ghost wraps every editor card in
. Only // captioned images need a rule (the defaults would smear the figcaption into // the surrounding text); every other card type degrades through the generic // rules — bookmark cards to links, embed iframes to nothing. turndown.addRule('kgImageCard', { filter: (node) => node.nodeName === 'FIGURE' && /\bkg-image-card\b/.test(node.getAttribute('class') ?? ''), replacement: (_content, node) => { const img = node.querySelector('img'); if (!img) return ''; const image = `![${img.getAttribute('alt') ?? ''}](${img.getAttribute('src') ?? ''})`; // Caption text is not markdown-escaped — a caption containing * or _ may // render with stray emphasis; accepted degradation. const caption = node.querySelector('figcaption')?.textContent?.trim(); return caption ? `\n\n${image}\n\n*${caption}*\n\n` : `\n\n${image}\n\n`; }, }); /** * Record-ready `content` union member for a post body, or null when there is * nothing to say: empty HTML, HTML that converts to empty markdown (e.g. an * embed-only post), or a converter throw. Null means the record simply omits * `content` — a conversion bug must never fail a sync. */ export function buildMarkpubContent(html: string): Record | null { if (!html.trim()) return null; let markdown: string; try { markdown = turndown.turndown(html); } catch (err) { console.error(' markdown conversion failed, omitting content:', (err as Error).message); return null; } if (!markdown.trim()) return null; return { $type: MARKPUB_MARKDOWN_TYPE, text: { markdown }, flavor: 'gfm', }; }