@uniweb/build 0.1.32 → 0.2.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.
@@ -28,6 +28,7 @@ import { existsSync } from 'node:fs'
28
28
  import yaml from 'js-yaml'
29
29
  import { collectSectionAssets, mergeAssetCollections } from './assets.js'
30
30
  import { parseFetchConfig, singularize } from './data-fetcher.js'
31
+ import { buildTheme, extractFoundationVars } from '../theme/index.js'
31
32
 
32
33
  // Try to import content-reader, fall back to simplified parser
33
34
  let markdownToProseMirror
@@ -66,6 +67,90 @@ function extractRouteParam(folderName) {
66
67
  return match ? match[1] : null
67
68
  }
68
69
 
70
+ // ─────────────────────────────────────────────────────────────────
71
+ // Version Detection
72
+ // ─────────────────────────────────────────────────────────────────
73
+
74
+ /**
75
+ * Check if a folder name represents a version (e.g., v1, v2, v1.0, v2.1)
76
+ * @param {string} folderName - The folder name to check
77
+ * @returns {boolean}
78
+ */
79
+ function isVersionFolder(folderName) {
80
+ return /^v\d+(\.\d+)?$/.test(folderName)
81
+ }
82
+
83
+ /**
84
+ * Parse version info from folder name
85
+ * @param {string} folderName - The folder name (e.g., "v1", "v2.1")
86
+ * @returns {Object} Version info { id, major, minor, sortKey }
87
+ */
88
+ function parseVersionInfo(folderName) {
89
+ const match = folderName.match(/^v(\d+)(?:\.(\d+))?$/)
90
+ if (!match) return null
91
+
92
+ const major = parseInt(match[1], 10)
93
+ const minor = match[2] ? parseInt(match[2], 10) : 0
94
+
95
+ return {
96
+ id: folderName,
97
+ major,
98
+ minor,
99
+ sortKey: major * 1000 + minor // For sorting: v2.1 > v2.0 > v1.9
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Detect if a set of folders contains version folders
105
+ * @param {Array<string>} folderNames - List of folder names
106
+ * @returns {Array<Object>|null} Sorted version infos (highest first) or null if not versioned
107
+ */
108
+ function detectVersions(folderNames) {
109
+ const versions = folderNames
110
+ .filter(isVersionFolder)
111
+ .map(parseVersionInfo)
112
+ .filter(Boolean)
113
+
114
+ if (versions.length === 0) return null
115
+
116
+ // Sort by version (highest first)
117
+ versions.sort((a, b) => b.sortKey - a.sortKey)
118
+
119
+ return versions
120
+ }
121
+
122
+ /**
123
+ * Build version metadata from detected versions and page.yml config
124
+ * @param {Array<Object>} detectedVersions - Detected version infos
125
+ * @param {Object} pageConfig - page.yml configuration
126
+ * @returns {Object} Version metadata { versions, latestId, scope }
127
+ */
128
+ function buildVersionMetadata(detectedVersions, pageConfig = {}) {
129
+ const configVersions = pageConfig.versions || {}
130
+
131
+ // Build version list with metadata
132
+ const versions = detectedVersions.map((v, index) => {
133
+ const config = configVersions[v.id] || {}
134
+ const isLatest = config.latest === true || (index === 0 && !Object.values(configVersions).some(c => c.latest))
135
+
136
+ return {
137
+ id: v.id,
138
+ label: config.label || v.id,
139
+ latest: isLatest,
140
+ deprecated: config.deprecated || false,
141
+ sortKey: v.sortKey
142
+ }
143
+ })
144
+
145
+ // Find the latest version
146
+ const latestVersion = versions.find(v => v.latest) || versions[0]
147
+
148
+ return {
149
+ versions,
150
+ latestId: latestVersion?.id || null
151
+ }
152
+ }
153
+
69
154
  /**
70
155
  * Parse YAML string using js-yaml
71
156
  */
@@ -143,11 +228,12 @@ function compareFilenames(a, b) {
143
228
  * Process a markdown file into a section
144
229
  *
145
230
  * @param {string} filePath - Path to markdown file
146
- * @param {string} id - Section ID
231
+ * @param {string} id - Section ID (numeric/positional)
147
232
  * @param {string} siteRoot - Site root directory for asset resolution
233
+ * @param {string} defaultStableId - Default stable ID from filename (can be overridden in frontmatter)
148
234
  * @returns {Object} Section data with assets manifest
149
235
  */
150
- async function processMarkdownFile(filePath, id, siteRoot) {
236
+ async function processMarkdownFile(filePath, id, siteRoot, defaultStableId = null) {
151
237
  const content = await readFile(filePath, 'utf8')
152
238
  let frontMatter = {}
153
239
  let markdown = content
@@ -161,19 +247,33 @@ async function processMarkdownFile(filePath, id, siteRoot) {
161
247
  }
162
248
  }
163
249
 
164
- const { type, component, preset, input, props, fetch, ...params } = frontMatter
250
+ const { type, component, preset, input, props, fetch, data, id: frontmatterId, ...params } = frontMatter
165
251
 
166
252
  // Convert markdown to ProseMirror
167
253
  const proseMirrorContent = markdownToProseMirror(markdown)
168
254
 
255
+ // Support 'data:' shorthand for collection fetch
256
+ // data: team → fetch: { collection: team }
257
+ // data: [team, articles] → fetch: { collection: team } (first item, others via inheritData)
258
+ let resolvedFetch = fetch
259
+ if (!fetch && data) {
260
+ const collectionName = Array.isArray(data) ? data[0] : data
261
+ resolvedFetch = { collection: collectionName }
262
+ }
263
+
264
+ // Stable ID for scroll targeting: frontmatter id > filename-derived > null
265
+ // This ID is stable across reordering (unlike the positional id)
266
+ const stableId = frontmatterId || defaultStableId || null
267
+
169
268
  const section = {
170
269
  id,
270
+ stableId,
171
271
  component: type || component || 'Section',
172
272
  preset,
173
273
  input,
174
274
  params: { ...params, ...props },
175
275
  content: proseMirrorContent,
176
- fetch: parseFetchConfig(fetch),
276
+ fetch: parseFetchConfig(resolvedFetch),
177
277
  subsections: []
178
278
  }
179
279
 
@@ -280,7 +380,8 @@ async function processExplicitSections(sectionsConfig, pagePath, siteRoot, paren
280
380
  }
281
381
 
282
382
  // Process the section
283
- const { section, assetCollection: sectionAssets } = await processMarkdownFile(filePath, id, siteRoot)
383
+ // Use sectionName as stable ID for scroll targeting (e.g., "hero", "features")
384
+ const { section, assetCollection: sectionAssets } = await processMarkdownFile(filePath, id, siteRoot, sectionName)
284
385
  assetCollection = mergeAssetCollections(assetCollection, sectionAssets)
285
386
 
286
387
  // Track last modified
@@ -316,9 +417,10 @@ async function processExplicitSections(sectionsConfig, pagePath, siteRoot, paren
316
417
  * @param {boolean} options.isIndex - Whether this page is the index for its parent route
317
418
  * @param {string} options.parentRoute - The parent route (e.g., '/' or '/docs')
318
419
  * @param {Object} options.parentFetch - Parent page's fetch config (for dynamic routes)
420
+ * @param {Object} options.versionContext - Version context from parent { version, versionMeta, scope }
319
421
  * @returns {Object} Page data with assets manifest
320
422
  */
321
- async function processPage(pagePath, pageName, siteRoot, { isIndex = false, parentRoute = '/', parentFetch = null } = {}) {
423
+ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, parentRoute = '/', parentFetch = null, versionContext = null } = {}) {
322
424
  const pageConfig = await readYamlFile(join(pagePath, 'page.yml'))
323
425
 
324
426
  // Note: We no longer skip hidden pages here - they still exist as valid pages,
@@ -344,10 +446,13 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
344
446
  const sections = []
345
447
  for (const file of mdFiles) {
346
448
  const { name } = parse(file)
347
- const { prefix } = parseNumericPrefix(name)
449
+ const { prefix, name: stableName } = parseNumericPrefix(name)
348
450
  const id = prefix || name
451
+ // Use the name part (after prefix) as stable ID for scroll targeting
452
+ // e.g., "1-intro.md" → stableId: "intro", "2-features.md" → stableId: "features"
453
+ const stableId = stableName || name
349
454
 
350
- const { section, assetCollection } = await processMarkdownFile(join(pagePath, file), id, siteRoot)
455
+ const { section, assetCollection } = await processMarkdownFile(join(pagePath, file), id, siteRoot, stableId)
351
456
  sections.push(section)
352
457
  pageAssetCollection = mergeAssetCollections(pageAssetCollection, assetCollection)
353
458
 
@@ -405,6 +510,7 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
405
510
  return {
406
511
  page: {
407
512
  route,
513
+ id: pageConfig.id || null, // Stable page ID for page: links (survives reorganization)
408
514
  isIndex, // Marks this page as the index for its parent route (accessible at parentRoute)
409
515
  title: pageConfig.title || pageName,
410
516
  description: pageConfig.description || '',
@@ -417,6 +523,11 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
417
523
  paramName, // e.g., "slug" from [slug]
418
524
  parentSchema, // e.g., "articles" - the data array to iterate over
419
525
 
526
+ // Version metadata (if within a versioned section)
527
+ version: versionContext?.version || null,
528
+ versionMeta: versionContext?.versionMeta || null,
529
+ versionScope: versionContext?.scope || null,
530
+
420
531
  // Navigation options
421
532
  hidden: pageConfig.hidden || false, // Hide from all navigation
422
533
  hideInHeader: pageConfig.hideInHeader || false, // Hide from header nav
@@ -438,7 +549,13 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
438
549
  },
439
550
 
440
551
  // Data fetching
441
- fetch: parseFetchConfig(pageConfig.fetch),
552
+ // Support 'data:' shorthand at page level
553
+ // data: team → fetch: { collection: team }
554
+ fetch: parseFetchConfig(
555
+ pageConfig.fetch || (pageConfig.data
556
+ ? { collection: Array.isArray(pageConfig.data) ? pageConfig.data[0] : pageConfig.data }
557
+ : undefined)
558
+ ),
442
559
 
443
560
  sections: hierarchicalSections
444
561
  },
@@ -488,9 +605,10 @@ function determineIndexPage(orderConfig, availableFolders) {
488
605
  * @param {string} siteRoot - Site root directory for asset resolution
489
606
  * @param {Object} orderConfig - { pages: [...], index: 'name' } from parent's config
490
607
  * @param {Object} parentFetch - Parent page's fetch config (for dynamic child routes)
491
- * @returns {Promise<Object>} { pages, assetCollection, header, footer, left, right, notFound }
608
+ * @param {Object} versionContext - Version context from parent { version, versionMeta }
609
+ * @returns {Promise<Object>} { pages, assetCollection, header, footer, left, right, notFound, versionedScopes }
492
610
  */
493
- async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig = {}, parentFetch = null) {
611
+ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig = {}, parentFetch = null, versionContext = null) {
494
612
  const entries = await readdir(dirPath)
495
613
  const pages = []
496
614
  let assetCollection = {
@@ -503,6 +621,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
503
621
  let left = null
504
622
  let right = null
505
623
  let notFound = null
624
+ const versionedScopes = new Map() // scope route → versionMeta
506
625
 
507
626
  // First pass: discover all page folders and read their order values
508
627
  const pageFolders = []
@@ -517,6 +636,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
517
636
  name: entry,
518
637
  path: entryPath,
519
638
  order: pageConfig.order,
639
+ pageConfig,
520
640
  childOrderConfig: {
521
641
  pages: pageConfig.pages,
522
642
  index: pageConfig.index
@@ -524,6 +644,72 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
524
644
  })
525
645
  }
526
646
 
647
+ // Check if this directory contains version folders (versioned section)
648
+ const folderNames = pageFolders.map(f => f.name)
649
+ const detectedVersions = detectVersions(folderNames)
650
+
651
+ // If versioned section, handle version folders specially
652
+ if (detectedVersions && !versionContext) {
653
+ // Read parent page.yml for version metadata
654
+ const parentConfig = await readYamlFile(join(dirPath, 'page.yml'))
655
+ const versionMeta = buildVersionMetadata(detectedVersions, parentConfig)
656
+
657
+ // Record this versioned scope
658
+ versionedScopes.set(parentRoute, versionMeta)
659
+
660
+ // Process version folders
661
+ for (const folder of pageFolders) {
662
+ const { name: entry, path: entryPath, childOrderConfig, pageConfig } = folder
663
+
664
+ if (isVersionFolder(entry)) {
665
+ // This is a version folder
666
+ const versionInfo = versionMeta.versions.find(v => v.id === entry)
667
+ const isLatest = versionInfo?.latest || false
668
+
669
+ // For latest version, use parent route directly
670
+ // For other versions, add version prefix to route
671
+ const versionRoute = isLatest ? parentRoute : `${parentRoute}/${entry}`
672
+
673
+ // Recurse into version folder with version context
674
+ const subResult = await collectPagesRecursive(
675
+ entryPath,
676
+ versionRoute,
677
+ siteRoot,
678
+ childOrderConfig,
679
+ parentFetch,
680
+ {
681
+ version: versionInfo,
682
+ versionMeta,
683
+ scope: parentRoute // The route where versioning is scoped
684
+ }
685
+ )
686
+
687
+ pages.push(...subResult.pages)
688
+ assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
689
+ // Merge any nested versioned scopes (shouldn't happen often, but possible)
690
+ for (const [scope, meta] of subResult.versionedScopes) {
691
+ versionedScopes.set(scope, meta)
692
+ }
693
+ } else if (!entry.startsWith('@')) {
694
+ // Non-version, non-special folders in a versioned section
695
+ // These could be shared across versions - process normally
696
+ const result = await processPage(entryPath, entry, siteRoot, {
697
+ isIndex: false,
698
+ parentRoute,
699
+ parentFetch
700
+ })
701
+
702
+ if (result) {
703
+ pages.push(result.page)
704
+ assetCollection = mergeAssetCollections(assetCollection, result.assetCollection)
705
+ }
706
+ }
707
+ }
708
+
709
+ // Return early - we've handled all children
710
+ return { pages, assetCollection, header, footer, left, right, notFound, versionedScopes }
711
+ }
712
+
527
713
  // Determine which page is the index for this level
528
714
  const regularFolders = pageFolders.filter(f => !f.name.startsWith('@'))
529
715
  const indexPageName = determineIndexPage(orderConfig, regularFolders)
@@ -539,7 +725,8 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
539
725
  const result = await processPage(entryPath, entry, siteRoot, {
540
726
  isIndex: isIndex && !isSpecial,
541
727
  parentRoute,
542
- parentFetch
728
+ parentFetch,
729
+ versionContext
543
730
  })
544
731
 
545
732
  if (result) {
@@ -571,34 +758,84 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
571
758
  const childParentRoute = isIndex ? parentRoute : page.route
572
759
  // Pass this page's fetch config to children (for dynamic routes that inherit parent data)
573
760
  const childFetch = page.fetch || parentFetch
574
- const subResult = await collectPagesRecursive(entryPath, childParentRoute, siteRoot, childOrderConfig, childFetch)
761
+ // Pass version context to children (maintains version scope)
762
+ const subResult = await collectPagesRecursive(entryPath, childParentRoute, siteRoot, childOrderConfig, childFetch, versionContext)
575
763
  pages.push(...subResult.pages)
576
764
  assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
765
+ // Merge any versioned scopes from children
766
+ for (const [scope, meta] of subResult.versionedScopes) {
767
+ versionedScopes.set(scope, meta)
768
+ }
577
769
  }
578
770
  }
579
771
  }
580
772
 
581
- return { pages, assetCollection, header, footer, left, right, notFound }
773
+ return { pages, assetCollection, header, footer, left, right, notFound, versionedScopes }
774
+ }
775
+
776
+ /**
777
+ * Load foundation variables from schema.json
778
+ *
779
+ * @param {string} foundationPath - Path to foundation directory
780
+ * @returns {Promise<Object>} Foundation variables or empty object
781
+ */
782
+ async function loadFoundationVars(foundationPath) {
783
+ if (!foundationPath) return {}
784
+
785
+ // Try dist/meta/schema.json first (built foundation), then root schema.json
786
+ const distSchemaPath = join(foundationPath, 'dist', 'meta', 'schema.json')
787
+ const rootSchemaPath = join(foundationPath, 'schema.json')
788
+
789
+ const schemaPath = existsSync(distSchemaPath) ? distSchemaPath : rootSchemaPath
790
+
791
+ if (!existsSync(schemaPath)) {
792
+ return {}
793
+ }
794
+
795
+ try {
796
+ const schemaContent = await readFile(schemaPath, 'utf8')
797
+ const schema = JSON.parse(schemaContent)
798
+ // Foundation config is in _self, support both 'vars' (new) and 'themeVars' (legacy)
799
+ return schema._self?.vars || schema._self?.themeVars || schema.themeVars || {}
800
+ } catch (err) {
801
+ console.warn('[content-collector] Failed to load foundation schema:', err.message)
802
+ return {}
803
+ }
582
804
  }
583
805
 
584
806
  /**
585
807
  * Collect all site content
586
808
  *
587
809
  * @param {string} sitePath - Path to site directory
810
+ * @param {Object} options - Collection options
811
+ * @param {string} options.foundationPath - Path to foundation directory (for theme vars)
588
812
  * @returns {Promise<Object>} Site content object with assets manifest
589
813
  */
590
- export async function collectSiteContent(sitePath) {
814
+ export async function collectSiteContent(sitePath, options = {}) {
815
+ const { foundationPath } = options
591
816
  const pagesPath = join(sitePath, 'pages')
592
817
 
593
- // Read site config
818
+ // Read site config and raw theme config
594
819
  const siteConfig = await readYamlFile(join(sitePath, 'site.yml'))
595
- const themeConfig = await readYamlFile(join(sitePath, 'theme.yml'))
820
+ const rawThemeConfig = await readYamlFile(join(sitePath, 'theme.yml'))
821
+
822
+ // Load foundation vars and process theme
823
+ const foundationVars = await loadFoundationVars(foundationPath)
824
+ const { config: processedTheme, css: themeCSS, warnings } = buildTheme(rawThemeConfig, { foundationVars })
825
+
826
+ // Log theme warnings
827
+ if (warnings?.length > 0) {
828
+ warnings.forEach(w => console.warn(`[theme] ${w}`))
829
+ }
596
830
 
597
831
  // Check if pages directory exists
598
832
  if (!existsSync(pagesPath)) {
599
833
  return {
600
834
  config: siteConfig,
601
- theme: themeConfig,
835
+ theme: {
836
+ ...processedTheme,
837
+ css: themeCSS
838
+ },
602
839
  pages: [],
603
840
  assets: {}
604
841
  }
@@ -611,7 +848,7 @@ export async function collectSiteContent(sitePath) {
611
848
  }
612
849
 
613
850
  // Recursively collect all pages
614
- const { pages, assetCollection, header, footer, left, right, notFound } =
851
+ const { pages, assetCollection, header, footer, left, right, notFound, versionedScopes } =
615
852
  await collectPagesRecursive(pagesPath, '/', sitePath, siteOrderConfig)
616
853
 
617
854
  // Sort pages by order
@@ -624,18 +861,26 @@ export async function collectSiteContent(sitePath) {
624
861
  console.log(`[content-collector] Found ${assetCount} asset references${explicitCount > 0 ? ` (${explicitCount} with explicit poster/preview)` : ''}`)
625
862
  }
626
863
 
864
+ // Convert versionedScopes Map to plain object for JSON serialization
865
+ const versionedScopesObj = Object.fromEntries(versionedScopes)
866
+
627
867
  return {
628
868
  config: {
629
869
  ...siteConfig,
630
870
  fetch: parseFetchConfig(siteConfig.fetch),
631
871
  },
632
- theme: themeConfig,
872
+ theme: {
873
+ ...processedTheme,
874
+ css: themeCSS
875
+ },
633
876
  pages,
634
877
  header,
635
878
  footer,
636
879
  left,
637
880
  right,
638
881
  notFound,
882
+ // Versioned scopes: route → { versions, latestId }
883
+ versionedScopes: versionedScopesObj,
639
884
  assets: assetCollection.assets,
640
885
  hasExplicitPoster: assetCollection.hasExplicitPoster,
641
886
  hasExplicitPreview: assetCollection.hasExplicitPreview
@@ -305,6 +305,7 @@ function escapeHtml(str) {
305
305
  * @param {Object} [options.search] - Search index configuration
306
306
  * @param {boolean} [options.search.enabled=true] - Generate search index (uses site.yml config by default)
307
307
  * @param {string} [options.search.filename='search-index.json'] - Search index filename
308
+ * @param {string} [options.foundationPath] - Path to foundation directory (for loading theme vars)
308
309
  */
309
310
  export function siteContentPlugin(options = {}) {
310
311
  const {
@@ -316,7 +317,8 @@ export function siteContentPlugin(options = {}) {
316
317
  watch: shouldWatch = true,
317
318
  seo = {},
318
319
  assets: assetsConfig = {},
319
- search: searchPluginConfig = {}
320
+ search: searchPluginConfig = {},
321
+ foundationPath
320
322
  } = options
321
323
 
322
324
  // Extract asset processing options
@@ -416,7 +418,7 @@ export function siteContentPlugin(options = {}) {
416
418
  if (!isProduction) {
417
419
  try {
418
420
  // Do an early content collection to get the collections config
419
- const earlyContent = await collectSiteContent(resolvedSitePath)
421
+ const earlyContent = await collectSiteContent(resolvedSitePath, { foundationPath })
420
422
  collectionsConfig = earlyContent.config?.collections
421
423
 
422
424
  if (collectionsConfig) {
@@ -433,7 +435,7 @@ export function siteContentPlugin(options = {}) {
433
435
  async buildStart() {
434
436
  // Collect content at build start
435
437
  try {
436
- siteContent = await collectSiteContent(resolvedSitePath)
438
+ siteContent = await collectSiteContent(resolvedSitePath, { foundationPath })
437
439
  console.log(`[site-content] Collected ${siteContent.pages?.length || 0} pages`)
438
440
 
439
441
  // Process content collections if defined in site.yml
@@ -480,7 +482,7 @@ export function siteContentPlugin(options = {}) {
480
482
  rebuildTimeout = setTimeout(async () => {
481
483
  console.log('[site-content] Content changed, rebuilding...')
482
484
  try {
483
- siteContent = await collectSiteContent(resolvedSitePath)
485
+ siteContent = await collectSiteContent(resolvedSitePath, { foundationPath })
484
486
  // Execute fetches for the updated content
485
487
  await executeDevFetches(siteContent, resolvedSitePath)
486
488
  console.log(`[site-content] Rebuilt ${siteContent.pages?.length || 0} pages`)
@@ -685,6 +687,11 @@ export function siteContentPlugin(options = {}) {
685
687
 
686
688
  let headInjection = ''
687
689
 
690
+ // Inject theme CSS
691
+ if (contentToInject.theme?.css) {
692
+ headInjection += ` <style id="uniweb-theme">\n${contentToInject.theme.css}\n </style>\n`
693
+ }
694
+
688
695
  // Inject SEO meta tags
689
696
  if (seoEnabled) {
690
697
  const metaTags = generateMetaTags(contentToInject, seoOptions)
@@ -791,6 +798,9 @@ export function siteContentPlugin(options = {}) {
791
798
  delete finalContent.hasExplicitPoster
792
799
  delete finalContent.hasExplicitPreview
793
800
 
801
+ // Note: theme.css is kept here so prerender can inject it into HTML
802
+ // Prerender will strip it from the JSON it injects into each page
803
+
794
804
  // Emit content as JSON file in production build
795
805
  this.emitFile({
796
806
  type: 'asset',