@aureuma/svelta 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +14 -0
  3. package/.changeset/publish-blogkit.md +5 -0
  4. package/.github/workflows/release.yml +65 -0
  5. package/LICENSE +22 -0
  6. package/README.md +35 -0
  7. package/docs/mintlify-blog-study.md +697 -0
  8. package/package.json +59 -0
  9. package/packages/blogkit/CHANGELOG.md +6 -0
  10. package/packages/blogkit/LICENSE +22 -0
  11. package/packages/blogkit/README.md +93 -0
  12. package/packages/blogkit/dist/components/blog/Avatar.svelte +15 -0
  13. package/packages/blogkit/dist/components/blog/Avatar.svelte.d.ts +22 -0
  14. package/packages/blogkit/dist/components/blog/BackLink.svelte +23 -0
  15. package/packages/blogkit/dist/components/blog/BackLink.svelte.d.ts +21 -0
  16. package/packages/blogkit/dist/components/blog/BlogCard.svelte +37 -0
  17. package/packages/blogkit/dist/components/blog/BlogCard.svelte.d.ts +22 -0
  18. package/packages/blogkit/dist/components/blog/BlogHeroCard.svelte +36 -0
  19. package/packages/blogkit/dist/components/blog/BlogHeroCard.svelte.d.ts +21 -0
  20. package/packages/blogkit/dist/components/blog/Container.svelte +8 -0
  21. package/packages/blogkit/dist/components/blog/Container.svelte.d.ts +29 -0
  22. package/packages/blogkit/dist/components/blog/ImageLightbox.svelte +58 -0
  23. package/packages/blogkit/dist/components/blog/ImageLightbox.svelte.d.ts +24 -0
  24. package/packages/blogkit/dist/components/blog/MorePosts.svelte +15 -0
  25. package/packages/blogkit/dist/components/blog/MorePosts.svelte.d.ts +21 -0
  26. package/packages/blogkit/dist/components/blog/ShareButtons.svelte +113 -0
  27. package/packages/blogkit/dist/components/blog/ShareButtons.svelte.d.ts +23 -0
  28. package/packages/blogkit/dist/components/blog/SummaryCard.svelte +11 -0
  29. package/packages/blogkit/dist/components/blog/SummaryCard.svelte.d.ts +20 -0
  30. package/packages/blogkit/dist/components/blog/TagTabs.svelte +32 -0
  31. package/packages/blogkit/dist/components/blog/TagTabs.svelte.d.ts +23 -0
  32. package/packages/blogkit/dist/index.d.ts +11 -0
  33. package/packages/blogkit/dist/index.js +11 -0
  34. package/packages/blogkit/dist/server/blog.d.ts +39 -0
  35. package/packages/blogkit/dist/server/blog.js +222 -0
  36. package/packages/blogkit/dist/server/index.d.ts +1 -0
  37. package/packages/blogkit/dist/server/index.js +1 -0
  38. package/packages/blogkit/dist/theme/ThemeSwitcher.svelte +34 -0
  39. package/packages/blogkit/dist/theme/ThemeSwitcher.svelte.d.ts +21 -0
  40. package/packages/blogkit/dist/theme/index.d.ts +2 -0
  41. package/packages/blogkit/dist/theme/index.js +2 -0
  42. package/packages/blogkit/dist/theme/store.d.ts +12 -0
  43. package/packages/blogkit/dist/theme/store.js +50 -0
  44. package/packages/blogkit/dist/types/blog.d.ts +31 -0
  45. package/packages/blogkit/dist/types/blog.js +1 -0
  46. package/packages/blogkit/package.json +66 -0
  47. package/packages/blogkit/src/lib/components/blog/Avatar.svelte +15 -0
  48. package/packages/blogkit/src/lib/components/blog/BackLink.svelte +23 -0
  49. package/packages/blogkit/src/lib/components/blog/BlogCard.svelte +37 -0
  50. package/packages/blogkit/src/lib/components/blog/BlogHeroCard.svelte +36 -0
  51. package/packages/blogkit/src/lib/components/blog/Container.svelte +8 -0
  52. package/packages/blogkit/src/lib/components/blog/ImageLightbox.svelte +58 -0
  53. package/packages/blogkit/src/lib/components/blog/MorePosts.svelte +15 -0
  54. package/packages/blogkit/src/lib/components/blog/ShareButtons.svelte +113 -0
  55. package/packages/blogkit/src/lib/components/blog/SummaryCard.svelte +11 -0
  56. package/packages/blogkit/src/lib/components/blog/TagTabs.svelte +32 -0
  57. package/packages/blogkit/src/lib/index.ts +15 -0
  58. package/packages/blogkit/src/lib/server/blog.ts +264 -0
  59. package/packages/blogkit/src/lib/server/index.ts +2 -0
  60. package/packages/blogkit/src/lib/theme/ThemeSwitcher.svelte +34 -0
  61. package/packages/blogkit/src/lib/theme/index.ts +3 -0
  62. package/packages/blogkit/src/lib/theme/store.ts +64 -0
  63. package/packages/blogkit/src/lib/types/blog.ts +36 -0
  64. package/packages/blogkit/svelte.config.js +8 -0
  65. package/packages/blogkit/tsconfig.json +5 -0
  66. package/playwright.config.ts +24 -0
  67. package/postcss.config.cjs +6 -0
  68. package/src/app.css +146 -0
  69. package/src/app.d.ts +13 -0
  70. package/src/app.html +26 -0
  71. package/src/content/blog/ai-summary-cards-with-frontmatter.md +32 -0
  72. package/src/content/blog/announcing-svelta-blog.md +19 -0
  73. package/src/content/blog/best-practices-ship-with-checklists.md +26 -0
  74. package/src/content/blog/building-a-mintlify-inspired-blog.md +49 -0
  75. package/src/content/blog/design-tokens-that-scale.md +47 -0
  76. package/src/content/blog/for-founders-why-speed-matters.md +23 -0
  77. package/src/content/blog/infinite-scroll-with-intersection-observer.md +37 -0
  78. package/src/content/blog/markdown-kitchen-sink.md +101 -0
  79. package/src/content/blog/markdown-pipeline-mdsvex-shiki.md +39 -0
  80. package/src/content/blog/rss-feeds-that-actually-work.md +25 -0
  81. package/src/content/blog/tag-tabs-and-mobile-fade-masks.md +25 -0
  82. package/src/lib/assets/favicon.svg +1 -0
  83. package/src/lib/components/site/SiteFooter.svelte +24 -0
  84. package/src/lib/components/site/SiteHeader.svelte +36 -0
  85. package/src/lib/content/authors.ts +28 -0
  86. package/src/lib/index.ts +1 -0
  87. package/src/lib/server/blog.ts +22 -0
  88. package/src/lib/server/rss.ts +58 -0
  89. package/src/lib/server/seo.ts +31 -0
  90. package/src/lib/stores/theme.ts +10 -0
  91. package/src/lib/types/blog.ts +1 -0
  92. package/src/routes/+layout.svelte +31 -0
  93. package/src/routes/+page.svelte +44 -0
  94. package/src/routes/blog/+page.server.ts +28 -0
  95. package/src/routes/blog/+page.svelte +122 -0
  96. package/src/routes/blog/[slug]/+page.server.ts +39 -0
  97. package/src/routes/blog/[slug]/+page.svelte +118 -0
  98. package/src/routes/blog/posts.json/+server.ts +32 -0
  99. package/src/routes/feed.xml/+server.ts +21 -0
  100. package/static/blog/authors/alex.svg +13 -0
  101. package/static/blog/authors/maria.svg +13 -0
  102. package/static/blog/authors/shawn.svg +13 -0
  103. package/static/blog/covers/ai-summary.svg +38 -0
  104. package/static/blog/covers/design-tokens.svg +37 -0
  105. package/static/blog/covers/infinite-scroll.svg +38 -0
  106. package/static/blog/covers/kitchen-sink.svg +36 -0
  107. package/static/blog/covers/markdown-pipeline.svg +41 -0
  108. package/static/blog/covers/mintlify-style.svg +35 -0
  109. package/static/blog/covers/rss.svg +34 -0
  110. package/static/robots.txt +3 -0
  111. package/svelte.config.js +70 -0
  112. package/tailwind.config.cjs +133 -0
  113. package/tests/blog.spec.ts +63 -0
  114. package/tsconfig.json +21 -0
  115. package/vite.config.ts +14 -0
