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

|
|
@@ -1,23 +0,0 @@
|
|
|
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
|
-
|
|
@@ -1,37 +0,0 @@
|
|
|
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
|
-
|
|
@@ -1,101 +0,0 @@
|
|
|
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.
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: "A Markdown Pipeline That Looks Like Product UI"
|
|
3
|
-
date: "2026-02-01"
|
|
4
|
-
category: "Engineering"
|
|
5
|
-
author: "alex"
|
|
6
|
-
cover: "/blog/covers/markdown-pipeline.svg"
|
|
7
|
-
tags:
|
|
8
|
-
- "Markdown"
|
|
9
|
-
- "Shiki"
|
|
10
|
-
excerpt: "mdsvex gets us Markdown-in-Svelte; Shiki gets us code blocks that don’t look like blogspot."
|
|
11
|
-
summaryAI: "Use mdsvex with `remark-gfm` (tables), `rehype-slug` + `rehype-autolink-headings` (deep links), and `@shikijs/rehype` (syntax highlighting with dual themes). Style `pre.shiki` to match Mintlify’s padding, radius, and line height."
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
There are two failure modes for Markdown blogs:
|
|
15
|
-
|
|
16
|
-
1. Everything looks like a generic “prose” template.
|
|
17
|
-
2. Code blocks are unreadable in either light or dark mode.
|
|
18
|
-
|
|
19
|
-
## Tables (GFM)
|
|
20
|
-
|
|
21
|
-
| Plugin | Why |
|
|
22
|
-
| --- | --- |
|
|
23
|
-
| `remark-gfm` | tables, strikethrough, task lists |
|
|
24
|
-
|
|
25
|
-
## Headings you can link to
|
|
26
|
-
|
|
27
|
-
We add `id`s and autolink headings so every section is shareable.
|
|
28
|
-
|
|
29
|
-
## Shiki, dual theme
|
|
30
|
-
|
|
31
|
-
Mintlify-style code blocks use different themes in light vs dark. We do the same.
|
|
32
|
-
|
|
33
|
-
```js
|
|
34
|
-
// svelte.config.js
|
|
35
|
-
rehypePlugins: [
|
|
36
|
-
[rehypeShiki, { themes: { light: "one-light", dark: "github-dark-default" } }]
|
|
37
|
-
]
|
|
38
|
-
```
|
|
39
|
-
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: "RSS Feeds That Actually Work"
|
|
3
|
-
date: "2026-02-04"
|
|
4
|
-
category: "Best practices"
|
|
5
|
-
author: "shawn"
|
|
6
|
-
cover: "/blog/covers/rss.svg"
|
|
7
|
-
tags:
|
|
8
|
-
- "RSS"
|
|
9
|
-
- "SEO"
|
|
10
|
-
excerpt: "Generate a real RSS 2.0 feed from your Markdown posts and expose it at /feed.xml."
|
|
11
|
-
summaryAI: "Emit RSS 2.0 with an `atom:link rel=\"self\"`, include `title`, `description`, `link`, `guid`, `pubDate`, and `category` for each post, and set `content-type: application/rss+xml`. Keep descriptions short and safe (use the excerpt)."
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
If you have a blog, you should have RSS. It’s a low-effort, high-trust feature.
|
|
15
|
-
|
|
16
|
-
## What we generate
|
|
17
|
-
|
|
18
|
-
- RSS 2.0
|
|
19
|
-
- `atom:link` self reference
|
|
20
|
-
- Items with title, link, description, pubDate, and category
|
|
21
|
-
|
|
22
|
-
## A note on HTML
|
|
23
|
-
|
|
24
|
-
RSS readers vary. Keep the description **plain text** (the excerpt) and let the post page carry the rich formatting.
|
|
25
|
-
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: "Tag Tabs + Mobile Fade Masks"
|
|
3
|
-
date: "2026-02-02"
|
|
4
|
-
category: "Design"
|
|
5
|
-
author: "maria"
|
|
6
|
-
cover: "/blog/covers/design-tokens.svg"
|
|
7
|
-
tags:
|
|
8
|
-
- "Responsive"
|
|
9
|
-
- "UI"
|
|
10
|
-
excerpt: "A horizontally scrollable tag bar on mobile, with fade edges so it feels intentional instead of broken."
|
|
11
|
-
summaryAI: "On small screens, render the tag row as `overflow-x-auto` with hidden scrollbar. Add a `mask-image` gradient to fade the left/right edges. On desktop, disable the mask and allow the pills to sit normally."
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
Pill tabs are easy on desktop, but mobile needs a different treatment.
|
|
15
|
-
|
|
16
|
-
## The goal
|
|
17
|
-
|
|
18
|
-
- Horizontal scroll
|
|
19
|
-
- No visible scrollbar
|
|
20
|
-
- Subtle fade at the edges
|
|
21
|
-
|
|
22
|
-
## CSS mask trick
|
|
23
|
-
|
|
24
|
-
We apply a `mask-image` gradient to the scrolling container, then remove it on larger screens.
|
|
25
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { ThemeSwitcher } from '@aureuma/blogkit/theme';
|
|
3
|
-
import { theme } from '$lib/stores/theme';
|
|
4
|
-
</script>
|
|
5
|
-
|
|
6
|
-
<footer class="border-t border-border-soft/10">
|
|
7
|
-
<div class="mx-auto flex w-full max-w-5xl flex-col gap-6 px-6 py-10 md:flex-row md:items-center md:justify-between">
|
|
8
|
-
<div class="text-xs font-mono uppercase tracking-[0.6px] text-text-muted">
|
|
9
|
-
© {new Date().getFullYear()} svelta
|
|
10
|
-
</div>
|
|
11
|
-
|
|
12
|
-
<div class="flex items-center justify-between gap-6 md:justify-end">
|
|
13
|
-
<a
|
|
14
|
-
href="/feed.xml"
|
|
15
|
-
class="inline-flex items-center gap-2 text-xs font-mono uppercase tracking-[0.6px] text-text-sub transition hover:text-text-main"
|
|
16
|
-
>
|
|
17
|
-
<span>RSS</span>
|
|
18
|
-
<span class="inline-block size-1.5 rounded-full bg-brand" aria-hidden="true"></span>
|
|
19
|
-
</a>
|
|
20
|
-
|
|
21
|
-
<ThemeSwitcher controller={theme} />
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
</footer>
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { page } from '$app/stores';
|
|
3
|
-
|
|
4
|
-
function isActive(path: string) {
|
|
5
|
-
return $page.url.pathname === path || $page.url.pathname.startsWith(`${path}/`);
|
|
6
|
-
}
|
|
7
|
-
</script>
|
|
8
|
-
|
|
9
|
-
<header
|
|
10
|
-
class="sticky top-0 z-50 border-b border-border-soft/10 bg-background-main/75 backdrop-blur supports-[backdrop-filter]:bg-background-main/55"
|
|
11
|
-
>
|
|
12
|
-
<div class="mx-auto flex h-16 w-full max-w-5xl items-center justify-between px-6">
|
|
13
|
-
<a href="/" class="group inline-flex items-center gap-2">
|
|
14
|
-
<span class="text-sm font-semibold tracking-tight">svelta</span>
|
|
15
|
-
<span
|
|
16
|
-
class="rounded-full bg-brand/15 px-2 py-0.5 text-[11px] font-mono uppercase tracking-[0.6px] text-brand"
|
|
17
|
-
>
|
|
18
|
-
Blog
|
|
19
|
-
</span>
|
|
20
|
-
</a>
|
|
21
|
-
|
|
22
|
-
<nav class="flex items-center gap-6 text-sm">
|
|
23
|
-
<a
|
|
24
|
-
href="/"
|
|
25
|
-
class="transition hover:text-text-main {isActive('/') ? 'text-text-main' : 'text-text-sub'}"
|
|
26
|
-
>Home</a
|
|
27
|
-
>
|
|
28
|
-
<a
|
|
29
|
-
href="/blog"
|
|
30
|
-
class="transition hover:text-text-main {isActive('/blog') ? 'text-text-main' : 'text-text-sub'}"
|
|
31
|
-
>Blog</a
|
|
32
|
-
>
|
|
33
|
-
</nav>
|
|
34
|
-
</div>
|
|
35
|
-
</header>
|
|
36
|
-
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { BlogAuthor } from '$lib/types/blog';
|
|
2
|
-
|
|
3
|
-
export const AUTHORS: Record<string, BlogAuthor> = {
|
|
4
|
-
shawn: {
|
|
5
|
-
id: 'shawn',
|
|
6
|
-
name: 'Shawn',
|
|
7
|
-
title: 'Builder at svelta',
|
|
8
|
-
avatar: '/blog/authors/shawn.svg'
|
|
9
|
-
},
|
|
10
|
-
alex: {
|
|
11
|
-
id: 'alex',
|
|
12
|
-
name: 'Alex Kim',
|
|
13
|
-
title: 'Product Engineer',
|
|
14
|
-
avatar: '/blog/authors/alex.svg'
|
|
15
|
-
},
|
|
16
|
-
maria: {
|
|
17
|
-
id: 'maria',
|
|
18
|
-
name: 'Maria Santos',
|
|
19
|
-
title: 'Design Lead',
|
|
20
|
-
avatar: '/blog/authors/maria.svg'
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export function getAuthor(id: string): BlogAuthor {
|
|
25
|
-
const author = AUTHORS[id];
|
|
26
|
-
if (!author) throw new Error(`Unknown author id: ${id}`);
|
|
27
|
-
return author;
|
|
28
|
-
}
|
package/src/lib/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
// place files you want to import through the `$lib` alias in this folder.
|
package/src/lib/server/blog.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { createBlog } from '@aureuma/blogkit/server';
|
|
2
|
-
import { getAuthor } from '$lib/content/authors';
|
|
3
|
-
import type { BlogPostFull } from '$lib/types/blog';
|
|
4
|
-
|
|
5
|
-
type CompiledModule = { default: BlogPostFull['component'] };
|
|
6
|
-
|
|
7
|
-
const compiledModules = import.meta.glob('/src/content/blog/*.md') as Record<
|
|
8
|
-
string,
|
|
9
|
-
() => Promise<CompiledModule>
|
|
10
|
-
>;
|
|
11
|
-
const rawModules = import.meta.glob('/src/content/blog/*.md', {
|
|
12
|
-
query: '?raw',
|
|
13
|
-
import: 'default'
|
|
14
|
-
}) as Record<string, () => Promise<string>>;
|
|
15
|
-
|
|
16
|
-
export const blog = createBlog({
|
|
17
|
-
compiledModules,
|
|
18
|
-
rawModules,
|
|
19
|
-
getAuthor
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
export const { getAllPosts, getAllPostsFull, getPostBySlug, getCategories, pickHero } = blog;
|
package/src/lib/server/rss.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import type { BlogPost } from '$lib/types/blog';
|
|
2
|
-
|
|
3
|
-
function escapeXml(input: string) {
|
|
4
|
-
return input
|
|
5
|
-
.replace(/&/g, '&')
|
|
6
|
-
.replace(/</g, '<')
|
|
7
|
-
.replace(/>/g, '>')
|
|
8
|
-
.replace(/"/g, '"')
|
|
9
|
-
.replace(/'/g, ''');
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function toPubDate(iso: string) {
|
|
13
|
-
const d = /^\d{4}-\d{2}-\d{2}$/.test(iso) ? new Date(`${iso}T00:00:00Z`) : new Date(iso);
|
|
14
|
-
return d.toUTCString();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function buildRss(opts: {
|
|
18
|
-
baseUrl: URL;
|
|
19
|
-
posts: BlogPost[];
|
|
20
|
-
siteTitle: string;
|
|
21
|
-
description: string;
|
|
22
|
-
maxItems?: number;
|
|
23
|
-
}) {
|
|
24
|
-
const siteLink = new URL('/blog', opts.baseUrl).toString();
|
|
25
|
-
const selfLink = new URL('/feed.xml', opts.baseUrl).toString();
|
|
26
|
-
const maxItems = Math.max(1, Math.min(200, opts.maxItems ?? 50));
|
|
27
|
-
|
|
28
|
-
const items = opts.posts
|
|
29
|
-
.slice(0, maxItems)
|
|
30
|
-
.map((post) => {
|
|
31
|
-
const link = new URL(`/blog/${post.slug}`, opts.baseUrl).toString();
|
|
32
|
-
return [
|
|
33
|
-
'<item>',
|
|
34
|
-
`<title>${escapeXml(post.title)}</title>`,
|
|
35
|
-
`<description>${escapeXml(post.excerpt)}</description>`,
|
|
36
|
-
`<link>${escapeXml(link)}</link>`,
|
|
37
|
-
`<guid isPermaLink="true">${escapeXml(link)}</guid>`,
|
|
38
|
-
`<pubDate>${escapeXml(toPubDate(post.date))}</pubDate>`,
|
|
39
|
-
`<category>${escapeXml(post.category.label)}</category>`,
|
|
40
|
-
`<author>${escapeXml(post.author.name)}</author>`,
|
|
41
|
-
'</item>'
|
|
42
|
-
].join('');
|
|
43
|
-
})
|
|
44
|
-
.join('');
|
|
45
|
-
|
|
46
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
47
|
-
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
48
|
-
<channel>
|
|
49
|
-
<title>${escapeXml(opts.siteTitle)}</title>
|
|
50
|
-
<link>${escapeXml(siteLink)}</link>
|
|
51
|
-
<atom:link href="${escapeXml(selfLink)}" rel="self" type="application/rss+xml" />
|
|
52
|
-
<description>${escapeXml(opts.description)}</description>
|
|
53
|
-
<language>en-us</language>
|
|
54
|
-
${items}
|
|
55
|
-
</channel>
|
|
56
|
-
</rss>`;
|
|
57
|
-
}
|
|
58
|
-
|
package/src/lib/server/seo.ts
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { BlogPost } from '$lib/types/blog';
|
|
2
|
-
|
|
3
|
-
export type PostSeo = {
|
|
4
|
-
title: string;
|
|
5
|
-
description: string;
|
|
6
|
-
canonicalUrl: string;
|
|
7
|
-
og: {
|
|
8
|
-
title: string;
|
|
9
|
-
description: string;
|
|
10
|
-
type: 'article';
|
|
11
|
-
url: string;
|
|
12
|
-
};
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export function buildPostSeo(post: BlogPost, canonicalUrl: string): PostSeo {
|
|
16
|
-
const title = `${post.title} | svelta Blog`;
|
|
17
|
-
const description = post.excerpt;
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
title,
|
|
21
|
-
description,
|
|
22
|
-
canonicalUrl,
|
|
23
|
-
og: {
|
|
24
|
-
title: post.title,
|
|
25
|
-
description,
|
|
26
|
-
type: 'article',
|
|
27
|
-
url: canonicalUrl
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
package/src/lib/stores/theme.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { createThemeController, type ThemeController, type ThemeMode } from '@aureuma/blogkit/theme';
|
|
2
|
-
|
|
3
|
-
// Keep the storage key stable for this app (also used in `src/app.html`).
|
|
4
|
-
export const theme: ThemeController = createThemeController({ storageKey: 'svelta-theme' });
|
|
5
|
-
|
|
6
|
-
export const themeMode = theme.themeMode;
|
|
7
|
-
export const initTheme = theme.initTheme;
|
|
8
|
-
export const setThemeMode = theme.setThemeMode;
|
|
9
|
-
|
|
10
|
-
export type { ThemeMode };
|
package/src/lib/types/blog.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export type { BlogAuthor, BlogCategory, BlogPost, BlogPostFull } from '@aureuma/blogkit';
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import favicon from '$lib/assets/favicon.svg';
|
|
3
|
-
import '@fontsource/inter/400.css';
|
|
4
|
-
import '@fontsource/inter/500.css';
|
|
5
|
-
import '@fontsource/inter/600.css';
|
|
6
|
-
import '@fontsource/geist-mono/400.css';
|
|
7
|
-
import '@fontsource/geist-mono/500.css';
|
|
8
|
-
import '../app.css';
|
|
9
|
-
import SiteFooter from '$lib/components/site/SiteFooter.svelte';
|
|
10
|
-
import SiteHeader from '$lib/components/site/SiteHeader.svelte';
|
|
11
|
-
import { initTheme } from '$lib/stores/theme';
|
|
12
|
-
import { onMount } from 'svelte';
|
|
13
|
-
|
|
14
|
-
let { children } = $props();
|
|
15
|
-
|
|
16
|
-
onMount(() => {
|
|
17
|
-
const cleanup = initTheme();
|
|
18
|
-
return cleanup;
|
|
19
|
-
});
|
|
20
|
-
</script>
|
|
21
|
-
|
|
22
|
-
<svelte:head>
|
|
23
|
-
<link rel="icon" href={favicon} />
|
|
24
|
-
<link rel="alternate" type="application/rss+xml" title="svelta Blog" href="/feed.xml" />
|
|
25
|
-
</svelte:head>
|
|
26
|
-
|
|
27
|
-
<div class="min-h-dvh bg-background-main text-text-main">
|
|
28
|
-
<SiteHeader />
|
|
29
|
-
<main class="flex-1">{@render children()}</main>
|
|
30
|
-
<SiteFooter />
|
|
31
|
-
</div>
|