From 689bbdcdd9072764c57028825ce7b89f4a36ef7a Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Fri, 6 Feb 2026 23:56:39 +0100 Subject: [PATCH] add markdown text sharing --- .claude/settings.local.json | 3 +- package.json | 4 + pnpm-lock.yaml | 59 +++++ src/app.css | 45 ++-- src/lib/atproto/settings.ts | 2 +- src/lib/index.ts | 11 + src/routes/+page.svelte | 224 ++++++++++++++++--- src/routes/m/[repo]/[rkey]/+page.server.ts | 23 ++ src/routes/m/[repo]/[rkey]/+page.svelte | 151 +++++++++++++ src/routes/m/[repo]/[rkey]/og.png/+server.ts | 41 ++++ 10 files changed, 504 insertions(+), 59 deletions(-) create mode 100644 src/routes/m/[repo]/[rkey]/+page.server.ts create mode 100644 src/routes/m/[repo]/[rkey]/+page.svelte create mode 100644 src/routes/m/[repo]/[rkey]/og.png/+server.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 1b2eb99..a4388ff 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -3,7 +3,8 @@ "allow": [ "Bash(npx tsc:*)", "mcp__plugin_svelte_svelte__svelte-autofixer", - "mcp__ide__getDiagnostics" + "mcp__ide__getDiagnostics", + "Bash(ls:*)" ] } } diff --git a/package.json b/package.json index dd15cb4..8ce5473 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@sveltejs/kit": "^2.50.0", "@sveltejs/vite-plugin-svelte": "^6.2.4", "@tailwindcss/forms": "^0.5.11", + "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.18", "bits-ui": "^2.15.4", "eslint": "^9.39.2", @@ -48,6 +49,9 @@ "dependencies": { "@ethercorps/sveltekit-og": "^4.2.1", "@sveltejs/adapter-cloudflare": "^7.2.6", + "@types/dompurify": "^3.2.0", + "dompurify": "^3.3.1", + "marked": "^17.0.1", "wrangler": "^4.61.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10d5332..19c7684 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,15 @@ importers: '@sveltejs/adapter-cloudflare': specifier: ^7.2.6 version: 7.2.6(@sveltejs/kit@2.50.0(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.48.0)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.48.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)))(wrangler@4.61.1(@cloudflare/workers-types@4.20260129.0)) + '@types/dompurify': + specifier: ^3.2.0 + version: 3.2.0 + dompurify: + specifier: ^3.3.1 + version: 3.3.1 + marked: + specifier: ^17.0.1 + version: 17.0.1 wrangler: specifier: ^4.61.1 version: 4.61.1(@cloudflare/workers-types@4.20260129.0) @@ -60,6 +69,9 @@ importers: '@tailwindcss/forms': specifier: ^0.5.11 version: 0.5.11(tailwindcss@4.1.18) + '@tailwindcss/typography': + specifier: ^0.5.19 + version: 0.5.19(tailwindcss@4.1.18) '@tailwindcss/vite': specifier: ^4.1.18 version: 4.1.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)) @@ -1059,6 +1071,11 @@ packages: resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==, tarball: https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz} engines: {node: '>= 10'} + '@tailwindcss/typography@0.5.19': + resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==, tarball: https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + '@tailwindcss/vite@4.1.18': resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==, tarball: https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz} peerDependencies: @@ -1131,6 +1148,10 @@ packages: '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==, tarball: https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz} + '@types/dompurify@3.2.0': + resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==, tarball: https://registry.npmjs.org/@types/dompurify/-/dompurify-3.2.0.tgz} + deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed. + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==, tarball: https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz} @@ -1140,6 +1161,9 @@ packages: '@types/node@25.0.10': resolution: {integrity: sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==, tarball: https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==, tarball: https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz} + '@typescript-eslint/eslint-plugin@8.53.1': resolution: {integrity: sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==, tarball: https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1345,6 +1369,9 @@ packages: devalue@5.6.2: resolution: {integrity: sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==, tarball: https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz} + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==, tarball: https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==, tarball: https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz} @@ -1668,6 +1695,11 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==, tarball: https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz} + marked@17.0.1: + resolution: {integrity: sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==, tarball: https://registry.npmjs.org/marked/-/marked-17.0.1.tgz} + engines: {node: '>= 20'} + hasBin: true + mini-svg-data-uri@1.4.4: resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==, tarball: https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz} hasBin: true @@ -1791,6 +1823,10 @@ packages: peerDependencies: postcss: ^8.4.29 + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==, tarball: https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz} + engines: {node: '>=4'} + postcss-selector-parser@7.1.1: resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==, tarball: https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz} engines: {node: '>=4'} @@ -2878,6 +2914,11 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + '@tailwindcss/typography@0.5.19(tailwindcss@4.1.18)': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 4.1.18 + '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2))': dependencies: '@tailwindcss/node': 4.1.18 @@ -2936,6 +2977,10 @@ snapshots: '@types/cookie@0.6.0': {} + '@types/dompurify@3.2.0': + dependencies: + dompurify: 3.3.1 + '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} @@ -2944,6 +2989,9 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/trusted-types@2.0.7': + optional: true + '@typescript-eslint/eslint-plugin@8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -3155,6 +3203,10 @@ snapshots: devalue@5.6.2: {} + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + emoji-regex@10.6.0: {} enhanced-resolve@5.18.4: @@ -3493,6 +3545,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + marked@17.0.1: {} + mini-svg-data-uri@1.4.4: {} miniflare@4.20260128.0: @@ -3605,6 +3659,11 @@ snapshots: dependencies: postcss: 8.5.6 + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss-selector-parser@7.1.1: dependencies: cssesc: 3.0.0 diff --git a/src/app.css b/src/app.css index a7a8b9c..788d97d 100644 --- a/src/app.css +++ b/src/app.css @@ -1,6 +1,7 @@ @import 'tailwindcss'; @plugin '@tailwindcss/forms'; +@plugin '@tailwindcss/typography'; @theme inline { --color-base-50: var(--base-50); @@ -30,28 +31,28 @@ @layer base { :root { - --accent-50: var(--color-rose-50); - --accent-100: var(--color-rose-100); - --accent-200: var(--color-rose-200); - --accent-300: var(--color-rose-300); - --accent-400: var(--color-rose-400); - --accent-500: var(--color-rose-500); - --accent-600: var(--color-rose-600); - --accent-700: var(--color-rose-700); - --accent-800: var(--color-rose-800); - --accent-900: var(--color-rose-900); - --accent-950: var(--color-rose-950); + --accent-50: var(--color-cyan-50); + --accent-100: var(--color-cyan-100); + --accent-200: var(--color-cyan-200); + --accent-300: var(--color-cyan-300); + --accent-400: var(--color-cyan-400); + --accent-500: var(--color-cyan-500); + --accent-600: var(--color-cyan-600); + --accent-700: var(--color-cyan-700); + --accent-800: var(--color-cyan-800); + --accent-900: var(--color-cyan-900); + --accent-950: var(--color-cyan-950); - --base-50: var(--color-stone-50); - --base-100: var(--color-stone-100); - --base-200: var(--color-stone-200); - --base-300: var(--color-stone-300); - --base-400: var(--color-stone-400); - --base-500: var(--color-stone-500); - --base-600: var(--color-stone-600); - --base-700: var(--color-stone-700); - --base-800: var(--color-stone-800); - --base-900: var(--color-stone-900); - --base-950: var(--color-stone-950); + --base-50: var(--color-zinc-50); + --base-100: var(--color-zinc-100); + --base-200: var(--color-zinc-200); + --base-300: var(--color-zinc-300); + --base-400: var(--color-zinc-400); + --base-500: var(--color-zinc-500); + --base-600: var(--color-zinc-600); + --base-700: var(--color-zinc-700); + --base-800: var(--color-zinc-800); + --base-900: var(--color-zinc-900); + --base-950: var(--color-zinc-950); } } diff --git a/src/lib/atproto/settings.ts b/src/lib/atproto/settings.ts index aa189af..034fb34 100644 --- a/src/lib/atproto/settings.ts +++ b/src/lib/atproto/settings.ts @@ -13,7 +13,7 @@ export const permissions = { // example: only allow create and delete // collections: ['xyz.statusphere.status?action=create&action=update'], - collections: ['pics.atmo.image', 'pics.atmo.video'], + collections: ['pics.atmo.image', 'pics.atmo.video', 'pics.atmo.markdown'], // what types of authenticated proxied requests you can make to services diff --git a/src/lib/index.ts b/src/lib/index.ts index 81ac813..9781983 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -13,6 +13,10 @@ export function getShareLinkFromUri(uri: string, handle?: string): string { return getVideoShareLink(repo, parts.rkey); } + if (parts.collection === 'pics.atmo.markdown') { + return getMarkdownShareLink(repo, parts.rkey); + } + return getShareLink(repo, parts.rkey); } @@ -29,3 +33,10 @@ export function getVideoShareLink(repo: string, rkey: string): string { rkey: rkey })}`; } + +export function getMarkdownShareLink(repo: string, rkey: string): string { + return `${window.location.origin}${resolve('/m/[repo]/[rkey]', { + repo: repo, + rkey: rkey + })}`; +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 081ec45..a8c2253 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -10,6 +10,7 @@ parseUri } from '$lib/atproto'; import { compressImage, getImageFromRecord } from '$lib/atproto/image-helper'; + import type { AllowedCollection } from '$lib/atproto/settings'; import { generateVideoThumbnail } from '$lib/atproto/video-helper'; import Avatar from '$lib/atproto/UI/Avatar.svelte'; import Button from '$lib/atproto/UI/Button.svelte'; @@ -20,7 +21,7 @@ type MediaRecord = { uri: string; value: Record; - type: 'image' | 'video'; + type: 'image' | 'video' | 'markdown'; }; let fileInput: HTMLInputElement | undefined = $state(); @@ -31,14 +32,19 @@ let loadingImages = $state(false); let isDragging = $state(false); let dragCounter = 0; + let showMarkdownEditor = $state(false); + let markdownTitle = $state(''); + let markdownContent = $state(''); + let isPublishingMarkdown = $state(false); async function loadItems() { if (!user.did) return; loadingImages = true; try { - const [imageRecords, videoRecords] = await Promise.all([ + const [imageRecords, videoRecords, markdownRecords] = await Promise.all([ listRecords({ collection: 'pics.atmo.image', limit: 0 }), - listRecords({ collection: 'pics.atmo.video', limit: 0 }) + listRecords({ collection: 'pics.atmo.video', limit: 0 }), + listRecords({ collection: 'pics.atmo.markdown', limit: 0 }) ]); const imageItems: MediaRecord[] = (imageRecords as any[]).map((r) => ({ ...r, @@ -48,7 +54,11 @@ ...r, type: 'video' as const })); - const all = [...imageItems, ...videoItems]; + const markdownItems: MediaRecord[] = (markdownRecords as any[]).map((r) => ({ + ...r, + type: 'markdown' as const + })); + const all = [...imageItems, ...videoItems, ...markdownItems]; all.sort((a, b) => { const dateA = (a.value as any).createdAt || ''; const dateB = (b.value as any).createdAt || ''; @@ -244,6 +254,50 @@ } } + async function publishMarkdown() { + if (!markdownContent.trim()) { + feedbackMessage = 'Please write some content'; + return; + } + isPublishingMarkdown = true; + feedbackMessage = ''; + try { + const rkey = createTID(); + const record = { + $type: 'pics.atmo.markdown', + title: markdownTitle.trim() || undefined, + content: markdownContent, + createdAt: new Date().toISOString() + }; + await putRecord({ + collection: 'pics.atmo.markdown', + rkey, + record + }); + const newItem: MediaRecord = { + uri: `at://${user.did}/pics.atmo.markdown/${rkey}`, + value: record, + type: 'markdown' + }; + items = [newItem, ...items]; + const shareLink = getShareLinkFromUri(newItem.uri, user.profile?.handle); + try { + await navigator.clipboard.writeText(shareLink); + feedbackMessage = 'Link copied to clipboard!'; + } catch { + feedbackMessage = 'Markdown published!'; + } + markdownTitle = ''; + markdownContent = ''; + showMarkdownEditor = false; + } catch (error) { + console.error('Publish failed:', error); + feedbackMessage = 'Publish failed. Please try again.'; + } finally { + isPublishingMarkdown = false; + } + } + function getThumbnailUrl(record: MediaRecord): string | undefined { if (!user.did) return; if (record.type === 'video') { @@ -258,6 +312,9 @@ if (item.type === 'video') { return resolve('/v/[repo]/[rkey]', { repo: handle, rkey }); } + if (item.type === 'markdown') { + return resolve('/m/[repo]/[rkey]', { repo: handle, rkey }); + } return resolve('/i/[repo]/[rkey]', { repo: handle, rkey }); } @@ -275,32 +332,44 @@ } } + function getItemLabel(item: MediaRecord): string { + if (item.type === 'video') return 'video'; + if (item.type === 'markdown') return 'post'; + return 'image'; + } + + function getItemCollection(item: MediaRecord): AllowedCollection { + if (item.type === 'video') return 'pics.atmo.video'; + if (item.type === 'markdown') return 'pics.atmo.markdown'; + return 'pics.atmo.image'; + } + async function deleteItem(event: Event, item: MediaRecord) { event.stopPropagation(); event.preventDefault(); - const isVideo = item.type === 'video'; - if (!confirm(`Delete this ${isVideo ? 'video' : 'image'}?`)) return; + const label = getItemLabel(item); + if (!confirm(`Delete this ${label}?`)) return; const parsed = parseUri(item.uri); if (!parsed?.rkey) { - feedbackMessage = `Failed to delete ${isVideo ? 'video' : 'image'}`; + feedbackMessage = `Failed to delete ${label}`; return; } try { await deleteRecord({ - collection: isVideo ? 'pics.atmo.video' : 'pics.atmo.image', + collection: getItemCollection(item), rkey: parsed.rkey }); items = items.filter((i) => i.uri !== item.uri); - feedbackMessage = `${isVideo ? 'Video' : 'Image'} deleted`; + feedbackMessage = `${label.charAt(0).toUpperCase() + label.slice(1)} deleted`; setTimeout(() => { if (feedbackMessage.includes('deleted')) feedbackMessage = ''; }, 2000); } catch (error) { console.error('Failed to delete:', error); - feedbackMessage = `Failed to delete ${isVideo ? 'video' : 'image'}`; + feedbackMessage = `Failed to delete ${label}`; } } @@ -402,11 +471,36 @@ {isUploading ? 'Uploading...' : 'Upload image or video'} + + + {#if showMarkdownEditor} +
+ + + +
+ {/if} +
{feedbackMessage} @@ -417,33 +511,17 @@ {:else if items.length > 0} {:else} -
No images or videos uploaded yet
+
No content uploaded yet
{/if} {/if}
diff --git a/src/routes/m/[repo]/[rkey]/+page.server.ts b/src/routes/m/[repo]/[rkey]/+page.server.ts new file mode 100644 index 0000000..7420e01 --- /dev/null +++ b/src/routes/m/[repo]/[rkey]/+page.server.ts @@ -0,0 +1,23 @@ +import { getRecord, resolveHandle } from '$lib/atproto'; +import { isDid, isHandle } from '@atcute/lexicons/syntax'; +import { error } from '@sveltejs/kit'; + +export async function load({ params }) { + const repo = params.repo; + + const did = isDid(repo) + ? repo + : isHandle(repo) + ? await resolveHandle({ handle: repo }) + : undefined; + + if (!did) throw error(404, 'User not found'); + + const record = await getRecord({ did, collection: 'pics.atmo.markdown', rkey: params.rkey }); + + if (!record.value.content) { + throw error(404, 'Post not found'); + } + + return { record, did }; +} diff --git a/src/routes/m/[repo]/[rkey]/+page.svelte b/src/routes/m/[repo]/[rkey]/+page.svelte new file mode 100644 index 0000000..b8bd853 --- /dev/null +++ b/src/routes/m/[repo]/[rkey]/+page.svelte @@ -0,0 +1,151 @@ + + + + + + + + {title} + + + + +
+
+ {#if title !== 'Markdown post'} +

{title}

+ {/if} + + +
+ {#if uploader?.avatar} + + {/if} +
+ {#if uploader?.displayName || uploader?.handle} +
{uploader.displayName || uploader.handle}
+ {/if} + {#if createdAt} +
{formatDate(createdAt)}
+ {/if} +
+ +
+ + +
+ {@html sanitizedHtml} +
+
+
diff --git a/src/routes/m/[repo]/[rkey]/og.png/+server.ts b/src/routes/m/[repo]/[rkey]/og.png/+server.ts new file mode 100644 index 0000000..7bf9be3 --- /dev/null +++ b/src/routes/m/[repo]/[rkey]/og.png/+server.ts @@ -0,0 +1,41 @@ +import { getRecord, resolveHandle } from '$lib/atproto'; +import { isDid, isHandle } from '@atcute/lexicons/syntax'; +import { error } from '@sveltejs/kit'; +import { ImageResponse } from '@ethercorps/sveltekit-og'; + +export async function GET({ params }) { + const repo = params.repo; + + const did = isDid(repo) + ? repo + : isHandle(repo) + ? await resolveHandle({ handle: repo }) + : undefined; + + if (!did) throw error(404, 'User not found'); + + const record = await getRecord({ did, collection: 'pics.atmo.markdown', rkey: params.rkey }); + + const title = (record.value.title as string) || 'Markdown post'; + const content = (record.value.content as string) || ''; + const preview = content.length > 300 ? content.slice(0, 300) + '...' : content; + + const ogWidth = 1200; + const ogHeight = 630; + + const escapedTitle = title.replace(/&/g, '&').replace(//g, '>'); + const escapedPreview = preview + .replace(/&/g, '&') + .replace(//g, '>'); + + const htmlString = `
+
${escapedTitle}
+
${escapedPreview}
+
`; + + return new ImageResponse(htmlString, { + width: ogWidth, + height: ogHeight + }); +} -- 2.51.2