cosmolo 0.3.3 → 0.3.4

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.
@@ -0,0 +1,105 @@
1
+ import type { Article, ResolvedCosmoloConfig } from './types.js';
2
+ /**
3
+ * Factory for the article listing page load function.
4
+ *
5
+ * @example
6
+ * // src/routes/+page.server.ts
7
+ * import { createArticlesLoader } from 'cosmolo';
8
+ * import config from '../../cosmolo.config';
9
+ * export const load = createArticlesLoader(config);
10
+ */
11
+ export declare function createArticlesLoader(config: ResolvedCosmoloConfig): () => {
12
+ articles: Article[];
13
+ };
14
+ /**
15
+ * Factory for a single article page load function.
16
+ * Resolves manual related articles, series prev/next, and git updated date.
17
+ *
18
+ * @example
19
+ * // src/routes/articles/[slug]/+page.server.ts
20
+ * import { createArticleLoader } from 'cosmolo';
21
+ * import config from '../../../../cosmolo.config';
22
+ * export const entries = () => getSlugs(config).map(slug => ({ slug }));
23
+ * export const load = createArticleLoader(config);
24
+ */
25
+ export declare function createArticleLoader(config: ResolvedCosmoloConfig, options?: {
26
+ getUpdatedAt?: (slug: string) => string;
27
+ }): ({ params }: {
28
+ params: {
29
+ slug: string;
30
+ };
31
+ }) => Promise<{
32
+ article: Article;
33
+ related: Article[];
34
+ updatedAt: string;
35
+ seriesPrev: Article | null;
36
+ seriesNext: Article | null;
37
+ seriesTotal: number;
38
+ }>;
39
+ /**
40
+ * Factory for the category listing page load function.
41
+ *
42
+ * @example
43
+ * // src/routes/categories/[slug]/+page.server.ts
44
+ * import { createCategoryLoader } from 'cosmolo';
45
+ * import config from '../../../../cosmolo.config';
46
+ * export const entries = () => getCategorySlugs(config).map(slug => ({ slug }));
47
+ * export const load = createCategoryLoader(config);
48
+ */
49
+ export declare function createCategoryLoader(config: ResolvedCosmoloConfig): ({ params }: {
50
+ params: {
51
+ slug: string;
52
+ };
53
+ }) => {
54
+ slug: string;
55
+ label: string;
56
+ description: string;
57
+ articles: Article[];
58
+ };
59
+ /**
60
+ * Factory for the tag listing page load function.
61
+ *
62
+ * @example
63
+ * // src/routes/tags/[tag]/+page.server.ts
64
+ * import { createTagLoader } from 'cosmolo';
65
+ * import config from '../../../../cosmolo.config';
66
+ * export const entries = () => getTags(config).map(tag => ({ tag }));
67
+ * export const load = createTagLoader(config);
68
+ */
69
+ export declare function createTagLoader(config: ResolvedCosmoloConfig): ({ params }: {
70
+ params: {
71
+ tag: string;
72
+ };
73
+ }) => {
74
+ tag: string;
75
+ articles: Article[];
76
+ };
77
+ /**
78
+ * Factory for the static page load function.
79
+ *
80
+ * @example
81
+ * // src/routes/(pages)/[slug]/+page.server.ts
82
+ * import { createPageLoader } from 'cosmolo';
83
+ * import config from '../../../../cosmolo.config';
84
+ * export const entries = () => getPageSlugs(config).map(slug => ({ slug }));
85
+ * export const load = createPageLoader(config);
86
+ */
87
+ export declare function createPageLoader(config: ResolvedCosmoloConfig): ({ params }: {
88
+ params: {
89
+ slug: string;
90
+ };
91
+ }) => Promise<{
92
+ page: import("./types.js").Page;
93
+ }>;
94
+ export declare function createArticleEntries(config: ResolvedCosmoloConfig): () => {
95
+ slug: string;
96
+ }[];
97
+ export declare function createCategoryEntries(config: ResolvedCosmoloConfig): () => {
98
+ slug: string;
99
+ }[];
100
+ export declare function createTagEntries(config: ResolvedCosmoloConfig): () => {
101
+ tag: string;
102
+ }[];
103
+ export declare function createPageEntries(config: ResolvedCosmoloConfig): () => {
104
+ slug: string;
105
+ }[];
@@ -0,0 +1,119 @@
1
+ import { getArticle, getArticles, getArticlesByCategory, getArticlesBySeries, getArticlesByTag, getSlugs, getTags } from './articles.js';
2
+ import { getCategorySlugs, getCategoryLabel, getCategoryDescription } from './categories.js';
3
+ import { getPage, getPageSlugs } from './pages.js';
4
+ /**
5
+ * Factory for the article listing page load function.
6
+ *
7
+ * @example
8
+ * // src/routes/+page.server.ts
9
+ * import { createArticlesLoader } from 'cosmolo';
10
+ * import config from '../../cosmolo.config';
11
+ * export const load = createArticlesLoader(config);
12
+ */
13
+ export function createArticlesLoader(config) {
14
+ return () => ({ articles: getArticles(config) });
15
+ }
16
+ /**
17
+ * Factory for a single article page load function.
18
+ * Resolves manual related articles, series prev/next, and git updated date.
19
+ *
20
+ * @example
21
+ * // src/routes/articles/[slug]/+page.server.ts
22
+ * import { createArticleLoader } from 'cosmolo';
23
+ * import config from '../../../../cosmolo.config';
24
+ * export const entries = () => getSlugs(config).map(slug => ({ slug }));
25
+ * export const load = createArticleLoader(config);
26
+ */
27
+ export function createArticleLoader(config, options = {}) {
28
+ return async ({ params }) => {
29
+ const article = await getArticle(config, params.slug);
30
+ const updatedAt = options.getUpdatedAt?.(params.slug) ?? '';
31
+ let related;
32
+ if (article.related.length > 0) {
33
+ const all = getArticles(config);
34
+ related = article.related
35
+ .map((s) => all.find((a) => a.slug === s))
36
+ .filter((a) => a !== undefined)
37
+ .slice(0, 4);
38
+ }
39
+ else {
40
+ related = getArticlesByCategory(config, article.category, params.slug).slice(0, 4);
41
+ }
42
+ let seriesPrev = null;
43
+ let seriesNext = null;
44
+ let seriesTotal = 0;
45
+ if (article.series) {
46
+ const seriesArticles = getArticlesBySeries(config, article.series);
47
+ seriesTotal = seriesArticles.length;
48
+ const idx = seriesArticles.findIndex((a) => a.slug === params.slug);
49
+ seriesPrev = idx > 0 ? seriesArticles[idx - 1] : null;
50
+ seriesNext = idx < seriesArticles.length - 1 ? seriesArticles[idx + 1] : null;
51
+ }
52
+ return { article, related, updatedAt, seriesPrev, seriesNext, seriesTotal };
53
+ };
54
+ }
55
+ /**
56
+ * Factory for the category listing page load function.
57
+ *
58
+ * @example
59
+ * // src/routes/categories/[slug]/+page.server.ts
60
+ * import { createCategoryLoader } from 'cosmolo';
61
+ * import config from '../../../../cosmolo.config';
62
+ * export const entries = () => getCategorySlugs(config).map(slug => ({ slug }));
63
+ * export const load = createCategoryLoader(config);
64
+ */
65
+ export function createCategoryLoader(config) {
66
+ return ({ params }) => {
67
+ const { slug } = params;
68
+ return {
69
+ slug,
70
+ label: getCategoryLabel(config, slug),
71
+ description: getCategoryDescription(config, slug),
72
+ articles: getArticlesByCategory(config, slug),
73
+ };
74
+ };
75
+ }
76
+ /**
77
+ * Factory for the tag listing page load function.
78
+ *
79
+ * @example
80
+ * // src/routes/tags/[tag]/+page.server.ts
81
+ * import { createTagLoader } from 'cosmolo';
82
+ * import config from '../../../../cosmolo.config';
83
+ * export const entries = () => getTags(config).map(tag => ({ tag }));
84
+ * export const load = createTagLoader(config);
85
+ */
86
+ export function createTagLoader(config) {
87
+ return ({ params }) => ({
88
+ tag: params.tag,
89
+ articles: getArticlesByTag(config, params.tag),
90
+ });
91
+ }
92
+ /**
93
+ * Factory for the static page load function.
94
+ *
95
+ * @example
96
+ * // src/routes/(pages)/[slug]/+page.server.ts
97
+ * import { createPageLoader } from 'cosmolo';
98
+ * import config from '../../../../cosmolo.config';
99
+ * export const entries = () => getPageSlugs(config).map(slug => ({ slug }));
100
+ * export const load = createPageLoader(config);
101
+ */
102
+ export function createPageLoader(config) {
103
+ return async ({ params }) => ({
104
+ page: await getPage(config, params.slug),
105
+ });
106
+ }
107
+ // ─── entries generators ───────────────────────────────────────────────────────
108
+ export function createArticleEntries(config) {
109
+ return () => getSlugs(config).map((slug) => ({ slug }));
110
+ }
111
+ export function createCategoryEntries(config) {
112
+ return () => getCategorySlugs(config).map((slug) => ({ slug }));
113
+ }
114
+ export function createTagEntries(config) {
115
+ return () => getTags(config).map((tag) => ({ tag }));
116
+ }
117
+ export function createPageEntries(config) {
118
+ return () => getPageSlugs(config).map((slug) => ({ slug }));
119
+ }
@@ -0,0 +1,3 @@
1
+ import type { TocEntry } from './types.js';
2
+ export declare function renderMarkdown(content: string): Promise<string>;
3
+ export declare function generateToc(content: string): TocEntry[];
@@ -0,0 +1,69 @@
1
+ import { marked } from 'marked';
2
+ function slugifyHeading(text) {
3
+ return text
4
+ .toLowerCase()
5
+ .replace(/<[^>]+>/g, '')
6
+ .replace(/[^\w\s-]/g, '')
7
+ .trim()
8
+ .replace(/\s+/g, '-');
9
+ }
10
+ marked.use({
11
+ extensions: [
12
+ {
13
+ name: 'youtube',
14
+ level: 'block',
15
+ start(src) {
16
+ return src.indexOf('::youtube[');
17
+ },
18
+ tokenizer(src) {
19
+ const match = /^::youtube\[([^\]]+)\]/.exec(src);
20
+ if (match)
21
+ return { type: 'youtube', raw: match[0], videoId: match[1].trim() };
22
+ },
23
+ renderer(token) {
24
+ const { videoId } = token;
25
+ return `<div class="youtube-embed"><iframe src="https://www.youtube.com/embed/${videoId}" title="YouTube video" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>\n`;
26
+ },
27
+ },
28
+ ],
29
+ });
30
+ marked.use({
31
+ renderer: {
32
+ link({ href, title, text }) {
33
+ const isExternal = /^https?:\/\//.test(href ?? '');
34
+ const rel = isExternal ? ' target="_blank" rel="noopener noreferrer"' : '';
35
+ const titleAttr = title ? ` title="${title}"` : '';
36
+ return `<a href="${href}"${titleAttr}${rel}>${text}</a>`;
37
+ },
38
+ heading({ text, depth }) {
39
+ const id = slugifyHeading(text);
40
+ return `<h${depth} id="${id}">${text}</h${depth}>\n`;
41
+ },
42
+ },
43
+ });
44
+ export async function renderMarkdown(content) {
45
+ return marked.parse(content);
46
+ }
47
+ export function generateToc(content) {
48
+ const entries = [];
49
+ const seenIds = new Map();
50
+ const headingRegex = /^(#{2,6})\s+(.+?)$/gm;
51
+ let match;
52
+ while ((match = headingRegex.exec(content)) !== null) {
53
+ const level = match[1].length;
54
+ const rawText = match[2].trim();
55
+ const plainText = rawText
56
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
57
+ .replace(/\*([^*]+)\*/g, '$1')
58
+ .replace(/__([^_]+)__/g, '$1')
59
+ .replace(/_([^_]+)_/g, '$1')
60
+ .replace(/`([^`]+)`/g, '$1')
61
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
62
+ const baseId = slugifyHeading(plainText);
63
+ const count = seenIds.get(baseId) ?? 0;
64
+ const id = count === 0 ? baseId : `${baseId}-${count}`;
65
+ seenIds.set(baseId, count + 1);
66
+ entries.push({ level, id, text: plainText });
67
+ }
68
+ return entries;
69
+ }
@@ -0,0 +1,3 @@
1
+ import type { Page, ResolvedCosmoloConfig } from './types.js';
2
+ export declare function getPageSlugs(config: ResolvedCosmoloConfig): string[];
3
+ export declare function getPage(config: ResolvedCosmoloConfig, slug: string): Promise<Page>;
package/dist/pages.js ADDED
@@ -0,0 +1,20 @@
1
+ import matter from 'gray-matter';
2
+ import { renderMarkdown } from './markdown.js';
3
+ import { rawPageFiles } from 'cosmolo:content';
4
+ function slugFromPath(filePath, dir) {
5
+ const prefix = dir.replace(/^\//, '');
6
+ return filePath.replace(new RegExp(`^/?${prefix}/`), '').replace(/\.md$/, '');
7
+ }
8
+ export function getPageSlugs(config) {
9
+ return Object.keys(rawPageFiles).map((p) => slugFromPath(p, config.pagesDir));
10
+ }
11
+ export async function getPage(config, slug) {
12
+ const dir = config.pagesDir.replace(/^\//, '');
13
+ const filePath = `/${dir}/${slug}.md`;
14
+ const raw = rawPageFiles[filePath];
15
+ if (raw === undefined)
16
+ throw new Error(`Page not found: ${slug}`);
17
+ const { data, content } = matter(raw);
18
+ const html = await renderMarkdown(content);
19
+ return { slug, title: String(data.title ?? slug), html };
20
+ }
@@ -0,0 +1,15 @@
1
+ import type { Plugin } from 'vite';
2
+ import type { CosmoloConfig } from './types.js';
3
+ /**
4
+ * Vite plugin that generates the `cosmolo:content` virtual module.
5
+ *
6
+ * Content files are bundled via `import.meta.glob` (evaluated at build time).
7
+ * JSON config files (categories, site config) are inlined as object literals so
8
+ * the runtime code has no `fs` dependency — required for Cloudflare Workers and
9
+ * other serverless runtimes.
10
+ *
11
+ * Usage in vite.config.ts:
12
+ * import { cosmoloPlugin } from 'cosmolo/plugin';
13
+ * plugins: [sveltekit(), cosmoloPlugin({ articlesDir: 'content/articles' })]
14
+ */
15
+ export declare function cosmoloPlugin(userConfig?: CosmoloConfig): Plugin;
@@ -1,12 +1,8 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import type { Plugin } from 'vite';
4
- import type { CosmoloConfig } from './types.js';
5
3
  import { resolveConfig } from './config.js';
6
-
7
4
  const VIRTUAL_ID = 'cosmolo:content';
8
5
  const RESOLVED_ID = '\0cosmolo:content';
9
-
10
6
  /**
11
7
  * Vite plugin that generates the `cosmolo:content` virtual module.
12
8
  *
@@ -19,30 +15,26 @@ const RESOLVED_ID = '\0cosmolo:content';
19
15
  * import { cosmoloPlugin } from 'cosmolo/plugin';
20
16
  * plugins: [sveltekit(), cosmoloPlugin({ articlesDir: 'content/articles' })]
21
17
  */
22
- export function cosmoloPlugin(userConfig: CosmoloConfig = {}): Plugin {
23
- const config = resolveConfig(userConfig);
24
-
25
- const articlesDir = '/' + config.articlesDir.replace(/^\/|\/$/g, '');
26
- const pagesDir = '/' + config.pagesDir.replace(/^\/|\/$/g, '');
27
-
28
- const categoriesAbsPath = path.resolve(process.cwd(), config.categoriesConfigPath);
29
- const siteConfigAbsPath = path.resolve(process.cwd(), config.siteConfigPath);
30
-
31
- return {
32
- name: 'cosmolo',
33
- resolveId(id) {
34
- if (id === VIRTUAL_ID) return RESOLVED_ID;
35
- },
36
- load(id) {
37
- if (id !== RESOLVED_ID) return;
38
-
39
- this.addWatchFile(categoriesAbsPath);
40
- this.addWatchFile(siteConfigAbsPath);
41
-
42
- const categoriesData = JSON.parse(fs.readFileSync(categoriesAbsPath, 'utf-8'));
43
- const siteConfigData = JSON.parse(fs.readFileSync(siteConfigAbsPath, 'utf-8'));
44
-
45
- return `
18
+ export function cosmoloPlugin(userConfig = {}) {
19
+ const config = resolveConfig(userConfig);
20
+ const articlesDir = '/' + config.articlesDir.replace(/^\/|\/$/g, '');
21
+ const pagesDir = '/' + config.pagesDir.replace(/^\/|\/$/g, '');
22
+ const categoriesAbsPath = path.resolve(process.cwd(), config.categoriesConfigPath);
23
+ const siteConfigAbsPath = path.resolve(process.cwd(), config.siteConfigPath);
24
+ return {
25
+ name: 'cosmolo',
26
+ resolveId(id) {
27
+ if (id === VIRTUAL_ID)
28
+ return RESOLVED_ID;
29
+ },
30
+ load(id) {
31
+ if (id !== RESOLVED_ID)
32
+ return;
33
+ this.addWatchFile(categoriesAbsPath);
34
+ this.addWatchFile(siteConfigAbsPath);
35
+ const categoriesData = JSON.parse(fs.readFileSync(categoriesAbsPath, 'utf-8'));
36
+ const siteConfigData = JSON.parse(fs.readFileSync(siteConfigAbsPath, 'utf-8'));
37
+ return `
46
38
  export const rawMdFiles = import.meta.glob(
47
39
  '${articlesDir}/*.md',
48
40
  { query: '?raw', import: 'default', eager: true }
@@ -62,6 +54,6 @@ export const categoriesData = ${JSON.stringify(categoriesData)};
62
54
 
63
55
  export const siteConfigData = ${JSON.stringify(siteConfigData)};
64
56
  `.trim();
65
- },
66
- };
57
+ },
58
+ };
67
59
  }
