@jasonshimmy/vite-plugin-cer-app 0.19.3 → 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.
Files changed (111) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/commits.txt +1 -1
  3. package/dist/cli/commands/preview.d.ts.map +1 -1
  4. package/dist/cli/commands/preview.js +2 -0
  5. package/dist/cli/commands/preview.js.map +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/plugin/build-ssg.d.ts.map +1 -1
  9. package/dist/plugin/build-ssg.js +11 -0
  10. package/dist/plugin/build-ssg.js.map +1 -1
  11. package/dist/plugin/content/emitter.d.ts +19 -0
  12. package/dist/plugin/content/emitter.d.ts.map +1 -0
  13. package/dist/plugin/content/emitter.js +42 -0
  14. package/dist/plugin/content/emitter.js.map +1 -0
  15. package/dist/plugin/content/index.d.ts +32 -0
  16. package/dist/plugin/content/index.d.ts.map +1 -0
  17. package/dist/plugin/content/index.js +199 -0
  18. package/dist/plugin/content/index.js.map +1 -0
  19. package/dist/plugin/content/parser.d.ts +18 -0
  20. package/dist/plugin/content/parser.d.ts.map +1 -0
  21. package/dist/plugin/content/parser.js +221 -0
  22. package/dist/plugin/content/parser.js.map +1 -0
  23. package/dist/plugin/content/path-utils.d.ts +19 -0
  24. package/dist/plugin/content/path-utils.d.ts.map +1 -0
  25. package/dist/plugin/content/path-utils.js +40 -0
  26. package/dist/plugin/content/path-utils.js.map +1 -0
  27. package/dist/plugin/content/scanner.d.ts +12 -0
  28. package/dist/plugin/content/scanner.d.ts.map +1 -0
  29. package/dist/plugin/content/scanner.js +18 -0
  30. package/dist/plugin/content/scanner.js.map +1 -0
  31. package/dist/plugin/content/search.d.ts +9 -0
  32. package/dist/plugin/content/search.d.ts.map +1 -0
  33. package/dist/plugin/content/search.js +24 -0
  34. package/dist/plugin/content/search.js.map +1 -0
  35. package/dist/plugin/dts-generator.d.ts.map +1 -1
  36. package/dist/plugin/dts-generator.js +10 -1
  37. package/dist/plugin/dts-generator.js.map +1 -1
  38. package/dist/plugin/index.d.ts.map +1 -1
  39. package/dist/plugin/index.js +4 -1
  40. package/dist/plugin/index.js.map +1 -1
  41. package/dist/plugin/transforms/auto-import.d.ts.map +1 -1
  42. package/dist/plugin/transforms/auto-import.js +2 -0
  43. package/dist/plugin/transforms/auto-import.js.map +1 -1
  44. package/dist/runtime/composables/index.d.ts +3 -0
  45. package/dist/runtime/composables/index.d.ts.map +1 -1
  46. package/dist/runtime/composables/index.js +2 -0
  47. package/dist/runtime/composables/index.js.map +1 -1
  48. package/dist/runtime/composables/use-content-search.d.ts +49 -0
  49. package/dist/runtime/composables/use-content-search.d.ts.map +1 -0
  50. package/dist/runtime/composables/use-content-search.js +101 -0
  51. package/dist/runtime/composables/use-content-search.js.map +1 -0
  52. package/dist/runtime/composables/use-content.d.ts +51 -0
  53. package/dist/runtime/composables/use-content.d.ts.map +1 -0
  54. package/dist/runtime/composables/use-content.js +127 -0
  55. package/dist/runtime/composables/use-content.js.map +1 -0
  56. package/dist/runtime/content/client.d.ts +20 -0
  57. package/dist/runtime/content/client.d.ts.map +1 -0
  58. package/dist/runtime/content/client.js +163 -0
  59. package/dist/runtime/content/client.js.map +1 -0
  60. package/dist/types/config.d.ts +2 -0
  61. package/dist/types/config.d.ts.map +1 -1
  62. package/dist/types/config.js.map +1 -1
  63. package/dist/types/content.d.ts +63 -0
  64. package/dist/types/content.d.ts.map +1 -0
  65. package/dist/types/content.js +2 -0
  66. package/dist/types/content.js.map +1 -0
  67. package/docs/composables.md +115 -10
  68. package/docs/configuration.md +33 -0
  69. package/docs/content.md +453 -0
  70. package/e2e/cypress/e2e/content.cy.ts +291 -0
  71. package/e2e/kitchen-sink/app/pages/content-blog.ts +37 -0
  72. package/e2e/kitchen-sink/app/pages/content-doc.ts +42 -0
  73. package/e2e/kitchen-sink/app/pages/content-fallback.ts +36 -0
  74. package/e2e/kitchen-sink/app/pages/content-index.ts +39 -0
  75. package/e2e/kitchen-sink/app/pages/content-search.ts +35 -0
  76. package/e2e/kitchen-sink/cer.config.ts +1 -0
  77. package/e2e/kitchen-sink/content/blog/2026-04-01-hello.md +26 -0
  78. package/e2e/kitchen-sink/content/blog/2026-04-02-draft.md +10 -0
  79. package/e2e/kitchen-sink/content/blog/index.md +8 -0
  80. package/e2e/kitchen-sink/content/blog/no-frontmatter.md +7 -0
  81. package/e2e/kitchen-sink/content/docs/getting-started.md +46 -0
  82. package/e2e/kitchen-sink/content/index.md +16 -0
  83. package/package.json +10 -7
  84. package/src/__tests__/plugin/build-ssg.test.ts +2 -1
  85. package/src/__tests__/plugin/content/emitter.test.ts +117 -0
  86. package/src/__tests__/plugin/content/loader.test.ts +162 -0
  87. package/src/__tests__/plugin/content/parser.test.ts +381 -0
  88. package/src/__tests__/plugin/content/path-utils.test.ts +53 -0
  89. package/src/__tests__/plugin/content/search.test.ts +119 -0
  90. package/src/__tests__/plugin/dts-generator.test.ts +39 -0
  91. package/src/__tests__/plugin/transforms/auto-import.test.ts +14 -0
  92. package/src/__tests__/runtime/use-content-search.test.ts +139 -0
  93. package/src/__tests__/runtime/use-content.test.ts +226 -0
  94. package/src/cli/commands/preview.ts +2 -0
  95. package/src/index.ts +3 -0
  96. package/src/plugin/build-ssg.ts +12 -0
  97. package/src/plugin/content/emitter.ts +50 -0
  98. package/src/plugin/content/index.ts +236 -0
  99. package/src/plugin/content/parser.ts +259 -0
  100. package/src/plugin/content/path-utils.ts +47 -0
  101. package/src/plugin/content/scanner.ts +26 -0
  102. package/src/plugin/content/search.ts +28 -0
  103. package/src/plugin/dts-generator.ts +10 -1
  104. package/src/plugin/index.ts +6 -1
  105. package/src/plugin/transforms/auto-import.ts +2 -0
  106. package/src/runtime/composables/index.ts +3 -0
  107. package/src/runtime/composables/use-content-search.ts +121 -0
  108. package/src/runtime/composables/use-content.ts +146 -0
  109. package/src/runtime/content/client.ts +168 -0
  110. package/src/types/config.ts +2 -0
  111. package/src/types/content.ts +66 -0
