@karaoke-cms/module-blog 0.9.7 → 0.10.3

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 CHANGED
@@ -48,33 +48,58 @@ All routes are relative to `mount`:
48
48
 
49
49
  ```yaml
50
50
  ---
51
- title: "My Post" # required
52
- publish: true # required to appear on site
53
- date: 2026-01-15 # optional — YYYY-MM-DD
54
- author: "Name" # optional — string or array
55
- featured_image: "img/hero.jpg" # optional — relative to vault
56
- description: "..." # optional OG tags, RSS, AI-enriched
57
- tags: [writing, tutorial] # optional — powers /tags pages
58
- reading_time: 5 # optional — AI-enriched, minutes
59
- related: [slug-a, slug-b] # optional — AI-enriched, slugs
60
- comments: true # optional — per-post Giscus override
51
+ title: "My Post" # required
52
+ publish: true # required to appear on site
53
+ date: 2026-01-15 # optional — YYYY-MM-DD
54
+ author: "Name" # optional — string or array
55
+ featured_image: "img/hero.jpg" # optional — relative path, absolute path, URL,
56
+ # or Obsidian wiki link: "[[hero.jpg]]"
57
+ description: "..." # optional — OG tags, RSS, AI-enriched
58
+ tags: [writing, tutorial] # optional — powers /tags pages
59
+ reading_time: 5 # optional — AI-enriched, minutes
60
+ related: [slug-a, slug-b] # optional — AI-enriched, slugs
61
+ comments: true # optional — per-post Giscus override
61
62
  ---
62
63
  ```
63
64
 
65
+ ### `featured_image` formats
66
+
67
+ All of the following work in `featured_image`:
68
+
69
+ ```yaml
70
+ featured_image: "./images/hero.jpg" # relative to the note
71
+ featured_image: "/blog/hero.jpg" # vault-absolute path
72
+ featured_image: "https://example.com/x.jpg" # remote URL
73
+ featured_image: "[[hero.jpg]]" # Obsidian wiki link — resolved via vault lookup
74
+ ```
75
+
76
+ When using a wiki link, the image is served from `/media/` during dev and copied to `dist/media/` at build.
77
+
64
78
  ## Components
65
79
 
66
80
  The module ships reusable components you can import in your own pages:
67
81
 
68
82
  ```ts
69
- import FeaturedPost from '@karaoke-cms/module-blog/components/FeaturedPost';
70
- import RecentPosts from '@karaoke-cms/module-blog/components/RecentPosts';
83
+ import FeaturedPost from '@karaoke-cms/module-blog/components/FeaturedPost';
84
+ import RecentPosts from '@karaoke-cms/module-blog/components/RecentPosts';
71
85
  import PaginatedPostList from '@karaoke-cms/module-blog/components/PaginatedPostList';
72
86
  ```
73
87
 
88
+ All three components call `resolveWikiImage()` internally, so wiki-linked featured images render correctly in list views without any extra work.
89
+
74
90
  ## Scaffold
75
91
 
76
92
  On first `npm run dev`, the module copies starter page files into your project's `src/pages/{mountDir}/`. You can edit these files to customise the layout without modifying the npm package.
77
93
 
94
+ ## What's new in 0.10.3
95
+
96
+ No user-facing changes in this release.
97
+
98
+ ## What's new in 0.10.0
99
+
100
+ - **`[[wiki link]]` in `featured_image`** — `featured_image: "[[hero.jpg]]"` now works in post cards, the featured hero card, and the individual post page; images are resolved via vault path lookup and served from `/media/`
101
+ - **Blog list page** — `/blog` now has a dedicated list page separate from the post template, fixing a route conflict with the theme-default fallback
102
+
78
103
  ## What's new in 0.9.5
79
104
 
80
105
  - **Full page scaffold** — on first dev run, `list.astro`, `[slug].astro`, and `page/[page].astro` are copied to your `src/pages/blog/` so you can customise them
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@karaoke-cms/module-blog",
3
3
  "type": "module",
4
- "version": "0.9.7",
4
+ "version": "0.10.3",
5
5
  "description": "Blog module for karaoke-cms — posts, tags, RSS",
6
6
  "main": "./src/index.ts",
7
7
  "exports": {
8
8
  ".": "./src/index.ts",
9
9
  "./schema": "./src/schema.ts",
10
+ "./pages/list": "./src/pages/list.astro",
10
11
  "./pages/post": "./src/pages/post.astro",
11
12
  "./pages/page": "./src/pages/page/[page].astro",
12
13
  "./components/FeaturedPost": "./src/components/FeaturedPost.astro",
@@ -27,13 +28,15 @@
27
28
  },
28
29
  "peerDependencies": {
29
30
  "astro": ">=6.0.0",
30
- "@karaoke-cms/astro": "^0.9.7"
31
+ "@karaoke-cms/contracts": "^0.10.3"
31
32
  },
