@jasonshimmy/vite-plugin-cer-app 0.20.0 → 0.20.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/CHANGELOG.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # Changelog
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
+ ## [v0.20.1] - 2026-04-11
5
+
6
+ - fix(content): add fallback title and description extraction from Markdown body (9bab321)
7
+
4
8
  ## [v0.20.0] - 2026-04-11
5
9
 
6
10
  - feat(content): implement file-based content layer with Markdown/JSON support (#1) (c6ef0f7)
package/commits.txt CHANGED
@@ -1,2 +1 @@
1
- - feat(content): implement file-based content layer with Markdown/JSON support (#1) (c6ef0f7)
2
- - chore: update dependencies for @jasonshimmy/custom-elements-runtime and typescript-eslint (15095bc)
1
+ - fix(content): add fallback title and description extraction from Markdown body (9bab321)
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../src/plugin/content/parser.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAkB,WAAW,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AACtF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AA0J/C;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,WAAW,EACjB,UAAU,EAAE,MAAM,GACjB,WAAW,CAGb;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,WAAW,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,WAAW,CAAC,CAGtB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,CAI5D"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../src/plugin/content/parser.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAkB,WAAW,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AACtF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AA6N/C;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,WAAW,EACjB,UAAU,EAAE,MAAM,GACjB,WAAW,CAGb;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,WAAW,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,WAAW,CAAC,CAGtB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,WAAW,CAI5D"}
@@ -40,6 +40,60 @@ function extractHeadings(tokens) {
40
40
  walk(tokens);
41
41
  return headings;
42
42
  }
43
+ // ─── Fallback title / description extraction ─────────────────────────────────
44
+ /**
45
+ * Converts a list of inline marked tokens to plain text by recursing into
46
+ * formatted tokens (strong, em, link, etc.) and collecting their leaf text.
47
+ * Used to derive readable fallback values from body content.
48
+ */
49
+ function inlineToPlainText(tokens) {
50
+ let result = '';
51
+ for (const t of tokens) {
52
+ const children = t.tokens;
53
+ if (Array.isArray(children) && children.length > 0) {
54
+ result += inlineToPlainText(children);
55
+ }
56
+ else if (t.type === 'br') {
57
+ result += ' ';
58
+ }
59
+ else if ('text' in t && typeof t.text === 'string') {
60
+ result += t.text;
61
+ }
62
+ }
63
+ return result;
64
+ }
65
+ const DESCRIPTION_MAX_LEN = 160;
66
+ /**
67
+ * Scans the top-level token list for a fallback `title` (first depth-1 heading)
68
+ * and `description` (first paragraph). Both are `undefined` when no matching
69
+ * token is found.
70
+ *
71
+ * These are applied only when the corresponding frontmatter field is absent, so
72
+ * frontmatter always wins.
73
+ */
74
+ function extractFallbacks(tokens) {
75
+ let title;
76
+ let description;
77
+ for (const token of tokens) {
78
+ if (title === undefined && token.type === 'heading' && token.depth === 1) {
79
+ const text = inlineToPlainText(token.tokens ?? []).trim();
80
+ if (text)
81
+ title = text;
82
+ }
83
+ if (description === undefined && token.type === 'paragraph') {
84
+ const text = inlineToPlainText(token.tokens ?? []).trim();
85
+ if (text) {
86
+ description =
87
+ text.length > DESCRIPTION_MAX_LEN
88
+ ? text.slice(0, DESCRIPTION_MAX_LEN).trimEnd() + '…'
89
+ : text;
90
+ }
91
+ }
92
+ if (title !== undefined && description !== undefined)
93
+ break;
94
+ }
95
+ return { title, description };
96
+ }
43
97
  // ─── Custom renderer: add id to heading tags ─────────────────────────────────
44
98
  const renderer = new marked.Renderer();
45
99
  renderer.heading = function ({ tokens, depth }) {
@@ -105,6 +159,9 @@ function parseContentFileFromRaw(file, contentDir, raw) {
105
159
  const excerpt = excerptSource !== null
106
160
  ? marked.parse(excerptSource, { renderer })
107
161
  : undefined;
162
+ // Derive fallback title / description from body tokens when frontmatter
163
+ // does not provide them. Frontmatter always wins — these only fill gaps.
164
+ const fallbacks = extractFallbacks(tokens);
108
165
  const item = {
109
166
  ...frontmatter,
110
167
  _path,
@@ -113,6 +170,12 @@ function parseContentFileFromRaw(file, contentDir, raw) {
113
170
  body,
114
171
  toc,
115
172
  };
173
+ if (item.title === undefined && fallbacks.title !== undefined) {
174
+ item.title = fallbacks.title;
175
+ }
176
+ if (item.description === undefined && fallbacks.description !== undefined) {
177
+ item.description = fallbacks.description;
178
+ }
116
179
  if (excerpt !== undefined) {
117
180
  item.excerpt = excerpt;
118
181
  }
@@ -1 +1 @@
1
- {"version":3,"file":"parser.js","sourceRoot":"","sources":["../../../src/plugin/content/parser.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"parser.js","sourceRoot":"","sources":["../../../src/plugin/content/parser.ts"],"names":[],"mappings":"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"}
package/docs/content.md CHANGED
@@ -131,6 +131,23 @@ Recognized frontmatter keys:
131
131
 
132
132
  Any additional frontmatter keys are stored verbatim in the `ContentMeta` / `ContentItem` object.
133
133
 
134
+ ### Automatic title and description
135
+
136
+ When `title` or `description` are absent from frontmatter, the parser derives them from the body:
137
+
138
+ - **`title`** — plain text of the first depth-1 heading (`# …`). Only `h1` is considered; `h2`–`h6` are ignored.
139
+ - **`description`** — plain text of the first paragraph, truncated to 160 characters (with `…` appended). Inline formatting is stripped.
140
+
141
+ Frontmatter values always win — these fallbacks only fill the gaps. JSON files do not receive fallbacks (they have no Markdown body to parse from).
142
+
143
+ ```md
144
+ # Hello World
145
+
146
+ This becomes the description because no description key is in frontmatter.
147
+ ```
148
+
149
+ Results in `title: 'Hello World'` and `description: 'This becomes the description because no description key is in frontmatter.'`.
150
+
134
151
  ### Date-prefixed filenames
135
152
 
136
153
  Filenames starting with `YYYY-MM-DD-` have the date prefix stripped when computing the content path:
@@ -2,10 +2,11 @@
2
2
  * Content layer e2e tests — exercises queryContent() and useContentSearch().
3
3
  *
4
4
  * Pages under test:
5
- * /content-index — queryContent().find() (all content)
6
- * /content-blog — queryContent('/blog').find() (blog prefix, draft exclusion)
7
- * /content-doc — queryContent('/docs/getting-started').first() (body + TOC)
8
- * /content-search — useContentSearch() (MiniSearch, client-side)
5
+ * /content-index — queryContent().find() (all content)
6
+ * /content-blog — queryContent('/blog').find() (blog prefix, draft exclusion)
7
+ * /content-doc — queryContent('/docs/getting-started').first() (body + TOC)
8
+ * /content-search — useContentSearch() (MiniSearch, client-side)
9
+ * /content-fallback — title/description derived from body when frontmatter omits them
9
10
  */
10
11
 
11
12
  const mode = Cypress.env('mode') as 'spa' | 'ssr' | 'ssg'
@@ -226,3 +227,65 @@ describe('Content search — useContentSearch()', () => {
226
227
  cy.get('[data-cy=content-search-result]').should('not.exist')
227
228
  })
228
229
  })
230
+
231
+ // ─── /content-fallback ────────────────────────────────────────────────────────
232
+
233
+ describe('Content fallback — title and description derived from body', () => {
234
+ // blog/no-frontmatter.md has no frontmatter at all.
235
+ // Expected derived values:
236
+ // title → "Derived From Body" (first h1)
237
+ // description → "This description is…" (first paragraph, ≤160 chars)
238
+
239
+ if (mode !== 'spa') {
240
+ it('pre-renders the derived title in initial HTML (SSR/SSG)', () => {
241
+ cy.request('/content-fallback').then((response) => {
242
+ expect(response.body).to.include('Derived From Body')
243
+ })
244
+ })
245
+
246
+ it('pre-renders the derived description in initial HTML (SSR/SSG)', () => {
247
+ cy.request('/content-fallback').then((response) => {
248
+ expect(response.body).to.include('This description is derived from the first paragraph')
249
+ })
250
+ })
251
+ }
252
+
253
+ it('renders the derived title after hydration', () => {
254
+ cy.visit('/content-fallback')
255
+ cy.get('[data-cy=content-fallback-title]', { timeout: 8000 })
256
+ .should('contain', 'Derived From Body')
257
+ })
258
+
259
+ it('renders the derived description after hydration', () => {
260
+ cy.visit('/content-fallback')
261
+ cy.get('[data-cy=content-fallback-desc]', { timeout: 8000 })
262
+ .should('contain', 'This description is derived from the first paragraph')
263
+ })
264
+
265
+ it('document is found — fallback-missing element is absent', () => {
266
+ cy.visit('/content-fallback')
267
+ cy.get('[data-cy=content-fallback-heading]', { timeout: 8000 }).should('exist')
268
+ cy.get('[data-cy=content-fallback-missing]').should('not.exist')
269
+ })
270
+
271
+ it('derived item appears in queryContent find() results (content-index)', () => {
272
+ cy.visit('/content-index')
273
+ cy.get('[data-cy=content-item][data-path="/blog/no-frontmatter"]', { timeout: 8000 })
274
+ .should('exist')
275
+ })
276
+
277
+ it('derived title is shown in the content-index listing', () => {
278
+ cy.visit('/content-index')
279
+ cy.get('[data-cy=content-item][data-path="/blog/no-frontmatter"] [data-cy=content-item-title]', { timeout: 8000 })
280
+ .should('contain', 'Derived From Body')
281
+ })
282
+
283
+ it('derived item appears in search results when searching by derived title', () => {
284
+ cy.intercept('GET', '/_content/search-index.json').as('searchIndex')
285
+ cy.visit('/content-search')
286
+ cy.wait('@searchIndex')
287
+ setSearchQuery('Derived')
288
+ cy.get('[data-cy=content-search-result]', { timeout: 8000 })
289
+ .should('contain', 'Derived From Body')
290
+ })
291
+ })
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Content fallback page — exercises title/description derivation from body
3
+ * when frontmatter does not supply them.
4
+ *
5
+ * Fetches /blog/no-frontmatter (no frontmatter at all) and renders its
6
+ * derived title and description so Cypress can assert both in all modes.
7
+ *
8
+ * Route: /content-fallback
9
+ */
10
+
11
+ component('page-content-fallback', () => {
12
+ useHead({ title: 'Content Fallback — Kitchen Sink' })
13
+
14
+ const ssrData = usePageData<{ doc: ContentItem | null }>()
15
+ const doc = ref<ContentItem | null>(ssrData?.doc ?? null)
16
+
17
+ useOnConnected(async () => {
18
+ if (ssrData) return // already hydrated
19
+ doc.value = await queryContent('/blog/no-frontmatter').first()
20
+ })
21
+
22
+ return html`
23
+ <div>
24
+ <h1 data-cy="content-fallback-heading">Content Fallback</h1>
25
+ ${doc.value ? html`
26
+ <p data-cy="content-fallback-title">${doc.value.title}</p>
27
+ <p data-cy="content-fallback-desc">${doc.value.description}</p>
28
+ ` : html`<p data-cy="content-fallback-missing">Document not found.</p>`}
29
+ </div>
30
+ `
31
+ })
32
+
33
+ export const loader = async () => {
34
+ const doc = await queryContent('/blog/no-frontmatter').first()
35
+ return { doc }
36
+ }
@@ -0,0 +1,7 @@
1
+ # Derived From Body
2
+
3
+ This description is derived from the first paragraph because no frontmatter is present.
4
+
5
+ ## More Content
6
+
7
+ Additional body content below the first paragraph.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jasonshimmy/vite-plugin-cer-app",
3
- "version": "0.20.0",
3
+ "version": "0.20.1",
4
4
  "description": "Nuxt-style meta-framework for @jasonshimmy/custom-elements-runtime",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, beforeAll } from 'vitest'
1
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest'
2
2
  import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'
3
3
  import { tmpdir } from 'node:os'
4
4
  import { join } from 'pathe'
@@ -62,6 +62,62 @@ Just content, no more marker.
62
62
  `)
63
63
 
64
64
  writeFileSync(join(contentDir, 'data', 'products.json'), JSON.stringify([{ id: 1, name: 'Widget' }]))
65
+
66
+ // ── Fallback fixtures ────────────────────────────────────────────────────
67
+ // No frontmatter at all — title and description derived from body
68
+ writeFileSync(join(contentDir, 'no-frontmatter.md'), `# Derived Title
69
+
70
+ First paragraph for description.
71
+
72
+ Second paragraph.
73
+ `)
74
+
75
+ // Frontmatter title only — description derived from body
76
+ writeFileSync(join(contentDir, 'title-only.md'), `---
77
+ title: Explicit Title
78
+ ---
79
+
80
+ Description will come from this paragraph.
81
+ `)
82
+
83
+ // Frontmatter description only — title derived from h1
84
+ writeFileSync(join(contentDir, 'desc-only.md'), `---
85
+ description: Explicit description
86
+ ---
87
+
88
+ # Derived H1 Title
89
+
90
+ Some paragraph.
91
+ `)
92
+
93
+ // Both set in frontmatter — body values must NOT overwrite them
94
+ writeFileSync(join(contentDir, 'both-frontmatter.md'), `---
95
+ title: FM Title
96
+ description: FM Description
97
+ ---
98
+
99
+ # Different H1
100
+
101
+ Different paragraph.
102
+ `)
103
+
104
+ // h1 with inline formatting — plain text only
105
+ writeFileSync(join(contentDir, 'formatted-h1.md'), `# Hello **World**
106
+
107
+ Some intro.
108
+ `)
109
+
110
+ // Long paragraph — description truncated to 160 chars + ellipsis
111
+ writeFileSync(join(contentDir, 'long-para.md'), `# Long
112
+
113
+ ${'A'.repeat(200)}
114
+ `)
115
+
116
+ // No h1, only h2 — title fallback must remain undefined
117
+ writeFileSync(join(contentDir, 'no-h1.md'), `## Section Only
118
+
119
+ A paragraph here.
120
+ `)
65
121
  })
66
122
 
67
123
  function makeFile(filePath: string, ext: 'md' | 'json'): ContentFile {
@@ -237,3 +293,89 @@ describe('parseContentFile — date normalisation', () => {
237
293
  expect(date < '2027-01-01').toBe(true)
238
294
  })
239
295
  })
296
+
297
+ // ─── Fallback title / description ────────────────────────────────────────────
298
+
299
+ describe('parseContentFile — fallback title from h1', () => {
300
+ it('derives title from h1 when frontmatter title is absent', () => {
301
+ const item = parseContentFile(makeFile(join(contentDir, 'no-frontmatter.md'), 'md'), contentDir)
302
+ expect(item.title).toBe('Derived Title')
303
+ })
304
+
305
+ it('frontmatter title is not overwritten when present', () => {
306
+ const item = parseContentFile(makeFile(join(contentDir, 'title-only.md'), 'md'), contentDir)
307
+ expect(item.title).toBe('Explicit Title')
308
+ })
309
+
310
+ it('derives title from h1 when only description is in frontmatter', () => {
311
+ const item = parseContentFile(makeFile(join(contentDir, 'desc-only.md'), 'md'), contentDir)
312
+ expect(item.title).toBe('Derived H1 Title')
313
+ })
314
+
315
+ it('frontmatter title wins over h1 when both set', () => {
316
+ const item = parseContentFile(makeFile(join(contentDir, 'both-frontmatter.md'), 'md'), contentDir)
317
+ expect(item.title).toBe('FM Title')
318
+ })
319
+
320
+ it('strips inline formatting — title is plain text', () => {
321
+ const item = parseContentFile(makeFile(join(contentDir, 'formatted-h1.md'), 'md'), contentDir)
322
+ expect(item.title).toBe('Hello World')
323
+ })
324
+
325
+ it('does not derive title from h2 — must be h1 only', () => {
326
+ const item = parseContentFile(makeFile(join(contentDir, 'no-h1.md'), 'md'), contentDir)
327
+ expect(item.title).toBeUndefined()
328
+ })
329
+ })
330
+
331
+ describe('parseContentFile — fallback description from first paragraph', () => {
332
+ it('derives description from first paragraph when frontmatter description is absent', () => {
333
+ const item = parseContentFile(makeFile(join(contentDir, 'no-frontmatter.md'), 'md'), contentDir)
334
+ expect(item.description).toBe('First paragraph for description.')
335
+ })
336
+
337
+ it('frontmatter description is not overwritten when present', () => {
338
+ const item = parseContentFile(makeFile(join(contentDir, 'desc-only.md'), 'md'), contentDir)
339
+ expect(item.description).toBe('Explicit description')
340
+ })
341
+
342
+ it('derives description from first paragraph when only title is in frontmatter', () => {
343
+ const item = parseContentFile(makeFile(join(contentDir, 'title-only.md'), 'md'), contentDir)
344
+ expect(item.description).toBe('Description will come from this paragraph.')
345
+ })
346
+
347
+ it('frontmatter description wins when both set', () => {
348
+ const item = parseContentFile(makeFile(join(contentDir, 'both-frontmatter.md'), 'md'), contentDir)
349
+ expect(item.description).toBe('FM Description')
350
+ })
351
+
352
+ it('truncates long paragraphs to 160 chars with ellipsis', () => {
353
+ const item = parseContentFile(makeFile(join(contentDir, 'long-para.md'), 'md'), contentDir)
354
+ expect(typeof item.description).toBe('string')
355
+ expect((item.description as string).length).toBeLessThanOrEqual(164) // 160 + '…' (3 bytes)
356
+ expect(item.description as string).toMatch(/…$/)
357
+ })
358
+
359
+ it('derived description is on ContentMeta (present in manifest)', async () => {
360
+ const { toContentMeta } = await import('../../../plugin/content/parser.js')
361
+ const item = parseContentFile(makeFile(join(contentDir, 'no-frontmatter.md'), 'md'), contentDir)
362
+ const meta = toContentMeta(item)
363
+ expect(meta.description).toBe('First paragraph for description.')
364
+ })
365
+ })
366
+
367
+ describe('parseContentFile — JSON fallback', () => {
368
+ it('does not apply title/description fallbacks to JSON files', () => {
369
+ // JSON files have no markdown body to extract from
370
+ const item = parseContentFile(
371
+ makeFile(join(contentDir, 'data', 'products.json'), 'json'),
372
+ contentDir,
373
+ )
374
+ expect(item.title).toBeUndefined()
375
+ expect(item.description).toBeUndefined()
376
+ })
377
+ })
378
+
379
+ afterAll(() => {
380
+ rmSync(tmpDir, { recursive: true, force: true })
381
+ })
@@ -48,6 +48,62 @@ function extractHeadings(tokens: Token[]): ContentHeading[] {
48
48
  return headings
49
49
  }
50
50
 
51
+ // ─── Fallback title / description extraction ─────────────────────────────────
52
+
53
+ /**
54
+ * Converts a list of inline marked tokens to plain text by recursing into
55
+ * formatted tokens (strong, em, link, etc.) and collecting their leaf text.
56
+ * Used to derive readable fallback values from body content.
57
+ */
58
+ function inlineToPlainText(tokens: Token[]): string {
59
+ let result = ''
60
+ for (const t of tokens) {
61
+ const children = (t as { tokens?: Token[] }).tokens
62
+ if (Array.isArray(children) && children.length > 0) {
63
+ result += inlineToPlainText(children)
64
+ } else if (t.type === 'br') {
65
+ result += ' '
66
+ } else if ('text' in t && typeof (t as { text: string }).text === 'string') {
67
+ result += (t as { text: string }).text
68
+ }
69
+ }
70
+ return result
71
+ }
72
+
73
+ const DESCRIPTION_MAX_LEN = 160
74
+
75
+ /**
76
+ * Scans the top-level token list for a fallback `title` (first depth-1 heading)
77
+ * and `description` (first paragraph). Both are `undefined` when no matching
78
+ * token is found.
79
+ *
80
+ * These are applied only when the corresponding frontmatter field is absent, so
81
+ * frontmatter always wins.
82
+ */
83
+ function extractFallbacks(tokens: Token[]): { title?: string; description?: string } {
84
+ let title: string | undefined
85
+ let description: string | undefined
86
+
87
+ for (const token of tokens) {
88
+ if (title === undefined && token.type === 'heading' && token.depth === 1) {
89
+ const text = inlineToPlainText((token as { tokens: Token[] }).tokens ?? []).trim()
90
+ if (text) title = text
91
+ }
92
+ if (description === undefined && token.type === 'paragraph') {
93
+ const text = inlineToPlainText((token as { tokens: Token[] }).tokens ?? []).trim()
94
+ if (text) {
95
+ description =
96
+ text.length > DESCRIPTION_MAX_LEN
97
+ ? text.slice(0, DESCRIPTION_MAX_LEN).trimEnd() + '…'
98
+ : text
99
+ }
100
+ }
101
+ if (title !== undefined && description !== undefined) break
102
+ }
103
+
104
+ return { title, description }
105
+ }
106
+
51
107
  // ─── Custom renderer: add id to heading tags ─────────────────────────────────
52
108
 
53
109
  const renderer = new marked.Renderer()
@@ -129,6 +185,10 @@ function parseContentFileFromRaw(
129
185
  ? (marked.parse(excerptSource, { renderer }) as string)
130
186
  : undefined
131
187
 
188
+ // Derive fallback title / description from body tokens when frontmatter
189
+ // does not provide them. Frontmatter always wins — these only fill gaps.
190
+ const fallbacks = extractFallbacks(tokens)
191
+
132
192
  const item: ContentItem = {
133
193
  ...frontmatter,
134
194
  _path,
@@ -138,6 +198,13 @@ function parseContentFileFromRaw(
138
198
  toc,
139
199
  }
140
200
 
201
+ if (item.title === undefined && fallbacks.title !== undefined) {
202
+ item.title = fallbacks.title
203
+ }
204
+ if (item.description === undefined && fallbacks.description !== undefined) {
205
+ item.description = fallbacks.description
206
+ }
207
+
141
208
  if (excerpt !== undefined) {
142
209
  item.excerpt = excerpt
143
210
  }