@@ -0,0 +1,58 @@
1
+ export interface CosmoloConfig {
2
+ /** Directory containing article files (.md, .svx). @default 'src/content/articles' */
3
+ articlesDir?: string;
4
+ /** Directory containing static page files (.md). @default 'src/content/pages' */
5
+ pagesDir?: string;
6
+ /** Path to site configuration JSON. @default 'config/site.json' */
7
+ siteConfigPath?: string;
8
+ /** Path to categories configuration JSON. @default 'config/categories.json' */
9
+ categoriesConfigPath?: string;
10
+ }
11
+ export type ResolvedCosmoloConfig = Required<CosmoloConfig>;
12
+ export interface TocEntry {
13
+ level: number;
14
+ id: string;
15
+ text: string;
16
+ }
17
+ export interface ArticleFrontmatter {
18
+ title: string;
19
+ category: string;
20
+ excerpt: string;
21
+ sort: number;
22
+ date: string;
23
+ tags: string[];
24
+ series?: string;
25
+ seriesOrder?: number;
26
+ draft: boolean;
27
+ related: string[];
28
+ }
29
+ export interface Article extends ArticleFrontmatter {
30
+ slug: string;
31
+ html: string;
32
+ markdown: string;
33
+ toc: TocEntry[];
34
+ }
35
+ export interface Page {
36
+ slug: string;
37
+ title: string;
38
+ html: string;
39
+ }
40
+ export interface CategoryEntry {
41
+ slug: string;
42
+ label: string;
43
+ description: string;
44
+ }
45
+ export interface SiteConfig {
46
+ url: string;
47
+ name: string;
48
+ description: string;
49
+ twitterHandle: string;
50
+ fallbackCategoryLabel: string;
51
+ articlesPerPage: number;
52
+ ogImage: {
53
+ mode: 'static' | 'generated';
54
+ };
55
+ api: {
56
+ articleBody: 'html' | 'markdown' | 'plaintext';
57
+ };
58
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ // ─── Package configuration ────────────────────────────────────────────────────
2
+ export {};
package/package.json CHANGED
@@ -1,27 +1,30 @@
1
1
  {
2
2
  "name": "cosmolo",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "bin": {
6
- "cosmolo": "src/cli/index.ts"
6
+ "cosmolo": "./dist/cli/index.js"
7
7
  },
8
8
  "exports": {
9
9
  ".": {
10
- "types": "./src/index.ts",
11
- "default": "./src/index.ts"
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
12
  },
13
13
  "./plugin": {
14
- "types": "./src/plugin.ts",
15
- "default": "./src/plugin.ts"
14
+ "types": "./dist/plugin.d.ts",
15
+ "default": "./dist/plugin.js"
16
16
  }
17
17
  },
18
18
  "files": [
19
- "src",
19
+ "dist",
20
20
  "templates",
21
21
  "README.md",
22
- "LICENSE",
23
- "!src/**/*.test.ts"
22
+ "LICENSE"
24
23
  ],
24
+ "scripts": {
25
+ "build": "tsc && cp src/virtual.d.ts dist/virtual.d.ts && chmod +x dist/cli/index.js",
26
+ "prepublishOnly": "npm run build"
27
+ },
25
28
  "dependencies": {
26
29
  "gray-matter": "^4.0.3",
27
30
  "marked": "^15.0.0",
package/src/articles.ts DELETED
@@ -1,124 +0,0 @@
1
- import { z } from 'zod';
2
- import matter from 'gray-matter';
3
- import { renderMarkdown, generateToc } from './markdown.js';
4
- import { isKnownCategory } from './categories.js';
5
- import type { Article, ResolvedCosmoloConfig } from './types.js';
6
- import { rawMdFiles, svxModules } from 'cosmolo:content';
7
-
8
- export const articleFrontmatterSchema = z.object({
9
- title: z.string(),
10
- category: z.string(),
11
- excerpt: z.string(),
12
- sort: z.number().default(0),
13
- date: z.preprocess(
14
- (val) => (val instanceof Date ? val.toISOString().split('T')[0] : val),
15
- z.string().default('')
16
- ),
17
- tags: z.array(z.string()).default([]),
18
- series: z.string().optional(),
19
- seriesOrder: z.number().optional(),
20
- draft: z.boolean().default(false),
21
- related: z.array(z.string()).default([]),
22
- });
23
-
24
- function slugFromPath(filePath: string, dir: string): string {
25
- const prefix = dir.replace(/^\//, '');
26
- return filePath
27
- .replace(new RegExp(`^/?${prefix}/`), '')
28
- .replace(/\.(md|svx)$/, '');
29
- }
30
-
31
- /** Returns all non-draft articles sorted by `sort` descending. */
32
- export function getArticles(config: ResolvedCosmoloConfig): Article[] {
33
- const articles: Article[] = [];
34
-
35
- for (const [filePath, raw] of Object.entries(rawMdFiles)) {
36
- const slug = slugFromPath(filePath, config.articlesDir);
37
- const { data } = matter(raw);
38
- const frontmatter = articleFrontmatterSchema.parse(data);
39
- if (frontmatter.draft) continue;
40
- articles.push({ ...frontmatter, slug, html: '', markdown: '', toc: [] });
41
- }
42
-
43
- for (const [filePath, mod] of Object.entries(svxModules)) {
44
- const slug = slugFromPath(filePath, config.articlesDir);
45
- const frontmatter = articleFrontmatterSchema.parse(
46
- (mod as { metadata: Record<string, unknown> }).metadata
47
- );
48
- if (frontmatter.draft) continue;
49
- articles.push({ ...frontmatter, slug, html: '', markdown: '', toc: [] });
50
- }
51
-
52
- return articles.sort((a, b) => b.sort - a.sort);
53
- }
54
-
55
- /** Returns all non-draft article slugs. */
56
- export function getSlugs(config: ResolvedCosmoloConfig): string[] {
57
- return getArticles(config).map((a) => a.slug);
58
- }
59
-
60
- /** Returns a single article with rendered HTML and TOC. Draft articles are included. */
61
- export async function getArticle(config: ResolvedCosmoloConfig, slug: string): Promise<Article> {
62
- const dir = config.articlesDir.replace(/^\//, '');
63
- const mdPath = `/${dir}/${slug}.md`;
64
- const svxPath = `/${dir}/${slug}.svx`;
65
-
66
- if (rawMdFiles[mdPath] !== undefined) {
67
- const raw = rawMdFiles[mdPath];
68
- const { data, content } = matter(raw);
69
- const frontmatter = articleFrontmatterSchema.parse(data);
70
- const html = await renderMarkdown(content);
71
- const toc = generateToc(content);
72
- return { ...frontmatter, slug, html, markdown: content, toc };
73
- }
74
-
75
- if (svxModules[svxPath] !== undefined) {
76
- const frontmatter = articleFrontmatterSchema.parse(
77
- (svxModules[svxPath] as { metadata: Record<string, unknown> }).metadata
78
- );
79
- return { ...frontmatter, slug, html: '', markdown: '', toc: [] };
80
- }
81
-
82
- throw new Error(`Article not found: ${slug}`);
83
- }
84
-
85
- export function getArticlesByCategory(
86
- config: ResolvedCosmoloConfig,
87
- categorySlug: string,
88
- excludeSlug?: string
89
- ): Article[] {
90
- const all = getArticles(config);
91
- const filtered =
92
- categorySlug === 'other'
93
- ? all.filter((a) => !isKnownCategory(config, a.category))
94
- : all.filter((a) => a.category === categorySlug);
95
- return excludeSlug ? filtered.filter((a) => a.slug !== excludeSlug) : filtered;
96
- }
97
-
98
- export function getArticlesByTag(config: ResolvedCosmoloConfig, tag: string): Article[] {
99
- return getArticles(config).filter((a) => a.tags.includes(tag));
100
- }
101
-
102
- export function getArticlesBySeries(config: ResolvedCosmoloConfig, seriesKey: string): Article[] {
103
- return getArticles(config)
104
- .filter((a) => a.series === seriesKey)
105
- .sort((a, b) => (a.seriesOrder ?? 0) - (b.seriesOrder ?? 0));
106
- }
107
-
108
- export function getTags(config: ResolvedCosmoloConfig): string[] {
109
- const tags = new Set<string>();
110
- getArticles(config).forEach((a) => a.tags.forEach((t) => tags.add(t)));
111
- return Array.from(tags).sort();
112
- }
113
-
114
- /**
115
- * Returns the Svelte component constructor for an .svx article, or undefined
116
- * if the article is a plain .md file. Safe to call from Svelte components.
117
- */
118
- export function getSvxComponent(
119
- config: ResolvedCosmoloConfig,
120
- slug: string
121
- ): unknown | undefined {
122
- const dir = '/' + config.articlesDir.replace(/^\/|\/$/g, '');
123
- return (svxModules[`${dir}/${slug}.svx`] as { default: unknown } | undefined)?.default;
124
- }
package/src/categories.ts DELETED
@@ -1,35 +0,0 @@
1
- import { categoriesData, siteConfigData } from 'cosmolo:content';
2
- import type { CategoryEntry, ResolvedCosmoloConfig, SiteConfig } from './types.js';
3
-
4
- type CategoriesMap = Record<string, { label: string; description: string }>;
5
-
6
- const map = categoriesData as CategoriesMap;
7
-
8
- export function getAllCategories(_config: ResolvedCosmoloConfig): CategoryEntry[] {
9
- return Object.entries(map).map(([slug, { label, description }]) => ({
10
- slug,
11
- label,
12
- description,
13
- }));
14
- }
15
-
16
- export function isKnownCategory(_config: ResolvedCosmoloConfig, key: string): boolean {
17
- return Object.prototype.hasOwnProperty.call(map, key);
18
- }
19
-
20
- export function getCategoryLabel(_config: ResolvedCosmoloConfig, key: string): string {
21
- if (Object.prototype.hasOwnProperty.call(map, key)) return map[key].label;
22
- return (siteConfigData as SiteConfig).fallbackCategoryLabel;
23
- }
24
-
25
- export function getCategoryDescription(_config: ResolvedCosmoloConfig, key: string): string {
26
- return map[key]?.description ?? '';
27
- }
28
-
29
- export function getCategorySlugs(_config: ResolvedCosmoloConfig): string[] {
30
- return [...Object.keys(map), 'other'];
31
- }
32
-
33
- export function loadSiteConfig(_config: ResolvedCosmoloConfig): SiteConfig {
34
- return siteConfigData as SiteConfig;
35
- }