Something went wrong. Try again.
Ghost is lightweight headless server that enables Ghost self hosters to bridge to standard.site with ease..
Something went wrong. Try again.
2.6 kB · 64 lines
TypeScript
at main
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465// 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 <figure class="kg-card kg-*-card">. 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 = ` ?? ''})`; // 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<string, unknown> | 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', };}