Skip to content

Commit 16d2584

Browse files
authored
Add Markdown export and Claude icon support (#13)
1 parent 1d9a15d commit 16d2584

File tree

3 files changed

+122
-6
lines changed

3 files changed

+122
-6
lines changed

public/claude.svg

Lines changed: 1 addition & 0 deletions
Loading

src/components/react/CopyPageButton.tsx

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,27 @@ const ChatGPTIcon = () => (
2525
</svg>
2626
);
2727

28-
// Claude logo (Anthropic asterisk)
28+
// Claude logo
2929
const ClaudeIcon = () => (
3030
<svg viewBox="0 0 24 24" className="size-4" fill="currentColor">
31-
<path
32-
d="M12 2L12 22M2 12L22 12M4.93 4.93L19.07 19.07M19.07 4.93L4.93 19.07"
33-
stroke="currentColor"
34-
strokeWidth="2"
35-
strokeLinecap="round"
31+
<path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" />
32+
</svg>
33+
);
34+
35+
// Markdown logo
36+
const MarkdownIcon = () => (
37+
<svg viewBox="0 0 208 128" className="size-4" fill="currentColor">
38+
<rect
39+
width="198"
40+
height="118"
41+
x="5"
42+
y="5"
3643
fill="none"
44+
stroke="currentColor"
45+
strokeWidth="10"
46+
rx="10"
3747
/>
48+
<path d="M30 98V30h20l20 25 20-25h20v68H90V59L70 84 50 59v39zm125 0l-30-33h20V30h20v35h20z" />
3849
</svg>
3950
);
4051

@@ -68,6 +79,11 @@ export function CopyPageButton({
6879
);
6980
};
7081

82+
const handleViewMarkdown = () => {
83+
const url = pageUrl.endsWith('/') ? pageUrl.slice(0, -1) : pageUrl;
84+
window.open(`${url}.md`, '_blank');
85+
};
86+
7187
return (
7288
<ButtonGroup>
7389
<Button
@@ -87,6 +103,19 @@ export function CopyPageButton({
87103
</Button>
88104
</DropdownMenuTrigger>
89105
<DropdownMenuContent align="end" className="w-64">
106+
<DropdownMenuItem
107+
onClick={handleViewMarkdown}
108+
className="flex flex-col items-start gap-0.5 py-2"
109+
>
110+
<div className="flex items-center gap-2">
111+
<MarkdownIcon />
112+
<span className="font-medium">View as Markdown</span>
113+
<ArrowUpRight className="size-3 opacity-50" />
114+
</div>
115+
<span className="text-xs text-muted-foreground pl-6">
116+
Open raw markdown in new tab
117+
</span>
118+
</DropdownMenuItem>
90119
<DropdownMenuItem
91120
onClick={handleOpenChatGPT}
92121
className="flex flex-col items-start gap-0.5 py-2"

src/pages/[...slug].md.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { getCollection } from 'astro:content';
2+
import type { APIRoute, GetStaticPaths } from 'astro';
3+
4+
export const prerender = true;
5+
6+
export const getStaticPaths: GetStaticPaths = async () => {
7+
const docs = await getCollection('docs');
8+
return docs.map((doc) => ({
9+
params: { slug: doc.id === 'index' ? undefined : doc.id },
10+
props: { doc },
11+
}));
12+
};
13+
14+
function cleanMdxContent(content: string): string {
15+
// Remove MDX import statements at the start of the file
16+
content = content.replace(
17+
/^(\s*import\s+.*?(?:from\s+['"].*?['"])?;?\s*\n)+/m,
18+
'',
19+
);
20+
21+
// Process Tabs components - extract TabItem contents
22+
content = content.replace(/<Tabs>[\s\S]*?<\/Tabs>/g, (match) => {
23+
const results: string[] = [];
24+
const tabItemRegex =
25+
/<TabItem[^>]*label="([^"]*)"[^>]*>([\s\S]*?)(?=<TabItem|<\/Tabs>)/g;
26+
27+
for (const [, label, tabContent] of match.matchAll(tabItemRegex)) {
28+
const cleanContent = tabContent.replace(/<\/TabItem>\s*$/, '').trim();
29+
if (cleanContent) {
30+
results.push(`**${label}:**\n${cleanContent}`);
31+
}
32+
}
33+
34+
return results.length > 0 ? results.join('\n\n') : '';
35+
});
36+
37+
// Remove self-closing JSX/MDX components
38+
content = content.replace(/<[A-Z][a-zA-Z]*\s+[^>]*\/>/g, '');
39+
40+
// Handle Callout components - keep content
41+
content = content.replace(
42+
/<Callout[^>]*>([\s\S]*?)<\/Callout>/g,
43+
(_, inner) => inner.trim(),
44+
);
45+
46+
// Remove remaining JSX component tags
47+
content = content.replace(/<[A-Z][a-zA-Z]*[^>]*>/g, '');
48+
content = content.replace(/<\/[A-Z][a-zA-Z]*>/g, '');
49+
50+
// Convert relative links to fully qualified URLs
51+
content = content.replace(
52+
/\[([^\]]+)\]\(\/([^)]*)\)/g,
53+
(_, text, path) => `[${text}](https://docs.sprites.dev/${path})`,
54+
);
55+
56+
// Clean up excessive blank lines
57+
content = content.replace(/\n{4,}/g, '\n\n\n');
58+
59+
return content.trim();
60+
}
61+
62+
export const GET: APIRoute = async ({ props }) => {
63+
const { doc } = props as {
64+
doc: {
65+
data: { title: string; description?: string };
66+
body: string;
67+
id: string;
68+
};
69+
};
70+
71+
const url = `https://docs.sprites.dev/${doc.id === 'index' ? '' : `${doc.id}/`}`;
72+
const cleanedContent = cleanMdxContent(doc.body);
73+
74+
const markdown = `# ${doc.data.title}
75+
76+
Source: ${url}
77+
${doc.data.description ? `\n${doc.data.description}\n` : ''}
78+
${cleanedContent}
79+
`;
80+
81+
return new Response(markdown, {
82+
headers: {
83+
'Content-Type': 'text/plain; charset=utf-8',
84+
},
85+
});
86+
};

0 commit comments

Comments
 (0)