@aureuma/svelta 0.0.1 → 0.1.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/README.md +1 -2
- package/package.json +35 -3
- package/packages/core/CHANGELOG.md +18 -0
- package/packages/core/README.md +11 -0
- package/packages/{blogkit → core}/dist/index.d.ts +1 -1
- package/packages/core/dist/server/blog.d.ts +106 -0
- package/packages/core/dist/server/blog.js +470 -0
- package/packages/core/dist/server/index.d.ts +1 -0
- package/packages/core/dist/server/index.js +1 -0
- package/packages/{blogkit → core}/dist/theme/store.js +1 -1
- package/packages/{blogkit → core}/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/CHANGELOG.md +0 -6
- package/packages/blogkit/README.md +0 -93
- package/packages/blogkit/dist/server/blog.d.ts +0 -39
- package/packages/blogkit/dist/server/blog.js +0 -222
- package/packages/blogkit/dist/server/index.d.ts +0 -1
- package/packages/blogkit/dist/server/index.js +0 -1
- 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/packages/{blogkit → core}/LICENSE +0 -0
- /package/packages/{blogkit → core}/dist/components/blog/Avatar.svelte +0 -0
- /package/packages/{blogkit → core}/dist/components/blog/Avatar.svelte.d.ts +0 -0
- /package/packages/{blogkit → core}/dist/components/blog/BackLink.svelte +0 -0
- /package/packages/{blogkit → core}/dist/components/blog/BackLink.svelte.d.ts +0 -0
- /package/packages/{blogkit → core}/dist/components/blog/BlogCard.svelte +0 -0
- /package/packages/{blogkit → core}/dist/components/blog/BlogCard.svelte.d.ts +0 -0
- /package/packages/{blogkit → core}/dist/components/blog/BlogHeroCard.svelte +0 -0
- /package/packages/{blogkit → core}/dist/components/blog/BlogHeroCard.svelte.d.ts +0 -0
- /package/packages/{blogkit → core}/dist/components/blog/Container.svelte +0 -0
- /package/packages/{blogkit → core}/dist/components/blog/Container.svelte.d.ts +0 -0
- /package/packages/{blogkit → core}/dist/components/blog/ImageLightbox.svelte +0 -0
- /package/packages/{blogkit → core}/dist/components/blog/ImageLightbox.svelte.d.ts +0 -0
- /package/packages/{blogkit → core}/dist/components/blog/MorePosts.svelte +0 -0
- /package/packages/{blogkit → core}/dist/components/blog/MorePosts.svelte.d.ts +0 -0
- /package/packages/{blogkit → core}/dist/components/blog/ShareButtons.svelte +0 -0
- /package/packages/{blogkit → core}/dist/components/blog/ShareButtons.svelte.d.ts +0 -0
- /package/packages/{blogkit → core}/dist/components/blog/SummaryCard.svelte +0 -0
- /package/packages/{blogkit → core}/dist/components/blog/SummaryCard.svelte.d.ts +0 -0
- /package/packages/{blogkit → core}/dist/components/blog/TagTabs.svelte +0 -0
- /package/packages/{blogkit → core}/dist/components/blog/TagTabs.svelte.d.ts +0 -0
- /package/packages/{blogkit → core}/dist/index.js +0 -0
- /package/packages/{blogkit → core}/dist/theme/ThemeSwitcher.svelte +0 -0
- /package/packages/{blogkit → core}/dist/theme/ThemeSwitcher.svelte.d.ts +0 -0
- /package/packages/{blogkit → core}/dist/theme/index.d.ts +0 -0
- /package/packages/{blogkit → core}/dist/theme/index.js +0 -0
- /package/packages/{blogkit → core}/dist/theme/store.d.ts +0 -0
- /package/packages/{blogkit → core}/dist/types/blog.js +0 -0
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
# @aureuma/blogkit
|
|
2
|
-
|
|
3
|
-
A Mintlify-inspired blog UI + content pipeline helpers for SvelteKit + mdsvex.
|
|
4
|
-
|
|
5
|
-
This repo keeps `svelta` as a demo app, and exposes the reusable parts as a local workspace package under `packages/blogkit`.
|
|
6
|
-
|
|
7
|
-
## What you get
|
|
8
|
-
|
|
9
|
-
- Blog UI components: hero card, post cards, tag tabs, share buttons, summary card, more-posts grid, image lightbox.
|
|
10
|
-
- Server helpers: `createBlog()` to load Markdown posts (frontmatter, excerpt, reading-time, categories, hero).
|
|
11
|
-
- Theme helper: `createThemeController()` and a simple theme switcher component.
|
|
12
|
-
|
|
13
|
-
### Compatibility
|
|
14
|
-
|
|
15
|
-
- Component source is compatible with Svelte 4 and Svelte 5 (no runes required).
|
|
16
|
-
- If you already have an existing frontmatter schema, use `mapFrontmatter` to adapt it to BlogKit's expected fields.
|
|
17
|
-
|
|
18
|
-
## Use In Another Repo
|
|
19
|
-
|
|
20
|
-
### Install from npm
|
|
21
|
-
|
|
22
|
-
```sh
|
|
23
|
-
npm i @aureuma/blogkit
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
### Option A: mono-repo / workspaces (recommended)
|
|
27
|
-
|
|
28
|
-
1. Add this repo as a submodule (or subtree) inside your project:
|
|
29
|
-
|
|
30
|
-
```sh
|
|
31
|
-
git submodule add git@github.com:Aureuma/svelta.git packages/svelta
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
2. Point your workspace config at the package:
|
|
35
|
-
|
|
36
|
-
```json
|
|
37
|
-
{
|
|
38
|
-
"workspaces": ["packages/*", "packages/svelta/packages/*"]
|
|
39
|
-
}
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
3. Import and use:
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
import { BlogCard } from '@aureuma/blogkit';
|
|
46
|
-
import { createBlog } from '@aureuma/blogkit/server';
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### Option B: publish to npm
|
|
50
|
-
|
|
51
|
-
1. Set `"private": false` in `packages/blogkit/package.json`.
|
|
52
|
-
2. From `packages/blogkit`, run `npm publish`.
|
|
53
|
-
|
|
54
|
-
## Integration Steps (SvelteKit)
|
|
55
|
-
|
|
56
|
-
1. mdsvex: configure Markdown compilation (`.md`) and Shiki/anchors (see this repo's `svelte.config.js`).
|
|
57
|
-
2. Content:
|
|
58
|
-
- posts: `src/content/blog/*.md` with YAML frontmatter
|
|
59
|
-
- assets: `static/blog/covers/*` and `static/blog/authors/*`
|
|
60
|
-
3. Blog loader glue:
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
// src/lib/server/blog.ts
|
|
64
|
-
import { createBlog } from '@aureuma/blogkit/server';
|
|
65
|
-
import { getAuthor } from '$lib/content/authors';
|
|
66
|
-
import type { BlogPostFull } from '@aureuma/blogkit';
|
|
67
|
-
|
|
68
|
-
type CompiledModule = { default: BlogPostFull['component'] };
|
|
69
|
-
|
|
70
|
-
const compiledModules = import.meta.glob('/src/content/blog/*.md') as Record<
|
|
71
|
-
string,
|
|
72
|
-
() => Promise<CompiledModule>
|
|
73
|
-
>;
|
|
74
|
-
const rawModules = import.meta.glob('/src/content/blog/*.md', {
|
|
75
|
-
query: '?raw',
|
|
76
|
-
import: 'default'
|
|
77
|
-
}) as Record<string, () => Promise<string>>;
|
|
78
|
-
|
|
79
|
-
export const blog = createBlog({ compiledModules, rawModules, getAuthor });
|
|
80
|
-
export const { getAllPosts, getPostBySlug, getCategories, pickHero } = blog;
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
4. Tailwind: ensure your `content` globs include the package so class names are not purged.
|
|
84
|
-
- If installed from npm: include `./node_modules/@aureuma/blogkit/dist/**/*`.
|
|
85
|
-
|
|
86
|
-
5. Routes: copy the blog routes from this repo:
|
|
87
|
-
- `src/routes/blog/*`
|
|
88
|
-
- `src/routes/feed.xml/*`
|
|
89
|
-
|
|
90
|
-
## Notes
|
|
91
|
-
|
|
92
|
-
- `createBlog()` builds the index from raw Markdown only (fast), and imports the compiled mdsvex component only for individual post pages.
|
|
93
|
-
- This package is intended for SvelteKit. UI uses Tailwind utility classes and expects your app to provide the CSS variables/tokens.
|
|
@@ -1,39 +0,0 @@
|
|
|
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 {};
|
|
@@ -1,222 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { createBlog, type BlogCreateConfig } from './blog';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { createBlog } from './blog';
|
|
@@ -1,66 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
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
|
-
/>
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
export let href: string = '/blog';
|
|
3
|
-
export let label: string = 'Back to blog';
|
|
4
|
-
</script>
|
|
5
|
-
|
|
6
|
-
<a
|
|
7
|
-
href={href}
|
|
8
|
-
class="inline-flex items-center gap-2 text-xs font-mono uppercase tracking-[0.6px] text-text-sub transition hover:text-text-main"
|
|
9
|
-
>
|
|
10
|
-
<svg
|
|
11
|
-
class="size-4"
|
|
12
|
-
viewBox="0 0 24 24"
|
|
13
|
-
fill="none"
|
|
14
|
-
stroke="currentColor"
|
|
15
|
-
stroke-width="2"
|
|
16
|
-
stroke-linecap="round"
|
|
17
|
-
stroke-linejoin="round"
|
|
18
|
-
aria-hidden="true"
|
|
19
|
-
>
|
|
20
|
-
<path d="M15 18l-6-6 6-6" />
|
|
21
|
-
</svg>
|
|
22
|
-
<span>{label}</span>
|
|
23
|
-
</a>
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { BlogPost } from '../../types/blog';
|
|
3
|
-
import Avatar from './Avatar.svelte';
|
|
4
|
-
|
|
5
|
-
export let post: BlogPost;
|
|
6
|
-
export let variant: 'default' | 'suggestion' = 'default';
|
|
7
|
-
$: thumbHeight = variant === 'suggestion' ? 'h-[190px]' : 'h-[280px]';
|
|
8
|
-
</script>
|
|
9
|
-
|
|
10
|
-
<a href={`/blog/${post.slug}`} class="group block" data-testid="blog-card">
|
|
11
|
-
<div class="relative overflow-hidden rounded-2xl border border-border-soft/10 bg-background-soft {thumbHeight}">
|
|
12
|
-
<img
|
|
13
|
-
src={post.cover}
|
|
14
|
-
alt={post.title}
|
|
15
|
-
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-[1.02]"
|
|
16
|
-
loading="lazy"
|
|
17
|
-
/>
|
|
18
|
-
</div>
|
|
19
|
-
|
|
20
|
-
<div class="mt-4">
|
|
21
|
-
<p class="text-xs font-mono uppercase tracking-[0.6px] text-brand">{post.category.label}</p>
|
|
22
|
-
<h3
|
|
23
|
-
class="mt-1 text-xl font-medium leading-[30px] tracking-tight underline-offset-[6px] decoration-border-soft/30 group-hover:underline"
|
|
24
|
-
>
|
|
25
|
-
{post.title}
|
|
26
|
-
</h3>
|
|
27
|
-
<p class="mt-2 text-sm leading-6 text-text-sub">{post.excerpt}</p>
|
|
28
|
-
|
|
29
|
-
<div class="mt-4 flex items-center gap-3">
|
|
30
|
-
<Avatar src={post.author.avatar} alt={post.author.name} size={24} />
|
|
31
|
-
<div class="leading-tight">
|
|
32
|
-
<div class="text-sm font-medium tracking-tight text-text-main">{post.author.name}</div>
|
|
33
|
-
<div class="text-xs text-text-muted">{post.author.title}</div>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
</div>
|
|
37
|
-
</a>
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { BlogPost } from '../../types/blog';
|
|
3
|
-
|
|
4
|
-
export let post: BlogPost;
|
|
5
|
-
</script>
|
|
6
|
-
|
|
7
|
-
<a
|
|
8
|
-
href={`/blog/${post.slug}`}
|
|
9
|
-
data-testid="blog-hero"
|
|
10
|
-
class="group relative my-12 block h-[480px] overflow-hidden rounded-3xl shadow-drop-md"
|
|
11
|
-
>
|
|
12
|
-
<img
|
|
13
|
-
src={post.cover}
|
|
14
|
-
alt={post.title}
|
|
15
|
-
class="absolute inset-0 h-full w-full object-cover transition-transform duration-700 group-hover:scale-[1.02]"
|
|
16
|
-
loading="eager"
|
|
17
|
-
/>
|
|
18
|
-
|
|
19
|
-
<div class="absolute inset-0 bg-gradient-to-t from-black/65 via-black/25 to-black/0"></div>
|
|
20
|
-
|
|
21
|
-
<div class="relative flex h-full flex-col justify-end p-8">
|
|
22
|
-
<p class="text-xs font-mono uppercase tracking-[0.6px] text-brand">{post.category.label}</p>
|
|
23
|
-
<h2
|
|
24
|
-
class="mt-2 max-w-3xl text-3xl font-semibold leading-tight tracking-[-0.8px] text-white md:text-4xl"
|
|
25
|
-
>
|
|
26
|
-
{post.title}
|
|
27
|
-
</h2>
|
|
28
|
-
<p class="mt-3 max-w-2xl text-base leading-6 text-white/80">{post.excerpt}</p>
|
|
29
|
-
|
|
30
|
-
<div class="mt-6 flex items-center gap-2 text-xs font-mono uppercase tracking-[0.6px] text-white/70">
|
|
31
|
-
<span>{post.dateShort}</span>
|
|
32
|
-
<span class="text-white/35" aria-hidden="true">•</span>
|
|
33
|
-
<span>{post.readingTimeShort}</span>
|
|
34
|
-
</div>
|
|
35
|
-
</div>
|
|
36
|
-
</a>
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { BROWSER } from 'esm-env';
|
|
3
|
-
import { onDestroy, onMount, tick } from 'svelte';
|
|
4
|
-
|
|
5
|
-
export let image: { src: string; alt: string } | null;
|
|
6
|
-
export let onClose: () => void;
|
|
7
|
-
|
|
8
|
-
let dialogEl: HTMLDivElement | null = null;
|
|
9
|
-
|
|
10
|
-
$: if (BROWSER) {
|
|
11
|
-
// Prevent background scroll while open.
|
|
12
|
-
document.body.style.overflow = image ? 'hidden' : '';
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
$: if (BROWSER && image) {
|
|
16
|
-
// Ensure the dialog is focusable for Escape-to-close and accessibility.
|
|
17
|
-
void (async () => {
|
|
18
|
-
await tick();
|
|
19
|
-
dialogEl?.focus();
|
|
20
|
-
})();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
onMount(() => {
|
|
24
|
-
if (!BROWSER) return;
|
|
25
|
-
const onKeyDown = (e: KeyboardEvent) => {
|
|
26
|
-
if (e.key === 'Escape') onClose();
|
|
27
|
-
};
|
|
28
|
-
window.addEventListener('keydown', onKeyDown);
|
|
29
|
-
return () => window.removeEventListener('keydown', onKeyDown);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
onDestroy(() => {
|
|
33
|
-
if (!BROWSER) return;
|
|
34
|
-
document.body.style.overflow = '';
|
|
35
|
-
});
|
|
36
|
-
</script>
|
|
37
|
-
|
|
38
|
-
{#if image}
|
|
39
|
-
<div
|
|
40
|
-
bind:this={dialogEl}
|
|
41
|
-
class="fixed inset-0 z-50 flex items-center justify-center bg-black/75 p-6"
|
|
42
|
-
role="dialog"
|
|
43
|
-
aria-modal="true"
|
|
44
|
-
tabindex="-1"
|
|
45
|
-
>
|
|
46
|
-
<button
|
|
47
|
-
type="button"
|
|
48
|
-
class="absolute inset-0 cursor-zoom-out"
|
|
49
|
-
on:click={onClose}
|
|
50
|
-
aria-label="Close image"
|
|
51
|
-
></button>
|
|
52
|
-
<img
|
|
53
|
-
src={image.src}
|
|
54
|
-
alt={image.alt}
|
|
55
|
-
class="relative max-h-[90vh] max-w-[92vw] rounded-2xl border border-white/10 bg-black/10 object-contain"
|
|
56
|
-
/>
|
|
57
|
-
</div>
|
|
58
|
-
{/if}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { BlogPost } from '../../types/blog';
|
|
3
|
-
import BlogCard from './BlogCard.svelte';
|
|
4
|
-
|
|
5
|
-
export let posts: BlogPost[];
|
|
6
|
-
</script>
|
|
7
|
-
|
|
8
|
-
<section class="mt-16" data-testid="blog-more-posts">
|
|
9
|
-
<h2 class="text-lg font-medium tracking-tight text-text-main">More blog posts to read</h2>
|
|
10
|
-
<div class="mt-8 grid grid-cols-1 gap-x-5 gap-y-12 md:grid-cols-2">
|
|
11
|
-
{#each posts as post (post.slug)}
|
|
12
|
-
<BlogCard post={post} variant="suggestion" />
|
|
13
|
-
{/each}
|
|
14
|
-
</div>
|
|
15
|
-
</section>
|