@@ -0,0 +1,63 @@
1
+ /** Heading extracted from Markdown during parsing. The `id` is slugified from the heading text and added as an HTML `id` attribute. */
2
+ export interface ContentHeading {
3
+ depth: 1 | 2 | 3 | 4 | 5 | 6;
4
+ /** Slugified heading text — matches the `id` attribute in the rendered body HTML. */
5
+ id: string;
6
+ /** Plain text of the heading. */
7
+ text: string;
8
+ }
9
+ /**
10
+ * Lean per-document metadata — returned by `.find()` and `.count()`.
11
+ * Kept small deliberately: no `body`, no `toc`, no `excerpt`.
12
+ * Use `description` for listing previews; set it in frontmatter.
13
+ */
14
+ export interface ContentMeta {
15
+ /** URL path (e.g. `"/blog/hello"`). */
16
+ _path: string;
17
+ /** Source file type. */
18
+ _type: 'markdown' | 'json';
19
+ title?: string;
20
+ /** Use for listing previews — included in search index. */
21
+ description?: string;
22
+ date?: string;
23
+ draft?: boolean;
24
+ /** Any other frontmatter key. */
25
+ [key: string]: unknown;
26
+ }
27
+ /**
28
+ * Full document — returned by `.first()`.
29
+ * Superset of `ContentMeta`; includes `body`, `toc`, `_file`, and optional `excerpt`.
30
+ */
31
+ export interface ContentItem extends ContentMeta {
32
+ /** Relative path from `content/` at the project root (e.g. `"blog/hello.md"`). */
33
+ _file: string;
34
+ /** Rendered HTML (Markdown) or the raw file contents (JSON files). */
35
+ body: string;
36
+ /** Extracted headings. Empty array for JSON files. */
37
+ toc: ContentHeading[];
38
+ /** HTML content before `<!-- more -->`. Absent when the marker is not present. */
39
+ excerpt?: string;
40
+ }
41
+ /**
42
+ * Search result item returned by `useContentSearch()`.
43
+ * Contains only the MiniSearch stored fields: `_path`, `title`, `description`.
44
+ */
45
+ export interface ContentSearchResult {
46
+ _path: string;
47
+ title: string;
48
+ description?: string;
49
+ }
50
+ /** Content layer configuration. Controls the directory and draft behaviour. */
51
+ export interface CerContentConfig {
52
+ /**
53
+ * Content directory relative to the project root. Defaults to `'content'`,
54
+ * which resolves to `{root}/content/` — at the same level as `app/`, `server/`, and `public/`.
55
+ */
56
+ dir?: string;
57
+ /**
58
+ * When `true`, draft items (`draft: true` in frontmatter) are included in
59
+ * production builds. Defaults to `false`.
60
+ */
61
+ drafts?: boolean;
62
+ }
63
+ //# sourceMappingURL=content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../src/types/content.ts"],"names":[],"mappings":"AAAA,uIAAuI;AACvI,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC5B,qFAAqF;IACrF,EAAE,EAAE,MAAM,CAAA;IACV,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAA;IACb,wBAAwB;IACxB,KAAK,EAAE,UAAU,GAAG,MAAM,CAAA;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,iCAAiC;IACjC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED;;;GAGG;AACH,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC9C,kFAAkF;IAClF,KAAK,EAAE,MAAM,CAAA;IACb,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAA;IACZ,sDAAsD;IACtD,GAAG,EAAE,cAAc,EAAE,CAAA;IACrB,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,+EAA+E;AAC/E,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.js","sourceRoot":"","sources":["../../src/types/content.ts"],"names":[],"mappings":""}
@@ -145,9 +145,9 @@ component('page-posts', () => {
145
145
  const { data: posts, pending, error } = useFetch<Post[]>('/api/posts')
146
146
 
147
147
  return html`
148
- ${pending.value ? html`<p>Loading…</p>` : ''}
149
- ${error.value ? html`<p>Error: ${error.value.message}</p>` : ''}
150
- <ul>${posts.value?.map(p => html`<li>${p.title}</li>`)}</ul>
148
+ ${when(pending.value, () => html`<p>Loading…</p>`)}
149
+ ${when(!!error.value, () => html`<p>Error: ${error.value!.message}</p>`)}
150
+ <ul>${each(posts.value ?? [], p => html`<li>${p.title}</li>`)}</ul>
151
151
  `
152
152
  })
