@uniweb/build 0.4.4 → 0.4.6
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 +4 -4
- package/src/i18n/collections.js +2 -1
- package/src/i18n/extract.js +26 -0
- package/src/i18n/merge.js +54 -0
- package/src/i18n/sync.js +8 -5
- package/src/prerender.js +3 -1
- package/src/site/config.js +30 -2
- package/src/site/plugin.js +28 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"sharp": "^0.33.2"
|
|
51
51
|
},
|
|
52
52
|
"optionalDependencies": {
|
|
53
|
-
"@uniweb/
|
|
54
|
-
"@uniweb/
|
|
53
|
+
"@uniweb/content-reader": "1.1.2",
|
|
54
|
+
"@uniweb/runtime": "0.5.8"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"@tailwindcss/vite": "^4.0.0",
|
|
61
61
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
62
62
|
"vite-plugin-svgr": "^4.0.0",
|
|
63
|
-
"@uniweb/core": "0.3.
|
|
63
|
+
"@uniweb/core": "0.3.9"
|
|
64
64
|
},
|
|
65
65
|
"peerDependenciesMeta": {
|
|
66
66
|
"vite": {
|
package/src/i18n/collections.js
CHANGED
|
@@ -178,7 +178,8 @@ function addUnit(units, source, field, context) {
|
|
|
178
178
|
const hash = computeHash(source)
|
|
179
179
|
|
|
180
180
|
if (units[hash]) {
|
|
181
|
-
const existingContexts = units[hash].contexts
|
|
181
|
+
const existingContexts = units[hash].contexts || []
|
|
182
|
+
units[hash].contexts = existingContexts
|
|
182
183
|
const contextKey = `${context.collection}:${context.item}`
|
|
183
184
|
const exists = existingContexts.some(
|
|
184
185
|
c => `${c.collection}:${c.item}` === contextKey
|
package/src/i18n/extract.js
CHANGED
|
@@ -27,6 +27,27 @@ export function extractTranslatableContent(siteContent) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Extract from 404 page (stored as top-level notFound)
|
|
31
|
+
if (siteContent.notFound) {
|
|
32
|
+
const notFoundPage = siteContent.notFound
|
|
33
|
+
const pageRoute = notFoundPage.route || '/404'
|
|
34
|
+
extractFromPageMeta(notFoundPage, pageRoute, units)
|
|
35
|
+
for (const section of notFoundPage.sections || []) {
|
|
36
|
+
extractFromSection(section, pageRoute, units)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Extract from shared layout pages (header, footer, left, right panels)
|
|
41
|
+
for (const layoutKey of ['header', 'footer', 'left', 'right']) {
|
|
42
|
+
const layoutPage = siteContent[layoutKey]
|
|
43
|
+
if (layoutPage?.sections) {
|
|
44
|
+
const pageRoute = layoutPage.route || `/@${layoutKey}`
|
|
45
|
+
for (const section of layoutPage.sections) {
|
|
46
|
+
extractFromSection(section, pageRoute, units)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
30
51
|
return {
|
|
31
52
|
version: '1.0',
|
|
32
53
|
defaultLocale: siteContent.config?.defaultLanguage || 'en',
|
|
@@ -50,6 +71,11 @@ function extractFromPageMeta(page, pageRoute, units) {
|
|
|
50
71
|
addUnit(units, page.title, 'page.title', context)
|
|
51
72
|
}
|
|
52
73
|
|
|
74
|
+
// Page label (short navigation label, distinct from title)
|
|
75
|
+
if (page.label && typeof page.label === 'string') {
|
|
76
|
+
addUnit(units, page.label, 'page.label', context)
|
|
77
|
+
}
|
|
78
|
+
|
|
53
79
|
// Page description
|
|
54
80
|
if (page.description && typeof page.description === 'string') {
|
|
55
81
|
addUnit(units, page.description, 'page.description', context)
|
package/src/i18n/merge.js
CHANGED
|
@@ -67,6 +67,26 @@ function mergeTranslationsSync(siteContent, translations, fallbackToSource) {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// Translate 404 page (stored as top-level notFound)
|
|
71
|
+
if (translated.notFound) {
|
|
72
|
+
const pageRoute = translated.notFound.route || '/404'
|
|
73
|
+
translatePageMeta(translated.notFound, pageRoute, translations, fallbackToSource)
|
|
74
|
+
for (const section of translated.notFound.sections || []) {
|
|
75
|
+
translateSectionSync(section, pageRoute, translations, fallbackToSource)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Translate shared layout sections (header, footer, sidebars)
|
|
80
|
+
for (const layoutKey of ['header', 'footer', 'left', 'right']) {
|
|
81
|
+
const layoutPage = translated[layoutKey]
|
|
82
|
+
if (layoutPage?.sections) {
|
|
83
|
+
const pageRoute = layoutPage.route || `/@${layoutKey}`
|
|
84
|
+
for (const section of layoutPage.sections) {
|
|
85
|
+
translateSectionSync(section, pageRoute, translations, fallbackToSource)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
70
90
|
return translated
|
|
71
91
|
}
|
|
72
92
|
|
|
@@ -95,6 +115,35 @@ async function mergeTranslationsAsync(siteContent, translations, options) {
|
|
|
95
115
|
}
|
|
96
116
|
}
|
|
97
117
|
|
|
118
|
+
// Translate 404 page (stored as top-level notFound)
|
|
119
|
+
if (translated.notFound) {
|
|
120
|
+
const pageRoute = translated.notFound.route || '/404'
|
|
121
|
+
translatePageMeta(translated.notFound, pageRoute, translations, fallbackToSource)
|
|
122
|
+
for (const section of translated.notFound.sections || []) {
|
|
123
|
+
await translateSectionAsync(section, translated.notFound, translations, {
|
|
124
|
+
fallbackToSource,
|
|
125
|
+
locale,
|
|
126
|
+
localesDir
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Translate shared layout sections (header, footer, sidebars)
|
|
132
|
+
for (const layoutKey of ['header', 'footer', 'left', 'right']) {
|
|
133
|
+
const layoutPage = translated[layoutKey]
|
|
134
|
+
if (layoutPage?.sections) {
|
|
135
|
+
// Ensure route is set for context matching (extract uses /@header, etc.)
|
|
136
|
+
if (!layoutPage.route) layoutPage.route = `/@${layoutKey}`
|
|
137
|
+
for (const section of layoutPage.sections) {
|
|
138
|
+
await translateSectionAsync(section, layoutPage, translations, {
|
|
139
|
+
fallbackToSource,
|
|
140
|
+
locale,
|
|
141
|
+
localesDir
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
98
147
|
return translated
|
|
99
148
|
}
|
|
100
149
|
|
|
@@ -109,6 +158,11 @@ function translatePageMeta(page, pageRoute, translations, fallbackToSource) {
|
|
|
109
158
|
page.title = lookupTranslation(page.title, context, translations, fallbackToSource)
|
|
110
159
|
}
|
|
111
160
|
|
|
161
|
+
// Translate label (short navigation label)
|
|
162
|
+
if (page.label && typeof page.label === 'string') {
|
|
163
|
+
page.label = lookupTranslation(page.label, context, translations, fallbackToSource)
|
|
164
|
+
}
|
|
165
|
+
|
|
112
166
|
// Translate description
|
|
113
167
|
if (page.description && typeof page.description === 'string') {
|
|
114
168
|
page.description = lookupTranslation(page.description, context, translations, fallbackToSource)
|
package/src/i18n/sync.js
CHANGED
|
@@ -94,10 +94,12 @@ export function syncManifests(previous, current) {
|
|
|
94
94
|
* Check if two context arrays are equal
|
|
95
95
|
*/
|
|
96
96
|
function contextsEqual(contexts1, contexts2) {
|
|
97
|
-
|
|
97
|
+
const c1 = contexts1 || []
|
|
98
|
+
const c2 = contexts2 || []
|
|
99
|
+
if (c1.length !== c2.length) return false
|
|
98
100
|
|
|
99
|
-
const set1 = new Set(
|
|
100
|
-
const set2 = new Set(
|
|
101
|
+
const set1 = new Set(c1.map(c => `${c.page || c.collection}:${c.section || c.item}`))
|
|
102
|
+
const set2 = new Set(c2.map(c => `${c.page || c.collection}:${c.section || c.item}`))
|
|
101
103
|
|
|
102
104
|
if (set1.size !== set2.size) return false
|
|
103
105
|
for (const key of set1) {
|
|
@@ -115,8 +117,9 @@ function findMatchingContext(currentContexts, previousUnits) {
|
|
|
115
117
|
const contextKey = `${context.page}:${context.section}`
|
|
116
118
|
|
|
117
119
|
for (const [hash, unit] of Object.entries(previousUnits)) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
+
const unitContexts = unit.contexts || []
|
|
121
|
+
const hasContext = unitContexts.some(
|
|
122
|
+
c => `${c.page || c.collection}:${c.section || c.item}` === contextKey
|
|
120
123
|
)
|
|
121
124
|
if (hasContext) {
|
|
122
125
|
return { hash, source: unit.source, contexts: unit.contexts }
|
package/src/prerender.js
CHANGED
|
@@ -570,7 +570,9 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
570
570
|
|
|
571
571
|
for (const page of pages) {
|
|
572
572
|
// Build the output route with locale prefix
|
|
573
|
-
|
|
573
|
+
// For non-default locales, translate route slugs (e.g., /about → /acerca-de)
|
|
574
|
+
const translatedPageRoute = isDefault ? page.route : website.translateRoute(page.route, locale)
|
|
575
|
+
const outputRoute = routePrefix + translatedPageRoute
|
|
574
576
|
|
|
575
577
|
onProgress(`Rendering ${outputRoute}...`)
|
|
576
578
|
|
package/src/site/config.js
CHANGED
|
@@ -25,6 +25,34 @@ import { resolve, dirname, join } from 'node:path'
|
|
|
25
25
|
import yaml from 'js-yaml'
|
|
26
26
|
import { generateEntryPoint } from '../generate-entry.js'
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Normalize a base path for Vite compatibility
|
|
30
|
+
*
|
|
31
|
+
* Handles common user mistakes:
|
|
32
|
+
* - Missing leading slash: "docs/" → "/docs/"
|
|
33
|
+
* - Missing trailing slash: "/docs" → "/docs/"
|
|
34
|
+
* - Extra slashes: "//docs///" → "/docs/"
|
|
35
|
+
* - Just a slash: "/" → undefined (root, no base needed)
|
|
36
|
+
*
|
|
37
|
+
* @param {string} raw - Raw base path from site.yml, env, or option
|
|
38
|
+
* @returns {string|undefined} Normalized path with leading+trailing slash, or undefined for root
|
|
39
|
+
*/
|
|
40
|
+
function normalizeBasePath(raw) {
|
|
41
|
+
// Collapse repeated slashes and trim whitespace
|
|
42
|
+
let path = raw.trim().replace(/\/{2,}/g, '/')
|
|
43
|
+
|
|
44
|
+
// Ensure leading slash
|
|
45
|
+
if (!path.startsWith('/')) path = '/' + path
|
|
46
|
+
|
|
47
|
+
// Ensure trailing slash (Vite requirement)
|
|
48
|
+
if (!path.endsWith('/')) path = path + '/'
|
|
49
|
+
|
|
50
|
+
// Root path means no base needed
|
|
51
|
+
if (path === '/') return undefined
|
|
52
|
+
|
|
53
|
+
return path
|
|
54
|
+
}
|
|
55
|
+
|
|
28
56
|
/**
|
|
29
57
|
* Detect foundation type from the foundation config value
|
|
30
58
|
*
|
|
@@ -143,9 +171,9 @@ export async function defineSiteConfig(options = {}) {
|
|
|
143
171
|
const siteConfig = readSiteConfig(siteRoot)
|
|
144
172
|
|
|
145
173
|
// Determine base path for deployment (priority: option > env > site.yml)
|
|
146
|
-
//
|
|
174
|
+
// Normalize: ensure leading slash, collapse repeated slashes, add trailing slash for Vite
|
|
147
175
|
const rawBase = baseOption || process.env.UNIWEB_BASE || siteConfig.base
|
|
148
|
-
const base = rawBase ? (
|
|
176
|
+
const base = rawBase ? normalizeBasePath(String(rawBase)) : undefined
|
|
149
177
|
|
|
150
178
|
// Detect foundation type
|
|
151
179
|
const foundationInfo = detectFoundationType(siteConfig.foundation, siteRoot)
|
package/src/site/plugin.js
CHANGED
|
@@ -117,10 +117,28 @@ async function processDevSectionFetches(sections, cascadedData, fetchOptions) {
|
|
|
117
117
|
import { generateSearchIndex, isSearchEnabled, getSearchIndexFilename } from '../search/index.js'
|
|
118
118
|
import { mergeTranslations } from '../i18n/merge.js'
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Translate a canonical route for a given locale using route translations config
|
|
122
|
+
* Supports exact and prefix matching (e.g., /blog → /noticias also applies to /blog/post)
|
|
123
|
+
*/
|
|
124
|
+
function applyRouteTranslation(route, locale, routeTranslations) {
|
|
125
|
+
const localeMap = routeTranslations?.[locale]
|
|
126
|
+
if (!localeMap) return route
|
|
127
|
+
// Exact match
|
|
128
|
+
if (localeMap[route]) return localeMap[route]
|
|
129
|
+
// Prefix match
|
|
130
|
+
for (const [canonical, translated] of Object.entries(localeMap)) {
|
|
131
|
+
if (route.startsWith(canonical + '/')) {
|
|
132
|
+
return translated + route.slice(canonical.length)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return route
|
|
136
|
+
}
|
|
137
|
+
|
|
120
138
|
/**
|
|
121
139
|
* Generate sitemap.xml content
|
|
122
140
|
*/
|
|
123
|
-
function generateSitemap(pages, baseUrl, locales = []) {
|
|
141
|
+
function generateSitemap(pages, baseUrl, locales = [], routeTranslations = {}) {
|
|
124
142
|
const urls = []
|
|
125
143
|
|
|
126
144
|
for (const page of pages) {
|
|
@@ -137,7 +155,13 @@ function generateSitemap(pages, baseUrl, locales = []) {
|
|
|
137
155
|
// Add hreflang entries for multi-locale sites
|
|
138
156
|
if (locales.length > 1) {
|
|
139
157
|
for (const locale of locales) {
|
|
140
|
-
|
|
158
|
+
let localeLoc
|
|
159
|
+
if (locale.default) {
|
|
160
|
+
localeLoc = loc
|
|
161
|
+
} else {
|
|
162
|
+
const translatedRoute = page.route === '/' ? '' : applyRouteTranslation(page.route, locale.code, routeTranslations)
|
|
163
|
+
localeLoc = `${baseUrl}/${locale.code}${translatedRoute}`
|
|
164
|
+
}
|
|
141
165
|
urlEntry += `\n <xhtml:link rel="alternate" hreflang="${locale.code}" href="${escapeXml(localeLoc)}" />`
|
|
142
166
|
}
|
|
143
167
|
// Add x-default pointing to default locale
|
|
@@ -664,7 +688,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
664
688
|
// Serve sitemap.xml in dev mode
|
|
665
689
|
if (req.url === '/sitemap.xml' && seoEnabled && siteContent?.pages) {
|
|
666
690
|
res.setHeader('Content-Type', 'application/xml')
|
|
667
|
-
res.end(generateSitemap(siteContent.pages, seoOptions.baseUrl, seoOptions.locales))
|
|
691
|
+
res.end(generateSitemap(siteContent.pages, seoOptions.baseUrl, seoOptions.locales, siteContent.config?.i18n?.routeTranslations))
|
|
668
692
|
return
|
|
669
693
|
}
|
|
670
694
|
|
|
@@ -851,7 +875,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
851
875
|
// Generate SEO files if enabled
|
|
852
876
|
if (seoEnabled && finalContent?.pages) {
|
|
853
877
|
// Generate sitemap.xml
|
|
854
|
-
const sitemap = generateSitemap(finalContent.pages, seoOptions.baseUrl, seoOptions.locales)
|
|
878
|
+
const sitemap = generateSitemap(finalContent.pages, seoOptions.baseUrl, seoOptions.locales, finalContent.config?.i18n?.routeTranslations)
|
|
855
879
|
this.emitFile({
|
|
856
880
|
type: 'asset',
|
|
857
881
|
fileName: 'sitemap.xml',
|