32
33
  "devDependencies": {
33
- "@karaoke-cms/astro": "workspace:*",
34
- "astro": "^6.0.8"
34
+ "astro": "^6.0.8",
35
+ "vitest": "^4.1.1",
36
+ "@karaoke-cms/astro": "0.10.3",
37
+ "@karaoke-cms/contracts": "0.10.3"
35
38
  },
36
39
  "scripts": {
37
- "test": "echo \"Stub — no tests\""
40
+ "test": "vitest run test/blog-factory.test.js"
38
41
  }
39
- }
42
+ }
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import type { CollectionEntry } from 'astro:content';
3
+ import { resolveWikiImage } from '@karaoke-cms/astro';
3
4
 
4
5
  interface Props {
5
6
  post: CollectionEntry<'blog'>;
@@ -13,7 +14,7 @@ const href = `${mount}/${post.id}`;
13
14
  <article class="blog-card blog-featured-post">
14
15
  {post.data.featured_image && (
15
16
  <a href={href} tabindex="-1" aria-hidden="true">
16
- <img src={post.data.featured_image} alt={post.data.title} class="blog-featured-image" />
17
+ <img src={resolveWikiImage(post.data.featured_image)} alt={post.data.title} class="blog-featured-image" />
17
18
  </a>
18
19
  )}
19
20
  <div class="blog-card-body">
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import type { CollectionEntry } from 'astro:content';
3
3
  import FeaturedPost from './FeaturedPost.astro';
4
+ import { resolveWikiImage } from '@karaoke-cms/astro';
4
5
 
5
6
  interface Props {
6
7
  posts: CollectionEntry<'blog'>[];
@@ -22,7 +23,7 @@ const listPosts = featuredPost ? posts.slice(1) : posts;
22
23
  <article class="blog-card">
23
24
  {post.data.featured_image && (
24
25
  <a href={`${mount}/${post.id}`} tabindex="-1" aria-hidden="true">
25
- <img src={post.data.featured_image} alt={post.data.title} class="blog-featured-image" />
26
+ <img src={resolveWikiImage(post.data.featured_image)} alt={post.data.title} class="blog-featured-image" />
26
27
  </a>
27
28
  )}
28
29
  <h2 class="blog-card-title">
@@ -1,29 +1,34 @@
1
+ import type { CssContract } from '@karaoke-cms/contracts';
2
+
1
3
  /**
2
4
  * CSS class names the blog module promises to use in its markup.
3
5
  * Themes implement these classes to style the blog.
6
+ *
7
+ * `required: true` — the theme MUST implement this class.
8
+ * `required: false` — optional / progressive enhancement.
4
9
  */
5
- export const cssContract = [
10
+ export const cssContract: CssContract = {
6
11
  // List / card layout
7
- 'blog-list',
8
- 'blog-card',
9
- 'blog-card-title',
10
- 'blog-card-meta',
11
- 'blog-card-description',
12
+ 'blog-list': { description: 'Post list page wrapper', required: true },
13
+ 'blog-card': { description: 'Individual post card in list', required: true },
14
+ 'blog-card-title': { description: 'Post title inside a card', required: true },
15
+ 'blog-card-meta': { description: 'Date/author meta row on a card', required: false },
16
+ 'blog-card-description': { description: 'Excerpt / description on a card', required: false },
12
17
  // Featured post
13
- 'blog-featured-post',
14
- 'blog-featured-image',
18
+ 'blog-featured-post': { description: 'Featured post hero block', required: false },
19
+ 'blog-featured-image': { description: 'Hero image inside featured post', required: false },
15
20
  // Post page
16
- 'blog-post',
17
- 'blog-post-title',
18
- 'blog-post-meta',
21
+ 'blog-post': { description: 'Individual post article wrapper', required: true },
22
+ 'blog-post-title': { description: 'Post title on the post page', required: true },
23
+ 'blog-post-meta': { description: 'Date/author meta on the post page', required: false },
19
24
  // Tags
20
- 'blog-tag',
21
- 'blog-tag-list',
25
+ 'blog-tag': { description: 'Tag chip on card or post page', required: false },
26
+ 'blog-tag-list': { description: 'Wrapper around a list of tag chips', required: false },
22
27
  // Pagination
23
- 'blog-pagination',
24
- 'blog-pagination-prev',
25
- 'blog-pagination-next',
26
- 'blog-pagination-current',
28
+ 'blog-pagination': { description: 'Pagination controls wrapper', required: false },
29
+ 'blog-pagination-prev': { description: 'Previous page link', required: false },
30
+ 'blog-pagination-next': { description: 'Next page link', required: false },
31
+ 'blog-pagination-current': { description: 'Current page indicator', required: false },
27
32
  // Sidebar
28
- 'blog-sidebar-recent',
29
- ] as const;
33
+ 'blog-sidebar-recent': { description: 'Recent posts sidebar widget', required: false },
34
+ };
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { defineModule } from '@karaoke-cms/astro';
1
+ import { defineModule } from '@karaoke-cms/contracts';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { join } from 'path';
4
4
  import { cssContract } from './css-contract.js';
@@ -28,8 +28,9 @@ export const blog = defineModule({
28
28
  defaultCssPath: join(_srcDir, 'styles', 'blog.css'),
29
29
 
30
30
  routes: (mount) => [
31
- { pattern: `${mount}/[slug]`, entrypoint: '@karaoke-cms/module-blog/pages/post' },
32
- { pattern: `${mount}/page/[page]`, entrypoint: '@karaoke-cms/module-blog/pages/page' },
31
+ { pattern: mount, entrypoint: '@karaoke-cms/module-blog/pages/list' },
32
+ { pattern: `${mount}/[slug]`, entrypoint: '@karaoke-cms/module-blog/pages/post' },
33
+ { pattern: `${mount}/page/[page]`, entrypoint: '@karaoke-cms/module-blog/pages/page' },
33
34
  ],
34
35
 
35
36
  menuEntries: (mount, id) => [
@@ -40,7 +41,8 @@ export const blog = defineModule({
40
41
  scaffoldPages: (mount) => {
41
42
  const mountDir = mount.replace(/^\//, '') || 'blog';
42
43
  return [
43
- { src: join(_srcDir, 'pages', 'post.astro'), dest: `${mountDir}/[slug].astro` },
44
+ { src: join(_srcDir, 'pages', 'list.astro'), dest: `${mountDir}/index.astro` },
45
+ { src: join(_srcDir, 'pages', 'post.astro'), dest: `${mountDir}/[slug].astro` },
44
46
  { src: join(_srcDir, 'pages', 'page', '[page].astro'), dest: `${mountDir}/page/[page].astro` },
45
47
  ];
46
48
  },
@@ -0,0 +1,26 @@
1
+ ---
2
+ import { getCollection } from 'astro:content';
3
+ import DefaultPage from '@karaoke-cms/astro/layouts/DefaultPage.astro';
4
+ import PaginatedPostList from '@karaoke-cms/module-blog/components/PaginatedPostList';
5
+ import RecentPosts from '@karaoke-cms/module-blog/components/RecentPosts';
6
+ import { siteTitle, blogMount } from 'virtual:karaoke-cms/config';
7
+
8
+ const PAGE_SIZE = 10;
9
+ const allPosts = (await getCollection('blog', ({ data }) => data.publish === true))
10
+ .sort((a, b) => (b.data.date?.valueOf() ?? 0) - (a.data.date?.valueOf() ?? 0));
11
+
12
+ const totalPages = Math.max(1, Math.ceil(allPosts.length / PAGE_SIZE));
13
+ const posts = allPosts.slice(0, PAGE_SIZE);
14
+ ---
15
+
16
+ <DefaultPage title={`Blog — ${siteTitle}`}>
17
+ {posts.length > 0 ? (
18
+ <PaginatedPostList posts={posts} mount={blogMount} currentPage={1} totalPages={totalPages} />
19
+ ) : (
20
+ <div class="empty-state">
21
+ <p>No posts published yet.</p>
22
+ <p>Create a Markdown file in your vault's <code>blog/</code> folder and set <code>publish: true</code> in the frontmatter to make it appear here.</p>
23
+ </div>
24
+ )}
25
+ <RecentPosts mount={blogMount} slot="right" />
26
+ </DefaultPage>
@@ -4,6 +4,7 @@ import DefaultPage from '@karaoke-cms/astro/layouts/DefaultPage.astro';
4
4
  import ModuleLoader from '@karaoke-cms/astro/components/ModuleLoader.astro';
5
5
  import RecentPosts from '@karaoke-cms/module-blog/components/RecentPosts';
6
6
  import { siteTitle, blogMount } from 'virtual:karaoke-cms/config';
7
+ import { resolveWikiImage } from '@karaoke-cms/astro';
7
8
 
8
9
  export async function getStaticPaths() {
9
10
  const posts = await getCollection('blog', ({ data }) => data.publish === true);
@@ -47,7 +48,7 @@ const related = relatedIds.length > 0
47
48
  )}
48
49
  </div>
49
50
  {entry.data.featured_image && (
50
- <img src={entry.data.featured_image} alt={entry.data.title} class="blog-featured-image" />
51
+ <img src={resolveWikiImage(entry.data.featured_image)} alt={entry.data.title} class="blog-featured-image" />
51
52
  )}
52
53
  <div class="prose blog-post">
53
54
  <Content />