Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. 🫖 Status page with uptime monitoring & API monitoring as code 🫖 openstatus.dev
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207import { customers, getCustomer } from "../data/customers";import { canonicalUrl, frontmatter } from "./frontmatter";import type { MDXData } from "./utils";
// Agents parse these dates; the human form stays on the HTML page.const isoDate = (date: Date) => date.toISOString().slice(0, 10);
/** * Converts MDX content to clean markdown format for AI tools * Handles YAML frontmatter generation and component serialization */export function convertMdxToMarkdown(data: MDXData): string { let output = "";
// Step 0: Generate YAML frontmatter from metadata const { metadata, content, href } = data;
// Same fallbacks as `getPageMetadata`, so the twin carries the <title> // and meta description the HTML page advertises. output += frontmatter({ title: metadata.seo?.title ?? metadata.title, url: canonicalUrl(metadata.seo?.canonical ?? href), date: metadata.publishedAt ? isoDate(metadata.publishedAt) : undefined, updated: metadata.updatedAt ? isoDate(metadata.updatedAt) : undefined, author: metadata.author, description: metadata.seo?.description ?? metadata.description, category: metadata.category, image: metadata.image ? canonicalUrl(metadata.image) : undefined, });
// Step 1: Strip import statements (confirmed: always at top of file) let markdown = content.replace(/^import\s+.*$/gm, "");
// Step 2: Convert <ButtonLink> to markdown links markdown = markdown.replace( /<ButtonLink\s+href="([^"]*)"[^>]*>([\s\S]*?)<\/ButtonLink>/g, (_match, href, children) => { // Extract plain text from children (remove any nested tags) const text = children.replace(/<[^>]*>/g, "").trim(); return `[${text}](${href})`; }, );
// Step 3: Convert <Image> to markdown images // Handle with alt attribute markdown = markdown.replace( /<Image\s+(?:[^>]*\s+)?src="([^"]*)"(?:[^>]*\s+)?alt="([^"]*)"[^>]*\/?>/g, (_match, src, alt) => { return ``; }, ); // Handle without alt attribute markdown = markdown.replace( /<Image\s+(?:[^>]*\s+)?src="([^"]*)"[^>]*\/?>/g, (_match, src) => { return ``; }, );
// Step 4: Replace <Table> components with a note to use GFM tables // TODO: Convert Table components in MDX source files to GFM tables markdown = markdown.replace(/<Table\s+data=\{[\s\S]*?\}\s*\/>/g, () => { return "<!-- Table: Please convert to GFM markdown table format for better AI tool support -->"; });
// Step 5: Convert <Details> to native HTML <details>/<summary> (always closed) markdown = markdown.replace( /<Details\s+summary="([^"]*)"[^>]*>([\s\S]*?)<\/Details>/g, (_match, summary, content) => { return `<details>\n <summary>${summary}</summary>\n ${content.trim()}\n</details>`; }, );
// Step 6: Replace interactive components with HTML comments markdown = markdown.replace( /<StatusPageExample[^>]*(?:\/>|>[\s\S]*?<\/StatusPageExample>)/g, "<!-- StatusPageExample omitted -->", ); markdown = markdown.replace( /<SimpleChart[^>]*(?:\/>|>[\s\S]*?<\/SimpleChart>)/g, "<!-- SimpleChart omitted -->", );
// Step 7: Convert <Aside> to a blockquote callout const asideLabels: Record<string, string> = { note: "Note", tip: "Tip", caution: "Caution", danger: "Danger", }; markdown = markdown.replace( /<Aside\b([^>]*)>([\s\S]*?)<\/Aside>/g, (_match, attrs, content) => { const type = attrs.match(/type="([^"]*)"/)?.[1] ?? "note"; const label = attrs.match(/title="([^"]*)"/)?.[1] ?? asideLabels[type] ?? "Note"; const lines = [`**${label}**`, "", ...content.trim().split("\n")]; return lines.map((line) => (line ? `> ${line}` : ">")).join("\n"); }, );
// Step 8: Convert <ShowcaseYouTube entries={[...]}> to a list of links markdown = markdown.replace(/<ShowcaseYouTube\b[\s\S]*?\/>/g, (match) => { const items: string[] = []; for (const entry of match.matchAll(/\{[^{}]*href:[^{}]*\}/g)) { const href = entry[0].match(/href:\s*['"]([^'"]+)['"]/)?.[1]; const title = entry[0].match(/title:\s*['"]([^'"]+)['"]/)?.[1]; if (href) items.push(`- [${title ?? "YouTube video"}](${href})`); } return items.join("\n"); });
// Step 9: Convert card components (unwrap grid, then serialize each card) markdown = markdown.replace( /<CardGrid[^>]*>([\s\S]*?)<\/CardGrid>/g, (_match, content) => content, ); markdown = markdown.replace( /<Card\b([^>]*)>([\s\S]*?)<\/Card>/g, (_match, attrs, content) => { const title = attrs.match(/title="([^"]*)"/)?.[1]; const body = content.trim(); return title ? `**${title}**\n\n${body}` : body; }, ); markdown = markdown.replace(/<LinkCard\b([^>]*?)\/>/g, (_match, attrs) => { const title = attrs.match(/title="([^"]*)"/)?.[1] ?? ""; const href = attrs.match(/href="([^"]*)"/)?.[1] ?? ""; const description = attrs.match(/description="([^"]*)"/)?.[1]; const link = `[${title}](${href})`; return description ? `- ${link} — ${description}` : `- ${link}`; });
// Step 9b: LogoCloud and Quote take their copy from `data/customers.ts`. markdown = markdown.replace(/<LogoCloud\b([^>]*?)\/>/g, (_match, attrs) => { const limit = Number(attrs.match(/limit=\{(\d+)\}/)?.[1]) || customers.length; return customers .slice(0, limit) .map((c) => `[${c.name}](${c.story ?? c.href})`) .join(", "); }); markdown = markdown.replace( /<Quote\s+customer="([^"]*)"[^>]*\/>/g, (_match, name) => { const customer = getCustomer(name); const { quote } = customer; if (!quote) return ""; const link = customer.story ? `[Read the story](${customer.story})` : `[${customer.name}](${customer.href})`; return `> “${quote.text}”\n>\n> — ${quote.name}, ${quote.role} · ${link}`; }, );
// Step 10: Unwrap layout-only wrappers. Their children are real copy // (CTA links, pricing caveats, quote attributions), only the styling goes. markdown = markdown.replace( /<Actions\b[^>]*>([\s\S]*?)<\/Actions>/g, (_match, content: string) => content .split("\n") .map((line) => line.trim()) .filter(Boolean) .join("\n\n"), ); // Eyebrow is a visual label with no meaning out of context, so it goes. markdown = markdown.replace(/<Eyebrow\b[^>]*>[\s\S]*?<\/Eyebrow>/g, ""); // SrOnly is a demo's text alternative: hidden on the page, plain copy here. // One pass skips wrappers nested inside a match, so repeat until stable. const wrapper = /<(Grid|Subtle|p|SrOnly|Timeline)\b[^>]*>([\s\S]*?)<\/\1>/g; while (wrapper.test(markdown)) { markdown = markdown.replace(wrapper, (_match, _tag, content) => content); }
// A break becomes a newline so `Before<br />After` keeps its word boundary. markdown = markdown.replace(/<br\s*\/?>|<\/br>/g, "\n");
// Step 11: Strip generic HTML containers but extract their text // This handles div, span, section, article markdown = markdown.replace( /<(div|span|section|article)(?:\s+[^>]*)?>[\s\S]*?<\/\1>/g, (match) => { // Extract text content (remove all tags); blank lines keep adjacent // containers from merging into one paragraph. return `\n\n${match.replace(/<[^>]*>/g, " ").trim()}\n\n`; }, );
// Step 12: Strip remaining unknown JSX components. Self-closing tags go // first: a `<Demo type="x" />` left for the paired rule reads as an opening // tag and swallows everything up to the next closing tag of any component. markdown = markdown.replace(/<[A-Z]\w*(?:\s[^>]*?)?\/>/g, ""); markdown = markdown.replace(/<([A-Z]\w*)\b[^>]*>[\s\S]*?<\/\1>/g, "");
// A tag removed mid-cell leaves a line holding one space; empty it so it collapses. markdown = markdown.replace(/^[ \t]+$/gm, ""); // Clean up: Remove excessive blank lines (more than 2 consecutive) markdown = markdown.replace(/\n{3,}/g, "\n\n");
// Trim whitespace markdown = markdown.trim();
output += markdown; return output;}