// Pure transform: Ghost post object → site.standard.document payload. // No I/O, no client state — caller passes the publication AT-URI and (optionally) // an already-uploaded cover-image blob ref. import { convert as htmlToText } from 'html-to-text'; import type { BlobRef } from '@atproto/api'; import { buildMarkpubContent } from './content-shape.js'; export const DOCUMENT_COLLECTION = 'site.standard.document'; export type { BlobRef }; export interface GhostPostLike { id: string; title: string; slug: string; status: string; published_at: string; updated_at?: string | null; custom_excerpt?: string | null; excerpt?: string | null; html?: string | null; feature_image?: string | null; tags?: { name: string }[]; } export function ghostPostToRecord( post: GhostPostLike, publicationUri: string, coverImageBlob: BlobRef | null, ): Record { const tags = (post.tags ?? []) .map((t) => t.name) .filter((name): name is string => !!name && !name.startsWith('#')); const textContent = post.html ? htmlToText(post.html, { wordwrap: false, selectors: [ { selector: 'a', options: { ignoreHref: true } }, { selector: 'img', format: 'skip' }, ], }) : ''; const record: Record = { $type: DOCUMENT_COLLECTION, site: publicationUri, title: post.title, publishedAt: post.published_at, path: `/${post.slug}/`, }; const description = post.custom_excerpt || post.excerpt; if (description) record.description = description.slice(0, 3000); if (tags.length) record.tags = tags; if (textContent) record.textContent = textContent; if (post.updated_at && post.updated_at !== post.published_at) { record.updatedAt = post.updated_at; } if (coverImageBlob) record.coverImage = coverImageBlob; // Post body as at.markpub.markdown — the document content union is open // (`closed: false`), so no lexicon registration is needed; the shared $type // is what markpub-aware readers key on. const content = buildMarkpubContent(post.html ?? ''); if (content) record.content = content; return record; }