uniweb 1.21.19 → 1.21.21
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 +5 -5
- package/partials/agents.md +2 -0
- package/src/commands/doctor.js +88 -1
- package/src/commands/publish.js +6 -1
- package/src/commands/push.js +14 -8
- package/src/framework-index.json +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.21",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,15 +41,15 @@
|
|
|
41
41
|
"js-yaml": "^4.3.2",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.5.21",
|
|
44
|
+
"@uniweb/content-writer": "~1.3.1",
|
|
44
45
|
"@uniweb/schemas": "~1.6.5",
|
|
45
46
|
"@uniweb/core": "~1.4.12",
|
|
46
|
-
"@uniweb/semantic-parser": "~1.13.0"
|
|
47
|
-
"@uniweb/content-writer": "~1.3.1"
|
|
47
|
+
"@uniweb/semantic-parser": "~1.13.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@uniweb/
|
|
50
|
+
"@uniweb/build": "~1.10.13",
|
|
51
51
|
"@uniweb/content-reader": "~1.5.7",
|
|
52
|
-
"@uniweb/
|
|
52
|
+
"@uniweb/semantic-parser": "~1.13.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
55
55
|
"@uniweb/build": {
|
package/partials/agents.md
CHANGED
|
@@ -733,6 +733,8 @@ end the list with `...`.**
|
|
|
733
733
|
|
|
734
734
|
**The tab icon:** `favicon: /favicon.svg` in `site.yml`, a file in `public/`. Name it even when the file is `public/favicon.svg`: a static build finds `public/favicon.{svg,ico,png}` on its own, but a site published with `uniweb publish` links only the favicon `site.yml` names (`uniweb doctor --fix favicon-unnamed` writes the line).
|
|
735
735
|
|
|
736
|
+
**The home-screen icon:** `touchIcon: /touch-icon.png` in `site.yml` — the icon iOS puts on a home screen, which an SVG favicon never is. Make it a 180×180 PNG in `public/` with no transparency: iOS draws a transparent pixel black. Name it even when the file is `public/apple-touch-icon.png`, which iOS finds on a static host by itself: a site published with `uniweb publish` has only the touch icon `site.yml` names. `uniweb doctor` checks the file.
|
|
737
|
+
|
|
736
738
|
```yaml
|
|
737
739
|
# site.yml
|
|
738
740
|
keywords: [components, react, cms]
|
package/src/commands/doctor.js
CHANGED
|
@@ -1307,6 +1307,92 @@ export async function checkFavicon({ sitePath, siteName, siteYml, issues, should
|
|
|
1307
1307
|
log(` Add ${colors.green}favicon: ${value}${colors.reset} to site.yml — ${colors.dim}uniweb doctor --fix ${id}${colors.reset}`)
|
|
1308
1308
|
}
|
|
1309
1309
|
|
|
1310
|
+
// The icon iOS puts on a home screen, from Apple's guidance (the archived Safari Web Content Guide,
|
|
1311
|
+
// read 2026-10-27): a PNG — an SVG favicon is not used — 180×180 for the largest iPhone screens,
|
|
1312
|
+
// with no transparency, which iOS draws black. A page that links none, iOS asks the site's root for
|
|
1313
|
+
// `/apple-touch-icon.png`.
|
|
1314
|
+
const TOUCH_ICON_SIZE = 180
|
|
1315
|
+
const TOUCH_ICON_FILE = 'apple-touch-icon.png'
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* The touch icon: `site.yml`'s `touchIcon:`. A static build links the one it names, and a push
|
|
1319
|
+
* sends that file and no other, so a Uniweb host has none the site does not name — not even the
|
|
1320
|
+
* `/apple-touch-icon.png` iOS asks a static host for. A named one is measured: a PNG, square, at
|
|
1321
|
+
* least 180×180, every pixel opaque.
|
|
1322
|
+
*/
|
|
1323
|
+
export async function checkTouchIcon({ sitePath, siteName, siteYml, issues, shouldFix, fixed: reportFixed }) {
|
|
1324
|
+
const named = typeof siteYml?.touchIcon === 'string' ? siteYml.touchIcon.trim() : ''
|
|
1325
|
+
if (!named) {
|
|
1326
|
+
if (!existsSync(join(sitePath, 'public', TOUCH_ICON_FILE))) {
|
|
1327
|
+
const id = 'touch-icon-none'
|
|
1328
|
+
issues.push({ id, type: 'info', site: siteName, message: 'site.yml names no touch icon, the icon iOS puts on a home screen' })
|
|
1329
|
+
info(`[${id}] site.yml names no touch icon — the icon iOS puts on a home screen and Safari shows on its start page`)
|
|
1330
|
+
log(` Add a ${TOUCH_ICON_SIZE}×${TOUCH_ICON_SIZE} PNG with no transparency to public/ and name it: ${colors.green}touchIcon: /touch-icon.png${colors.reset}`)
|
|
1331
|
+
return
|
|
1332
|
+
}
|
|
1333
|
+
const id = 'touch-icon-unnamed'
|
|
1334
|
+
const value = `/${TOUCH_ICON_FILE}`
|
|
1335
|
+
const issue = {
|
|
1336
|
+
id,
|
|
1337
|
+
type: 'info',
|
|
1338
|
+
site: siteName,
|
|
1339
|
+
message: `public/${TOUCH_ICON_FILE} is not named in site.yml, so a Uniweb host has no touch icon`,
|
|
1340
|
+
fixable: true
|
|
1341
|
+
}
|
|
1342
|
+
issues.push(issue)
|
|
1343
|
+
info(`[${id}] public/${TOUCH_ICON_FILE} is not named in site.yml — iOS finds it on a static host, but a Uniweb host is never sent it`)
|
|
1344
|
+
if (shouldFix?.(id)) {
|
|
1345
|
+
try {
|
|
1346
|
+
writeSiteConfig(sitePath, { touchIcon: value })
|
|
1347
|
+
issue.fixed = true
|
|
1348
|
+
reportFixed?.(`site.yml now names touchIcon: ${value}`)
|
|
1349
|
+
return
|
|
1350
|
+
} catch (err) {
|
|
1351
|
+
warn(`[${id}] could not write site.yml: ${err.message}`)
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
log(` Add ${colors.green}touchIcon: ${value}${colors.reset} to site.yml — ${colors.dim}uniweb doctor --fix ${id}${colors.reset}`)
|
|
1355
|
+
return
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
if (!isSitePath(named)) return
|
|
1359
|
+
const file = siteFile(sitePath, named)
|
|
1360
|
+
if (!file) {
|
|
1361
|
+
const id = 'touch-icon-missing'
|
|
1362
|
+
issues.push({ id, type: 'warn', site: siteName, message: `touchIcon: ${named} is not in public/ or assets/` })
|
|
1363
|
+
warn(`[${id}] touchIcon: ${named} is not in public/ or assets/ — every page links an icon that is not there`)
|
|
1364
|
+
return
|
|
1365
|
+
}
|
|
1366
|
+
const { readImageSize, isImageOpaque } = await import('@uniweb/build/images')
|
|
1367
|
+
const size = await readImageSize(file)
|
|
1368
|
+
if (!size || size.format !== 'png') {
|
|
1369
|
+
const id = 'touch-icon-format'
|
|
1370
|
+
const what = !size ? 'could not be read as an image' : `is ${size.format.toUpperCase()}, not a PNG`
|
|
1371
|
+
issues.push({ id, type: 'warn', site: siteName, message: `touchIcon: ${named} ${what}` })
|
|
1372
|
+
warn(`[${id}] touchIcon: ${named} ${what} — iOS takes a PNG`)
|
|
1373
|
+
return
|
|
1374
|
+
}
|
|
1375
|
+
if ((await isImageOpaque(file)) === false) {
|
|
1376
|
+
const id = 'touch-icon-transparent'
|
|
1377
|
+
issues.push({ id, type: 'warn', site: siteName, message: `touchIcon: ${named} has transparent pixels, which iOS draws black` })
|
|
1378
|
+
warn(`[${id}] touchIcon: ${named} has transparent pixels — iOS draws them black`)
|
|
1379
|
+
log(` Give the icon a background of its own.`)
|
|
1380
|
+
}
|
|
1381
|
+
const { width, height } = size
|
|
1382
|
+
const advice =
|
|
1383
|
+
width !== height
|
|
1384
|
+
? 'not square, as a touch icon is'
|
|
1385
|
+
: width < TOUCH_ICON_SIZE
|
|
1386
|
+
? `under ${TOUCH_ICON_SIZE}×${TOUCH_ICON_SIZE}, iPhone screens draw it soft`
|
|
1387
|
+
: null
|
|
1388
|
+
if (advice) {
|
|
1389
|
+
const id = 'touch-icon-shape'
|
|
1390
|
+
issues.push({ id, type: 'info', site: siteName, message: `touchIcon: ${named} is ${width}×${height}: ${advice}` })
|
|
1391
|
+
info(`[${id}] touchIcon: ${named} is ${width}×${height}: ${advice}`)
|
|
1392
|
+
log(` A ${TOUCH_ICON_SIZE}×${TOUCH_ICON_SIZE} PNG is the size iOS asks for.`)
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1310
1396
|
/** The bare map in `queries.yml`, or `{}` when there is none. */
|
|
1311
1397
|
function readQueriesYml(sitePath) {
|
|
1312
1398
|
try {
|
|
@@ -1836,9 +1922,10 @@ async function runDoctor(args = []) {
|
|
|
1836
1922
|
}
|
|
1837
1923
|
|
|
1838
1924
|
await checkSiteAddress({ sitePath, siteName, siteYml, workspaceDir, issues })
|
|
1839
|
-
// What a link to the site shows, and its
|
|
1925
|
+
// What a link to the site shows, its tab icon, and its home-screen icon.
|
|
1840
1926
|
await checkLinkPreviews({ sitePath, siteName, issues })
|
|
1841
1927
|
await checkFavicon({ sitePath, siteName, siteYml, issues, shouldFix, fixed })
|
|
1928
|
+
await checkTouchIcon({ sitePath, siteName, siteYml, issues, shouldFix, fixed })
|
|
1842
1929
|
|
|
1843
1930
|
// Forms need a destination, and having none is only visible on the page.
|
|
1844
1931
|
await checkFormSubmitTarget({ sitePath, siteName, siteYml, issues })
|
package/src/commands/publish.js
CHANGED
|
@@ -630,10 +630,16 @@ export async function publish(args = []) {
|
|
|
630
630
|
// 3. Partition the queries by schema presence (a first emit reads `schemaless`
|
|
631
631
|
// — the queries with no data schema, whose compiled files 4c uploads; no
|
|
632
632
|
// ball carries them since 2026-08-18).
|
|
633
|
+
// ⭐ With the keys this deployment's Sections take (`deploymentFields`), as the push below
|
|
634
|
+
// is: this emit says which files go up, and a key sent only where it is taken can name one —
|
|
635
|
+
// `touch_icon` does. ⛔ Until 2026-10-28 it went without them, so a touch icon was sent as the
|
|
636
|
+
// site path it was written as, and never uploaded.
|
|
637
|
+
const fields = await deploymentFields({ client, siteDir })
|
|
633
638
|
let probe
|
|
634
639
|
try {
|
|
635
640
|
probe = await emitSyncPackages(siteDir, {
|
|
636
641
|
backend: client.origin,
|
|
642
|
+
...fields,
|
|
637
643
|
// Placement identity for the folder — see writeFolderItemUuids.
|
|
638
644
|
folderItemUuids: readFolderItemUuids(siteDir, client.origin),
|
|
639
645
|
...(foundationDir ? { foundationDir } : {}),
|
|
@@ -922,7 +928,6 @@ export async function publish(args = []) {
|
|
|
922
928
|
// edited since this clone last synced, the push is refused rather than
|
|
923
929
|
// overwriting them, and nothing goes live. `--force` drops the precondition.
|
|
924
930
|
const forced = args.includes('--force')
|
|
925
|
-
const fields = await deploymentFields({ client, siteDir })
|
|
926
931
|
// A foundation named by catalog ref — a clone's: its kept registered version types a query named
|
|
927
932
|
// for a data key (`ensureRegisteredFoundation`).
|
|
928
933
|
await ensureRegisteredFoundation({
|
package/src/commands/push.js
CHANGED
|
@@ -446,6 +446,16 @@ export async function push(args = [], deps = {}) {
|
|
|
446
446
|
if (!guard.ok) return { exitCode: 1 }
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
+
// The keys this deployment's Sections take (`deploymentFields`) — read before the media probe,
|
|
450
|
+
// which says which files go up: a key sent only where it is taken can name one (`touch_icon`).
|
|
451
|
+
// ⛔ Until 2026-10-28 the probe went without them, so a touch icon was sent as the site path it
|
|
452
|
+
// was written as, and never uploaded.
|
|
453
|
+
const fields = await deploymentFields({
|
|
454
|
+
client,
|
|
455
|
+
siteDir,
|
|
456
|
+
offline: Boolean(output) || dryRun
|
|
457
|
+
})
|
|
458
|
+
|
|
449
459
|
let assetRewrite = null
|
|
450
460
|
let assetIds = null
|
|
451
461
|
if (!output && !dryRun) {
|
|
@@ -453,9 +463,10 @@ export async function push(args = [], deps = {}) {
|
|
|
453
463
|
let refusals = []
|
|
454
464
|
try {
|
|
455
465
|
const probe = await emitSyncPackages(siteDir, {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
466
|
+
backend: client.origin,
|
|
467
|
+
...fields,
|
|
468
|
+
// Placement identity for the folder — see writeFolderItemUuids.
|
|
469
|
+
folderItemUuids: readFolderItemUuids(siteDir, client.origin),
|
|
459
470
|
...(foundationDir ? { foundationDir } : {}),
|
|
460
471
|
resolveModel: makeModelResolver({ client, offline: false, siteDir })
|
|
461
472
|
})
|
|
@@ -553,11 +564,6 @@ export async function push(args = [], deps = {}) {
|
|
|
553
564
|
output || dryRun
|
|
554
565
|
? readItemUuids(siteDir, client.origin)
|
|
555
566
|
: await ensureItemUuids({ client, siteDir, note })
|
|
556
|
-
const fields = await deploymentFields({
|
|
557
|
-
client,
|
|
558
|
-
siteDir,
|
|
559
|
-
offline: Boolean(output) || dryRun
|
|
560
|
-
})
|
|
561
567
|
// A foundation named by catalog ref — a clone's: its kept registered version types a query named
|
|
562
568
|
// for a data key (`ensureRegisteredFoundation`). Offline, only what is already kept.
|
|
563
569
|
if (!output && !dryRun) {
|
package/src/framework-index.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"generatedAt": "2026-10-28T04:08:25.023Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
6
|
"version": "1.6.4"
|
|
7
7
|
},
|
|
8
8
|
"@uniweb/build": {
|
|
9
|
-
"version": "1.10.
|
|
9
|
+
"version": "1.10.13"
|
|
10
10
|
},
|
|
11
11
|
"@uniweb/content-reader": {
|
|
12
12
|
"version": "1.5.7"
|