@uniweb/build 0.52.5 → 0.52.7
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/package.json +9 -5
- package/src/site/config.js +4 -1
- package/src/site/plugin.js +68 -20
- package/src/uwx/emit-surface.json +0 -3
- package/src/uwx/site-project.js +6 -6
- package/src/uwx/site.js +11 -11
- package/src/uwx/yaml-upsert.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.52.
|
|
3
|
+
"version": "0.52.7",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -57,12 +57,12 @@
|
|
|
57
57
|
"js-yaml": "^4.1.0",
|
|
58
58
|
"sharp": "^0.35.3",
|
|
59
59
|
"yaml": "^2.5.0",
|
|
60
|
-
"@uniweb/projections": "^0.7.3",
|
|
61
|
-
"@uniweb/semantic-parser": "^1.4.1",
|
|
62
|
-
"@uniweb/schemas": "^0.3.1",
|
|
63
60
|
"@uniweb/content-reader": "^1.2.5",
|
|
61
|
+
"@uniweb/semantic-parser": "^1.4.1",
|
|
62
|
+
"@uniweb/schemas": "^0.3.3",
|
|
63
|
+
"@uniweb/theming": "^0.1.15",
|
|
64
64
|
"@uniweb/content-writer": "^0.3.4",
|
|
65
|
-
"@uniweb/
|
|
65
|
+
"@uniweb/projections": "^0.7.3"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
68
|
"@uniweb/runtime": "^0.26.4"
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"@tailwindcss/vite": "^4.0.0",
|
|
75
75
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
76
76
|
"vite-plugin-svgr": "^4.0.0",
|
|
77
|
+
"@uniweb/runtime": "^0.26.4",
|
|
77
78
|
"@uniweb/core": "^0.29.4"
|
|
78
79
|
},
|
|
79
80
|
"peerDependenciesMeta": {
|
|
@@ -97,6 +98,9 @@
|
|
|
97
98
|
},
|
|
98
99
|
"vite-plugin-svgr": {
|
|
99
100
|
"optional": true
|
|
101
|
+
},
|
|
102
|
+
"@uniweb/runtime": {
|
|
103
|
+
"optional": true
|
|
100
104
|
}
|
|
101
105
|
},
|
|
102
106
|
"scripts": {
|
package/src/site/config.js
CHANGED
|
@@ -97,7 +97,10 @@ export function readSiteConfig(siteRoot) {
|
|
|
97
97
|
* @param {Array} [options.plugins] - Additional Vite plugins
|
|
98
98
|
* @param {Object} [options.build] - Vite build options
|
|
99
99
|
* @param {Object} [options.resolve] - Vite resolve options
|
|
100
|
-
* @param {Object} [options.seo] - SEO
|
|
100
|
+
* @param {Object} [options.seo] - SEO OVERRIDES for siteContentPlugin, merged
|
|
101
|
+
* OVER the site's own `site.yml::seo` block. Omit it and `site.yml` decides —
|
|
102
|
+
* a site's `vite.config.js` is scaffolding, so nothing a site GETS may depend
|
|
103
|
+
* on what it passes here.
|
|
101
104
|
* @param {Object} [options.assets] - Asset processing configuration
|
|
102
105
|
* @param {Object} [options.search] - Search index configuration
|
|
103
106
|
* @param {boolean} [options.tailwind] - Include Tailwind CSS v4 Vite plugin (default: true)
|
package/src/site/plugin.js
CHANGED
|
@@ -493,6 +493,51 @@ function escapeHtml(str) {
|
|
|
493
493
|
.replace(/"/g, '"')
|
|
494
494
|
}
|
|
495
495
|
|
|
496
|
+
/**
|
|
497
|
+
* The effective `seo` block for a collected site.
|
|
498
|
+
*
|
|
499
|
+
* ⛔ **`site.yml::seo` IS THE SOURCE. The plugin option is an OVERRIDE.**
|
|
500
|
+
* This plugin read `options.seo` alone until 2026-09-17, and nothing ever put
|
|
501
|
+
* `site.yml`'s block there: `defineSiteConfig` passes its own `seo` option
|
|
502
|
+
* straight through and defaults it to `{}`, while the scaffolded
|
|
503
|
+
* `vite.config.js` calls `defineSiteConfig()` with no arguments. So on the
|
|
504
|
+
* bundle lane `seo.baseUrl` and `seo.robots` were authored, documented
|
|
505
|
+
* (`cli/partials/agents.md`), read by `uniweb doctor` — and dead. The symptom,
|
|
506
|
+
* reported from a real site: `seo.baseUrl` set, doctor's
|
|
507
|
+
* `agents-index-relative-links` warning cleared, and `uniweb build` / `uniweb
|
|
508
|
+
* export` emitting neither `sitemap.xml` nor `robots.txt` while `llms.txt`
|
|
509
|
+
* links stayed root-relative.
|
|
510
|
+
*
|
|
511
|
+
* ⭐ **The data-only path was never wrong** — `build-site-data.js::writeProjections`
|
|
512
|
+
* reads `siteContent.config.seo.baseUrl`. Reading the same place here is what
|
|
513
|
+
* makes the two lanes unable to disagree, which is the whole reason
|
|
514
|
+
* `projectionOptions` exists.
|
|
515
|
+
*
|
|
516
|
+
* ⚠️ The generators were unit-tested throughout (`tests/robots-content-signals.test.js`
|
|
517
|
+
* calls `generateRobotsTxt` directly), which is why a green suite proved
|
|
518
|
+
* nothing: the tests supplied the input the wiring never delivered. That is why
|
|
519
|
+
* this is an exported function rather than a closure — the RESOLUTION is the
|
|
520
|
+
* part that was missing, so it is the part that gets asserted.
|
|
521
|
+
*
|
|
522
|
+
* A site's `vite.config.js` is CLI scaffolding — strip it and the site is
|
|
523
|
+
* unchanged — so a site's SEO must not depend on what it passes. An explicit
|
|
524
|
+
* override still wins, key by key, the way `base` does.
|
|
525
|
+
*
|
|
526
|
+
* @param {Object} [content] - Collected site content (reads `content.config.seo`)
|
|
527
|
+
* @param {Object} [overrides] - Caller overrides, merged OVER the site's block
|
|
528
|
+
* @returns {{ baseUrl: string, defaultImage: ?string, twitterHandle: ?string, locales: Array, robots: Object }}
|
|
529
|
+
*/
|
|
530
|
+
export function resolveEffectiveSeo(content, overrides = {}) {
|
|
531
|
+
const merged = { ...(content?.config?.seo || {}), ...overrides }
|
|
532
|
+
return {
|
|
533
|
+
baseUrl: merged.baseUrl?.replace(/\/$/, '') || '', // Remove trailing slash
|
|
534
|
+
defaultImage: merged.defaultImage || null,
|
|
535
|
+
twitterHandle: merged.twitterHandle || null,
|
|
536
|
+
locales: merged.locales || [],
|
|
537
|
+
robots: merged.robots || {}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
496
541
|
/**
|
|
497
542
|
* Create the site content plugin
|
|
498
543
|
*
|
|
@@ -503,7 +548,10 @@ function escapeHtml(str) {
|
|
|
503
548
|
* @param {boolean} [options.inject=true] - Inject content into HTML
|
|
504
549
|
* @param {string} [options.filename='site-content.json'] - Output filename
|
|
505
550
|
* @param {boolean} [options.watch=true] - Watch for changes in dev mode
|
|
506
|
-
* @param {Object} [options.seo] - SEO
|
|
551
|
+
* @param {Object} [options.seo] - SEO OVERRIDES, merged OVER `site.yml::seo`
|
|
552
|
+
* key by key. The site's own block is the source; this is for a caller that
|
|
553
|
+
* must override it (a preview build pointing at a staging origin, say).
|
|
554
|
+
* ⛔ Do not reach for this to make `site.yml::seo` work — it already does.
|
|
507
555
|
* @param {string} [options.seo.baseUrl] - Base URL for sitemap and canonical URLs
|
|
508
556
|
* @param {string} [options.seo.defaultImage] - Default OG image path
|
|
509
557
|
* @param {string} [options.seo.twitterHandle] - Twitter handle for cards
|
|
@@ -548,15 +596,8 @@ export function siteContentPlugin(options = {}) {
|
|
|
548
596
|
pdfThumbnails: assetsConfig.pdfThumbnails !== false // Default true
|
|
549
597
|
}
|
|
550
598
|
|
|
551
|
-
|
|
552
|
-
const
|
|
553
|
-
const seoOptions = {
|
|
554
|
-
baseUrl: seo.baseUrl?.replace(/\/$/, '') || '', // Remove trailing slash
|
|
555
|
-
defaultImage: seo.defaultImage || null,
|
|
556
|
-
twitterHandle: seo.twitterHandle || null,
|
|
557
|
-
locales: seo.locales || [],
|
|
558
|
-
robots: seo.robots || {}
|
|
559
|
-
}
|
|
599
|
+
/** This plugin's SEO resolution — `site.yml::seo`, with `options.seo` over it. */
|
|
600
|
+
const resolveSeo = content => resolveEffectiveSeo(content, seo)
|
|
560
601
|
|
|
561
602
|
// Warn once per build, not once per collection — the dev server re-collects
|
|
562
603
|
// on every content change and would otherwise repeat this on every keystroke.
|
|
@@ -756,7 +797,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
756
797
|
function projectionOptions(content) {
|
|
757
798
|
const defaultLocale = resolveDefaultLocale(content.config)
|
|
758
799
|
return {
|
|
759
|
-
baseUrl:
|
|
800
|
+
baseUrl: resolveSeo(content).baseUrl,
|
|
760
801
|
basePath,
|
|
761
802
|
locale: content.config?.activeLocale || defaultLocale,
|
|
762
803
|
defaultLocale
|
|
@@ -1185,17 +1226,22 @@ export function siteContentPlugin(options = {}) {
|
|
|
1185
1226
|
// If no translations, fall through to serve default content
|
|
1186
1227
|
}
|
|
1187
1228
|
|
|
1229
|
+
// Dev serves the same two SEO artifacts the build emits, off the same
|
|
1230
|
+
// resolution — so `uniweb dev` and `uniweb build` cannot disagree about
|
|
1231
|
+
// whether a site has them.
|
|
1232
|
+
const devSeo = resolveSeo(siteContent)
|
|
1233
|
+
|
|
1188
1234
|
// Serve sitemap.xml in dev mode
|
|
1189
|
-
if (req.url === '/sitemap.xml' &&
|
|
1235
|
+
if (req.url === '/sitemap.xml' && devSeo.baseUrl && siteContent?.pages) {
|
|
1190
1236
|
res.setHeader('Content-Type', 'application/xml')
|
|
1191
|
-
res.end(generateSitemap(siteContent.pages,
|
|
1237
|
+
res.end(generateSitemap(siteContent.pages, devSeo.baseUrl, filterSeoLocales(devSeo.locales, siteContent.config), siteContent.config?.i18n?.routeTranslations))
|
|
1192
1238
|
return
|
|
1193
1239
|
}
|
|
1194
1240
|
|
|
1195
1241
|
// Serve robots.txt in dev mode
|
|
1196
|
-
if (req.url === '/robots.txt' &&
|
|
1242
|
+
if (req.url === '/robots.txt' && devSeo.baseUrl) {
|
|
1197
1243
|
res.setHeader('Content-Type', 'text/plain')
|
|
1198
|
-
res.end(generateRobotsTxt(
|
|
1244
|
+
res.end(generateRobotsTxt(devSeo.baseUrl, devSeo.robots))
|
|
1199
1245
|
return
|
|
1200
1246
|
}
|
|
1201
1247
|
|
|
@@ -1398,8 +1444,9 @@ export function siteContentPlugin(options = {}) {
|
|
|
1398
1444
|
}
|
|
1399
1445
|
|
|
1400
1446
|
// Inject SEO meta tags
|
|
1401
|
-
|
|
1402
|
-
|
|
1447
|
+
const injectSeo = resolveSeo(contentToInject)
|
|
1448
|
+
if (injectSeo.baseUrl) {
|
|
1449
|
+
const metaTags = generateMetaTags(contentToInject, injectSeo)
|
|
1403
1450
|
if (metaTags) {
|
|
1404
1451
|
headInjection += ` ${metaTags}\n`
|
|
1405
1452
|
}
|
|
@@ -1555,9 +1602,10 @@ export function siteContentPlugin(options = {}) {
|
|
|
1555
1602
|
})
|
|
1556
1603
|
|
|
1557
1604
|
// Generate SEO files if enabled
|
|
1558
|
-
|
|
1605
|
+
const buildSeo = resolveSeo(finalContent)
|
|
1606
|
+
if (buildSeo.baseUrl && finalContent?.pages) {
|
|
1559
1607
|
// Generate sitemap.xml
|
|
1560
|
-
const sitemap = generateSitemap(finalContent.pages,
|
|
1608
|
+
const sitemap = generateSitemap(finalContent.pages, buildSeo.baseUrl, filterSeoLocales(buildSeo.locales, finalContent.config), finalContent.config?.i18n?.routeTranslations)
|
|
1561
1609
|
this.emitFile({
|
|
1562
1610
|
type: 'asset',
|
|
1563
1611
|
fileName: 'sitemap.xml',
|
|
@@ -1566,7 +1614,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
1566
1614
|
console.log('[site-content] Generated sitemap.xml')
|
|
1567
1615
|
|
|
1568
1616
|
// Generate robots.txt
|
|
1569
|
-
const robotsTxt = generateRobotsTxt(
|
|
1617
|
+
const robotsTxt = generateRobotsTxt(buildSeo.baseUrl, buildSeo.robots)
|
|
1570
1618
|
this.emitFile({
|
|
1571
1619
|
type: 'asset',
|
|
1572
1620
|
fileName: 'robots.txt',
|
package/src/uwx/site-project.js
CHANGED
|
@@ -150,15 +150,15 @@ const INFO_TO_SITE_YML = {
|
|
|
150
150
|
// the path an author wrote for an image in the project, which `restoreAssetRefs`
|
|
151
151
|
// has already put back by recognizing the serve URL the push recorded in
|
|
152
152
|
// `assets.json` (asset-map.js). [Diego, 2026-09-10]
|
|
153
|
-
preview: 'preview',
|
|
154
|
-
// ⭐ `url` — where the site is live → `site.yml::$url`. The backend records it at
|
|
155
|
-
// every publish, and pull is its only way onto disk; recorded rather than authored,
|
|
156
|
-
// so it lands under the `$` like `$uuid`.
|
|
157
153
|
//
|
|
158
|
-
// ⚠️
|
|
154
|
+
// ⚠️ It was listed here as "DELIBERATELY ABSENT… BACKEND-STAMPED" until
|
|
159
155
|
// 2026-09-10. Leaving an app-written field out of this map is what lets the next
|
|
160
156
|
// push destroy it, since `info` is replaced whole.
|
|
161
|
-
|
|
157
|
+
preview: 'preview',
|
|
158
|
+
// ⛔ `url` is RETIRED (2026-09-17) and must not come back: where a site went live
|
|
159
|
+
// is a deploy fact, which `publish` records in `deploy.yml`, not site config. A
|
|
160
|
+
// document that still carries `info.url` writes nothing — an unlisted key is
|
|
161
|
+
// ignored — and a `$url` already on disk is left alone and never read.
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
// ── `settings` Section → site.yml ─────────────────────────────────────────────
|
package/src/uwx/site.js
CHANGED
|
@@ -1332,18 +1332,18 @@ export async function siteProjectToDocument(siteRoot, opts = {}) {
|
|
|
1332
1332
|
// fingerprint `assets.json` recorded for it).
|
|
1333
1333
|
// The app leaves an author's value alone.
|
|
1334
1334
|
//
|
|
1335
|
-
// ⚠️
|
|
1336
|
-
//
|
|
1337
|
-
//
|
|
1338
|
-
// is replaced whole.
|
|
1335
|
+
// ⚠️ Until 2026-09-10 framework emitted no preview, on the premise that the host
|
|
1336
|
+
// stamps it. Nothing did, and leaving an app-written field off an allowlist
|
|
1337
|
+
// destroys it on every push, because `info` is replaced whole.
|
|
1339
1338
|
setIf(info, 'preview', scalarString(siteYml.preview))
|
|
1340
|
-
//
|
|
1341
|
-
//
|
|
1342
|
-
//
|
|
1343
|
-
//
|
|
1344
|
-
//
|
|
1345
|
-
// the
|
|
1346
|
-
|
|
1339
|
+
// ⛔ `url` IS RETIRED (2026-09-17) — do not reintroduce it, in either direction.
|
|
1340
|
+
// It carried where the site went live, read from `site.yml::$url` (which pull
|
|
1341
|
+
// wrote), so a site card could link to it. That is a fact about a DEPLOY, not
|
|
1342
|
+
// about the site: `publish` prints it and records it in `deploy.yml`, and a
|
|
1343
|
+
// derived value must not round-trip (the round-trip law, uwx-format.md). The host
|
|
1344
|
+
// no longer declares the field and refuses a push that carries it. A `$url` line
|
|
1345
|
+
// left in an old site.yml is inert — nothing reads it, so nothing sends it.
|
|
1346
|
+
// (uwx-format.md → info.url.)
|
|
1347
1347
|
|
|
1348
1348
|
const ctx = { siteRoot, siteIndex: siteYml.index, sourceLocale, translations }
|
|
1349
1349
|
const pagesPath = siteYml.paths?.pages
|
package/src/uwx/yaml-upsert.js
CHANGED
|
@@ -60,7 +60,7 @@ export function upsertYamlScalar(filePath, key, value) {
|
|
|
60
60
|
* rather than half-removed.
|
|
61
61
|
*
|
|
62
62
|
* @param {string} filePath
|
|
63
|
-
* @param {string} key - a top-level scalar key (e.g.
|
|
63
|
+
* @param {string} key - a top-level scalar key (e.g. `preview`)
|
|
64
64
|
* @returns {boolean} true if the file changed
|
|
65
65
|
*/
|
|
66
66
|
export function removeYamlScalar(filePath, key) {
|