@voltro/cli 0.52.0 → 0.53.0
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 +229 -0
- package/THIRD-PARTY-NOTICES.md +8311 -3318
- package/dist/{apiBuild-CSFI8QGq.js → apiBuild-CaPfoWku.js} +11 -5
- package/dist/apiBuild-DHtLXYx9.js +2 -0
- package/dist/bin.js +1 -1
- package/dist/build-D-OnvNMf.js +843 -0
- package/dist/{checkCommand-COmqc2cB.js → checkCommand-C5elt0tW.js} +6 -6
- package/dist/{checkCommand-2SbqzukH.js → checkCommand-D2ZduVlh.js} +1 -1
- package/dist/codegen-BWpt3VgF.js +2 -0
- package/dist/{codegen-VF479Cnb.js → codegen-FEk8AZHb.js} +2 -2
- package/dist/{codegenCommand-DCdG2JN-.js → codegenCommand-BOiWQ5hz.js} +4 -4
- package/dist/{codemodRunner-r7J9lIa7.js → codemodRunner-BjtB2lq6.js} +109 -11
- package/dist/{commands-Cc_nV8WI.js → commands-DyxAmhP0.js} +30 -30
- package/dist/{dashboardCommand-C-gKvwqh.js → dashboardCommand-BdKTyT13.js} +5 -5
- package/dist/{dataCommand-BgpBHnlB.js → dataCommand-Bab9X7s8.js} +2 -2
- package/dist/{dbCommand-sHedr-NJ.js → dbCommand-06O2finM.js} +277 -236
- package/dist/dbCommand-B1EXBC6f.js +2 -0
- package/dist/{dev-CRHoCEiy.js → dev-C6LGF4iY.js} +3006 -2426
- package/dist/{dev--A3nsxA3.js → dev-GjJWAYo2.js} +1 -1
- package/dist/doctorCommand-B0hX0tdz.js +2 -0
- package/dist/{doctorCommand-DtfJ3FA6.js → doctorCommand-etMkflRc.js} +101 -69
- package/dist/{dormancyCommand-Drn7o0No.js → dormancyCommand-UwZ1AZzB.js} +1 -1
- package/dist/{embeddingsCommand-Z-jO1fWN.js → embeddingsCommand-C70zWHwo.js} +1 -1
- package/dist/{envCommand-D4gCrrTZ.js → envCommand-dSyKvRkM.js} +18 -18
- package/dist/{evolveCommand-CMROeKeA.js → evolveCommand-CG0_ebO5.js} +2 -2
- package/dist/fileConventions-DASGEmj-.js +35 -0
- package/dist/{fileTaxonomy-DvDUV9wq.js → fileTaxonomy-B7uxipWS.js} +55 -55
- package/dist/fontPipeline-LxIHa1vo.js +2 -0
- package/dist/fontPipeline-Tsh8kZfA.js +152 -0
- package/dist/frameworkTableAssembly-C_7Z-rMs.js +2 -0
- package/dist/{frameworkTableAssembly-w-XnLa3q.js → frameworkTableAssembly-DKx3ba3S.js} +5 -5
- package/dist/imagePipeline-B_GVJgm6.js +2 -0
- package/dist/imagePipeline-CBZmjT4i.js +127 -0
- package/dist/index.js +1 -1
- package/dist/{infoCommand-DXM868o_.js → infoCommand-_53iOc_j.js} +1 -1
- package/dist/{inspect-CtL_xTbu.js → inspect-Bd8-9wsi.js} +1 -1
- package/dist/inspect-CuoDInfZ.js +2 -0
- package/dist/{inspectGateHint-BjnFubmH.js → inspectGateHint-4LxkNtrz.js} +1 -1
- package/dist/manifestBuild-C4-J1-m_.js +2 -0
- package/dist/{manifestBuild-CuU1VrSm.js → manifestBuild-Cqgsx2bM.js} +1 -1
- package/dist/{metaCommands-C6RFmF1r.js → metaCommands-Cn2oboG4.js} +8 -2
- package/dist/{migrate-D0F-eTlK.js → migrate-Cko9rswM.js} +2 -2
- package/dist/{pageConvention-CzUiSbtU.js → pageConvention-C938S8oC.js} +1 -1
- package/dist/{privacyCommand-DGdopOI6.js → privacyCommand-DWTQMC6R.js} +2 -2
- package/dist/{probeCommand-C9gazU0H.js → probeCommand-DkGGLknv.js} +83 -24
- package/dist/{renderModeScan-CcH2X1_D.js → renderModeScan-CUbOeOAg.js} +3 -1
- package/dist/{renderProfile-Ck32Fzxr.js → renderProfile-CskIgAfn.js} +2 -2
- package/dist/{runtimeTrace-BPQyCmC5.js → runtimeTrace-c0APJz7E.js} +1 -1
- package/dist/{sdkgen-Se88ifTd.js → sdkgen-BiQCgIEr.js} +1 -1
- package/dist/{serveCommand-DkP3OT0W.js → serveCommand-CueKQgzl.js} +869 -805
- package/dist/serveCommand-DsnrVN3U.js +2 -0
- package/dist/serveEntry.js +1 -1
- package/dist/start-BJzZLbt8.js +3 -0
- package/dist/start-ekPan8BT.js +1510 -0
- package/dist/startEntry.js +1 -1
- package/dist/{staticCommand-BwNEDlSU.js → staticCommand-xlSL-IWk.js} +1 -1
- package/dist/{test-f3amja6a.js → test-BWPQcRoB.js} +1 -1
- package/dist/updateCommand-Bqql_rsQ.js +2 -0
- package/dist/{updateCommand-BMk2e4ky.js → updateCommand-C_8I8Rzo.js} +139 -115
- package/dist/webDev-C7jWJ5dX.js +2 -0
- package/dist/{webDev-BgWL9gKV.js → webDev-oczpugbx.js} +1455 -922
- package/dist/{webhooksCommand-CoIO3jbj.js → webhooksCommand-4SVPDjKg.js} +1 -1
- package/package.json +60 -19
- package/templates/AGENTS.md +5 -3
- package/templates/agent-docs/_index.md +5 -3
- package/templates/agent-docs/_manifest.json +20 -4
- package/templates/agent-docs/ai.md +2 -2
- package/templates/agent-docs/authentication.md +1 -1
- package/templates/agent-docs/cli.md +2 -2
- package/templates/agent-docs/configuration.md +17 -0
- package/templates/agent-docs/data.md +470 -26
- package/templates/agent-docs/database/advancedqueries.md +7 -7
- package/templates/agent-docs/database/columntypes.md +2 -2
- package/templates/agent-docs/database/querying.md +1 -1
- package/templates/agent-docs/database/schema.md +1 -1
- package/templates/agent-docs/database/seedsdialects.md +1 -1
- package/templates/agent-docs/database/transactions.md +3 -3
- package/templates/agent-docs/deployment.md +8 -0
- package/templates/agent-docs/internationalization.md +2 -2
- package/templates/agent-docs/introduction.md +7 -1
- package/templates/agent-docs/local-first-mobile.md +102 -4
- package/templates/agent-docs/observability.md +2 -0
- package/templates/agent-docs/plugins/auth.md +1 -1
- package/templates/agent-docs/plugins/comments.md +142 -0
- package/templates/agent-docs/plugins/notifications.md +47 -4
- package/templates/agent-docs/plugins/presence.md +16 -3
- package/templates/agent-docs/plugins/prometheus.md +1 -1
- package/templates/agent-docs/plugins/queue.md +129 -0
- package/templates/agent-docs/plugins.md +11 -7
- package/templates/agent-docs/reference.md +16 -4
- package/templates/agent-docs/routing.md +527 -3
- package/templates/agent-docs/schema-driven-ui.md +214 -3
- package/templates/agent-docs/security.md +3 -3
- package/templates/agent-docs/whats-new.md +160 -66
- package/templates/apps/api-ai/package.json +6 -6
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/package.json +8 -8
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/package.json +9 -9
- package/templates/apps/api-collab/package.json +8 -8
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-row-history/package.json +8 -8
- package/templates/apps/api-saas/package.json +10 -10
- package/templates/apps/api-saas-starter/package.json +10 -10
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/app.config.ts +26 -2
- package/templates/apps/changelog/content/releases/{0.1.0.mdx → v0-1-0.mdx} +0 -1
- package/templates/apps/changelog/content/releases/{0.2.0.mdx → v0-2-0.mdx} +0 -1
- package/templates/apps/changelog/package.json +8 -8
- package/templates/apps/changelog/src/collections/releases.collection.ts +48 -0
- package/templates/apps/changelog/src/globals.d.ts +1 -1
- package/templates/apps/changelog/src/locales/de.ts +1 -1
- package/templates/apps/changelog/src/locales/en.ts +1 -1
- package/templates/apps/changelog/src/pages/[locale]/[slug]/page.tsx +7 -4
- package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +13 -41
- package/templates/apps/changelog/src/pages/[slug]/page.test.tsx +26 -51
- package/templates/apps/changelog/src/pages/[slug]/page.tsx +21 -19
- package/templates/apps/changelog/src/pages/page.test.tsx +14 -37
- package/templates/apps/changelog/src/pages/page.tsx +18 -12
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +9 -9
- package/templates/apps/frontend-auth/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-cms/package.json +9 -9
- package/templates/apps/frontend-collab/package.json +10 -10
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/content/docs/de/guides/first-page.md +4 -0
- package/templates/apps/frontend-docs/content/docs/de/intro/getting-started.md +4 -0
- package/templates/apps/frontend-docs/content/docs/en/guides/first-page.md +4 -0
- package/templates/apps/frontend-docs/content/docs/en/intro/getting-started.md +4 -0
- package/templates/apps/frontend-docs/package.json +8 -6
- package/templates/apps/frontend-docs/src/collections/docs.collection.ts +21 -0
- package/templates/apps/frontend-docs/src/locales/de.ts +0 -5
- package/templates/apps/frontend-docs/src/locales/en.ts +0 -5
- package/templates/apps/frontend-docs/src/pages/[locale]/docs/[...slug]/page.tsx +17 -9
- package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +10 -5
- package/templates/apps/frontend-docs/src/pages/[locale]/page.tsx +8 -0
- package/templates/apps/frontend-docs/src/pages/docs/[...slug]/page.test.tsx +53 -15
- package/templates/apps/frontend-docs/src/pages/docs/[...slug]/page.tsx +27 -33
- package/templates/apps/frontend-docs/src/pages/page.test.tsx +17 -3
- package/templates/apps/frontend-docs/src/pages/page.tsx +16 -12
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +6 -6
- package/templates/apps/frontend-portal/package.json +8 -8
- package/templates/apps/frontend-saas/package.json +8 -8
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/content/posts/cms-to-ssg.md +12 -0
- package/templates/apps/frontend-static-blog/content/posts/hello-static.md +12 -0
- package/templates/apps/frontend-static-blog/content/posts/islands-not-hydration.md +14 -0
- package/templates/apps/frontend-static-blog/package.json +8 -6
- package/templates/apps/frontend-static-blog/src/collections/posts.collection.ts +27 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/[slug]/page.tsx +7 -4
- package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +7 -1
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug]/page.test.tsx +19 -9
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug]/page.tsx +19 -21
- package/templates/apps/frontend-static-blog/src/pages/page.test.tsx +18 -5
- package/templates/apps/frontend-static-blog/src/pages/page.tsx +21 -14
- package/templates/apps/frontend-status/package.json +8 -8
- package/templates/apps/mobile-app/package.json +4 -4
- package/dist/apiBuild-BYBpL7Pz.js +0 -2
- package/dist/build-CPgcMQug.js +0 -793
- package/dist/codegen-CctkDO-1.js +0 -2
- package/dist/dbCommand-DNb6yeOG.js +0 -2
- package/dist/doctorCommand-CqoWA2p5.js +0 -2
- package/dist/fileConventions-DOqD3lPS.js +0 -34
- package/dist/frameworkTableAssembly-C6ETawPR.js +0 -2
- package/dist/inspect-CuGDYES0.js +0 -2
- package/dist/manifestBuild-CPjhvM62.js +0 -2
- package/dist/serveCommand-DLc-BznW.js +0 -2
- package/dist/start-DfL3fOiN.js +0 -3
- package/dist/start-jw89Xbqy.js +0 -1339
- package/dist/updateCommand-5gFVfK5q.js +0 -2
- package/dist/webDev-CZbTsDcH.js +0 -2
- package/templates/apps/changelog/scripts/generate-rss.mjs +0 -38
- package/templates/apps/changelog/src/lib/releases.ts +0 -21
- package/templates/apps/frontend-static-blog/src/content/posts.ts +0 -64
|
@@ -2,55 +2,50 @@
|
|
|
2
2
|
// `/docs/<path>` URL (and its `/de/docs/<path>` mirror). `getStaticPaths`
|
|
3
3
|
// enumerates the doc tree at build time so every page is pre-rendered.
|
|
4
4
|
//
|
|
5
|
-
// Bilingual
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// [locale] mirror
|
|
9
|
-
//
|
|
5
|
+
// Bilingual via the `docs` CONTENT COLLECTION (content/docs/{en,de}/**):
|
|
6
|
+
// the SLUG is stable across locales — only the /de URL prefix and the
|
|
7
|
+
// locale tree differ. This bare route reads the DEFAULT locale's tree; the
|
|
8
|
+
// [locale] mirror re-exports the component and loads the requested tree
|
|
9
|
+
// (with fallback to English for untranslated entries).
|
|
10
10
|
|
|
11
11
|
import type { ReactNode } from 'react'
|
|
12
12
|
import type { PageMeta } from '@voltro/web'
|
|
13
|
-
import { useParams } from '@voltro/web'
|
|
14
|
-
import { T
|
|
15
|
-
import {
|
|
13
|
+
import { useLoaderData, useParams } from '@voltro/web'
|
|
14
|
+
import { T } from '@voltro/i18n'
|
|
15
|
+
import { getCollection, getEntry, type ContentEntry } from '@voltro/content'
|
|
16
|
+
import { docs, type Doc } from '../../../collections/docs.collection'
|
|
17
|
+
import { getCatalog } from '../../../lib/locale'
|
|
16
18
|
|
|
17
19
|
export const renderMode = 'static' as const
|
|
18
20
|
export const interactive = 'none' as const
|
|
19
21
|
|
|
20
|
-
interface DocEntry {
|
|
21
|
-
readonly slug: string
|
|
22
|
-
readonly titleKey: keyof Messages
|
|
23
|
-
readonly bodyKey: keyof Messages
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// The doc tree — slugs stay identical across locales; the per-locale prose
|
|
27
|
-
// lives in the catalogs. Replace with a loader over a markdown folder / CMS
|
|
28
|
-
// / database query; the shape stays the same.
|
|
29
|
-
export const DOCS: ReadonlyArray<DocEntry> = [
|
|
30
|
-
{ slug: 'intro/getting-started', titleKey: 'docs.getting-started.title', bodyKey: 'docs.getting-started.body' },
|
|
31
|
-
{ slug: 'guides/first-page', titleKey: 'docs.first-page.title', bodyKey: 'docs.first-page.body' },
|
|
32
|
-
]
|
|
33
|
-
|
|
34
22
|
// getStaticPaths returns `{ params: { slug: '<path>' } }` entries — the
|
|
35
23
|
// framework substitutes each into `/docs/[...slug]` to enumerate the URLs
|
|
36
24
|
// to pre-render. The captured slug is the raw `/`-joined path (NOT a string
|
|
37
25
|
// array) so a single deep slug like 'intro/getting-started' renders one file.
|
|
38
26
|
export const getStaticPaths = async (): Promise<Array<{ params: { slug: string } }>> =>
|
|
39
|
-
|
|
27
|
+
(await getCollection(docs.name)).map((d) => ({ params: { slug: d.slug } }))
|
|
28
|
+
|
|
29
|
+
// Loads THIS doc's entry at build time — frontmatter decoded against the
|
|
30
|
+
// collection schema, body rendered to HTML (code fences highlighted
|
|
31
|
+
// server-side). The [locale] mirror passes the URL locale instead.
|
|
32
|
+
export const loader = async ({ params, locale }: {
|
|
33
|
+
readonly params: { readonly slug: string }
|
|
34
|
+
readonly locale?: string
|
|
35
|
+
}): Promise<ContentEntry<Doc> | null> =>
|
|
36
|
+
await getEntry<Doc>(docs.name, params.slug, locale !== undefined ? { locale } : {})
|
|
40
37
|
|
|
41
|
-
// Per-locale, per-doc <title
|
|
42
|
-
// (both threaded in at SSG time) so each variant's head is localised.
|
|
38
|
+
// Per-locale, per-doc <title> from the entry's frontmatter.
|
|
43
39
|
export const meta = ({
|
|
44
40
|
locale,
|
|
45
|
-
|
|
41
|
+
loaderData,
|
|
46
42
|
}: {
|
|
47
43
|
locale: string
|
|
48
|
-
|
|
44
|
+
loaderData: ContentEntry<Doc> | null
|
|
49
45
|
}): PageMeta => {
|
|
50
46
|
const c = getCatalog(locale)
|
|
51
|
-
const doc = DOCS.find((d) => d.slug === params.slug)
|
|
52
47
|
return {
|
|
53
|
-
title:
|
|
48
|
+
title: loaderData?.data.title ?? c['docs.notFound.title'],
|
|
54
49
|
description: c['meta.docs.description'],
|
|
55
50
|
}
|
|
56
51
|
}
|
|
@@ -59,8 +54,7 @@ const code = (chunks: ReactNode): ReactNode => <code>{chunks}</code>
|
|
|
59
54
|
|
|
60
55
|
export default function DocPage(): ReactNode {
|
|
61
56
|
const { slug } = useParams<{ slug: string }>()
|
|
62
|
-
const
|
|
63
|
-
const doc = DOCS.find((d) => d.slug === slug)
|
|
57
|
+
const doc = useLoaderData<ContentEntry<Doc> | null>()
|
|
64
58
|
if (!doc) {
|
|
65
59
|
return (
|
|
66
60
|
<article>
|
|
@@ -75,8 +69,8 @@ export default function DocPage(): ReactNode {
|
|
|
75
69
|
}
|
|
76
70
|
return (
|
|
77
71
|
<article>
|
|
78
|
-
<h1>{
|
|
79
|
-
<
|
|
72
|
+
<h1>{doc.data.title}</h1>
|
|
73
|
+
<div dangerouslySetInnerHTML={{ __html: doc.html ?? '' }} />
|
|
80
74
|
</article>
|
|
81
75
|
)
|
|
82
76
|
}
|
|
@@ -10,15 +10,25 @@
|
|
|
10
10
|
import { afterEach, describe, expect, test, vi } from 'vitest'
|
|
11
11
|
import { act, createElement, type ReactNode } from 'react'
|
|
12
12
|
import { createRoot, type Root } from 'react-dom/client'
|
|
13
|
+
import { dirname, join } from 'node:path'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
import { setContentRoot } from '@voltro/content'
|
|
13
16
|
import en from '../locales/en'
|
|
14
17
|
import de from '../locales/de'
|
|
15
18
|
|
|
19
|
+
// The link list comes from the docs CONTENT COLLECTION — real files.
|
|
20
|
+
setContentRoot(join(dirname(fileURLToPath(import.meta.url)), '..', '..'))
|
|
21
|
+
|
|
16
22
|
const catalogs: Record<string, Record<string, string>> = { en, de }
|
|
17
23
|
let currentLocale = 'en'
|
|
18
24
|
|
|
19
25
|
const fill = (msg: string, values?: Record<string, unknown>): string =>
|
|
20
26
|
values ? msg.replace(/\{(\w+)\}/g, (_, k: string) => String(values[k] ?? `{${k}}`)) : msg
|
|
21
27
|
|
|
28
|
+
vi.mock('@voltro/web', () => ({
|
|
29
|
+
useLoaderData: () => entriesByLocale[currentLocale] ?? entriesByLocale['en'],
|
|
30
|
+
}))
|
|
31
|
+
|
|
22
32
|
vi.mock('@voltro/i18n', () => ({
|
|
23
33
|
// The locale catalogs call these at module top-level; identity mirrors the
|
|
24
34
|
// real (compile-time-only) implementations so importing them doesn't throw.
|
|
@@ -32,7 +42,11 @@ vi.mock('@voltro/i18n', () => ({
|
|
|
32
42
|
fill(catalogs[currentLocale]?.[id] ?? id, values),
|
|
33
43
|
}))
|
|
34
44
|
|
|
35
|
-
const { default: Index, meta, renderMode, interactive } = await import('./page')
|
|
45
|
+
const { default: Index, meta, renderMode, interactive, loader } = await import('./page')
|
|
46
|
+
const entriesByLocale: Record<string, Awaited<ReturnType<typeof loader>>> = {
|
|
47
|
+
en: await loader(),
|
|
48
|
+
de: await loader({ locale: 'de' }),
|
|
49
|
+
}
|
|
36
50
|
|
|
37
51
|
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
38
52
|
|
|
@@ -75,7 +89,7 @@ describe('index — localized render', () => {
|
|
|
75
89
|
test('links to both doc pages with bare (en) URLs and English titles', () => {
|
|
76
90
|
currentLocale = 'en'
|
|
77
91
|
render(createElement(Index))
|
|
78
|
-
expect(hrefs()).toEqual(['/docs/
|
|
92
|
+
expect(hrefs()).toEqual(['/docs/guides/first-page', '/docs/intro/getting-started'])
|
|
79
93
|
expect(container.textContent).toContain('Getting started')
|
|
80
94
|
expect(container.textContent).toContain('Your first page')
|
|
81
95
|
})
|
|
@@ -83,7 +97,7 @@ describe('index — localized render', () => {
|
|
|
83
97
|
test('prefixes the doc links with /de and shows German titles for locale de', () => {
|
|
84
98
|
currentLocale = 'de'
|
|
85
99
|
render(createElement(Index))
|
|
86
|
-
expect(hrefs()).toEqual(['/de/docs/
|
|
100
|
+
expect(hrefs()).toEqual(['/de/docs/guides/first-page', '/de/docs/intro/getting-started'])
|
|
87
101
|
expect(container.textContent).toContain('Erste Schritte')
|
|
88
102
|
expect(container.textContent).toContain('Deine erste Seite')
|
|
89
103
|
})
|
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
// themselves live under `src/pages/docs/[...slug].tsx`.
|
|
3
3
|
//
|
|
4
4
|
// Bilingual (Strategy B): mounted at `/` (default locale) and mirrored at
|
|
5
|
-
// `/de` by src/pages/[locale]/index.tsx. The
|
|
6
|
-
//
|
|
5
|
+
// `/de` by src/pages/[locale]/index.tsx. The link list comes from the `docs`
|
|
6
|
+
// CONTENT COLLECTION — titles are the entries' frontmatter, per locale tree
|
|
7
|
+
// (the /de mirror passes its locale into the loader), so a new .md file
|
|
8
|
+
// appears here without touching this page.
|
|
7
9
|
|
|
8
10
|
import type { ReactNode } from 'react'
|
|
9
11
|
import type { PageMeta } from '@voltro/web'
|
|
12
|
+
import { useLoaderData } from '@voltro/web'
|
|
13
|
+
import { getCollection, type ContentEntry } from '@voltro/content'
|
|
10
14
|
import { T, useLocale } from '@voltro/i18n'
|
|
15
|
+
import { docs, type Doc } from '../collections/docs.collection'
|
|
11
16
|
import { getCatalog, withLocalePrefix } from '../lib/locale'
|
|
12
17
|
|
|
13
18
|
export const renderMode = 'static' as const
|
|
@@ -21,7 +26,11 @@ export const meta = ({ locale }: { locale: string }): PageMeta => {
|
|
|
21
26
|
}
|
|
22
27
|
}
|
|
23
28
|
|
|
29
|
+
export const loader = async ({ locale }: { readonly locale?: string } = {}): Promise<ReadonlyArray<ContentEntry<Doc>>> =>
|
|
30
|
+
await getCollection<Doc>(docs.name, locale !== undefined ? { locale } : {})
|
|
31
|
+
|
|
24
32
|
export default function Index(): ReactNode {
|
|
33
|
+
const entries = useLoaderData<ReadonlyArray<ContentEntry<Doc>>>()
|
|
25
34
|
const locale = useLocale()
|
|
26
35
|
return (
|
|
27
36
|
<main>
|
|
@@ -29,16 +38,11 @@ export default function Index(): ReactNode {
|
|
|
29
38
|
<T id="home.title" values={{ project: '{{capProjectName}}' }} />
|
|
30
39
|
</h1>
|
|
31
40
|
<ul>
|
|
32
|
-
|
|
33
|
-
<
|
|
34
|
-
<
|
|
35
|
-
</
|
|
36
|
-
|
|
37
|
-
<li>
|
|
38
|
-
<a href={withLocalePrefix('/docs/guides/first-page', locale)}>
|
|
39
|
-
<T id="docs.first-page.title" />
|
|
40
|
-
</a>
|
|
41
|
-
</li>
|
|
41
|
+
{entries.map((doc) => (
|
|
42
|
+
<li key={doc.slug}>
|
|
43
|
+
<a href={withLocalePrefix(`/docs/${doc.slug}`, locale)}>{doc.data.title}</a>
|
|
44
|
+
</li>
|
|
45
|
+
))}
|
|
42
46
|
</ul>
|
|
43
47
|
</main>
|
|
44
48
|
)
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/client": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/i18n": "0.
|
|
19
|
-
"@voltro/web": "0.
|
|
15
|
+
"@voltro/cli": "0.53.0",
|
|
16
|
+
"@voltro/client": "0.53.0",
|
|
17
|
+
"@voltro/env": "0.53.0",
|
|
18
|
+
"@voltro/i18n": "0.53.0",
|
|
19
|
+
"@voltro/web": "0.53.0",
|
|
20
20
|
"react": "^19.0.0",
|
|
21
21
|
"react-dom": "^19.0.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/devtools": "0.
|
|
24
|
+
"@voltro/devtools": "0.53.0",
|
|
25
25
|
"@types/react": "^19.0.0",
|
|
26
26
|
"@types/react-dom": "^19.0.0",
|
|
27
27
|
"jsdom": "^30.0.0",
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
"typecheck": "tsc --noEmit"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/client": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/i18n": "0.
|
|
19
|
-
"@voltro/web": "0.
|
|
15
|
+
"@voltro/cli": "0.53.0",
|
|
16
|
+
"@voltro/client": "0.53.0",
|
|
17
|
+
"@voltro/env": "0.53.0",
|
|
18
|
+
"@voltro/i18n": "0.53.0",
|
|
19
|
+
"@voltro/web": "0.53.0",
|
|
20
20
|
"react": "^19.0.0",
|
|
21
21
|
"react-dom": "^19.0.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/devtools": "0.
|
|
24
|
+
"@voltro/devtools": "0.53.0",
|
|
25
25
|
"@types/react": "^19.0.0",
|
|
26
26
|
"@types/react-dom": "^19.0.0",
|
|
27
27
|
"jsdom": "^30.0.0",
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@{{projectName}}/api": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/client": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/i18n": "0.
|
|
20
|
-
"@voltro/ui-shadcn": "0.
|
|
21
|
-
"@voltro/web": "0.
|
|
15
|
+
"@{{projectName}}/api": "0.53.0",
|
|
16
|
+
"@voltro/cli": "0.53.0",
|
|
17
|
+
"@voltro/client": "0.53.0",
|
|
18
|
+
"@voltro/env": "0.53.0",
|
|
19
|
+
"@voltro/i18n": "0.53.0",
|
|
20
|
+
"@voltro/ui-shadcn": "0.53.0",
|
|
21
|
+
"@voltro/web": "0.53.0",
|
|
22
22
|
"react": "^19.0.0",
|
|
23
23
|
"react-dom": "^19.0.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/devtools": "0.
|
|
26
|
+
"@voltro/devtools": "0.53.0",
|
|
27
27
|
"@types/react": "^19.0.0",
|
|
28
28
|
"@types/react-dom": "^19.0.0",
|
|
29
29
|
"jsdom": "^30.0.0",
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@{{projectName}}/api": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/client": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/i18n": "0.
|
|
20
|
-
"@voltro/ui-shadcn": "0.
|
|
21
|
-
"@voltro/web": "0.
|
|
15
|
+
"@{{projectName}}/api": "0.53.0",
|
|
16
|
+
"@voltro/cli": "0.53.0",
|
|
17
|
+
"@voltro/client": "0.53.0",
|
|
18
|
+
"@voltro/env": "0.53.0",
|
|
19
|
+
"@voltro/i18n": "0.53.0",
|
|
20
|
+
"@voltro/ui-shadcn": "0.53.0",
|
|
21
|
+
"@voltro/web": "0.53.0",
|
|
22
22
|
"react": "^19.0.0",
|
|
23
23
|
"react-dom": "^19.0.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/devtools": "0.
|
|
26
|
+
"@voltro/devtools": "0.53.0",
|
|
27
27
|
"@types/react": "^19.0.0",
|
|
28
28
|
"@types/react-dom": "^19.0.0",
|
|
29
29
|
"jsdom": "^30.0.0",
|
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
"typecheck": "tsc --noEmit"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/client": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/i18n": "0.
|
|
19
|
-
"@voltro/ui-shadcn": "0.
|
|
20
|
-
"@voltro/web": "0.
|
|
15
|
+
"@voltro/cli": "0.53.0",
|
|
16
|
+
"@voltro/client": "0.53.0",
|
|
17
|
+
"@voltro/env": "0.53.0",
|
|
18
|
+
"@voltro/i18n": "0.53.0",
|
|
19
|
+
"@voltro/ui-shadcn": "0.53.0",
|
|
20
|
+
"@voltro/web": "0.53.0",
|
|
21
21
|
"react": "^19.0.0",
|
|
22
22
|
"react-dom": "^19.0.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/devtools": "0.
|
|
25
|
+
"@voltro/devtools": "0.53.0",
|
|
26
26
|
"@types/react": "^19.0.0",
|
|
27
27
|
"@types/react-dom": "^19.0.0",
|
|
28
28
|
"jsdom": "^30.0.0",
|
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/client": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/i18n": "0.
|
|
19
|
-
"@voltro/ui-shadcn": "0.
|
|
20
|
-
"@voltro/web": "0.
|
|
15
|
+
"@voltro/cli": "0.53.0",
|
|
16
|
+
"@voltro/client": "0.53.0",
|
|
17
|
+
"@voltro/env": "0.53.0",
|
|
18
|
+
"@voltro/i18n": "0.53.0",
|
|
19
|
+
"@voltro/ui-shadcn": "0.53.0",
|
|
20
|
+
"@voltro/web": "0.53.0",
|
|
21
21
|
"react": "^19.0.0",
|
|
22
22
|
"react-dom": "^19.0.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/devtools": "0.
|
|
25
|
+
"@voltro/devtools": "0.53.0",
|
|
26
26
|
"@types/react": "^19.0.0",
|
|
27
27
|
"@types/react-dom": "^19.0.0",
|
|
28
28
|
"jsdom": "^30.0.0",
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@{{projectName}}/api": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/client": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/i18n": "0.
|
|
20
|
-
"@voltro/ui-shadcn": "0.
|
|
21
|
-
"@voltro/web": "0.
|
|
15
|
+
"@{{projectName}}/api": "0.53.0",
|
|
16
|
+
"@voltro/cli": "0.53.0",
|
|
17
|
+
"@voltro/client": "0.53.0",
|
|
18
|
+
"@voltro/env": "0.53.0",
|
|
19
|
+
"@voltro/i18n": "0.53.0",
|
|
20
|
+
"@voltro/ui-shadcn": "0.53.0",
|
|
21
|
+
"@voltro/web": "0.53.0",
|
|
22
22
|
"react": "^19.0.0",
|
|
23
23
|
"react-dom": "^19.0.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/devtools": "0.
|
|
26
|
+
"@voltro/devtools": "0.53.0",
|
|
27
27
|
"@types/react": "^19.0.0",
|
|
28
28
|
"@types/react-dom": "^19.0.0",
|
|
29
29
|
"jsdom": "^30.0.0",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: From CMS to static pages with getStaticPaths
|
|
3
|
+
date: 2025-02-03
|
|
4
|
+
excerpt: Enumerate your content once; the build emits one pre-rendered page per entry.
|
|
5
|
+
readingMinutes: 3
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
A dynamic route like `src/pages/blog/[slug].tsx` matches infinitely many URLs. To pre-render it statically, the build needs to know WHICH slugs exist — that is what `getStaticPaths` answers.
|
|
9
|
+
|
|
10
|
+
In this template, `getStaticPaths` maps over the `posts` array in `src/content/posts.ts`. Point it at a CMS API, a database query, or a folder of markdown files instead, and the build will pre-render exactly those pages — the Next.js "static generation from a content source" pattern, with no extra config.
|
|
11
|
+
|
|
12
|
+
URLs you do not enumerate simply are not built. If a page ALSO sets `renderMode = 'ssr' | 'isr'`, `voltro start` renders the un-enumerated ones on demand; a pure `'static'` page 404s for unknown slugs, which is exactly what you want for a fixed content set.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Why this blog ships zero JavaScript
|
|
3
|
+
date: 2025-01-15
|
|
4
|
+
excerpt: Every page is pre-rendered at build time, so the browser downloads HTML and nothing else.
|
|
5
|
+
readingMinutes: 3
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
This page was rendered ONCE, when you ran `voltro build`. The result is a flat `dist/blog/hello-static/index.html` that any CDN can serve in single-digit milliseconds.
|
|
9
|
+
|
|
10
|
+
There is no server rendering per request, no React hydration on the list page, no rpc client on the wire. The list page even declares `interactive = 'none'`, which strips every `<script>` the framework would otherwise emit.
|
|
11
|
+
|
|
12
|
+
When you DO need a sprinkle of interactivity — a reading-progress bar, a copy button, a comment box — you reach for an island instead of hydrating the whole tree. Read the next post for how that works.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Islands: hydrate the 5% that moves
|
|
3
|
+
date: 2025-01-22
|
|
4
|
+
excerpt: A static page with one interactive widget should ship one widget of JavaScript — not the whole page.
|
|
5
|
+
readingMinutes: 4
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Look at the top of this page: a thin progress bar tracks how far you have scrolled. That bar is an ISLAND — a single component the framework hydrates inside otherwise-static HTML.
|
|
9
|
+
|
|
10
|
+
This page declares `interactive = 'islands'`, so the framework renders the article once and only attaches React to the `island()`-wrapped component. The surrounding HTML has zero React lifecycle running.
|
|
11
|
+
|
|
12
|
+
Each island picks WHEN it wakes up via a hydrate strategy: `load` (immediately), `idle`, `visible` (on scroll-into-view), or `interaction` (on first click). The progress bar uses `load` because it must track scrolling from the very first frame.
|
|
13
|
+
|
|
14
|
+
The lesson: don't default to `islands` because it sounds fast. Use it when most of the page is static and only a few widgets move. A dashboard where everything is interactive wants `full` hydration instead.
|
|
@@ -12,16 +12,18 @@
|
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@voltro/
|
|
16
|
-
"
|
|
17
|
-
"@voltro/
|
|
18
|
-
"@voltro/
|
|
19
|
-
"@voltro/
|
|
15
|
+
"@voltro/content": "0.53.0",
|
|
16
|
+
"effect": "^3.22.0",
|
|
17
|
+
"@voltro/cli": "0.53.0",
|
|
18
|
+
"@voltro/client": "0.53.0",
|
|
19
|
+
"@voltro/env": "0.53.0",
|
|
20
|
+
"@voltro/i18n": "0.53.0",
|
|
21
|
+
"@voltro/web": "0.53.0",
|
|
20
22
|
"react": "^19.0.0",
|
|
21
23
|
"react-dom": "^19.0.0"
|
|
22
24
|
},
|
|
23
25
|
"devDependencies": {
|
|
24
|
-
"@voltro/devtools": "0.
|
|
26
|
+
"@voltro/devtools": "0.53.0",
|
|
25
27
|
"@types/react": "^19.0.0",
|
|
26
28
|
"@types/react-dom": "^19.0.0",
|
|
27
29
|
"jsdom": "^30.0.0",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The blog's content collection — content/posts/**/*.md through the
|
|
2
|
+
// framework's content pipeline (@voltro/content). Frontmatter is
|
|
3
|
+
// schema-checked at build time (a violation FAILS the build naming the file),
|
|
4
|
+
// bodies render to HTML with syntax-highlighted code fences, and no markdown
|
|
5
|
+
// engine ships to the browser: SSG reads the filesystem; SPA navigations
|
|
6
|
+
// fetch the build-emitted JSON artifacts under /assets/content/posts/.
|
|
7
|
+
//
|
|
8
|
+
// Point a CMS/DB-backed site at @voltro/cms instead — files are versioned
|
|
9
|
+
// DEVELOPER content, a CMS is editorial content.
|
|
10
|
+
|
|
11
|
+
import { Schema } from 'effect'
|
|
12
|
+
import { defineCollection } from '@voltro/content'
|
|
13
|
+
|
|
14
|
+
export const posts = defineCollection({
|
|
15
|
+
name: 'posts',
|
|
16
|
+
directory: 'content/posts',
|
|
17
|
+
schema: Schema.Struct({
|
|
18
|
+
title: Schema.String,
|
|
19
|
+
/** ISO date, shown + used for ordering. */
|
|
20
|
+
date: Schema.String,
|
|
21
|
+
/** One-line summary — feeds the list + `<meta name="description">`. */
|
|
22
|
+
excerpt: Schema.String,
|
|
23
|
+
readingMinutes: Schema.Union(Schema.NumberFromString, Schema.Number),
|
|
24
|
+
}),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export type Post = Schema.Schema.Type<typeof posts.schema>
|
|
@@ -7,14 +7,17 @@
|
|
|
7
7
|
// once per non-default locale). `voltro build` produces one HTML file per
|
|
8
8
|
// pair, e.g. /de/blog/hello-static.
|
|
9
9
|
import { SUPPORTED_LOCALES, DEFAULT_LOCALE } from '../../../../lib/locale'
|
|
10
|
-
import {
|
|
10
|
+
import { getCollection } from '@voltro/content'
|
|
11
|
+
import { posts } from '../../../../collections/posts.collection'
|
|
11
12
|
|
|
12
13
|
export { default } from '../../../blog/[slug]/page'
|
|
13
14
|
export { renderMode, interactive, loader, meta } from '../../../blog/[slug]/page'
|
|
14
15
|
|
|
15
16
|
export const getStaticPaths = async (): Promise<
|
|
16
17
|
Array<{ params: { locale: string; slug: string } }>
|
|
17
|
-
> =>
|
|
18
|
-
|
|
18
|
+
> => {
|
|
19
|
+
const entries = await getCollection(posts.name)
|
|
20
|
+
return SUPPORTED_LOCALES
|
|
19
21
|
.filter((l) => l !== DEFAULT_LOCALE)
|
|
20
|
-
.flatMap((locale) =>
|
|
22
|
+
.flatMap((locale) => entries.map((post) => ({ params: { locale, slug: post.slug } })))
|
|
23
|
+
}
|
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
|
|
9
9
|
import { describe, expect, test, vi } from 'vitest'
|
|
10
10
|
import type { ComponentType } from 'react'
|
|
11
|
-
import {
|
|
11
|
+
import { dirname, join } from 'node:path'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
import { setContentRoot, getCollection } from '@voltro/content'
|
|
14
|
+
import { posts as postsCollection } from '../../collections/posts.collection'
|
|
15
|
+
|
|
16
|
+
setContentRoot(join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..'))
|
|
17
|
+
const posts = await getCollection(postsCollection.name)
|
|
12
18
|
|
|
13
19
|
// The mirrors transitively import the base pages, which import @voltro/web
|
|
14
20
|
// (island / loader / notFound / useLocation via lib/locale) and @voltro/i18n.
|
|
@@ -10,10 +10,16 @@
|
|
|
10
10
|
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
|
11
11
|
import { act, createElement, type ComponentType, type ReactNode } from 'react'
|
|
12
12
|
import { createRoot, type Root } from 'react-dom/client'
|
|
13
|
-
import {
|
|
13
|
+
import { dirname, join } from 'node:path'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
import { setContentRoot, getCollection, type ContentEntry } from '@voltro/content'
|
|
16
|
+
import { posts as postsCollection, type Post } from '../../../collections/posts.collection'
|
|
14
17
|
import en from '../../../locales/en'
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
// Real content: the collection reads this app's content/posts/*.md.
|
|
20
|
+
setContentRoot(join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..'))
|
|
21
|
+
|
|
22
|
+
const loaderData = vi.fn<() => ContentEntry<Post>>()
|
|
17
23
|
|
|
18
24
|
const fill = (msg: string, values?: Record<string, unknown>): string =>
|
|
19
25
|
values ? msg.replace(/\{(\w+)\}/g, (_, k: string) => String(values[k] ?? `{${k}}`)) : msg
|
|
@@ -48,6 +54,7 @@ vi.mock('@voltro/i18n', () => ({
|
|
|
48
54
|
}))
|
|
49
55
|
|
|
50
56
|
const { default: BlogPost, getStaticPaths, loader, meta } = await import('./page')
|
|
57
|
+
const posts = await getCollection<Post>(postsCollection.name)
|
|
51
58
|
|
|
52
59
|
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
53
60
|
|
|
@@ -76,6 +83,7 @@ afterEach(() => {
|
|
|
76
83
|
describe('blog [slug] — getStaticPaths', () => {
|
|
77
84
|
test('enumerates one path per post', async () => {
|
|
78
85
|
const paths = await getStaticPaths()
|
|
86
|
+
expect(posts.length).toBeGreaterThan(0)
|
|
79
87
|
expect(paths.map((p) => p.params.slug)).toEqual(posts.map((p) => p.slug))
|
|
80
88
|
})
|
|
81
89
|
})
|
|
@@ -84,7 +92,9 @@ describe('blog [slug] — loader', () => {
|
|
|
84
92
|
test('returns the matching post for a known slug', async () => {
|
|
85
93
|
const post = await loader({ params: { slug: 'hello-static' } } as never)
|
|
86
94
|
expect(post.slug).toBe('hello-static')
|
|
87
|
-
expect(post.title).toContain('zero JavaScript')
|
|
95
|
+
expect(post.data.title).toContain('zero JavaScript')
|
|
96
|
+
// The loader returns RENDERED html — the component never sees markdown.
|
|
97
|
+
expect(post.html).toContain('<p>')
|
|
88
98
|
})
|
|
89
99
|
|
|
90
100
|
test('throws notFound for an unknown slug', async () => {
|
|
@@ -98,8 +108,8 @@ describe('blog [slug] — meta', () => {
|
|
|
98
108
|
test('builds a per-post title + description from the loader data', () => {
|
|
99
109
|
const post = posts[0]!
|
|
100
110
|
const m = meta({ loaderData: post })
|
|
101
|
-
expect(m.title).toContain(post.title)
|
|
102
|
-
expect(m.description).toBe(post.excerpt)
|
|
111
|
+
expect(m.title).toContain(post.data.title)
|
|
112
|
+
expect(m.description).toBe(post.data.excerpt)
|
|
103
113
|
})
|
|
104
114
|
})
|
|
105
115
|
|
|
@@ -108,9 +118,9 @@ describe('blog [slug] — render', () => {
|
|
|
108
118
|
const post = posts[1]!
|
|
109
119
|
loaderData.mockReturnValue(post)
|
|
110
120
|
render(createElement(BlogPost))
|
|
111
|
-
expect(container.querySelector('article h1')?.textContent).toBe(post.title)
|
|
112
|
-
expect(container.textContent).toContain(`${post.readingMinutes} min read`)
|
|
113
|
-
|
|
114
|
-
expect(container.querySelectorAll('article p').length).toBeGreaterThanOrEqual(
|
|
121
|
+
expect(container.querySelector('article h1')?.textContent).toBe(post.data.title)
|
|
122
|
+
expect(container.textContent).toContain(`${post.data.readingMinutes} min read`)
|
|
123
|
+
// The body arrives as rendered html paragraphs.
|
|
124
|
+
expect(container.querySelectorAll('article div p').length).toBeGreaterThanOrEqual(2)
|
|
115
125
|
})
|
|
116
126
|
})
|