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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "1.21.19",
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/semantic-parser": "~1.13.0",
50
+ "@uniweb/build": "~1.10.13",
51
51
  "@uniweb/content-reader": "~1.5.7",
52
- "@uniweb/build": "~1.10.12"
52
+ "@uniweb/semantic-parser": "~1.13.0"
53
53
  },
54
54
  "peerDependenciesMeta": {
55
55
  "@uniweb/build": {
@@ -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]
@@ -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 tab icon.
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 })
@@ -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({
@@ -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
- backend: client.origin,
457
- // Placement identity for the folder — see writeFolderItemUuids.
458
- folderItemUuids: readFolderItemUuids(siteDir, client.origin),
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) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-10-28T03:03:01.883Z",
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.12"
9
+ "version": "1.10.13"
10
10
  },
11
11
  "@uniweb/content-reader": {
12
12
  "version": "1.5.7"