153
153
  ```
@@ -165,10 +165,10 @@ component('page-nav', () => {
165
165
  const { user, loggedIn, login, logout } = useAuth()
166
166
 
167
167
  return html`
168
- ${loggedIn
169
- ? html`<span>${user?.name}</span><button @click="${logout}">Log out</button>`
170
- : html`<button @click="${() => login('github')}">Log in</button>`
171
- }
168
+ ${match()
169
+ .when(loggedIn, () => html`<span>${user?.name}</span><button @click="${logout}">Log out</button>`)
170
+ .otherwise(() => html`<button @click="${() => login('github')}">Log in</button>`)
171
+ .done()}
172
172
  `
173
173
  })
174
174
  ```
@@ -736,10 +736,10 @@ component('locale-switcher', () => {
736
736
 
737
737
  return html`
738
738
  <nav>
739
- ${locales.map((l) => html`
739
+ ${each(locales, (l) => html`
740
740
  <a
741
- href="${switchLocalePath(l)}"
742
- aria-current="${l === locale ? 'true' : 'false'}"
741
+ :href="${switchLocalePath(l)}"
742
+ :aria-current="${l === locale ? 'true' : 'false'}"
743
743
  >${l.toUpperCase()}</a>
744
744
  `)}
745
745
  </nav>
@@ -798,3 +798,108 @@ If you need it outside auto-imported directories:
798
798
  ```ts
799
799
  import { navigateTo } from '@jasonshimmy/vite-plugin-cer-app/composables'
800
800
  ```
801
+
802
+ ---
803
+
804
+ ### `queryContent(path?)`
805
+
806
+ Queries content items from the file-based content layer. Returns a `QueryBuilder` that can be filtered, sorted, paginated, and terminated with `.find()`, `.first()`, or `.count()`.
807
+
808
+ Requires `content: {}` in `cer.config.ts`. See [content.md](./content.md) for full configuration, type reference, and rendering-mode behavior.
809
+
810
+ ```ts
811
+ // All items
812
+ const all = await queryContent().find()
813
+
814
+ // Blog posts only (path prefix)
815
+ const posts = await queryContent('/blog').sortBy('date', 'desc').find()
816
+
817
+ // Single full document (includes body + TOC)
818
+ const doc = await queryContent('/docs/getting-started').first()
819
+
820
+ // Count
821
+ const total = await queryContent().count()
822
+ ```
823
+
824
+ **QueryBuilder methods:**
825
+
826
+ | Method | Returns | Description |
827
+ |---|---|---|
828
+ | `.where(predicate)` | `QueryBuilder` | Predicate function — `(doc: ContentMeta) => boolean`. |
829
+ | `.sortBy(field, order?)` | `QueryBuilder` | Sort ascending (`'asc'`) or descending (`'desc'`). |
830
+ | `.limit(n)` | `QueryBuilder` | Return at most `n` items. |
831
+ | `.skip(n)` | `QueryBuilder` | Skip the first `n` items (pagination). |
832
+ | `.find()` | `Promise<ContentMeta[]>` | Execute, return lean metadata array. |
833
+ | `.first()` | `Promise<ContentItem \| null>` | Execute, return first full document with body and TOC. |
834
+ | `.count()` | `Promise<number>` | Execute, return count only. |
835
+
836
+ **Usage with a page loader:**
837
+
838
+ ```ts
839
+ component('page-blog', () => {
840
+ const ssrData = usePageData<{ posts: ContentMeta[] }>()
841
+ const posts = ref<ContentMeta[]>(ssrData?.posts ?? [])
842
+
843
+ useOnConnected(async () => {
844
+ if (ssrData) return // hydrated from loader
845
+ posts.value = await queryContent('/blog').find()
846
+ })
847
+
848
+ return html`
849
+ <ul>
850
+ ${each(posts.value, p => html`<li><a :href="${p._path}">${p.title}</a></li>`)}
851
+ </ul>
852
+ `
853
+ })
854
+
855
+ export const loader = async () => {
856
+ const posts = await queryContent('/blog').find()
857
+ return { posts }
858
+ }
859
+ ```
860
+
861
+ If you need it outside auto-imported directories:
862
+
863
+ ```ts
864
+ import { queryContent } from '@jasonshimmy/vite-plugin-cer-app/composables'
865
+ ```
866
+
867
+ ---
868
+
869
+ ### `useContentSearch()`
870
+
871
+ Reactive full-text search over the content layer. Loads the MiniSearch index lazily on first use. Returns `query` and `results` refs that update reactively as the user types.
872
+
873
+ Requires `content: {}` in `cer.config.ts`. See [content.md](./content.md) for full documentation.
874
+
875
+ ```ts
876
+ component('page-search', () => {
877
+ const { query, results } = useContentSearch()
878
+
879
+ return html`
880
+ <input type="search" :model="${query}" placeholder="Search…" />
881
+ <ul>
882
+ ${each(results.value, r => html`
883
+ <li><a :href="${r._path}">${r.title}</a></li>
884
+ `)}
885
+ </ul>
886
+ `
887
+ })
888
+ ```
889
+
890
+ **Return value:**
891
+
892
+ ```ts
893
+ interface UseContentSearchReturn {
894
+ query: Ref<string> // bind with :model
895
+ results: Ref<ContentSearchResult[]> // reactive search results
896
+ }
897
+ ```
898
+
899
+ Search activates when `query.value.length >= 2`. MiniSearch is loaded once and cached for the lifetime of the page. Searched fields are `title` and `description`.
900
+
901
+ If you need it outside auto-imported directories:
902
+
903
+ ```ts
904
+ import { useContentSearch } from '@jasonshimmy/vite-plugin-cer-app/composables'
905
+ ```
@@ -220,6 +220,8 @@ import {
220
220
  useCookie,
221
221
  useSession,
222
222
  useLocale,
223
+ queryContent,
224
+ useContentSearch,
223
225
  defineMiddleware,
224
226
  defineServerMiddleware,
225
227
  navigateTo,
@@ -284,6 +286,37 @@ See [cli.md](./cli.md#cer-app-adapt) for full details.
284
286
 
285
287
  ---
286
288
 
289
+ ## `content` options
290
+
291
+ Enables the file-based content layer. Drop Markdown or JSON files into `content/` at the project root and query them with `queryContent()` or search with `useContentSearch()`.
292
+
293
+ ```ts
294
+ export default defineConfig({
295
+ content: {
296
+ dir: 'content', // default
297
+ drafts: false, // default
298
+ },
299
+ })
300
+ ```
301
+
302
+ ### `content.dir`
303
+
304
+ **Type:** `string`
305
+ **Default:** `'content'`
306
+
307
+ Content directory relative to the project root. The default resolves to `{root}/content/`.
308
+
309
+ ### `content.drafts`
310
+
311
+ **Type:** `boolean`
312
+ **Default:** `false`
313
+
314
+ When `false`, files with `draft: true` in their frontmatter are excluded from the content store and search index in production builds.
315
+
316
+ See [content.md](./content.md) for full documentation.
317
+
318
+ ---
319
+
287
320
  ## `i18n` options
288
321
 
289
322
  Enables locale-aware URL routing and the `useLocale()` composable. No external package is required.