Something went wrong. Try again.
a tool for shared writing and social publishing
Something went wrong. Try again.
2.9 kB · 94 lines
TypeScript
at main
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495import { v7 } from "uuid";import { gateContent } from "src/import/content";import { buildPage, coverImage, wrapInLeaflet, type ImportedLeaflet, type ResolveImage,} from "src/import/leaflet";import type { SubstackPost } from "./parseSubstackExport";import { substackFullSizeUrl, substackHtmlToBlocks, substackImageSource, substackPreviewUrl,} from "./substackToBlocks";
export type SubstackLeaflet = ImportedLeaflet & { substackId: string; slug: string;};
// Build a Substack post as a standalone leaflet. Posts for paid subscribers// are placed entirely behind a members-only delimiter: the export doesn't// say where their free preview ended.export async function substackPostToLeaflet( post: SubstackPost, resolveImage: ResolveImage, opts: { rewriteLink?: (href: string) => string } = {},): Promise<SubstackLeaflet> { const pageId = v7(); const content = substackHtmlToBlocks(post.html, { parent: pageId, rewriteLink: opts.rewriteLink, }); if (post.audience !== "everyone") gateContent(content, pageId); const page = await buildPage( pageId, content, coverImage(post.coverImage && substackImageSource(post.coverImage)), resolveImage, ); return { ...wrapInLeaflet(page), substackId: post.id, slug: post.slug, title: post.title, description: post.subtitle, tags: post.tags, publishedAt: post.publishedAt ?? new Date().toISOString(), };}
// A preview shows the CDN's page-size rendition rather than pulling down the// originals. Where the source states no size (HEIC gallery images), a 4:3// box stands in so the image is at least visible.export const substackPreviewImage: ResolveImage = async (image) => ({ src: substackPreviewUrl(image.url), width: image.width ?? 4, height: image.height ?? 3, fallback: "",});
// Images uploaded before Substack moved buckets are no longer readable where// they're stored (403), only through the CDN. When the original can't be// fetched, take the CDN's full-size rendition instead.export function withCdnFallback(resolveImage: ResolveImage): ResolveImage { return async (image) => { try { return await resolveImage(image); } catch (e) { const url = substackFullSizeUrl(image.url); if (url === image.url) throw e; return resolveImage({ ...image, url }); } };}
// Rewrites the author's links to their own Substack posts so they point at// the imported copies, which keep their slugs.export function substackLinkRewriter(siteUrl: string, publicationUrl: string) { const host = new URL(siteUrl).host; const base = publicationUrl.replace(/\/+$/, ""); return (href: string) => { let url: URL; try { url = new URL(href); } catch { return href; } const slug = /^\/p\/([^/]+)\/?$/.exec(url.pathname)?.[1]; return url.host === host && slug ? `${base}/${slug}${url.hash}` : href; };}