@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
package/src/routes/+page.svelte
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
<section class="mx-auto w-full max-w-5xl px-6 py-20">
|
|
2
|
-
<div class="max-w-2xl">
|
|
3
|
-
<p class="text-xs font-mono uppercase tracking-[0.6px] text-text-muted">svelta</p>
|
|
4
|
-
<h1 class="mt-3 text-4xl font-semibold leading-[44px] tracking-[-0.8px]">
|
|
5
|
-
A Mintlify-inspired blog system, built in SvelteKit.
|
|
6
|
-
</h1>
|
|
7
|
-
<p class="mt-4 text-base leading-6 text-text-sub">
|
|
8
|
-
Markdown posts, Shiki code highlighting, tag filtering, infinite scroll, author profiles, share
|
|
9
|
-
widgets, AI summary cards, and an RSS feed.
|
|
10
|
-
</p>
|
|
11
|
-
|
|
12
|
-
<div class="mt-8 flex items-center gap-3">
|
|
13
|
-
<a
|
|
14
|
-
href="/blog"
|
|
15
|
-
class="inline-flex items-center justify-center rounded-full bg-text-main px-5 py-2 text-sm font-medium text-text-invert transition hover:opacity-90"
|
|
16
|
-
>
|
|
17
|
-
Go to Blog
|
|
18
|
-
</a>
|
|
19
|
-
<a
|
|
20
|
-
href="/feed.xml"
|
|
21
|
-
class="inline-flex items-center justify-center rounded-full border border-border-soft/15 bg-background-soft px-5 py-2 text-sm font-medium text-text-main transition hover:bg-background-main/60"
|
|
22
|
-
>
|
|
23
|
-
RSS Feed
|
|
24
|
-
</a>
|
|
25
|
-
</div>
|
|
26
|
-
</div>
|
|
27
|
-
|
|
28
|
-
<div class="mt-16 grid grid-cols-1 gap-5 md:grid-cols-2">
|
|
29
|
-
<div class="rounded-3xl border border-border-soft/10 bg-background-soft p-8">
|
|
30
|
-
<p class="text-xs font-mono uppercase tracking-[0.6px] text-text-muted">Design</p>
|
|
31
|
-
<p class="mt-2 text-lg font-medium tracking-tight">Proportions first</p>
|
|
32
|
-
<p class="mt-2 text-sm leading-6 text-text-sub">
|
|
33
|
-
Containers, spacing, and typography scale tuned to match the Mintlify blog’s feel.
|
|
34
|
-
</p>
|
|
35
|
-
</div>
|
|
36
|
-
<div class="rounded-3xl border border-border-soft/10 bg-background-soft p-8">
|
|
37
|
-
<p class="text-xs font-mono uppercase tracking-[0.6px] text-text-muted">Engineering</p>
|
|
38
|
-
<p class="mt-2 text-lg font-medium tracking-tight">Fast content pipeline</p>
|
|
39
|
-
<p class="mt-2 text-sm leading-6 text-text-sub">
|
|
40
|
-
mdsvex + Shiki + reading-time on the server, with incremental loading on the client.
|
|
41
|
-
</p>
|
|
42
|
-
</div>
|
|
43
|
-
</div>
|
|
44
|
-
</section>
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { getAllPosts, getCategories, pickHero } from '$lib/server/blog';
|
|
2
|
-
import type { PageServerLoad } from './$types';
|
|
3
|
-
|
|
4
|
-
const PAGE_SIZE = 8;
|
|
5
|
-
|
|
6
|
-
export const load: PageServerLoad = async ({ url }) => {
|
|
7
|
-
const [all, categories] = await Promise.all([getAllPosts(), getCategories()]);
|
|
8
|
-
const hero = await pickHero(all);
|
|
9
|
-
|
|
10
|
-
const selected = url.searchParams.get('category') ?? '';
|
|
11
|
-
const validSelected = selected && categories.some((c) => c.slug === selected) ? selected : '';
|
|
12
|
-
|
|
13
|
-
const rest = all.filter((p) => p.slug !== hero.slug);
|
|
14
|
-
const filtered = validSelected ? rest.filter((p) => p.category.slug === validSelected) : rest;
|
|
15
|
-
|
|
16
|
-
const initialPosts = filtered.slice(0, PAGE_SIZE);
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
hero,
|
|
20
|
-
categories,
|
|
21
|
-
selectedCategory: validSelected,
|
|
22
|
-
initialPosts,
|
|
23
|
-
pageSize: PAGE_SIZE,
|
|
24
|
-
hasMore: filtered.length > initialPosts.length,
|
|
25
|
-
total: filtered.length
|
|
26
|
-
};
|
|
27
|
-
};
|
|
28
|
-
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { goto } from '$app/navigation';
|
|
3
|
-
import { page } from '$app/stores';
|
|
4
|
-
import { BlogCard, BlogHeroCard, Container, TagTabs } from '@aureuma/blogkit';
|
|
5
|
-
import type { BlogPost } from '$lib/types/blog';
|
|
6
|
-
import { onMount } from 'svelte';
|
|
7
|
-
|
|
8
|
-
let { data } = $props<{
|
|
9
|
-
data: {
|
|
10
|
-
hero: BlogPost;
|
|
11
|
-
categories: { label: string; slug: string }[];
|
|
12
|
-
selectedCategory: string;
|
|
13
|
-
initialPosts: BlogPost[];
|
|
14
|
-
pageSize: number;
|
|
15
|
-
hasMore: boolean;
|
|
16
|
-
total: number;
|
|
17
|
-
};
|
|
18
|
-
}>();
|
|
19
|
-
|
|
20
|
-
let posts = $state<BlogPost[]>([]);
|
|
21
|
-
let offset = $state(0);
|
|
22
|
-
let hasMore = $state(false);
|
|
23
|
-
let loading = $state(false);
|
|
24
|
-
let sentinel: HTMLDivElement | null = $state(null);
|
|
25
|
-
|
|
26
|
-
$effect(() => {
|
|
27
|
-
posts = data.initialPosts;
|
|
28
|
-
offset = data.initialPosts.length;
|
|
29
|
-
hasMore = data.hasMore;
|
|
30
|
-
loading = false;
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
function selectCategory(slug: string) {
|
|
34
|
-
const u = new URL($page.url);
|
|
35
|
-
if (!slug) u.searchParams.delete('category');
|
|
36
|
-
else u.searchParams.set('category', slug);
|
|
37
|
-
goto(`${u.pathname}${u.searchParams.toString() ? `?${u.searchParams.toString()}` : ''}`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function loadMore() {
|
|
41
|
-
if (!hasMore || loading) return;
|
|
42
|
-
loading = true;
|
|
43
|
-
|
|
44
|
-
const u = new URL('/blog/posts.json', $page.url.origin);
|
|
45
|
-
u.searchParams.set('offset', String(offset));
|
|
46
|
-
u.searchParams.set('limit', String(data.pageSize));
|
|
47
|
-
if (data.selectedCategory) u.searchParams.set('category', data.selectedCategory);
|
|
48
|
-
|
|
49
|
-
const res = await fetch(u);
|
|
50
|
-
if (!res.ok) {
|
|
51
|
-
loading = false;
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const payload = (await res.json()) as { posts: BlogPost[]; hasMore: boolean };
|
|
56
|
-
posts = [...posts, ...payload.posts];
|
|
57
|
-
offset += payload.posts.length;
|
|
58
|
-
hasMore = payload.hasMore;
|
|
59
|
-
loading = false;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
onMount(() => {
|
|
63
|
-
if (!sentinel) return;
|
|
64
|
-
const io = new IntersectionObserver(
|
|
65
|
-
(entries) => {
|
|
66
|
-
if (entries.some((e) => e.isIntersecting)) loadMore();
|
|
67
|
-
},
|
|
68
|
-
{ rootMargin: '800px 0px' }
|
|
69
|
-
);
|
|
70
|
-
io.observe(sentinel);
|
|
71
|
-
return () => io.disconnect();
|
|
72
|
-
});
|
|
73
|
-
</script>
|
|
74
|
-
|
|
75
|
-
<Container>
|
|
76
|
-
<BlogHeroCard post={data.hero} />
|
|
77
|
-
|
|
78
|
-
<div class="flex items-center justify-between gap-4">
|
|
79
|
-
<div class="min-w-0 flex-1">
|
|
80
|
-
<TagTabs
|
|
81
|
-
categories={data.categories}
|
|
82
|
-
selected={data.selectedCategory}
|
|
83
|
-
onSelect={selectCategory}
|
|
84
|
-
/>
|
|
85
|
-
</div>
|
|
86
|
-
|
|
87
|
-
<a
|
|
88
|
-
href="/feed.xml"
|
|
89
|
-
class="inline-flex size-9 shrink-0 items-center justify-center rounded-full border border-border-soft/10 bg-background-soft text-text-sub transition hover:bg-background-main/60 hover:text-text-main"
|
|
90
|
-
aria-label="RSS feed"
|
|
91
|
-
>
|
|
92
|
-
<svg viewBox="0 0 24 24" class="size-4" aria-hidden="true">
|
|
93
|
-
<path
|
|
94
|
-
fill="currentColor"
|
|
95
|
-
d="M6.18 17.82a2.18 2.18 0 1 1 0 4.36 2.18 2.18 0 0 1 0-4.36ZM2 8.5v3.1c5.7 0 10.4 4.7 10.4 10.4h3.1C15.5 14.6 9.4 8.5 2 8.5Zm0-6v3.1c9.1 0 16.4 7.3 16.4 16.4H22C22 11.2 12.8 2 2 2Z"
|
|
96
|
-
/>
|
|
97
|
-
</svg>
|
|
98
|
-
</a>
|
|
99
|
-
</div>
|
|
100
|
-
|
|
101
|
-
<section class="pb-32 pt-8">
|
|
102
|
-
<div class="grid grid-cols-1 gap-x-5 gap-y-12 md:grid-cols-2">
|
|
103
|
-
{#if posts.length === 0}
|
|
104
|
-
<p class="text-sm leading-6 text-text-sub">No posts in this category yet.</p>
|
|
105
|
-
{:else}
|
|
106
|
-
{#each posts as post (post.slug)}
|
|
107
|
-
<BlogCard post={post} />
|
|
108
|
-
{/each}
|
|
109
|
-
{/if}
|
|
110
|
-
</div>
|
|
111
|
-
|
|
112
|
-
<div class="mt-16 flex items-center justify-center">
|
|
113
|
-
<div bind:this={sentinel} class="h-10 w-full" aria-hidden="true"></div>
|
|
114
|
-
</div>
|
|
115
|
-
|
|
116
|
-
{#if loading}
|
|
117
|
-
<p class="mt-6 text-center text-xs font-mono uppercase tracking-[0.6px] text-text-muted">
|
|
118
|
-
Loading…
|
|
119
|
-
</p>
|
|
120
|
-
{/if}
|
|
121
|
-
</section>
|
|
122
|
-
</Container>
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { getAllPosts, getPostBySlug } from '$lib/server/blog';
|
|
2
|
-
import { buildPostSeo } from '$lib/server/seo';
|
|
3
|
-
import { error } from '@sveltejs/kit';
|
|
4
|
-
import { render } from 'svelte/server';
|
|
5
|
-
import type { PageServerLoad } from './$types';
|
|
6
|
-
|
|
7
|
-
export const load: PageServerLoad = async ({ params, url }) => {
|
|
8
|
-
const found = await getPostBySlug(params.slug);
|
|
9
|
-
if (!found) throw error(404, 'Post not found');
|
|
10
|
-
|
|
11
|
-
const { component, ...post } = found;
|
|
12
|
-
const rendered = render(component);
|
|
13
|
-
|
|
14
|
-
const all = await getAllPosts();
|
|
15
|
-
const candidates = all.filter((p) => p.slug !== post.slug);
|
|
16
|
-
|
|
17
|
-
const sameCategory = candidates.filter((p) => p.category.slug === post.category.slug);
|
|
18
|
-
const morePosts: typeof candidates = [];
|
|
19
|
-
|
|
20
|
-
for (const p of sameCategory) {
|
|
21
|
-
if (morePosts.length >= 2) break;
|
|
22
|
-
morePosts.push(p);
|
|
23
|
-
}
|
|
24
|
-
for (const p of candidates) {
|
|
25
|
-
if (morePosts.length >= 2) break;
|
|
26
|
-
if (morePosts.some((x) => x.slug === p.slug)) continue;
|
|
27
|
-
morePosts.push(p);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const canonicalUrl = new URL(`/blog/${post.slug}`, url).toString();
|
|
31
|
-
|
|
32
|
-
return {
|
|
33
|
-
post,
|
|
34
|
-
contentHtml: rendered.html,
|
|
35
|
-
morePosts,
|
|
36
|
-
canonicalUrl,
|
|
37
|
-
seo: buildPostSeo(post, canonicalUrl)
|
|
38
|
-
};
|
|
39
|
-
};
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { Avatar, BackLink, Container, ImageLightbox, MorePosts, ShareButtons, SummaryCard } from '@aureuma/blogkit';
|
|
3
|
-
import type { BlogPost } from '$lib/types/blog';
|
|
4
|
-
|
|
5
|
-
let { data } = $props<{
|
|
6
|
-
data: {
|
|
7
|
-
post: BlogPost;
|
|
8
|
-
contentHtml: string;
|
|
9
|
-
morePosts: BlogPost[];
|
|
10
|
-
canonicalUrl: string;
|
|
11
|
-
seo: {
|
|
12
|
-
title: string;
|
|
13
|
-
description: string;
|
|
14
|
-
canonicalUrl: string;
|
|
15
|
-
og: { title: string; description: string; type: 'article'; url: string };
|
|
16
|
-
};
|
|
17
|
-
};
|
|
18
|
-
}>();
|
|
19
|
-
|
|
20
|
-
let lightbox = $state<{ src: string; alt: string } | null>(null);
|
|
21
|
-
let articleEl = $state<HTMLElement | null>(null);
|
|
22
|
-
|
|
23
|
-
$effect(() => {
|
|
24
|
-
// Re-bind image zoom handlers when content changes (e.g. client-side navigation).
|
|
25
|
-
const _ = data.contentHtml;
|
|
26
|
-
if (!articleEl) return;
|
|
27
|
-
|
|
28
|
-
const imgs = Array.from(articleEl.querySelectorAll('img'));
|
|
29
|
-
const onClick = (e: Event) => {
|
|
30
|
-
const img = e.currentTarget as HTMLImageElement;
|
|
31
|
-
lightbox = { src: img.currentSrc || img.src, alt: img.alt || data.post.title };
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
for (const img of imgs) img.addEventListener('click', onClick);
|
|
35
|
-
return () => {
|
|
36
|
-
for (const img of imgs) img.removeEventListener('click', onClick);
|
|
37
|
-
};
|
|
38
|
-
});
|
|
39
|
-
</script>
|
|
40
|
-
|
|
41
|
-
<svelte:head>
|
|
42
|
-
<title>{data.seo.title}</title>
|
|
43
|
-
<meta name="description" content={data.seo.description} />
|
|
44
|
-
<link rel="canonical" href={data.seo.canonicalUrl} />
|
|
45
|
-
<meta property="og:title" content={data.seo.og.title} />
|
|
46
|
-
<meta property="og:description" content={data.seo.og.description} />
|
|
47
|
-
<meta property="og:type" content={data.seo.og.type} />
|
|
48
|
-
<meta property="og:url" content={data.seo.og.url} />
|
|
49
|
-
</svelte:head>
|
|
50
|
-
|
|
51
|
-
<Container size="4xl">
|
|
52
|
-
<div class="mt-[4.5rem] pb-[7.5rem]">
|
|
53
|
-
<BackLink />
|
|
54
|
-
|
|
55
|
-
<header class="mt-8 border-b border-border-soft/10 pb-8">
|
|
56
|
-
<div class="flex items-center gap-2 text-xs font-mono uppercase tracking-[0.6px]">
|
|
57
|
-
<span class="text-brand">{data.post.category.label}</span>
|
|
58
|
-
<span class="text-text-muted" aria-hidden="true">/</span>
|
|
59
|
-
<span class="text-text-muted">{data.post.readingTimeLong}</span>
|
|
60
|
-
</div>
|
|
61
|
-
|
|
62
|
-
<h1 class="mt-4 text-4xl font-semibold leading-[44px] tracking-[-0.8px] text-text-main">
|
|
63
|
-
{data.post.title}
|
|
64
|
-
</h1>
|
|
65
|
-
|
|
66
|
-
<p class="mt-4 text-xs font-mono uppercase tracking-[0.6px] text-text-muted">
|
|
67
|
-
{data.post.dateLong}
|
|
68
|
-
</p>
|
|
69
|
-
</header>
|
|
70
|
-
|
|
71
|
-
<div class="mt-10 grid grid-cols-1 gap-x-16 md:grid-cols-[minmax(0,628px)_160px]">
|
|
72
|
-
<div class="min-w-0">
|
|
73
|
-
<div class="overflow-hidden rounded-3xl border border-border-soft/10 bg-background-soft">
|
|
74
|
-
<img src={data.post.cover} alt={data.post.title} class="h-[360px] w-full object-cover" />
|
|
75
|
-
</div>
|
|
76
|
-
|
|
77
|
-
{#if data.post.summaryAI}
|
|
78
|
-
<div class="mt-6">
|
|
79
|
-
<SummaryCard summary={data.post.summaryAI} />
|
|
80
|
-
</div>
|
|
81
|
-
{/if}
|
|
82
|
-
|
|
83
|
-
<div class="mt-8 flex flex-col gap-6 md:hidden">
|
|
84
|
-
<div class="flex items-center gap-3">
|
|
85
|
-
<Avatar src={data.post.author.avatar} alt={data.post.author.name} size={48} />
|
|
86
|
-
<div class="leading-tight">
|
|
87
|
-
<div class="text-sm font-medium tracking-tight text-text-main">
|
|
88
|
-
{data.post.author.name}
|
|
89
|
-
</div>
|
|
90
|
-
<div class="text-xs text-text-muted">{data.post.author.title}</div>
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
<ShareButtons title={data.post.title} url={data.canonicalUrl} testId="blog-share-mobile" />
|
|
94
|
-
</div>
|
|
95
|
-
|
|
96
|
-
<article bind:this={articleEl} class="blog-prose prose mt-10">
|
|
97
|
-
{@html data.contentHtml}
|
|
98
|
-
</article>
|
|
99
|
-
|
|
100
|
-
<MorePosts posts={data.morePosts} />
|
|
101
|
-
</div>
|
|
102
|
-
|
|
103
|
-
<aside class="sticky top-20 hidden self-start md:flex md:flex-col md:gap-8">
|
|
104
|
-
<div class="flex items-center gap-3">
|
|
105
|
-
<Avatar src={data.post.author.avatar} alt={data.post.author.name} size={48} />
|
|
106
|
-
<div class="leading-tight">
|
|
107
|
-
<div class="text-sm font-medium tracking-tight text-text-main">{data.post.author.name}</div>
|
|
108
|
-
<div class="text-xs text-text-muted">{data.post.author.title}</div>
|
|
109
|
-
</div>
|
|
110
|
-
</div>
|
|
111
|
-
|
|
112
|
-
<ShareButtons title={data.post.title} url={data.canonicalUrl} testId="blog-share-desktop" />
|
|
113
|
-
</aside>
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
</Container>
|
|
117
|
-
|
|
118
|
-
<ImageLightbox image={lightbox} onClose={() => (lightbox = null)} />
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { getAllPosts, getCategories, pickHero } from '$lib/server/blog';
|
|
2
|
-
import { json } from '@sveltejs/kit';
|
|
3
|
-
import type { RequestHandler } from './$types';
|
|
4
|
-
|
|
5
|
-
const DEFAULT_LIMIT = 8;
|
|
6
|
-
const MAX_LIMIT = 24;
|
|
7
|
-
|
|
8
|
-
export const GET: RequestHandler = async ({ url }) => {
|
|
9
|
-
const offset = Math.max(0, Number.parseInt(url.searchParams.get('offset') ?? '0', 10) || 0);
|
|
10
|
-
const limitRaw =
|
|
11
|
-
Number.parseInt(url.searchParams.get('limit') ?? String(DEFAULT_LIMIT), 10) || DEFAULT_LIMIT;
|
|
12
|
-
const limit = Math.min(MAX_LIMIT, Math.max(1, limitRaw));
|
|
13
|
-
|
|
14
|
-
const [all, categories] = await Promise.all([getAllPosts(), getCategories()]);
|
|
15
|
-
const hero = await pickHero(all);
|
|
16
|
-
|
|
17
|
-
const requestedCategory = url.searchParams.get('category') ?? '';
|
|
18
|
-
const category =
|
|
19
|
-
requestedCategory && categories.some((c) => c.slug === requestedCategory)
|
|
20
|
-
? requestedCategory
|
|
21
|
-
: '';
|
|
22
|
-
|
|
23
|
-
const rest = all.filter((p) => p.slug !== hero.slug);
|
|
24
|
-
const filtered = category ? rest.filter((p) => p.category.slug === category) : rest;
|
|
25
|
-
const posts = filtered.slice(offset, offset + limit);
|
|
26
|
-
|
|
27
|
-
return json({
|
|
28
|
-
posts,
|
|
29
|
-
hasMore: filtered.length > offset + posts.length
|
|
30
|
-
});
|
|
31
|
-
};
|
|
32
|
-
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { getAllPosts } from '$lib/server/blog';
|
|
2
|
-
import { buildRss } from '$lib/server/rss';
|
|
3
|
-
import type { RequestHandler } from './$types';
|
|
4
|
-
|
|
5
|
-
export const GET: RequestHandler = async ({ url }) => {
|
|
6
|
-
const posts = await getAllPosts();
|
|
7
|
-
|
|
8
|
-
const rss = buildRss({
|
|
9
|
-
baseUrl: url,
|
|
10
|
-
posts,
|
|
11
|
-
siteTitle: 'svelta Blog',
|
|
12
|
-
description: 'Engineering, design, and product notes from svelta.'
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
return new Response(rss, {
|
|
16
|
-
headers: {
|
|
17
|
-
'content-type': 'application/rss+xml; charset=utf-8',
|
|
18
|
-
'cache-control': 'max-age=0, s-maxage=3600'
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
-
<stop offset="0" stop-color="#fb7185"/>
|
|
5
|
-
<stop offset="1" stop-color="#f59e0b"/>
|
|
6
|
-
</linearGradient>
|
|
7
|
-
</defs>
|
|
8
|
-
<rect width="128" height="128" rx="64" fill="url(#g)"/>
|
|
9
|
-
<circle cx="64" cy="58" r="22" fill="rgba(255,255,255,0.23)"/>
|
|
10
|
-
<path d="M26 116c10-22 26-32 38-32s28 10 38 32" fill="rgba(255,255,255,0.23)"/>
|
|
11
|
-
<text x="64" y="76" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="28" font-weight="700" fill="rgba(255,255,255,0.92)">A</text>
|
|
12
|
-
</svg>
|
|
13
|
-
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
-
<stop offset="0" stop-color="#a78bfa"/>
|
|
5
|
-
<stop offset="1" stop-color="#22c55e"/>
|
|
6
|
-
</linearGradient>
|
|
7
|
-
</defs>
|
|
8
|
-
<rect width="128" height="128" rx="64" fill="url(#g)"/>
|
|
9
|
-
<circle cx="64" cy="58" r="22" fill="rgba(255,255,255,0.23)"/>
|
|
10
|
-
<path d="M26 116c10-22 26-32 38-32s28 10 38 32" fill="rgba(255,255,255,0.23)"/>
|
|
11
|
-
<text x="64" y="76" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="28" font-weight="700" fill="rgba(255,255,255,0.92)">M</text>
|
|
12
|
-
</svg>
|
|
13
|
-
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
-
<stop offset="0" stop-color="#22c55e"/>
|
|
5
|
-
<stop offset="1" stop-color="#0ea5e9"/>
|
|
6
|
-
</linearGradient>
|
|
7
|
-
</defs>
|
|
8
|
-
<rect width="128" height="128" rx="64" fill="url(#g)"/>
|
|
9
|
-
<circle cx="64" cy="58" r="22" fill="rgba(255,255,255,0.25)"/>
|
|
10
|
-
<path d="M26 116c10-22 26-32 38-32s28 10 38 32" fill="rgba(255,255,255,0.25)"/>
|
|
11
|
-
<text x="64" y="76" text-anchor="middle" font-family="Inter, Arial, sans-serif" font-size="28" font-weight="700" fill="rgba(255,255,255,0.92)">S</text>
|
|
12
|
-
</svg>
|
|
13
|
-
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 1600 900">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
-
<stop offset="0" stop-color="#0b1220"/>
|
|
5
|
-
<stop offset="1" stop-color="#111827"/>
|
|
6
|
-
</linearGradient>
|
|
7
|
-
<radialGradient id="g1" cx="50%" cy="40%" r="60%">
|
|
8
|
-
<stop offset="0" stop-color="rgba(167,139,250,0.50)"/>
|
|
9
|
-
<stop offset="1" stop-color="rgba(167,139,250,0)"/>
|
|
10
|
-
</radialGradient>
|
|
11
|
-
<radialGradient id="g2" cx="20%" cy="80%" r="60%">
|
|
12
|
-
<stop offset="0" stop-color="rgba(34,197,94,0.45)"/>
|
|
13
|
-
<stop offset="1" stop-color="rgba(34,197,94,0)"/>
|
|
14
|
-
</radialGradient>
|
|
15
|
-
</defs>
|
|
16
|
-
|
|
17
|
-
<rect width="1600" height="900" fill="url(#bg)"/>
|
|
18
|
-
<rect width="1600" height="900" fill="url(#g1)"/>
|
|
19
|
-
<rect width="1600" height="900" fill="url(#g2)"/>
|
|
20
|
-
|
|
21
|
-
<rect x="260" y="220" width="1080" height="460" rx="48" fill="rgba(255,255,255,0.08)" stroke="rgba(255,255,255,0.18)"/>
|
|
22
|
-
|
|
23
|
-
<g>
|
|
24
|
-
<rect x="320" y="280" width="180" height="30" rx="15" fill="rgba(255,255,255,0.14)"/>
|
|
25
|
-
<rect x="320" y="330" width="520" height="16" rx="8" fill="rgba(255,255,255,0.22)"/>
|
|
26
|
-
<rect x="320" y="360" width="720" height="16" rx="8" fill="rgba(255,255,255,0.18)"/>
|
|
27
|
-
<rect x="320" y="390" width="640" height="16" rx="8" fill="rgba(255,255,255,0.18)"/>
|
|
28
|
-
<rect x="320" y="450" width="900" height="14" rx="7" fill="rgba(255,255,255,0.14)"/>
|
|
29
|
-
<rect x="320" y="476" width="820" height="14" rx="7" fill="rgba(255,255,255,0.14)"/>
|
|
30
|
-
<rect x="320" y="502" width="740" height="14" rx="7" fill="rgba(255,255,255,0.14)"/>
|
|
31
|
-
</g>
|
|
32
|
-
|
|
33
|
-
<g opacity="0.9">
|
|
34
|
-
<circle cx="1200" cy="320" r="54" fill="rgba(255,255,255,0.08)"/>
|
|
35
|
-
<path d="M1186 320h28M1200 306v28" stroke="rgba(255,255,255,0.55)" stroke-width="6" stroke-linecap="round"/>
|
|
36
|
-
</g>
|
|
37
|
-
</svg>
|
|
38
|
-
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 1600 900">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
-
<stop offset="0" stop-color="#f8fafc"/>
|
|
5
|
-
<stop offset="1" stop-color="#eef2ff"/>
|
|
6
|
-
</linearGradient>
|
|
7
|
-
<linearGradient id="chip" x1="0" y1="0" x2="1" y2="1">
|
|
8
|
-
<stop offset="0" stop-color="#22c55e"/>
|
|
9
|
-
<stop offset="1" stop-color="#a78bfa"/>
|
|
10
|
-
</linearGradient>
|
|
11
|
-
</defs>
|
|
12
|
-
|
|
13
|
-
<rect width="1600" height="900" fill="url(#bg)"/>
|
|
14
|
-
|
|
15
|
-
<g opacity="0.18">
|
|
16
|
-
<circle cx="420" cy="260" r="220" fill="#22c55e"/>
|
|
17
|
-
<circle cx="1250" cy="520" r="320" fill="#a78bfa"/>
|
|
18
|
-
</g>
|
|
19
|
-
|
|
20
|
-
<g>
|
|
21
|
-
<rect x="220" y="210" width="520" height="420" rx="36" fill="rgba(15,23,42,0.06)"/>
|
|
22
|
-
<rect x="860" y="270" width="520" height="360" rx="36" fill="rgba(15,23,42,0.06)"/>
|
|
23
|
-
</g>
|
|
24
|
-
|
|
25
|
-
<g fill="rgba(15,23,42,0.10)">
|
|
26
|
-
<rect x="270" y="260" width="220" height="44" rx="22"/>
|
|
27
|
-
<rect x="270" y="320" width="280" height="14" rx="7"/>
|
|
28
|
-
<rect x="270" y="350" width="320" height="14" rx="7"/>
|
|
29
|
-
<rect x="270" y="380" width="250" height="14" rx="7"/>
|
|
30
|
-
|
|
31
|
-
<rect x="910" y="320" width="260" height="44" rx="22" fill="url(#chip)"/>
|
|
32
|
-
<rect x="910" y="385" width="340" height="14" rx="7"/>
|
|
33
|
-
<rect x="910" y="415" width="300" height="14" rx="7"/>
|
|
34
|
-
<rect x="910" y="445" width="220" height="14" rx="7"/>
|
|
35
|
-
</g>
|
|
36
|
-
</svg>
|
|
37
|
-
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 1600 900">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
-
<stop offset="0" stop-color="#0f172a"/>
|
|
5
|
-
<stop offset="1" stop-color="#0b1220"/>
|
|
6
|
-
</linearGradient>
|
|
7
|
-
<linearGradient id="acc" x1="0" y1="0" x2="1" y2="0">
|
|
8
|
-
<stop offset="0" stop-color="#22c55e"/>
|
|
9
|
-
<stop offset="1" stop-color="#38bdf8"/>
|
|
10
|
-
</linearGradient>
|
|
11
|
-
</defs>
|
|
12
|
-
|
|
13
|
-
<rect width="1600" height="900" fill="url(#bg)"/>
|
|
14
|
-
|
|
15
|
-
<g opacity="0.18">
|
|
16
|
-
<circle cx="420" cy="260" r="240" fill="#22c55e"/>
|
|
17
|
-
<circle cx="1220" cy="680" r="280" fill="#38bdf8"/>
|
|
18
|
-
</g>
|
|
19
|
-
|
|
20
|
-
<g fill="rgba(255,255,255,0.10)">
|
|
21
|
-
<rect x="260" y="200" width="1080" height="84" rx="42"/>
|
|
22
|
-
<rect x="260" y="320" width="1080" height="84" rx="42"/>
|
|
23
|
-
<rect x="260" y="440" width="1080" height="84" rx="42"/>
|
|
24
|
-
<rect x="260" y="560" width="1080" height="84" rx="42"/>
|
|
25
|
-
<rect x="260" y="680" width="1080" height="84" rx="42"/>
|
|
26
|
-
</g>
|
|
27
|
-
|
|
28
|
-
<g>
|
|
29
|
-
<rect x="300" y="236" width="420" height="12" rx="6" fill="rgba(255,255,255,0.22)"/>
|
|
30
|
-
<rect x="300" y="356" width="560" height="12" rx="6" fill="rgba(255,255,255,0.22)"/>
|
|
31
|
-
<rect x="300" y="476" width="520" height="12" rx="6" fill="rgba(255,255,255,0.22)"/>
|
|
32
|
-
<rect x="300" y="596" width="680" height="12" rx="6" fill="rgba(255,255,255,0.22)"/>
|
|
33
|
-
<rect x="300" y="716" width="460" height="12" rx="6" fill="rgba(255,255,255,0.22)"/>
|
|
34
|
-
</g>
|
|
35
|
-
|
|
36
|
-
<path d="M1240 240c0 0-20 110-140 120s-160 120-160 120" fill="none" stroke="url(#acc)" stroke-width="10" stroke-linecap="round" opacity="0.75"/>
|
|
37
|
-
</svg>
|
|
38
|
-
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
<svg width="1200" height="630" viewBox="0 0 1200 630" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="bg" x1="0" y1="0" x2="1200" y2="630" gradientUnits="userSpaceOnUse">
|
|
4
|
-
<stop offset="0" stop-color="#0B1020"/>
|
|
5
|
-
<stop offset="1" stop-color="#0F2A1B"/>
|
|
6
|
-
</linearGradient>
|
|
7
|
-
<linearGradient id="card" x1="0" y1="0" x2="1" y2="1">
|
|
8
|
-
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.10"/>
|
|
9
|
-
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0.04"/>
|
|
10
|
-
</linearGradient>
|
|
11
|
-
<filter id="shadow" x="-30" y="-30" width="1260" height="690" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
12
|
-
<feDropShadow dx="0" dy="18" stdDeviation="24" flood-color="#000000" flood-opacity="0.35"/>
|
|
13
|
-
</filter>
|
|
14
|
-
</defs>
|
|
15
|
-
|
|
16
|
-
<rect width="1200" height="630" fill="url(#bg)"/>
|
|
17
|
-
|
|
18
|
-
<g filter="url(#shadow)">
|
|
19
|
-
<rect x="120" y="126" width="960" height="378" rx="36" fill="url(#card)" stroke="#FFFFFF" stroke-opacity="0.12"/>
|
|
20
|
-
</g>
|
|
21
|
-
|
|
22
|
-
<circle cx="240" cy="252" r="48" fill="#34D399" fill-opacity="0.22"/>
|
|
23
|
-
<circle cx="316" cy="280" r="14" fill="#34D399" fill-opacity="0.55"/>
|
|
24
|
-
|
|
25
|
-
<path d="M240 360H960" stroke="#FFFFFF" stroke-opacity="0.10" stroke-width="2"/>
|
|
26
|
-
<path d="M240 400H840" stroke="#FFFFFF" stroke-opacity="0.08" stroke-width="2"/>
|
|
27
|
-
<path d="M240 440H900" stroke="#FFFFFF" stroke-opacity="0.06" stroke-width="2"/>
|
|
28
|
-
|
|
29
|
-
<text x="240" y="332" fill="#FFFFFF" fill-opacity="0.92" font-family="ui-sans-serif, system-ui" font-size="54" font-weight="700" letter-spacing="-0.8">
|
|
30
|
-
Markdown Kitchen Sink
|
|
31
|
-
</text>
|
|
32
|
-
<text x="240" y="520" fill="#FFFFFF" fill-opacity="0.70" font-family="ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace" font-size="18" letter-spacing="0.6">
|
|
33
|
-
tables • lists • code • quotes • images
|
|
34
|
-
</text>
|
|
35
|
-
</svg>
|
|
36
|
-
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 1600 900">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
-
<stop offset="0" stop-color="#0b1220"/>
|
|
5
|
-
<stop offset="1" stop-color="#0f172a"/>
|
|
6
|
-
</linearGradient>
|
|
7
|
-
<linearGradient id="acc" x1="0" y1="0" x2="1" y2="0">
|
|
8
|
-
<stop offset="0" stop-color="#38bdf8"/>
|
|
9
|
-
<stop offset="1" stop-color="#22c55e"/>
|
|
10
|
-
</linearGradient>
|
|
11
|
-
</defs>
|
|
12
|
-
|
|
13
|
-
<rect width="1600" height="900" fill="url(#bg)"/>
|
|
14
|
-
<g opacity="0.18">
|
|
15
|
-
<circle cx="460" cy="520" r="320" fill="#38bdf8"/>
|
|
16
|
-
<circle cx="1180" cy="300" r="260" fill="#22c55e"/>
|
|
17
|
-
</g>
|
|
18
|
-
|
|
19
|
-
<g>
|
|
20
|
-
<rect x="260" y="250" width="520" height="400" rx="40" fill="rgba(255,255,255,0.08)" stroke="rgba(255,255,255,0.16)"/>
|
|
21
|
-
<rect x="820" y="250" width="520" height="400" rx="40" fill="rgba(255,255,255,0.08)" stroke="rgba(255,255,255,0.16)"/>
|
|
22
|
-
<path d="M820 450h-40c-40 0-60-20-60-60v-20c0-40-20-60-60-60h-60" fill="none" stroke="url(#acc)" stroke-width="10" stroke-linecap="round" opacity="0.85"/>
|
|
23
|
-
</g>
|
|
24
|
-
|
|
25
|
-
<g fill="rgba(255,255,255,0.20)">
|
|
26
|
-
<rect x="320" y="320" width="240" height="16" rx="8"/>
|
|
27
|
-
<rect x="320" y="352" width="360" height="16" rx="8"/>
|
|
28
|
-
<rect x="320" y="384" width="320" height="16" rx="8"/>
|
|
29
|
-
<rect x="320" y="450" width="420" height="12" rx="6" opacity="0.8"/>
|
|
30
|
-
<rect x="320" y="474" width="380" height="12" rx="6" opacity="0.8"/>
|
|
31
|
-
</g>
|
|
32
|
-
|
|
33
|
-
<g fill="rgba(255,255,255,0.20)">
|
|
34
|
-
<rect x="880" y="320" width="260" height="16" rx="8"/>
|
|
35
|
-
<rect x="880" y="352" width="360" height="16" rx="8"/>
|
|
36
|
-
<rect x="880" y="384" width="280" height="16" rx="8"/>
|
|
37
|
-
<rect x="880" y="450" width="420" height="12" rx="6" opacity="0.8"/>
|
|
38
|
-
<rect x="880" y="474" width="340" height="12" rx="6" opacity="0.8"/>
|
|
39
|
-
</g>
|
|
40
|
-
</svg>
|
|
41
|
-
|