From 0994f3128154017384a0e60fd24be4dad04f6f43 Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Tue, 1 Sep 2026 11:19:13 +0000 Subject: [PATCH] add markdown formatting with code support --- package-lock.json | 61 ++++++++++++++++++++++++ package.json | 4 ++ src/app.css | 1 + src/lib/server/markdown.ts | 18 +++++++ src/routes/blog/[slug]/+page.server.ts | 2 + src/routes/blog/[slug]/+page.svelte | 5 +- src/routes/blog/[slug]/edit/+page.svelte | 4 +- 7 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 src/lib/server/markdown.ts diff --git a/package-lock.json b/package-lock.json index eb81508..bc0f8e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,9 @@ "@sveltejs/adapter-node": "^5.5.7", "ai": "^7.0.85", "drizzle-orm": "^1.0.0-rc.4", + "highlight.js": "^11.12.0", + "marked": "^18.0.11", + "marked-highlight": "^2.2.4", "mode-watcher": "^1.1.0", "postgres": "^3.4.9", "zod": "^4.5.4" @@ -26,6 +29,7 @@ "@sveltejs/enhanced-img": "^0.11.0", "@sveltejs/kit": "^2.70.3", "@sveltejs/vite-plugin-svelte": "^7.3.0", + "@tailwindcss/typography": "^0.5.20", "@tailwindcss/vite": "^4.3.3", "@types/eslint": "9.6.1", "@types/pg": "^8.23.1", @@ -3237,6 +3241,33 @@ "node": ">= 20" } }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.20.tgz", + "integrity": "sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >=4.0.0 || insiders" + } + }, + "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@tailwindcss/vite": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", @@ -5932,6 +5963,15 @@ "node": ">= 0.4" } }, + "node_modules/highlight.js": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.12.0.tgz", + "integrity": "sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -6537,6 +6577,27 @@ "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", "license": "BSD-2-Clause" }, + "node_modules/marked": { + "version": "18.0.11", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.11.tgz", + "integrity": "sha512-HnslJfsZkRPBDJRHvVtAaWlZHEpSu7u8LgQuJCELjRKuWR+hpq4A7sLq3p8HaI9ypVoXDXxV34CsQJEe1+J5Aw==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/marked-highlight": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/marked-highlight/-/marked-highlight-2.2.4.tgz", + "integrity": "sha512-PZxisNMJDduSjc0q6uvjsnqqHCXc9s0eyzxDO9sB1eNGJnd/H1/Fu+z6g/liC1dfJdFW4SftMwMlLvsBhUPrqQ==", + "license": "MIT", + "peerDependencies": { + "marked": ">=4 <19" + } + }, "node_modules/minimatch": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", diff --git a/package.json b/package.json index 162aa99..f0938a0 100755 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@sveltejs/enhanced-img": "^0.11.0", "@sveltejs/kit": "^2.70.3", "@sveltejs/vite-plugin-svelte": "^7.3.0", + "@tailwindcss/typography": "^0.5.20", "@tailwindcss/vite": "^4.3.3", "@types/eslint": "9.6.1", "@types/pg": "^8.23.1", @@ -64,6 +65,9 @@ "@sveltejs/adapter-node": "^5.5.7", "ai": "^7.0.85", "drizzle-orm": "^1.0.0-rc.4", + "highlight.js": "^11.12.0", + "marked": "^18.0.11", + "marked-highlight": "^2.2.4", "mode-watcher": "^1.1.0", "postgres": "^3.4.9", "zod": "^4.5.4" diff --git a/src/app.css b/src/app.css index 50b8e5e..ec5a3d6 100755 --- a/src/app.css +++ b/src/app.css @@ -1,4 +1,5 @@ @import 'tailwindcss'; +@plugin '@tailwindcss/typography'; @theme { --color-bg: #f6f4f0; diff --git a/src/lib/server/markdown.ts b/src/lib/server/markdown.ts new file mode 100644 index 0000000..72ba31a --- /dev/null +++ b/src/lib/server/markdown.ts @@ -0,0 +1,18 @@ +import { Marked } from 'marked'; +import { markedHighlight } from 'marked-highlight'; +import hljs from 'highlight.js'; + +const marked = new Marked( + markedHighlight({ + emptyLangClass: 'hljs', + langPrefix: 'hljs language-', + highlight(code, lang) { + const language = hljs.getLanguage(lang) ? lang : 'plaintext'; + return hljs.highlight(code, { language }).value; + } + }) +); + +export function renderMarkdown(content: string): string { + return marked.parse(content, { async: false }); +} diff --git a/src/routes/blog/[slug]/+page.server.ts b/src/routes/blog/[slug]/+page.server.ts index c9293ee..49adbef 100644 --- a/src/routes/blog/[slug]/+page.server.ts +++ b/src/routes/blog/[slug]/+page.server.ts @@ -1,6 +1,7 @@ import { db } from '$lib/server/db'; import { blogArticles } from '$lib/server/db/schema'; import { isAdmin } from '$lib/server/auth'; +import { renderMarkdown } from '$lib/server/markdown'; import { eq } from 'drizzle-orm'; import type { PageServerLoad } from './$types.js'; @@ -28,6 +29,7 @@ export const load: PageServerLoad = async (event) => { return { blogArticle: article, + contentHtml: renderMarkdown(article.content ?? ''), isAdmin: admin }; }; diff --git a/src/routes/blog/[slug]/+page.svelte b/src/routes/blog/[slug]/+page.svelte index 5ef4d8a..539bbf4 100644 --- a/src/routes/blog/[slug]/+page.svelte +++ b/src/routes/blog/[slug]/+page.svelte @@ -1,5 +1,6 @@ @@ -17,5 +18,7 @@ {/if}

{data.blogArticle.createdAt?.toDateString()}

-
{data.blogArticle.content}
+
+ {@html data.contentHtml} +
diff --git a/src/routes/blog/[slug]/edit/+page.svelte b/src/routes/blog/[slug]/edit/+page.svelte index 6c68e26..d13ac96 100644 --- a/src/routes/blog/[slug]/edit/+page.svelte +++ b/src/routes/blog/[slug]/edit/+page.svelte @@ -52,7 +52,9 @@ name="content" bind:value={content} rows="20" - class="rounded border border-gray-300 px-3 py-2 font-mono text-sm outline-none"> + class="rounded border border-gray-300 px-3 py-2 font-body text-base leading-relaxed text-gray-900 outline-none" + > +

Supports Markdown formatting.