@@ -0,0 +1,64 @@
1
+ import { BROWSER } from 'esm-env';
2
+ import { writable, type Writable } from 'svelte/store';
3
+
4
+ export type ThemeMode = 'system' | 'light' | 'dark';
5
+
6
+ export type ThemeController = {
7
+ storageKey: string;
8
+ themeMode: Writable<ThemeMode>;
9
+ initTheme: () => void | (() => void);
10
+ setThemeMode: (mode: ThemeMode) => void;
11
+ };
12
+
13
+ function resolve(mode: ThemeMode): 'light' | 'dark' {
14
+ if (mode === 'light' || mode === 'dark') return mode;
15
+ const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)')?.matches ?? false;
16
+ return prefersDark ? 'dark' : 'light';
17
+ }
18
+
19
+ function apply(mode: ThemeMode) {
20
+ const resolved = resolve(mode);
21
+ document.documentElement.classList.remove('light', 'dark');
22
+ document.documentElement.classList.add(resolved);
23
+ document.documentElement.dataset.theme = mode;
24
+ }
25
+
26
+ export function createThemeController(opts?: { storageKey?: string; defaultMode?: ThemeMode }): ThemeController {
27
+ const storageKey = opts?.storageKey ?? 'blogkit-theme';
28
+ const defaultMode = opts?.defaultMode ?? 'system';
29
+ const themeMode = writable<ThemeMode>(defaultMode);
30
+
31
+ function readStored(): ThemeMode {
32
+ const v = localStorage.getItem(storageKey);
33
+ if (v === 'light' || v === 'dark' || v === 'system') return v;
34
+ return defaultMode;
35
+ }
36
+
37
+ function initTheme() {
38
+ if (!BROWSER) return;
39
+
40
+ const mode = readStored();
41
+ themeMode.set(mode);
42
+ apply(mode);
43
+
44
+ const mq = window.matchMedia?.('(prefers-color-scheme: dark)');
45
+ const onChange = () => {
46
+ let current: ThemeMode = mode;
47
+ const unsub = themeMode.subscribe((v) => (current = v));
48
+ unsub();
49
+ if (current === 'system') apply('system');
50
+ };
51
+
52
+ mq?.addEventListener?.('change', onChange);
53
+ return () => mq?.removeEventListener?.('change', onChange);
54
+ }
55
+
56
+ function setThemeMode(mode: ThemeMode) {
57
+ themeMode.set(mode);
58
+ if (!BROWSER) return;
59
+ localStorage.setItem(storageKey, mode);
60
+ apply(mode);
61
+ }
62
+
63
+ return { storageKey, themeMode, initTheme, setThemeMode };
64
+ }
@@ -0,0 +1,36 @@
1
+ import type { ComponentType } from 'svelte';
2
+
3
+ export type BlogCategory = {
4
+ label: string;
5
+ slug: string; // "engineering"
6
+ };
7
+
8
+ export type BlogAuthor = {
9
+ id: string;
10
+ name: string;
11
+ title: string;
12
+ avatar: string; // public path under /static
13
+ };
14
+
15
+ export type BlogPost = {
16
+ slug: string;
17
+ title: string;
18
+ excerpt: string;
19
+ category: BlogCategory;
20
+ tags: string[];
21
+ author: BlogAuthor;
22
+ date: string; // ISO (YYYY-MM-DD)
23
+ dateLong: string; // "February 8, 2026"
24
+ dateShort: string; // "Feb 8, 2026"
25
+ readingMinutes: number;
26
+ readingTimeShort: string; // "5 min read"
27
+ readingTimeLong: string; // "5 minutes read"
28
+ cover: string; // public path under /static
29
+ summaryAI?: string;
30
+ featured: boolean;
31
+ };
32
+
33
+ export type BlogPostFull = BlogPost & {
34
+ component: ComponentType;
35
+ };
36
+
@@ -0,0 +1,8 @@
1
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
2
+
3
+ /** @type {import('@sveltejs/kit').Config} */
4
+ const config = {
5
+ preprocess: vitePreprocess()
6
+ };
7
+
8
+ export default config;
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "include": ["src/**/*.ts", "src/**/*.svelte"],
4
+ "exclude": ["dist/**"]
5
+ }
@@ -0,0 +1,24 @@
1
+ import { defineConfig, devices } from '@playwright/test';
2
+
3
+ export default defineConfig({
4
+ testDir: './tests',
5
+ timeout: 30_000,
6
+ expect: {
7
+ timeout: 10_000
8
+ },
9
+ use: {
10
+ baseURL: 'http://localhost:4173',
11
+ trace: 'on-first-retry'
12
+ },
13
+ webServer: {
14
+ command: 'npm run build && npm run preview -- --port 4173',
15
+ port: 4173,
16
+ reuseExistingServer: !process.env.CI
17
+ },
18
+ projects: [
19
+ {
20
+ name: 'chromium',
21
+ use: { ...devices['Desktop Chrome'] }
22
+ }
23
+ ]
24
+ });
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ plugins: {
3
+ '@tailwindcss/postcss': {},
4
+ autoprefixer: {}
5
+ }
6
+ };
package/src/app.css ADDED
@@ -0,0 +1,146 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /* Theme tokens (Mintlify-inspired, not copied) */
6
+ html.light {
7
+ color-scheme: light;
8
+ --c-background-main: 252 252 252;
9
+ --c-background-soft: 245 246 248;
10
+ --c-background-invert: 8 12 20;
11
+ --c-text-main: 12 18 28;
12
+ --c-text-sub: 71 85 105;
13
+ --c-text-muted: 100 116 139;
14
+ --c-text-invert: 248 250 252;
15
+ --c-border-soft: 15 23 42;
16
+ --c-brand: 34 197 94;
17
+ --c-brand-soft: 220 252 231;
18
+ }
19
+
20
+ html.dark {
21
+ color-scheme: dark;
22
+ --c-background-main: 9 12 18;
23
+ --c-background-soft: 14 19 28;
24
+ --c-background-invert: 250 250 252;
25
+ --c-text-main: 238 242 247;
26
+ --c-text-sub: 148 163 184;
27
+ --c-text-muted: 100 116 139;
28
+ --c-text-invert: 10 12 16;
29
+ --c-border-soft: 148 163 184;
30
+ --c-brand: 52 211 153;
31
+ --c-brand-soft: 5 46 22;
32
+ }
33
+
34
+ html {
35
+ background: rgb(var(--c-background-main));
36
+ color: rgb(var(--c-text-main));
37
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial,
38
+ Apple Color Emoji, Segoe UI Emoji;
39
+ text-rendering: geometricPrecision;
40
+ -webkit-font-smoothing: antialiased;
41
+ -moz-osx-font-smoothing: grayscale;
42
+ }
43
+
44
+ body {
45
+ min-height: 100dvh;
46
+ }
47
+
48
+ /* Utilities */
49
+ .no-scrollbar {
50
+ -ms-overflow-style: none;
51
+ scrollbar-width: none;
52
+ }
53
+ .no-scrollbar::-webkit-scrollbar {
54
+ display: none;
55
+ }
56
+
57
+ /* For the mobile tag bar fade edges */
58
+ .fade-mask-x {
59
+ -webkit-mask-image: linear-gradient(
60
+ to right,
61
+ transparent 0,
62
+ black 24px,
63
+ black calc(100% - 24px),
64
+ transparent 100%
65
+ );
66
+ mask-image: linear-gradient(
67
+ to right,
68
+ transparent 0,
69
+ black 24px,
70
+ black calc(100% - 24px),
71
+ transparent 100%
72
+ );
73
+ }
74
+
75
+ /* Shiki blocks (Mintlify-like proportions) */
76
+ pre.shiki {
77
+ border-radius: 6px;
78
+ padding: 12px 16px;
79
+ line-height: 24px;
80
+ font-size: 14px;
81
+ overflow-x: auto;
82
+ border: 1px solid rgb(var(--c-border-soft) / 0.12);
83
+ /* Shiki provides inline light theme styles; we keep a soft fallback. */
84
+ background: rgb(var(--c-background-soft));
85
+ }
86
+
87
+ html.dark pre.shiki {
88
+ border-color: rgb(var(--c-border-soft) / 0.2);
89
+ background: var(--shiki-dark-bg) !important;
90
+ color: var(--shiki-dark) !important;
91
+ }
92
+
93
+ html.dark pre.shiki span {
94
+ color: var(--shiki-dark, inherit) !important;
95
+ font-style: var(--shiki-dark-font-style, inherit) !important;
96
+ }
97
+
98
+ pre.shiki code {
99
+ background: transparent !important;
100
+ padding: 0 !important;
101
+ border-radius: 0 !important;
102
+ }
103
+
104
+ /* Give in-article images the “frame” + zoom affordance */
105
+ .blog-prose img {
106
+ cursor: zoom-in;
107
+ }
108
+
109
+ /* Mintlify-like heading anchors (subtle + only on hover) */
110
+ .blog-prose .heading-anchor {
111
+ color: inherit;
112
+ text-decoration: none;
113
+ }
114
+ .blog-prose .heading-anchor::after {
115
+ content: '#';
116
+ margin-left: 8px;
117
+ font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
118
+ 'Courier New', monospace;
119
+ font-size: 12px;
120
+ opacity: 0;
121
+ color: rgb(var(--c-text-muted));
122
+ transition: opacity 120ms ease;
123
+ }
124
+ .blog-prose h2:hover .heading-anchor::after,
125
+ .blog-prose h3:hover .heading-anchor::after {
126
+ opacity: 1;
127
+ }
128
+
129
+ /* Restore spacing around Shiki blocks (we zeroed out base `pre` in typography) */
130
+ .blog-prose pre.shiki {
131
+ margin: 24px 0;
132
+ }
133
+
134
+ /* Markdown edge-case hardening */
135
+ .blog-prose {
136
+ overflow-wrap: anywhere;
137
+ }
138
+ .blog-prose table {
139
+ display: block;
140
+ max-width: 100%;
141
+ overflow-x: auto;
142
+ -webkit-overflow-scrolling: touch;
143
+ }
144
+ .blog-prose table thead th {
145
+ white-space: nowrap;
146
+ }
package/src/app.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ // See https://svelte.dev/docs/kit/types#app.d.ts
2
+ // for information about these interfaces
3
+ declare global {
4
+ namespace App {
5
+ // interface Error {}
6
+ // interface Locals {}
7
+ // interface PageData {}
8
+ // interface PageState {}
9
+ // interface Platform {}
10
+ }
11
+ }
12
+
13
+ export {};
package/src/app.html ADDED
@@ -0,0 +1,26 @@
1
+ <!doctype html>
2
+ <html lang="en" class="light">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <script>
7
+ (() => {
8
+ try {
9
+ const stored = localStorage.getItem('svelta-theme') || 'system';
10
+ const prefersDark =
11
+ window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
12
+ const resolved = stored === 'system' ? (prefersDark ? 'dark' : 'light') : stored;
13
+ document.documentElement.classList.remove('light', 'dark');
14
+ document.documentElement.classList.add(resolved);
15
+ document.documentElement.dataset.theme = stored;
16
+ } catch {
17
+ document.documentElement.classList.add('light');
18
+ }
19
+ })();
20
+ </script>
21
+ %sveltekit.head%
22
+ </head>
23
+ <body data-sveltekit-preload-data="hover">
24
+ <div style="display: contents">%sveltekit.body%</div>
25
+ </body>
26
+ </html>
@@ -0,0 +1,32 @@
1
+ ---
2
+ title: "AI Summary Cards With Frontmatter"
3
+ date: "2026-02-03"
4
+ category: "AI trends"
5
+ author: "maria"
6
+ cover: "/blog/covers/ai-summary.svg"
7
+ tags:
8
+ - "UX"
9
+ - "Content"
10
+ excerpt: "A skimmable summary card that appears near the top of the post, controlled by a single frontmatter field."
11
+ summaryAI: "Add an optional `summaryAI` field in frontmatter. If present, render a soft card after the hero image with a mono uppercase label (“AI SUMMARY”) and a short paragraph. If absent, omit the card entirely (Mintlify does this on some posts)."
12
+ ---
13
+
14
+ Readers don’t always want the whole story. They often want the gist, then decide.
15
+
16
+ ## Placement
17
+
18
+ The summary works best **after the hero image** and **before the article body**.
19
+
20
+ ## Implementation idea
21
+
22
+ ```md
23
+ summaryAI: "One paragraph that makes the post skimmable."
24
+ ```
25
+
26
+ Then in the post page:
27
+
28
+ ```svelte
29
+ {#if post.summaryAI}
30
+ <SummaryCard summary={post.summaryAI} />
31
+ {/if}
32
+ ```
@@ -0,0 +1,19 @@
1
+ ---
2
+ title: "Announcing the svelta Blog"
3
+ date: "2026-01-30"
4
+ category: "Announcements"
5
+ author: "shawn"
6
+ cover: "/blog/covers/mintlify-style.svg"
7
+ tags:
8
+ - "Launch"
9
+ excerpt: "A small, fast blog system that feels like product documentation: clean, structured, and easy to skim."
10
+ ---
11
+
12
+ We’re publishing more of the thinking behind svelta: engineering decisions, design constraints, and practical patterns.
13
+
14
+ ## What you’ll see here
15
+
16
+ - Implementation notes (with real code)
17
+ - Design systems that stay crisp in dark mode
18
+ - Product-focused writing, not marketing fluff
19
+
@@ -0,0 +1,26 @@
1
+ ---
2
+ title: "Best Practices: Ship With Checklists"
3
+ date: "2026-01-25"
4
+ category: "Best practices"
5
+ author: "maria"
6
+ cover: "/blog/covers/rss.svg"
7
+ tags:
8
+ - "Quality"
9
+ - "Process"
10
+ excerpt: "If something matters, put it in a checklist. Then automate the boring parts."
11
+ ---
12
+
13
+ Checklists aren’t bureaucracy. They’re a compression format for your standards.
14
+
15
+ ## A tiny publishing checklist
16
+
17
+ - Title is clear and specific
18
+ - Cover image is present and readable
19
+ - Excerpt is 1–2 sentences
20
+ - Code blocks are highlighted
21
+ - A “more posts” section exists
22
+
23
+ ## Bonus
24
+
25
+ Use RSS to distribute without algorithms.
26
+
@@ -0,0 +1,49 @@
1
+ ---
2
+ title: "Building a Mintlify-Inspired Blog in SvelteKit"
3
+ date: "2026-02-08"
4
+ category: "Engineering"
5
+ author: "shawn"
6
+ cover: "/blog/covers/mintlify-style.svg"
7
+ featured: true
8
+ tags:
9
+ - "SvelteKit"
10
+ - "mdsvex"
11
+ - "Tailwind"
12
+ excerpt: "A practical blueprint for a Mintlify-inspired blog: proportions, theming, Markdown rendering, reading-time, share widgets, infinite scroll, and RSS."
13
+ summaryAI: "We mirror Mintlify’s blog *system* (not its assets): fixed-width containers, hero+grid index, pill category tabs, a two-column post layout with a sticky author/share rail on desktop, Shiki-highlighted Markdown, reading-time labels, framed images, bottom recommendations, and an RSS feed. The implementation is SvelteKit-first: mdsvex for Markdown, Tailwind tokens for theming, and a JSON pagination endpoint for infinite scroll."
14
+ ---
15
+
16
+ This post documents the structure we’re building: a blog that *feels* like Mintlify’s in layout and typographic rhythm, while remaining our own code + assets.
17
+
18
+ ## What “Mintlify-inspired” means
19
+
20
+ It’s mainly about **placement and proportions**:
21
+
22
+ - A large **hero card** on the index, followed by a tag bar and a two-column grid
23
+ - Post pages that use a **narrow reading column** with a sticky right rail (desktop)
24
+ - Small mono uppercase metadata: category + reading-time + date
25
+
26
+ ## The content pipeline
27
+
28
+ We use Markdown for content, but we want code blocks to look like “real UI”. That means Shiki.
29
+
30
+ ```ts
31
+ // src/lib/server/blog.ts
32
+ import readingTime from "reading-time";
33
+
34
+ const minutes = Math.max(1, Math.round(readingTime(markdown).minutes));
35
+ ```
36
+
37
+ ## A tiny checklist
38
+
39
+ | Feature | Why it matters |
40
+ | --- | --- |
41
+ | Category pills | Fast scanning |
42
+ | AI summary card | Better “skim” mode |
43
+ | Sticky share rail | Share UX without interrupting reading |
44
+ | RSS feed | Works in every reader |
45
+
46
+ ## Next
47
+
48
+ In the following posts we’ll break down each part (tabs, infinite scroll, Markdown, RSS) into its own implementation.
49
+
@@ -0,0 +1,47 @@
1
+ ---
2
+ title: "Design Tokens That Scale (Without Getting Mushy)"
3
+ date: "2026-02-06"
4
+ category: "Design"
5
+ author: "maria"
6
+ cover: "/blog/covers/design-tokens.svg"
7
+ tags:
8
+ - "Design Systems"
9
+ - "Theming"
10
+ excerpt: "A small, opinionated token set that keeps contrast crisp across light/dark, while preserving the Mintlify-like quiet UI."
11
+ summaryAI: "Use a tiny set of semantic tokens: `background.main/soft`, `text.main/sub/muted`, `border.soft`, and `brand`. Map them to CSS variables on `html.light`/`html.dark`, then reference them via Tailwind colors. Keep borders extremely subtle and reserve the brand color for metadata accents."
12
+ ---
13
+
14
+ Mintlify’s blog looks “clean” because everything is token-driven and low-noise.
15
+
16
+ ## Token strategy
17
+
18
+ We intentionally keep the token set small:
19
+
20
+ - `background.main`, `background.soft`
21
+ - `text.main`, `text.sub`, `text.muted`
22
+ - `border.soft`
23
+ - `brand`
24
+
25
+ ## Why “soft borders” matter
26
+
27
+ When borders are too strong, the UI becomes a spreadsheet. The goal is to have frames that are visible *only when you’re looking for them*.
28
+
29
+ ## Tailwind mapping
30
+
31
+ We map CSS variables to Tailwind colors so components stay readable:
32
+
33
+ ```js
34
+ // tailwind.config.cjs
35
+ colors: {
36
+ background: {
37
+ main: "rgb(var(--c-background-main) / <alpha-value>)",
38
+ soft: "rgb(var(--c-background-soft) / <alpha-value>)",
39
+ },
40
+ }
41
+ ```
42
+
43
+ ## In-content images
44
+
45
+ We frame every in-article image with a subtle 1px border and a 10px radius to match the overall “rounded media” identity:
46
+
47
+ ![Framed example](/blog/covers/design-tokens.svg)
@@ -0,0 +1,23 @@
1
+ ---
2
+ title: "For Founders: Why Speed Matters More Than You Think"
3
+ date: "2026-01-28"
4
+ category: "For founders"
5
+ author: "alex"
6
+ cover: "/blog/covers/infinite-scroll.svg"
7
+ tags:
8
+ - "Product"
9
+ - "Strategy"
10
+ excerpt: "Shipping faster isn’t about heroics. It’s about removing friction everywhere: tooling, content, and communication."
11
+ summaryAI: "Speed compounds when you remove tiny blockers: slow builds, unclear docs, non-skimmable posts, and missing distribution channels like RSS. A blog that’s easy to publish and easy to read reduces context-switch costs for your team and your audience."
12
+ ---
13
+
14
+ If you’re building a product, your blog isn’t “marketing”. It’s a leverage tool.
15
+
16
+ ## The compounding effect
17
+
18
+ Small improvements in clarity and publishing speed add up:
19
+
20
+ - fewer repeated explanations
21
+ - faster onboarding
22
+ - better customer self-serve
23
+
@@ -0,0 +1,37 @@
1
+ ---
2
+ title: "Infinite Scroll With IntersectionObserver (Without Jank)"
3
+ date: "2026-02-05"
4
+ category: "Engineering"
5
+ author: "alex"
6
+ cover: "/blog/covers/infinite-scroll.svg"
7
+ tags:
8
+ - "Performance"
9
+ - "Svelte"
10
+ excerpt: "Mintlify-style content loading: a paginated JSON endpoint plus a sentinel at the bottom of the grid."
11
+ summaryAI: "Render the first page on the server for SEO, then append pages on the client using an `IntersectionObserver` watching a sentinel div. The endpoint should accept `offset`, `limit`, and `category`, and it should exclude the hero post to avoid duplicates."
12
+ ---
13
+
14
+ The Mintlify blog index loads more cards as you scroll. There’s no “Load more” button.
15
+
16
+ ## The shape of the API
17
+
18
+ We keep it simple:
19
+
20
+ ```http
21
+ GET /blog/posts.json?offset=8&limit=8&category=engineering
22
+ ```
23
+
24
+ ## The sentinel pattern
25
+
26
+ ```ts
27
+ const io = new IntersectionObserver(([entry]) => {
28
+ if (entry.isIntersecting) loadMore();
29
+ });
30
+
31
+ io.observe(sentinel);
32
+ ```
33
+
34
+ ## Optional: virtualization
35
+
36
+ If you ever have thousands of posts, add virtualization. Until then, pagination is enough.
37
+
@@ -0,0 +1,101 @@
1
+ ---
2
+ title: "Markdown Kitchen Sink (Rendering QA)"
3
+ date: "2026-02-08"
4
+ category: "Engineering"
5
+ author: "alex"
6
+ cover: "/blog/covers/kitchen-sink.svg"
7
+ tags:
8
+ - "Markdown"
9
+ - "QA"
10
+ excerpt: "A deliberately dense post to validate typography, spacing, tables, lists, code blocks, images, and edge cases in our Markdown renderer."
11
+ summaryAI: "This post exists to stress-test Markdown rendering: tables should scroll on mobile, code blocks should be readable in light/dark mode, headings should get anchors, and images should look framed and open in the lightbox."
12
+ ---
13
+
14
+ This post is intentionally packed. It is here to catch layout regressions before real content does.
15
+
16
+ ## Headings and anchors
17
+
18
+ If you hover headings on desktop, you should see a subtle `#` anchor indicator.
19
+
20
+ ### A smaller heading
21
+
22
+ Anchors should not shift layout, and scroll margins should land nicely below the sticky header.
23
+
24
+ ## Lists (including nesting)
25
+
26
+ - One
27
+ - Two
28
+ - Two.A
29
+ - Two.B
30
+ - Three
31
+
32
+ 1. First
33
+ 2. Second
34
+ 1. Second.A
35
+ 2. Second.B
36
+ 3. Third
37
+
38
+ Task list (GFM):
39
+
40
+ - [x] Basic list spacing
41
+ - [x] Nested list spacing
42
+ - [ ] Task lists render correctly
43
+
44
+ ## Blockquotes
45
+
46
+ > A blockquote should feel like a callout: a soft left border, readable spacing, and no weird quote marks.
47
+ >
48
+ > Multiple paragraphs should keep the border and spacing intact.
49
+
50
+ ## Inline formatting
51
+
52
+ This has **bold**, *italic*, ~~strikethrough~~, and `inline code`.
53
+
54
+ Long URL wrapping should not overflow:
55
+ https://example.com/a/really/really/really/really/really/really/really/long/path?with=query&and=more
56
+
57
+ ## Tables (GFM)
58
+
59
+ | Column | Description | Notes |
60
+ | --- | --- | --- |
61
+ | Tags | Category pills on `/blog` | Scrollable on mobile |
62
+ | RSS | `/feed.xml` | Auto-discoverable via `<link rel="alternate">` |
63
+ | Share | X / LinkedIn / Facebook / Copy | Sticky rail on desktop |
64
+
65
+ Tables should be horizontally scrollable on narrow screens.
66
+
67
+ ## Code blocks (Shiki)
68
+
69
+ ```ts
70
+ type PostFrontmatter = {
71
+ title: string;
72
+ date: string; // YYYY-MM-DD
73
+ category: string;
74
+ author: string;
75
+ cover: string;
76
+ excerpt?: string;
77
+ summaryAI?: string;
78
+ tags?: string[];
79
+ featured?: boolean;
80
+ };
81
+ ```
82
+
83
+ ```svelte
84
+ {#if post.summaryAI}
85
+ <SummaryCard summary={post.summaryAI} />
86
+ {/if}
87
+ ```
88
+
89
+ ## Images (frame + lightbox)
90
+
91
+ Click the image to open the lightbox.
92
+
93
+ ![RSS cover used as an in-article graphic](/blog/covers/rss.svg)
94
+
95
+ ## Horizontal rule
96
+
97
+ ---
98
+
99
+ ## Final note
100
+
101
+ If something looks wrong here, it will look wrong everywhere. Fix it here first.