From 1c38a9f3b30d98c1fbcf689c25545c0d7b29f083 Mon Sep 17 00:00:00 2001 From: Dinesh <13635627+HumbleBee14@users.noreply.github.com> Date: Mon, 13 Jul 2026 02:44:57 -0700 Subject: [PATCH] Add SVG/diagram block to editor; split homepage into Featured + Recent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SVG block: paste raw SVG, live Preview⇄Code segmented toggle, strict DOMParser validation with inline error, serializes as inline SVG inside
(theme-aware via currentColor, fully round-trippable). Author controls size via the markup; site gives a responsive default box. Homepage: rename the pinned section to Featured (shown only when posts are pinned) and add a Recent section below that auto-lists newest posts. --- src/pages/index.astro | 99 ++++++++++++------ src/styles/global.css | 4 + src/write/editor/blocks/SvgBlock.tsx | 144 +++++++++++++++++++++++++++ src/write/editor/editor-theme.css | 94 +++++++++++++++++ src/write/editor/schema.ts | 2 + src/write/editor/slashMenu.tsx | 16 +++ src/write/serialize/toMdx.test.ts | 18 ++++ src/write/serialize/toMdx.ts | 12 +++ src/write/serialize/validate.ts | 3 + 9 files changed, 362 insertions(+), 30 deletions(-) create mode 100644 src/write/editor/blocks/SvgBlock.tsx diff --git a/src/pages/index.astro b/src/pages/index.astro index a41c889..5ba3ffb 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -16,7 +16,8 @@ const allPosts = await Promise.all( const authors = await getCollection('authors'); const featured = allPosts.filter((p) => p.data.featured).slice(0, 3); -const featuredFinal = featured.length >= 3 ? featured : allPosts.slice(0, 3); +const featuredIds = new Set(featured.map((p) => p.id)); +const recent = allPosts.filter((p) => !featuredIds.has(p.id)).slice(0, 6); const topicCounts = countPostsByTopic(allPosts); const articleCount = allPosts.length; @@ -91,37 +92,75 @@ const featuredTools = allTools - -
-
+ ) + } + + { + recent.length > 0 && ( +
0 ? undefined : 'padding-top: 0;'}> + -
+ +
+ {recent.map((p) => ( + + +

{p.data.title}

+

{p.data.summary}

+
+ {p.authors.map((a) => a.data.name).join(', ')} + + {formatDate(p.data.date.toISOString())} · {p.data.readMin} min + +
+
+ ))} +
+ + + ) + }
diff --git a/src/styles/global.css b/src/styles/global.css index c22a896..d635f76 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -1501,6 +1501,10 @@ a.hashtag:hover { background: var(--paper-2); padding: 20px; } +.article-body .inline-figure svg { + max-width: 100%; + height: auto; +} .article-body .inline-figure-caption { font-family: var(--font-display); font-style: italic; diff --git a/src/write/editor/blocks/SvgBlock.tsx b/src/write/editor/blocks/SvgBlock.tsx new file mode 100644 index 0000000..60fcd0d --- /dev/null +++ b/src/write/editor/blocks/SvgBlock.tsx @@ -0,0 +1,144 @@ +import { useMemo, useState } from 'react'; +import { createReactBlockSpec } from '@blocknote/react'; + +const looksLikeSvg = (s: string): boolean => /]/i.test(s); + +// The SVG lives in the author's own browser and is reviewed before publish, but +// strip scripts so the in-editor preview can never execute pasted markup. +function stripScripts(svg: string): string { + return svg.replace(//gi, ''); +} + +type Check = + | { state: 'empty' } + | { state: 'ok'; html: string } + | { state: 'error'; message: string }; + +// Strict XML parsing mirrors what MDX/JSX needs to build (closed tags, quoted +// attrs, escaped &), so a preview that passes here won't break the published page. +function checkSvg(raw: string): Check { + const code = stripScripts(raw).trim(); + if (!code) return { state: 'empty' }; + if (!looksLikeSvg(code)) + return { state: 'error', message: 'The markup must contain an element.' }; + try { + const doc = new DOMParser().parseFromString(code, 'image/svg+xml'); + const err = doc.querySelector('parsererror'); + if (err) { + const detail = (err.textContent || '').replace(/\s+/g, ' ').trim(); + return { + state: 'error', + message: detail || 'This isn’t valid SVG — check for unclosed tags.', + }; + } + if (doc.documentElement.tagName.toLowerCase() !== 'svg') { + return { state: 'error', message: 'The markup must start with an element.' }; + } + return { state: 'ok', html: code }; + } catch { + return { state: 'error', message: 'This isn’t valid SVG markup.' }; + } +} + +export const createSvgBlock = createReactBlockSpec( + { + type: 'svg', + propSchema: { + code: { default: '' }, + caption: { default: '' }, + }, + content: 'none', + }, + { + render: ({ block, editor }) => { + const hasSvg = looksLikeSvg(block.props.code); + const check = useMemo(() => checkSvg(block.props.code), [block.props.code]); + const [mode, setMode] = useState<'edit' | 'preview'>(hasSvg ? 'preview' : 'edit'); + const setProps = (patch: Partial) => + editor.updateBlock(block, { props: { ...block.props, ...patch } }); + + return ( +
+
+
+ + +
+ {hasSvg && ( + + )} +
+ + {mode === 'edit' ? ( +
+ + Paste SVG markup — use currentColor for strokes and fills so it adapts + to light & dark + +