@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.
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +14 -0
- package/.changeset/publish-blogkit.md +5 -0
- package/.github/workflows/release.yml +65 -0
- package/LICENSE +22 -0
- package/README.md +35 -0
- package/docs/mintlify-blog-study.md +697 -0
- package/package.json +59 -0
- package/packages/blogkit/CHANGELOG.md +6 -0
- package/packages/blogkit/LICENSE +22 -0
- package/packages/blogkit/README.md +93 -0
- package/packages/blogkit/dist/components/blog/Avatar.svelte +15 -0
- package/packages/blogkit/dist/components/blog/Avatar.svelte.d.ts +22 -0
- package/packages/blogkit/dist/components/blog/BackLink.svelte +23 -0
- package/packages/blogkit/dist/components/blog/BackLink.svelte.d.ts +21 -0
- package/packages/blogkit/dist/components/blog/BlogCard.svelte +37 -0
- package/packages/blogkit/dist/components/blog/BlogCard.svelte.d.ts +22 -0
- package/packages/blogkit/dist/components/blog/BlogHeroCard.svelte +36 -0
- package/packages/blogkit/dist/components/blog/BlogHeroCard.svelte.d.ts +21 -0
- package/packages/blogkit/dist/components/blog/Container.svelte +8 -0
- package/packages/blogkit/dist/components/blog/Container.svelte.d.ts +29 -0
- package/packages/blogkit/dist/components/blog/ImageLightbox.svelte +58 -0
- package/packages/blogkit/dist/components/blog/ImageLightbox.svelte.d.ts +24 -0
- package/packages/blogkit/dist/components/blog/MorePosts.svelte +15 -0
- package/packages/blogkit/dist/components/blog/MorePosts.svelte.d.ts +21 -0
- package/packages/blogkit/dist/components/blog/ShareButtons.svelte +113 -0
- package/packages/blogkit/dist/components/blog/ShareButtons.svelte.d.ts +23 -0
- package/packages/blogkit/dist/components/blog/SummaryCard.svelte +11 -0
- package/packages/blogkit/dist/components/blog/SummaryCard.svelte.d.ts +20 -0
- package/packages/blogkit/dist/components/blog/TagTabs.svelte +32 -0
- package/packages/blogkit/dist/components/blog/TagTabs.svelte.d.ts +23 -0
- package/packages/blogkit/dist/index.d.ts +11 -0
- package/packages/blogkit/dist/index.js +11 -0
- package/packages/blogkit/dist/server/blog.d.ts +39 -0
- package/packages/blogkit/dist/server/blog.js +222 -0
- package/packages/blogkit/dist/server/index.d.ts +1 -0
- package/packages/blogkit/dist/server/index.js +1 -0
- package/packages/blogkit/dist/theme/ThemeSwitcher.svelte +34 -0
- package/packages/blogkit/dist/theme/ThemeSwitcher.svelte.d.ts +21 -0
- package/packages/blogkit/dist/theme/index.d.ts +2 -0
- package/packages/blogkit/dist/theme/index.js +2 -0
- package/packages/blogkit/dist/theme/store.d.ts +12 -0
- package/packages/blogkit/dist/theme/store.js +50 -0
- package/packages/blogkit/dist/types/blog.d.ts +31 -0
- package/packages/blogkit/dist/types/blog.js +1 -0
- package/packages/blogkit/package.json +66 -0
- package/packages/blogkit/src/lib/components/blog/Avatar.svelte +15 -0
- package/packages/blogkit/src/lib/components/blog/BackLink.svelte +23 -0
- package/packages/blogkit/src/lib/components/blog/BlogCard.svelte +37 -0
- package/packages/blogkit/src/lib/components/blog/BlogHeroCard.svelte +36 -0
- package/packages/blogkit/src/lib/components/blog/Container.svelte +8 -0
- package/packages/blogkit/src/lib/components/blog/ImageLightbox.svelte +58 -0
- package/packages/blogkit/src/lib/components/blog/MorePosts.svelte +15 -0
- package/packages/blogkit/src/lib/components/blog/ShareButtons.svelte +113 -0
- package/packages/blogkit/src/lib/components/blog/SummaryCard.svelte +11 -0
- package/packages/blogkit/src/lib/components/blog/TagTabs.svelte +32 -0
- package/packages/blogkit/src/lib/index.ts +15 -0
- package/packages/blogkit/src/lib/server/blog.ts +264 -0
- package/packages/blogkit/src/lib/server/index.ts +2 -0
- package/packages/blogkit/src/lib/theme/ThemeSwitcher.svelte +34 -0
- package/packages/blogkit/src/lib/theme/index.ts +3 -0
- package/packages/blogkit/src/lib/theme/store.ts +64 -0
- package/packages/blogkit/src/lib/types/blog.ts +36 -0
- package/packages/blogkit/svelte.config.js +8 -0
- package/packages/blogkit/tsconfig.json +5 -0
- package/playwright.config.ts +24 -0
- package/postcss.config.cjs +6 -0
- package/src/app.css +146 -0
- package/src/app.d.ts +13 -0
- package/src/app.html +26 -0
- package/src/content/blog/ai-summary-cards-with-frontmatter.md +32 -0
- package/src/content/blog/announcing-svelta-blog.md +19 -0
- package/src/content/blog/best-practices-ship-with-checklists.md +26 -0
- package/src/content/blog/building-a-mintlify-inspired-blog.md +49 -0
- package/src/content/blog/design-tokens-that-scale.md +47 -0
- package/src/content/blog/for-founders-why-speed-matters.md +23 -0
- package/src/content/blog/infinite-scroll-with-intersection-observer.md +37 -0
- package/src/content/blog/markdown-kitchen-sink.md +101 -0
- package/src/content/blog/markdown-pipeline-mdsvex-shiki.md +39 -0
- package/src/content/blog/rss-feeds-that-actually-work.md +25 -0
- package/src/content/blog/tag-tabs-and-mobile-fade-masks.md +25 -0
- package/src/lib/assets/favicon.svg +1 -0
- package/src/lib/components/site/SiteFooter.svelte +24 -0
- package/src/lib/components/site/SiteHeader.svelte +36 -0
- package/src/lib/content/authors.ts +28 -0
- package/src/lib/index.ts +1 -0
- package/src/lib/server/blog.ts +22 -0
- package/src/lib/server/rss.ts +58 -0
- package/src/lib/server/seo.ts +31 -0
- package/src/lib/stores/theme.ts +10 -0
- package/src/lib/types/blog.ts +1 -0
- package/src/routes/+layout.svelte +31 -0
- package/src/routes/+page.svelte +44 -0
- package/src/routes/blog/+page.server.ts +28 -0
- package/src/routes/blog/+page.svelte +122 -0
- package/src/routes/blog/[slug]/+page.server.ts +39 -0
- package/src/routes/blog/[slug]/+page.svelte +118 -0
- package/src/routes/blog/posts.json/+server.ts +32 -0
- package/src/routes/feed.xml/+server.ts +21 -0
- package/static/blog/authors/alex.svg +13 -0
- package/static/blog/authors/maria.svg +13 -0
- package/static/blog/authors/shawn.svg +13 -0
- package/static/blog/covers/ai-summary.svg +38 -0
- package/static/blog/covers/design-tokens.svg +37 -0
- package/static/blog/covers/infinite-scroll.svg +38 -0
- package/static/blog/covers/kitchen-sink.svg +36 -0
- package/static/blog/covers/markdown-pipeline.svg +41 -0
- package/static/blog/covers/mintlify-style.svg +35 -0
- package/static/blog/covers/rss.svg +34 -0
- package/static/robots.txt +3 -0
- package/svelte.config.js +70 -0
- package/tailwind.config.cjs +133 -0
- package/tests/blog.spec.ts +63 -0
- package/tsconfig.json +21 -0
- 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,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
|
+
});
|
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
|
+

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

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