@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,23 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: Props & {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const ShareButtons: $$__sveltets_2_IsomorphicComponent<{
|
|
15
|
+
title: string;
|
|
16
|
+
url: string;
|
|
17
|
+
label?: string;
|
|
18
|
+
testId?: string | undefined;
|
|
19
|
+
}, {
|
|
20
|
+
[evt: string]: CustomEvent<any>;
|
|
21
|
+
}, {}, {}, string>;
|
|
22
|
+
type ShareButtons = InstanceType<typeof ShareButtons>;
|
|
23
|
+
export default ShareButtons;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export let summary: string;
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<section
|
|
6
|
+
class="rounded-2xl border border-border-soft/10 bg-background-soft p-4"
|
|
7
|
+
data-testid="blog-summary"
|
|
8
|
+
>
|
|
9
|
+
<p class="text-xs font-mono uppercase tracking-[0.6px] text-text-muted">AI SUMMARY</p>
|
|
10
|
+
<p class="mt-2 text-sm leading-6 text-text-sub">{summary}</p>
|
|
11
|
+
</section>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: Props & {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const SummaryCard: $$__sveltets_2_IsomorphicComponent<{
|
|
15
|
+
summary: string;
|
|
16
|
+
}, {
|
|
17
|
+
[evt: string]: CustomEvent<any>;
|
|
18
|
+
}, {}, {}, string>;
|
|
19
|
+
type SummaryCard = InstanceType<typeof SummaryCard>;
|
|
20
|
+
export default SummaryCard;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { BlogCategory } from '../../types/blog';
|
|
3
|
+
|
|
4
|
+
export let categories: BlogCategory[];
|
|
5
|
+
export let selected: string; // "" means all
|
|
6
|
+
export let onSelect: (slug: string) => void;
|
|
7
|
+
$: items = [{ label: 'All articles', slug: '' }, ...categories];
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<div class="relative" data-testid="blog-tags">
|
|
11
|
+
<div
|
|
12
|
+
class="fade-mask-x no-scrollbar flex gap-2 overflow-x-auto py-2 md:flex-wrap md:overflow-visible md:[-webkit-mask-image:none] md:[mask-image:none]"
|
|
13
|
+
role="tablist"
|
|
14
|
+
aria-label="Blog categories"
|
|
15
|
+
>
|
|
16
|
+
{#each items as item (item.slug)}
|
|
17
|
+
<button
|
|
18
|
+
type="button"
|
|
19
|
+
role="tab"
|
|
20
|
+
aria-selected={selected === item.slug}
|
|
21
|
+
class="h-[31px] whitespace-nowrap rounded-full border px-3 text-xs font-mono uppercase tracking-[0.6px] transition
|
|
22
|
+
hover:bg-background-main/60
|
|
23
|
+
{selected === item.slug
|
|
24
|
+
? 'border-border-soft/15 bg-background-main text-text-main'
|
|
25
|
+
: 'border-border-soft/10 bg-background-soft text-text-sub'}"
|
|
26
|
+
on:click={() => onSelect(item.slug)}
|
|
27
|
+
>
|
|
28
|
+
{item.label}
|
|
29
|
+
</button>
|
|
30
|
+
{/each}
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { BlogCategory } from '../../types/blog';
|
|
2
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
3
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
4
|
+
$$bindings?: Bindings;
|
|
5
|
+
} & Exports;
|
|
6
|
+
(internal: unknown, props: Props & {
|
|
7
|
+
$$events?: Events;
|
|
8
|
+
$$slots?: Slots;
|
|
9
|
+
}): Exports & {
|
|
10
|
+
$set?: any;
|
|
11
|
+
$on?: any;
|
|
12
|
+
};
|
|
13
|
+
z_$$bindings?: Bindings;
|
|
14
|
+
}
|
|
15
|
+
declare const TagTabs: $$__sveltets_2_IsomorphicComponent<{
|
|
16
|
+
categories: BlogCategory[];
|
|
17
|
+
selected: string;
|
|
18
|
+
onSelect: (slug: string) => void;
|
|
19
|
+
}, {
|
|
20
|
+
[evt: string]: CustomEvent<any>;
|
|
21
|
+
}, {}, {}, string>;
|
|
22
|
+
type TagTabs = InstanceType<typeof TagTabs>;
|
|
23
|
+
export default TagTabs;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { default as Avatar } from './components/blog/Avatar.svelte';
|
|
2
|
+
export { default as BackLink } from './components/blog/BackLink.svelte';
|
|
3
|
+
export { default as BlogCard } from './components/blog/BlogCard.svelte';
|
|
4
|
+
export { default as BlogHeroCard } from './components/blog/BlogHeroCard.svelte';
|
|
5
|
+
export { default as Container } from './components/blog/Container.svelte';
|
|
6
|
+
export { default as ImageLightbox } from './components/blog/ImageLightbox.svelte';
|
|
7
|
+
export { default as MorePosts } from './components/blog/MorePosts.svelte';
|
|
8
|
+
export { default as ShareButtons } from './components/blog/ShareButtons.svelte';
|
|
9
|
+
export { default as SummaryCard } from './components/blog/SummaryCard.svelte';
|
|
10
|
+
export { default as TagTabs } from './components/blog/TagTabs.svelte';
|
|
11
|
+
export type { BlogAuthor, BlogCategory, BlogPost, BlogPostFull } from './types/blog';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Components
|
|
2
|
+
export { default as Avatar } from './components/blog/Avatar.svelte';
|
|
3
|
+
export { default as BackLink } from './components/blog/BackLink.svelte';
|
|
4
|
+
export { default as BlogCard } from './components/blog/BlogCard.svelte';
|
|
5
|
+
export { default as BlogHeroCard } from './components/blog/BlogHeroCard.svelte';
|
|
6
|
+
export { default as Container } from './components/blog/Container.svelte';
|
|
7
|
+
export { default as ImageLightbox } from './components/blog/ImageLightbox.svelte';
|
|
8
|
+
export { default as MorePosts } from './components/blog/MorePosts.svelte';
|
|
9
|
+
export { default as ShareButtons } from './components/blog/ShareButtons.svelte';
|
|
10
|
+
export { default as SummaryCard } from './components/blog/SummaryCard.svelte';
|
|
11
|
+
export { default as TagTabs } from './components/blog/TagTabs.svelte';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { BlogAuthor, BlogCategory, BlogPost, BlogPostFull } from '../types/blog';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
declare const frontmatterSchema: z.ZodObject<{
|
|
4
|
+
title: z.ZodString;
|
|
5
|
+
date: z.ZodString;
|
|
6
|
+
category: z.ZodString;
|
|
7
|
+
author: z.ZodString;
|
|
8
|
+
cover: z.ZodString;
|
|
9
|
+
excerpt: z.ZodOptional<z.ZodString>;
|
|
10
|
+
summaryAI: z.ZodOptional<z.ZodString>;
|
|
11
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
12
|
+
featured: z.ZodOptional<z.ZodBoolean>;
|
|
13
|
+
draft: z.ZodOptional<z.ZodBoolean>;
|
|
14
|
+
}, z.core.$strip>;
|
|
15
|
+
export type BlogFrontmatter = z.infer<typeof frontmatterSchema>;
|
|
16
|
+
export type BlogFrontmatterAdapter = (args: {
|
|
17
|
+
data: unknown;
|
|
18
|
+
content: string;
|
|
19
|
+
slug: string;
|
|
20
|
+
path: string;
|
|
21
|
+
}) => BlogFrontmatter;
|
|
22
|
+
type CompiledModule = {
|
|
23
|
+
default: BlogPostFull['component'];
|
|
24
|
+
};
|
|
25
|
+
export type BlogCreateConfig = {
|
|
26
|
+
compiledModules: Record<string, () => Promise<CompiledModule>>;
|
|
27
|
+
rawModules: Record<string, () => Promise<string>>;
|
|
28
|
+
getAuthor: (id: string) => BlogAuthor;
|
|
29
|
+
categoryOrder?: string[];
|
|
30
|
+
mapFrontmatter?: BlogFrontmatterAdapter;
|
|
31
|
+
};
|
|
32
|
+
export declare function createBlog(config: BlogCreateConfig): {
|
|
33
|
+
getAllPosts: () => Promise<BlogPost[]>;
|
|
34
|
+
getAllPostsFull: () => Promise<BlogPostFull[]>;
|
|
35
|
+
getPostBySlug: (slug: string) => Promise<BlogPostFull | null>;
|
|
36
|
+
getCategories: () => Promise<BlogCategory[]>;
|
|
37
|
+
pickHero: (posts?: BlogPost[]) => Promise<BlogPost>;
|
|
38
|
+
};
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { DEV } from 'esm-env';
|
|
2
|
+
import matter from 'gray-matter';
|
|
3
|
+
import readingTime from 'reading-time';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
const frontmatterSchema = z.object({
|
|
6
|
+
title: z.string(),
|
|
7
|
+
date: z.string(),
|
|
8
|
+
category: z.string(),
|
|
9
|
+
author: z.string(),
|
|
10
|
+
cover: z.string(),
|
|
11
|
+
excerpt: z.string().optional(),
|
|
12
|
+
summaryAI: z.string().optional(),
|
|
13
|
+
tags: z.array(z.string()).optional(),
|
|
14
|
+
featured: z.boolean().optional(),
|
|
15
|
+
draft: z.boolean().optional()
|
|
16
|
+
});
|
|
17
|
+
const DEFAULT_CATEGORY_ORDER = [
|
|
18
|
+
'all',
|
|
19
|
+
'ai-trends',
|
|
20
|
+
'announcements',
|
|
21
|
+
'for-founders',
|
|
22
|
+
'engineering',
|
|
23
|
+
'design',
|
|
24
|
+
'best-practices'
|
|
25
|
+
];
|
|
26
|
+
function slugify(input) {
|
|
27
|
+
return input
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.trim()
|
|
30
|
+
.replace(/['"]/g, '')
|
|
31
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
32
|
+
.replace(/^-+|-+$/g, '');
|
|
33
|
+
}
|
|
34
|
+
function normalizeCategory(label) {
|
|
35
|
+
const slug = slugify(label);
|
|
36
|
+
return { label, slug };
|
|
37
|
+
}
|
|
38
|
+
function parseISODate(date) {
|
|
39
|
+
// Prefer stable UTC parsing for YYYY-MM-DD.
|
|
40
|
+
if (/^\\d{4}-\\d{2}-\\d{2}$/.test(date)) {
|
|
41
|
+
const d = new Date(`${date}T00:00:00Z`);
|
|
42
|
+
if (!Number.isNaN(d.getTime()))
|
|
43
|
+
return d;
|
|
44
|
+
}
|
|
45
|
+
const d = new Date(date);
|
|
46
|
+
if (Number.isNaN(d.getTime()))
|
|
47
|
+
throw new Error(`Invalid date: ${date}`);
|
|
48
|
+
return d;
|
|
49
|
+
}
|
|
50
|
+
const fmtLong = new Intl.DateTimeFormat('en-US', {
|
|
51
|
+
month: 'long',
|
|
52
|
+
day: 'numeric',
|
|
53
|
+
year: 'numeric',
|
|
54
|
+
timeZone: 'UTC'
|
|
55
|
+
});
|
|
56
|
+
const fmtShort = new Intl.DateTimeFormat('en-US', {
|
|
57
|
+
month: 'short',
|
|
58
|
+
day: 'numeric',
|
|
59
|
+
year: 'numeric',
|
|
60
|
+
timeZone: 'UTC'
|
|
61
|
+
});
|
|
62
|
+
function stripForExcerpt(markdown) {
|
|
63
|
+
return (markdown
|
|
64
|
+
// remove fenced code blocks
|
|
65
|
+
.replace(/```[\s\S]*?```/g, '')
|
|
66
|
+
// remove images
|
|
67
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
|
|
68
|
+
// remove links but keep text
|
|
69
|
+
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
|
|
70
|
+
// remove headings markers
|
|
71
|
+
.replace(/^#{1,6}\s+/gm, '')
|
|
72
|
+
// remove blockquote markers
|
|
73
|
+
.replace(/^>\s+/gm, '')
|
|
74
|
+
// remove emphasis markers
|
|
75
|
+
.replace(/[*_`]/g, '')
|
|
76
|
+
// collapse whitespace
|
|
77
|
+
.replace(/\s+/g, ' ')
|
|
78
|
+
.trim());
|
|
79
|
+
}
|
|
80
|
+
function excerptFromContent(content) {
|
|
81
|
+
const text = stripForExcerpt(content);
|
|
82
|
+
if (!text)
|
|
83
|
+
return '';
|
|
84
|
+
return text.length > 180 ? `${text.slice(0, 177).trimEnd()}...` : text;
|
|
85
|
+
}
|
|
86
|
+
function minutesToLabels(minutes) {
|
|
87
|
+
const m = Math.max(1, Math.round(minutes));
|
|
88
|
+
const unit = m === 1 ? 'minute' : 'minutes';
|
|
89
|
+
return {
|
|
90
|
+
minutes: m,
|
|
91
|
+
short: `${m} min read`,
|
|
92
|
+
long: `${m} ${unit} read`
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export function createBlog(config) {
|
|
96
|
+
const categoryOrder = config.categoryOrder ?? DEFAULT_CATEGORY_ORDER;
|
|
97
|
+
let cachedMetaIndex = null;
|
|
98
|
+
let cachedFullIndex = null;
|
|
99
|
+
let cachedSlugToPath = null;
|
|
100
|
+
function getSlugToPath() {
|
|
101
|
+
if (!DEV && cachedSlugToPath)
|
|
102
|
+
return cachedSlugToPath;
|
|
103
|
+
const m = new Map();
|
|
104
|
+
const paths = Object.keys(config.rawModules).sort();
|
|
105
|
+
for (const path of paths) {
|
|
106
|
+
const file = path.split('/').pop();
|
|
107
|
+
const slug = file?.replace(/\.md(?:\?.*)?$/, '');
|
|
108
|
+
if (!slug)
|
|
109
|
+
continue;
|
|
110
|
+
m.set(slug, path);
|
|
111
|
+
}
|
|
112
|
+
if (!DEV)
|
|
113
|
+
cachedSlugToPath = m;
|
|
114
|
+
return m;
|
|
115
|
+
}
|
|
116
|
+
async function buildMetaIndex() {
|
|
117
|
+
if (!DEV && cachedMetaIndex)
|
|
118
|
+
return cachedMetaIndex;
|
|
119
|
+
const posts = [];
|
|
120
|
+
const paths = Object.keys(config.rawModules).sort();
|
|
121
|
+
for (const path of paths) {
|
|
122
|
+
const file = path.split('/').pop();
|
|
123
|
+
// Glob keys can include query strings depending on bundler usage; normalize aggressively.
|
|
124
|
+
const slug = file?.replace(/\.md(?:\?.*)?$/, '');
|
|
125
|
+
if (!slug)
|
|
126
|
+
continue;
|
|
127
|
+
const rawFn = config.rawModules[path];
|
|
128
|
+
if (!rawFn)
|
|
129
|
+
continue;
|
|
130
|
+
const raw = await rawFn();
|
|
131
|
+
const { data, content } = matter(raw);
|
|
132
|
+
const metadata = config.mapFrontmatter
|
|
133
|
+
? config.mapFrontmatter({ data, content, slug, path })
|
|
134
|
+
: frontmatterSchema.parse(data);
|
|
135
|
+
if (metadata.draft)
|
|
136
|
+
continue;
|
|
137
|
+
const dateObj = parseISODate(metadata.date);
|
|
138
|
+
const rt = minutesToLabels(readingTime(content).minutes);
|
|
139
|
+
const category = normalizeCategory(metadata.category);
|
|
140
|
+
const excerpt = metadata.excerpt?.trim() || excerptFromContent(content);
|
|
141
|
+
posts.push({
|
|
142
|
+
slug,
|
|
143
|
+
title: metadata.title.trim(),
|
|
144
|
+
excerpt,
|
|
145
|
+
category,
|
|
146
|
+
tags: metadata.tags ?? [],
|
|
147
|
+
author: config.getAuthor(metadata.author),
|
|
148
|
+
date: metadata.date,
|
|
149
|
+
dateLong: fmtLong.format(dateObj),
|
|
150
|
+
dateShort: fmtShort.format(dateObj),
|
|
151
|
+
readingMinutes: rt.minutes,
|
|
152
|
+
readingTimeShort: rt.short,
|
|
153
|
+
readingTimeLong: rt.long,
|
|
154
|
+
cover: metadata.cover,
|
|
155
|
+
summaryAI: metadata.summaryAI,
|
|
156
|
+
featured: Boolean(metadata.featured)
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
posts.sort((a, b) => parseISODate(b.date).getTime() - parseISODate(a.date).getTime());
|
|
160
|
+
if (!DEV)
|
|
161
|
+
cachedMetaIndex = posts;
|
|
162
|
+
return posts;
|
|
163
|
+
}
|
|
164
|
+
async function getAllPosts() {
|
|
165
|
+
return buildMetaIndex();
|
|
166
|
+
}
|
|
167
|
+
async function getAllPostsFull() {
|
|
168
|
+
if (!DEV && cachedFullIndex)
|
|
169
|
+
return cachedFullIndex;
|
|
170
|
+
const meta = await buildMetaIndex();
|
|
171
|
+
const slugToPath = getSlugToPath();
|
|
172
|
+
const full = [];
|
|
173
|
+
for (const post of meta) {
|
|
174
|
+
const path = slugToPath.get(post.slug);
|
|
175
|
+
const compiledFn = path ? config.compiledModules[path] : undefined;
|
|
176
|
+
if (!compiledFn)
|
|
177
|
+
continue;
|
|
178
|
+
const compiled = await compiledFn();
|
|
179
|
+
full.push({ ...post, component: compiled.default });
|
|
180
|
+
}
|
|
181
|
+
if (!DEV)
|
|
182
|
+
cachedFullIndex = full;
|
|
183
|
+
return full;
|
|
184
|
+
}
|
|
185
|
+
async function getPostBySlug(slug) {
|
|
186
|
+
const meta = await buildMetaIndex();
|
|
187
|
+
const post = meta.find((p) => p.slug === slug) ?? null;
|
|
188
|
+
if (!post)
|
|
189
|
+
return null;
|
|
190
|
+
const path = getSlugToPath().get(slug);
|
|
191
|
+
const compiledFn = path ? config.compiledModules[path] : undefined;
|
|
192
|
+
if (!compiledFn)
|
|
193
|
+
return null;
|
|
194
|
+
const compiled = await compiledFn();
|
|
195
|
+
return { ...post, component: compiled.default };
|
|
196
|
+
}
|
|
197
|
+
async function getCategories() {
|
|
198
|
+
const posts = await getAllPosts();
|
|
199
|
+
const map = new Map();
|
|
200
|
+
for (const p of posts)
|
|
201
|
+
map.set(p.category.slug, p.category.label);
|
|
202
|
+
return Array.from(map.entries())
|
|
203
|
+
.map(([slug, label]) => ({ slug, label }))
|
|
204
|
+
.sort((a, b) => {
|
|
205
|
+
const ai = categoryOrder.indexOf(a.slug);
|
|
206
|
+
const bi = categoryOrder.indexOf(b.slug);
|
|
207
|
+
if (ai === -1 && bi === -1)
|
|
208
|
+
return a.label.localeCompare(b.label);
|
|
209
|
+
if (ai === -1)
|
|
210
|
+
return 1;
|
|
211
|
+
if (bi === -1)
|
|
212
|
+
return -1;
|
|
213
|
+
return ai - bi;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
async function pickHero(posts) {
|
|
217
|
+
const list = posts ?? (await getAllPosts());
|
|
218
|
+
const featured = list.filter((p) => p.featured);
|
|
219
|
+
return (featured[0] ?? list[0]);
|
|
220
|
+
}
|
|
221
|
+
return { getAllPosts, getAllPostsFull, getPostBySlug, getCategories, pickHero };
|
|
222
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createBlog, type BlogCreateConfig } from './blog';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createBlog } from './blog';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ThemeController, ThemeMode } from './store';
|
|
3
|
+
|
|
4
|
+
export let controller: ThemeController;
|
|
5
|
+
|
|
6
|
+
const options: { id: ThemeMode; label: string }[] = [
|
|
7
|
+
{ id: 'system', label: 'System' },
|
|
8
|
+
{ id: 'light', label: 'Light' },
|
|
9
|
+
{ id: 'dark', label: 'Dark' }
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
// Store auto-subscriptions only work on identifiers, so alias it.
|
|
13
|
+
const themeMode = controller.themeMode;
|
|
14
|
+
$: currentMode = $themeMode;
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<div class="flex items-center gap-2">
|
|
18
|
+
<span class="text-xs font-mono uppercase tracking-[0.6px] text-text-muted">Theme</span>
|
|
19
|
+
<div class="inline-flex rounded-full border border-border-soft/10 bg-background-soft p-1">
|
|
20
|
+
{#each options as opt (opt.id)}
|
|
21
|
+
<button
|
|
22
|
+
type="button"
|
|
23
|
+
class="rounded-full px-3 py-1 text-xs font-mono uppercase tracking-[0.6px] transition
|
|
24
|
+
hover:bg-background-main/60
|
|
25
|
+
{(currentMode === opt.id && 'bg-background-main shadow-sm') || 'text-text-sub'}"
|
|
26
|
+
onclick={() => controller.setThemeMode(opt.id)}
|
|
27
|
+
aria-pressed={currentMode === opt.id}
|
|
28
|
+
>
|
|
29
|
+
{opt.label}
|
|
30
|
+
</button>
|
|
31
|
+
{/each}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ThemeController } from './store';
|
|
2
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
3
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
4
|
+
$$bindings?: Bindings;
|
|
5
|
+
} & Exports;
|
|
6
|
+
(internal: unknown, props: Props & {
|
|
7
|
+
$$events?: Events;
|
|
8
|
+
$$slots?: Slots;
|
|
9
|
+
}): Exports & {
|
|
10
|
+
$set?: any;
|
|
11
|
+
$on?: any;
|
|
12
|
+
};
|
|
13
|
+
z_$$bindings?: Bindings;
|
|
14
|
+
}
|
|
15
|
+
declare const ThemeSwitcher: $$__sveltets_2_IsomorphicComponent<{
|
|
16
|
+
controller: ThemeController;
|
|
17
|
+
}, {
|
|
18
|
+
[evt: string]: CustomEvent<any>;
|
|
19
|
+
}, {}, {}, string>;
|
|
20
|
+
type ThemeSwitcher = InstanceType<typeof ThemeSwitcher>;
|
|
21
|
+
export default ThemeSwitcher;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Writable } from 'svelte/store';
|
|
2
|
+
export type ThemeMode = 'system' | 'light' | 'dark';
|
|
3
|
+
export type ThemeController = {
|
|
4
|
+
storageKey: string;
|
|
5
|
+
themeMode: Writable<ThemeMode>;
|
|
6
|
+
initTheme: () => void | (() => void);
|
|
7
|
+
setThemeMode: (mode: ThemeMode) => void;
|
|
8
|
+
};
|
|
9
|
+
export declare function createThemeController(opts?: {
|
|
10
|
+
storageKey?: string;
|
|
11
|
+
defaultMode?: ThemeMode;
|
|
12
|
+
}): ThemeController;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { BROWSER } from 'esm-env';
|
|
2
|
+
import { writable } from 'svelte/store';
|
|
3
|
+
function resolve(mode) {
|
|
4
|
+
if (mode === 'light' || mode === 'dark')
|
|
5
|
+
return mode;
|
|
6
|
+
const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)')?.matches ?? false;
|
|
7
|
+
return prefersDark ? 'dark' : 'light';
|
|
8
|
+
}
|
|
9
|
+
function apply(mode) {
|
|
10
|
+
const resolved = resolve(mode);
|
|
11
|
+
document.documentElement.classList.remove('light', 'dark');
|
|
12
|
+
document.documentElement.classList.add(resolved);
|
|
13
|
+
document.documentElement.dataset.theme = mode;
|
|
14
|
+
}
|
|
15
|
+
export function createThemeController(opts) {
|
|
16
|
+
const storageKey = opts?.storageKey ?? 'blogkit-theme';
|
|
17
|
+
const defaultMode = opts?.defaultMode ?? 'system';
|
|
18
|
+
const themeMode = writable(defaultMode);
|
|
19
|
+
function readStored() {
|
|
20
|
+
const v = localStorage.getItem(storageKey);
|
|
21
|
+
if (v === 'light' || v === 'dark' || v === 'system')
|
|
22
|
+
return v;
|
|
23
|
+
return defaultMode;
|
|
24
|
+
}
|
|
25
|
+
function initTheme() {
|
|
26
|
+
if (!BROWSER)
|
|
27
|
+
return;
|
|
28
|
+
const mode = readStored();
|
|
29
|
+
themeMode.set(mode);
|
|
30
|
+
apply(mode);
|
|
31
|
+
const mq = window.matchMedia?.('(prefers-color-scheme: dark)');
|
|
32
|
+
const onChange = () => {
|
|
33
|
+
let current = mode;
|
|
34
|
+
const unsub = themeMode.subscribe((v) => (current = v));
|
|
35
|
+
unsub();
|
|
36
|
+
if (current === 'system')
|
|
37
|
+
apply('system');
|
|
38
|
+
};
|
|
39
|
+
mq?.addEventListener?.('change', onChange);
|
|
40
|
+
return () => mq?.removeEventListener?.('change', onChange);
|
|
41
|
+
}
|
|
42
|
+
function setThemeMode(mode) {
|
|
43
|
+
themeMode.set(mode);
|
|
44
|
+
if (!BROWSER)
|
|
45
|
+
return;
|
|
46
|
+
localStorage.setItem(storageKey, mode);
|
|
47
|
+
apply(mode);
|
|
48
|
+
}
|
|
49
|
+
return { storageKey, themeMode, initTheme, setThemeMode };
|
|
50
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ComponentType } from 'svelte';
|
|
2
|
+
export type BlogCategory = {
|
|
3
|
+
label: string;
|
|
4
|
+
slug: string;
|
|
5
|
+
};
|
|
6
|
+
export type BlogAuthor = {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
title: string;
|
|
10
|
+
avatar: string;
|
|
11
|
+
};
|
|
12
|
+
export type BlogPost = {
|
|
13
|
+
slug: string;
|
|
14
|
+
title: string;
|
|
15
|
+
excerpt: string;
|
|
16
|
+
category: BlogCategory;
|
|
17
|
+
tags: string[];
|
|
18
|
+
author: BlogAuthor;
|
|
19
|
+
date: string;
|
|
20
|
+
dateLong: string;
|
|
21
|
+
dateShort: string;
|
|
22
|
+
readingMinutes: number;
|
|
23
|
+
readingTimeShort: string;
|
|
24
|
+
readingTimeLong: string;
|
|
25
|
+
cover: string;
|
|
26
|
+
summaryAI?: string;
|
|
27
|
+
featured: boolean;
|
|
28
|
+
};
|
|
29
|
+
export type BlogPostFull = BlogPost & {
|
|
30
|
+
component: ComponentType;
|
|
31
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aureuma/blogkit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"README.md",
|
|
9
|
+
"CHANGELOG.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"svelte": "./dist/index.js",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./server": {
|
|
19
|
+
"types": "./dist/server/index.d.ts",
|
|
20
|
+
"default": "./dist/server/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./theme": {
|
|
23
|
+
"types": "./dist/theme/index.d.ts",
|
|
24
|
+
"svelte": "./dist/theme/index.js",
|
|
25
|
+
"default": "./dist/theme/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"main": "./dist/index.js",
|
|
30
|
+
"svelte": "./dist/index.js",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+ssh://git@github.com/Aureuma/svelta.git",
|
|
35
|
+
"directory": "packages/blogkit"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/Aureuma/svelta/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/Aureuma/svelta/tree/main/packages/blogkit#readme",
|
|
41
|
+
"keywords": [
|
|
42
|
+
"svelte",
|
|
43
|
+
"sveltekit",
|
|
44
|
+
"blog",
|
|
45
|
+
"markdown",
|
|
46
|
+
"mdsvex",
|
|
47
|
+
"shiki",
|
|
48
|
+
"tailwind"
|
|
49
|
+
],
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "svelte-package --tsconfig ./tsconfig.json",
|
|
55
|
+
"prepack": "npm run build"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"svelte": "^4.0.0 || ^5.0.0"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"esm-env": "^1.2.2",
|
|
62
|
+
"gray-matter": "^4.0.3",
|
|
63
|
+
"reading-time": "^1.5.0",
|
|
64
|
+
"zod": "^4.3.6"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export let src: string;
|
|
3
|
+
export let alt: string;
|
|
4
|
+
export let size: number = 48;
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<img
|
|
8
|
+
src={src}
|
|
9
|
+
alt={alt}
|
|
10
|
+
width={size}
|
|
11
|
+
height={size}
|
|
12
|
+
class="shrink-0 rounded-full border border-border-soft/10 bg-background-soft object-cover"
|
|
13
|
+
style="width: {size}px; height: {size}px;"
|
|
14
|
+
loading="lazy"
|
|
15
|
+
/>
|