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 + +