uniweb 1.21.23 → 1.22.1

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.23",
3
+ "version": "1.22.1",
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",
45
44
  "@uniweb/core": "~1.4.12",
45
+ "@uniweb/semantic-parser": "~1.13.0",
46
46
  "@uniweb/schemas": "~1.6.5",
47
- "@uniweb/semantic-parser": "~1.13.0"
47
+ "@uniweb/content-writer": "~1.3.1"
48
48
  },
49
49
  "peerDependencies": {
50
- "@uniweb/build": "~1.10.14",
50
+ "@uniweb/semantic-parser": "~1.13.0",
51
51
  "@uniweb/content-reader": "~1.5.7",
52
- "@uniweb/semantic-parser": "~1.13.0"
52
+ "@uniweb/build": "~1.11.1"
53
53
  },
54
54
  "peerDependenciesMeta": {
55
55
  "@uniweb/build": {
@@ -544,7 +544,9 @@ Optional attributes: `{size=20}`, `{color=red}`. Custom SVGs: `![Logo](./logo.sv
544
544
  ![alt](./img.jpg){href=/products} <!-- Clickable media: href + target ride along -->
545
545
  ```
546
546
 
547
- Sizing and loading ride on the image: `{width=800 height=600 loading=lazy fit=cover position=center}`, and its credit as `{author="Jane Doe"}`. A video adds `{poster=./thumb.jpg autoplay muted loop controls}`; a `{role=pdf}` document adds `{preview=./cover.jpg author=… description=…}` and arrives in `content.documents` — `{ url, name, mime, size?, alt, caption, … }` — declare it in `content:` as `documents`.
547
+ Sizing and loading ride on the image: `{width=800 height=600 loading=lazy fit=cover position=center}`, and its credit as `{author="Jane Doe"}`. A video adds `{poster=./thumb.jpg autoplay muted loop controls}`; a `{role=pdf}` document adds `{preview=./cover.jpg author=… description=…}` and arrives in `content.documents` — `{ url, name, mime, size?, alt, caption, … }` — declare it in `content:` as `documents`. Name a video's poster: a static build makes one from the video only when none is named and ffmpeg is installed, and `uniweb publish` sends only the poster you name.
548
+
549
+ **Where media lives.** `./x` names a file beside the page, `/x` a file in `public/`, and both reach a static build and a site published with Uniweb. Keep a page's media beside it, and keep in `public/` what needs a fixed address: the favicon, fonts, a file other sites link to. A static build copies `public/` whole and adds its own copy of each file a page names there, so a video only a page shows is in `dist/` twice from `public/` and once from beside the page. `uniweb push` uploads a file beside a page wherever the page is, in a folder `paths:` mounts too; a file outside the site and the folders it mounts is not uploaded, so put that in `public/`. A site has no `assets/` folder for media: a build stops on a `/x` only `assets/` holds.
548
550
 
549
551
  **Placement — where a block sits in a section rendered in order.** `{placement=left}` (or `right`, `wide`, `full`) on an image or a video; a table takes it on its **caption line**, the paragraph after the table, after a blank line — `: Plans and prices {placement=wide #tbl-plans}`, which also gives the table a caption and a cross-reference id. It is the author's intent, not a size: kit's `<Render>`, `<Prose>` and `<Article>` lay all four out (`data-placement`, styled by `prose-tokens.css`), and any other component ignores it. A section type says which it honours in `meta.js` — `sequence: { label, placement: true }` — and the build warns when a file asks for one its type does not offer.
550
552
 
@@ -2877,8 +2879,8 @@ Foundations have their own free path too: `uniweb add ci --target foundation` pu
2877
2879
  **Three facts about a site, and they are independent — `uniweb status` opens with all three:**
2878
2880
 
2879
2881
  - **Synced** — the project has a site on a backend, so authors can edit it in the Uniweb app. `sync.json` names it on each backend: `https://uniweb.app` by default, or a server of your own (`uniweb login --server <url>`).
2880
- - **Published** — that backend serves it live: Uniweb Cloud, on `https://uniweb.app`. A published site is a synced site; a synced site need not be published — it can sit in the app as a draft.
2881
- - **Deployed** — a build of it is on another host, such as GitHub Pages or S3: by CI on every push (`uniweb add ci`), or by hand (`uniweb deploy`). `deploy.yml` lists those hosts, and a bare `uniweb deploy` goes to its default target.
2882
+ - **Published** — that backend serves it live: Uniweb Cloud, on `https://uniweb.app`. A published site is a synced site; a synced site need not be published — it can sit in the app as a draft. A publish is recorded in `deploy.yml` too, under a `host: uniweb` target: `deploys.<target>.url` is the address that publish delivered to. Where visitors go now (a custom domain, while one is active) is the backend's answer, and `uniweb status` shows it.
2883
+ - **Deployed** — a build of it is on another host, such as GitHub Pages or S3: by CI on every push (`uniweb add ci`), or by hand (`uniweb deploy`). `deploy.yml` lists those hosts beside the one a publish records, and a bare `uniweb deploy` goes to its default target.
2882
2884
 
2883
2885
  A site can be in more than one place — usually while it moves from one host to another. Before you ship, read where it is: `publish` makes a change live on Uniweb Cloud, `git push` on a CI-deployed host, and `uniweb deploy --target <name>` on one deployed by hand — and each of these is the person's to decide. `uniweb status` also lists the services the live site has (`services:` in `site.yml` is the request, never proof of what the site has).
2884
2886
 
@@ -3074,6 +3076,8 @@ These are **body only — no frontmatter**; params and config still come from th
3074
3076
 
3075
3077
  **A tagged data block translates by the shape its key declares** in the component's `meta.js` `data:` — a data schema or an inline field map: its text fields, never an enum, a URL or a `translatable: false` field. A key declared `{}` leaves its blocks to a heuristic that offers any string whose field name and value don't look structural, so a `style: display-black` naming one of your component's looks gets translated too. Declare the shape of every block a translator will see — `{ name: 'string', style: { type: 'string', enum: [...] } }`.
3076
3078
 
3079
+ **An image translates by its alt text.** Words alone give it its alt text in that language and keep its file. A translation that is an image also names the file that language shows: `"2d093636": "![La superposition](/docs-media/fr/overview.png)"`. Name it with a `/` path, a file in `public/`, and a build processes it as it does the page's own images; a `./` path has no page's folder to start from, and the build reports it. A caption or a video's `poster` can be given the same way. An image with no alt text has nothing to translate, so give every image one.
3080
+
3077
3081
  **Records translate in the same `extract` run**, into their own manifest at `locales/records/manifest.json` — keyed by the RECORD, so a record translated once is found by every query that returns it. A record whose query has a data schema translates exactly the fields the schema says are text — never an enum, a URL, a date, a number, a file, a reference or a `translatable: false` field — which are the fields a push carries in each language; mark a string that is a code or a key `translatable: false` and no translator is asked for it. An excerpt the build derives is re-derived from each language's body.
3078
3082
 
3079
3083
  **Component side.** Nothing to do: `content.title` arrives in the active language. The one thing a foundation builds is a switcher.
@@ -5,7 +5,8 @@
5
5
  *
6
6
  * Input is the site-root asset refs the producer surfaced in
7
7
  * `emitSyncPackages().localAssets` (`/images/hero.png`); `resolveAssetPath` finds the
8
- * file under the site's `public/` (or `assets/`). A ref whose file is missing is
8
+ * file under the site's `public/`, and refuses one only `assets/` holds, a folder retired
9
+ * 2026-10-28. A ref whose file is missing is
9
10
  * skipped (warned), never a broken serve URL. The serve URL is the backend's canonical
10
11
  * `serve_url`, read verbatim — origin-relative forms included; nothing here composes one.
11
12
  * Content-addressed like every asset: identical bytes → same id → a re-deploy of
@@ -49,8 +50,9 @@ export async function uploadSiteMedia(
49
50
  const files = []
50
51
  const missing = []
51
52
  // ⭐ A ref is a site-root media path (`/images/x.png`), resolved under the site — or a file the
52
- // caller already located, `{ ref, path, contentType }`: a file record's file and its preview, which
53
- // live in the records directory and declare their own type (`@uniweb/file`, 2026-09-28).
53
+ // caller already located, `{ ref, path, contentType? }`: a file record's file and its preview, which
54
+ // live in the records directory and declare their own type (`@uniweb/file`, 2026-09-28), and a
55
+ // section's co-located file, keyed by its path from the site root (`uwx/located-media.js`, 2026-10-28).
54
56
  const keys = []
55
57
  for (const item of refs) {
56
58
  const ref = typeof item === 'string' ? item : item?.ref
@@ -29,7 +29,6 @@ import {
29
29
  } from '../utils/sync-point.js'
30
30
  import { siteContentRoots } from '../utils/git.js'
31
31
  import { humanBytes } from '../utils/bytes.js'
32
- import { isAuthoredPreview } from '../utils/preview.js'
33
32
  import {
34
33
  describeRequestError,
35
34
  describeWorkspace,
@@ -56,7 +55,6 @@ import {
56
55
  updateBackendMap,
57
56
  clearBackendSections,
58
57
  normalizeBackendOrigin,
59
- writeSiteConfig,
60
58
  harvestRecordItems,
61
59
  storedRecordItems,
62
60
  reprintRecordItems,
@@ -734,51 +732,6 @@ export function clearRemoteSyncStateIfUnbound(siteDir, backend) {
734
732
  return stale
735
733
  }
736
734
 
737
- /**
738
- * Drop the `site.yml` values that describe ONE PARTICULAR backend site, when this
739
- * project is bound to none (no `$uuid`) — a brand-new site, or the state our own
740
- * "clear `$uuid` to re-publish as a new site" recovery puts you in. Today that is
741
- * one value: `preview` in the APP's form — a timestamp naming the previous site's
742
- * generated card image, which is keyed by that site's uuid and would dangle on this
743
- * one.
744
- *
745
- * ⛔ An AUTHOR's preview is theirs and stays: a URL, or a path to an image in the
746
- * project, is as good for the new site as for the old.
747
- *
748
- * (`$url`, where the previous site was live, was dropped here too until it was
749
- * retired on 2026-09-17. A leftover line is inert — nothing reads or sends it.)
750
- *
751
- * The site.yml sibling of `clearRemoteSyncStateIfUnbound`, called beside it for the
752
- * same reason — before the create mints a uuid and makes the project look bound.
753
- *
754
- * @param {string} siteDir
755
- * @returns {string[]} the keys removed
756
- */
757
- export function dropSiteBoundValues(siteDir, backend) {
758
- const file = join(siteDir, 'site.yml')
759
- let y
760
- try {
761
- y = yaml.load(readFileSync(file, 'utf8'))
762
- } catch {
763
- return []
764
- }
765
- // ⛔ "Bound" is a question about THIS backend, answered by sync.json. This read
766
- // `site.yml::$uuid` until 2026-09-20 and kept doing so after step 4 moved the key —
767
- // so every project read as unbound and every push stripped a bound site's
768
- // generated preview. A write, from a stale read, with nothing to notice it.
769
- if (!y || typeof y !== 'object') return []
770
- if (readBackendState(siteDir, backend).site?.uuid) return []
771
- const dropped = []
772
- if (
773
- y.preview !== undefined &&
774
- !isAuthoredPreview(y.preview) &&
775
- writeSiteConfig(siteDir, { preview: null }) === 'updated'
776
- ) {
777
- dropped.push('preview')
778
- }
779
- return dropped
780
- }
781
-
782
735
  export function readSyncCache(siteDir, backend) {
783
736
  return readMap(siteDir, backend, 'hashes')
784
737
  }
@@ -69,10 +69,6 @@ import {
69
69
  isExtensionPackage,
70
70
  detectFoundationType
71
71
  } from '@uniweb/build'
72
- import {
73
- resolveDefaultLocale,
74
- normalizeLanguageList
75
- } from '@uniweb/core/locale-config'
76
72
  import { readSiteConfig } from '@uniweb/build/site'
77
73
  import { workspacePackages } from '../utils/workspace-packages.js'
78
74
 
@@ -424,34 +420,25 @@ async function loadI18nConfig(projectDir, siteConfig = null) {
424
420
  const localesDir = config.i18n?.localesDir || 'locales'
425
421
  const localesPath = join(projectDir, localesDir)
426
422
 
427
- // Resolve locales (undefined/'*' → all available, array → specific)
428
- const { resolveLocales } = await import('@uniweb/build/i18n')
429
- let locales = await resolveLocales(config.languages, localesPath)
430
-
431
- // Publish filter: build outputs are visitor artifacts. With an explicit
432
- // `publishLanguages:`, only listed locales get dist/{locale}/ content,
433
- // per-locale HTML, hreflang, and translated collections. Draft locales
434
- // stay previewable in dev, which doesn't run this path. Intersecting the
435
- // DISK-resolved list here also covers wildcard (`languages: '*'`) sites.
436
- if (config.publishLanguages != null) {
437
- const publishSet = new Set(normalizeLanguageList(config.publishLanguages))
438
- const dropped = locales.filter((l) => !publishSet.has(l))
439
- locales = locales.filter((l) => publishSet.has(l))
440
- if (dropped.length > 0) {
441
- info(
442
- `Skipping non-publishable locale(s): ${dropped.join(', ')} (not in publishLanguages)`
443
- )
444
- }
423
+ // The site's locales (undefined/'*' → all available, array → specific), with
424
+ // two filters, by the rule the bundle uses to decide whose files to process
425
+ // (`localesToBuild`):
426
+ // - Publish: build outputs are visitor artifacts. With an explicit
427
+ // `publishLanguages:`, only listed locales get dist/{locale}/ content,
428
+ // per-locale HTML, hreflang, and translated collections. Draft locales
429
+ // stay previewable in dev, which doesn't run this path. Intersecting the
430
+ // DISK-resolved list also covers wildcard (`languages: '*'`) sites.
431
+ // - ⛔ The default locale is the root tree, not `dist/<default>/`. An explicit
432
+ // `languages: [en, fr]` resolves verbatim — default included — so the DECLARED
433
+ // form (the one the contract tells authors to use) minted a duplicate copy of
434
+ // every page, while the wildcard form did not. Dropping it keeps content,
435
+ // per-locale HTML and translated records agreeing on one set.
436
+ const { localesToBuild } = await import('@uniweb/build/i18n')
437
+ const { locales, skipped, defaultLocale } = localesToBuild(config, localesPath)
438
+ if (skipped.length > 0) {
439
+ info(`Skipping non-publishable locale(s): ${skipped.join(', ')} (not in publishLanguages)`)
445
440
  }
446
441
 
447
- // ⛔ The default locale is the root tree, not `dist/<default>/`. An explicit
448
- // `languages: [en, fr]` resolves verbatim — default included — so the DECLARED
449
- // form (the one the contract tells authors to use) minted a duplicate copy of
450
- // every page, while the wildcard form did not. Dropping it here keeps content,
451
- // per-locale HTML and translated records agreeing on one set.
452
- const defaultLocale = resolveDefaultLocale(config)
453
- locales = locales.filter((l) => l !== defaultLocale)
454
-
455
442
  if (locales.length === 0) return null
456
443
 
457
444
  return {
@@ -1112,9 +1112,16 @@ const DESCRIPTION_SHOWN = 160
1112
1112
  /** A path into the site's own files (`/images/card.jpg`), not an address of its own. */
1113
1113
  const isSitePath = (v) => typeof v === 'string' && v.startsWith('/') && !v.startsWith('//')
1114
1114
 
1115
- /** `/x` → the file under the site's public/ or assets/ that serves it, or null when there is none. */
1115
+ /** `/x` → the file under the site's public/ that serves it, or null when there is none. */
1116
1116
  function siteFile(sitePath, ref) {
1117
- const { resolved } = resolveAssetPath(ref, join(sitePath, 'site.yml'), sitePath)
1117
+ let resolved
1118
+ try {
1119
+ ;({ resolved } = resolveAssetPath(ref, join(sitePath, 'site.yml'), sitePath))
1120
+ } catch (err) {
1121
+ // A file in assets/, which no longer serves a `/x` — the build says how to move it.
1122
+ if (err?.code === 'ASSETS_FOLDER_RETIRED') return null
1123
+ throw err
1124
+ }
1118
1125
  return resolved && existsSync(resolved) ? resolved : null
1119
1126
  }
1120
1127
 
@@ -1195,8 +1202,8 @@ export async function checkLinkPreviews({ sitePath, siteName, issues }) {
1195
1202
  const file = siteFile(sitePath, image)
1196
1203
  if (!file) {
1197
1204
  const id = 'share-image-missing'
1198
- issues.push({ id, type: 'warn', site: siteName, message: `${image}, the image a link to ${shownOn} shows, is not in public/ or assets/` })
1199
- warn(`[${id}] ${image} is not in public/ or assets/ — a link to ${shownOn} shows no image`)
1205
+ issues.push({ id, type: 'warn', site: siteName, message: `${image}, the image a link to ${shownOn} shows, is not in public/` })
1206
+ warn(`[${id}] ${image} is not in public/ — a link to ${shownOn} shows no image`)
1200
1207
  continue
1201
1208
  }
1202
1209
  readImageSize ??= (await import('@uniweb/build/images')).readImageSize
@@ -1276,8 +1283,8 @@ export async function checkFavicon({ sitePath, siteName, siteYml, issues, should
1276
1283
  if (named) {
1277
1284
  if (!isSitePath(named) || siteFile(sitePath, named)) return
1278
1285
  const id = 'favicon-missing'
1279
- issues.push({ id, type: 'warn', site: siteName, message: `favicon: ${named} is not in public/ or assets/` })
1280
- warn(`[${id}] favicon: ${named} is not in public/ or assets/ — every page links an icon that is not there`)
1286
+ issues.push({ id, type: 'warn', site: siteName, message: `favicon: ${named} is not in public/` })
1287
+ warn(`[${id}] favicon: ${named} is not in public/ — every page links an icon that is not there`)
1281
1288
  return
1282
1289
  }
1283
1290
 
@@ -1359,8 +1366,8 @@ export async function checkTouchIcon({ sitePath, siteName, siteYml, issues, shou
1359
1366
  const file = siteFile(sitePath, named)
1360
1367
  if (!file) {
1361
1368
  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`)
1369
+ issues.push({ id, type: 'warn', site: siteName, message: `touchIcon: ${named} is not in public/` })
1370
+ warn(`[${id}] touchIcon: ${named} is not in public/ — every page links an icon that is not there`)
1364
1371
  return
1365
1372
  }
1366
1373
  const { readImageSize, isImageOpaque } = await import('@uniweb/build/images')
@@ -110,7 +110,6 @@ import {
110
110
  unsettledFiles,
111
111
  commitWriteBacks,
112
112
  reportWriteBacks,
113
- dropSiteBoundValues,
114
113
  pushSyncPackages,
115
114
  readSiteWorkspace
116
115
  } from '../backend/site-sync.js'
@@ -672,12 +671,6 @@ export async function publish(args = []) {
672
671
  `Cleared stale sync state from a previous site (${droppedState.join(', ')}).`
673
672
  )
674
673
  }
675
- const droppedValues = dropSiteBoundValues(siteDir, client.origin)
676
- if (droppedValues.length) {
677
- say.dim(
678
- `Dropped the previous site's ${droppedValues.join(' and ')} from site.yml.`
679
- )
680
- }
681
674
  // 3b. Make sure the SITE EXISTS before a single byte is uploaded.
682
675
  //
683
676
  // Uploaded bytes are metered against an owning entity and reclaimed by
@@ -26,7 +26,8 @@
26
26
  * `uniweb login && uniweb push`. Run from a site, or a workspace with one site.
27
27
  *
28
28
  * Usage:
29
- * uniweb push Build, push both lanes, back-fill $uuid
29
+ * uniweb push Bring the foundation along, push both lanes,
30
+ * back-fill $uuid
30
31
  * uniweb push --org @org Work in @org for this push, instead of the
31
32
  * uniweb push --personal workspace chosen at `uniweb login` (or your
32
33
  * personal workspace). A site this push creates
@@ -97,7 +98,6 @@ import {
97
98
  refuseUnsendableRecords,
98
99
  ensureSiteExists,
99
100
  clearRemoteSyncStateIfUnbound,
100
- dropSiteBoundValues,
101
101
  untakenBase,
102
102
  unsettledFiles,
103
103
  commitWriteBacks,
@@ -367,10 +367,6 @@ export async function push(args = [], deps = {}) {
367
367
  `Cleared stale sync state from a previous site (${dropped.join(', ')}).`
368
368
  )
369
369
  }
370
- const stale = dropSiteBoundValues(siteDir, client.origin)
371
- if (stale.length) {
372
- note(`Dropped the previous site's ${stale.join(' and ')} from site.yml.`)
373
- }
374
370
  }
375
371
  const priorHashes = readSyncCache(siteDir, client.origin)
376
372
  // Per-item identity, without which the backend reads every record as new and
@@ -23,8 +23,9 @@
23
23
  * A site keeps its chosen look in `site/snapshot.yml` (utils/snapshot-settings.js):
24
24
  * the command's defaults for that site, which flags override and `--save` writes.
25
25
  *
26
- * `preview:` is written only when site.yml has none, or holds the app's generated
27
- * token. An address the author wrote is never replaced.
26
+ * `preview:` is written only when site.yml has none, or holds something that is no
27
+ * author's address — the token an older pull wrote there for a card image the app
28
+ * generated. An address the author wrote is never replaced.
28
29
  */
29
30
 
30
31
  import { spawn } from 'node:child_process'
@@ -33,13 +34,12 @@ import { createRequire } from 'node:module'
33
34
  import { isAbsolute, join, relative, resolve, sep } from 'node:path'
34
35
  import { pathToFileURL } from 'node:url'
35
36
  import yaml from 'js-yaml'
36
- import { writeSiteConfig } from '@uniweb/build/uwx'
37
+ import { isAuthoredPreview, writeSiteConfig } from '@uniweb/build/uwx'
37
38
 
38
39
  import { didYouMean } from '../utils/args.js'
39
40
  import { humanBytes } from '../utils/bytes.js'
40
41
  import { discoverSites } from '../utils/discover.js'
41
42
  import { detectWorkspacePm } from '../utils/pm.js'
42
- import { isAuthoredPreview } from '../utils/preview.js'
43
43
  import {
44
44
  SETTINGS_FILE,
45
45
  SettingsError,
@@ -447,7 +447,7 @@ export async function snapshot(args = []) {
447
447
  if (setPreview()) console.log(` site.yml: ${CYAN}preview: ${value}${RESET}`)
448
448
  break
449
449
  case 'replace':
450
- if (setPreview()) console.log(` site.yml: ${CYAN}preview: ${value}${RESET} ${DIM}(replaces the app-generated preview)${RESET}`)
450
+ if (setPreview()) console.log(` site.yml: ${CYAN}preview: ${value}${RESET} ${DIM}(replaces ${current}, which is not an image address)${RESET}`)
451
451
  break
452
452
  case 'unchanged':
453
453
  console.log(` ${DIM}site.yml already has preview: ${value}${RESET}`)
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-10-28T14:10:12.997Z",
3
+ "generatedAt": "2026-10-29T02:26:03.448Z",
4
4
  "packages": {
5
5
  "@uniweb/api": {
6
- "version": "1.6.4"
6
+ "version": "1.6.5"
7
7
  },
8
8
  "@uniweb/build": {
9
- "version": "1.10.14"
9
+ "version": "1.11.1"
10
10
  },
11
11
  "@uniweb/content-reader": {
12
12
  "version": "1.5.7"
@@ -36,7 +36,7 @@
36
36
  "version": "1.2.7"
37
37
  },
38
38
  "@uniweb/runtime": {
39
- "version": "1.2.12"
39
+ "version": "1.2.13"
40
40
  },
41
41
  "@uniweb/scene": {
42
42
  "version": "1.0.1"
@@ -60,7 +60,7 @@
60
60
  "version": "1.2.1"
61
61
  },
62
62
  "@uniweb/unipress": {
63
- "version": "1.1.16"
63
+ "version": "1.1.17"
64
64
  }
65
65
  },
66
66
  "templates": {
package/src/index.js CHANGED
@@ -1607,8 +1607,8 @@ ${colors.bright}Usage:${colors.reset}
1607
1607
 
1608
1608
  Opens the site in a headless Chrome, captures it, and composes the captures into
1609
1609
  one image: by default ${colors.bright}site/public/preview.webp${colors.reset}, recorded as ${colors.cyan}preview:${colors.reset} in site.yml
1610
- when site.yml has none, or only the app's generated one. A URL or image path you
1611
- wrote is never replaced.
1610
+ when site.yml has none, or what it has is not an image address. A URL or image path
1611
+ you wrote is never replaced.
1612
1612
 
1613
1613
  A page that scrolls gets the ${colors.bright}split${colors.reset} layout: the first view in a browser window,
1614
1614
  beside a long strip of the page. A page that does not scroll as a page (a docs
@@ -1,20 +0,0 @@
1
- /**
2
- * `site.yml::preview` — the site's card image — has two writers: an author, who
3
- * writes an address (a URL, or a path to an image in the project), and the app,
4
- * which writes a token naming an image it generated. This is the one test that
5
- * tells them apart.
6
- *
7
- * It recognizes the author's shapes rather than the app's, so it does not depend
8
- * on the token's format: a URL, a path starting `/`, `./` or `../`, or a relative
9
- * path to an image file (`images/card.png`).
10
- */
11
-
12
- const IMAGE_FILE = /\.(avif|gif|jpe?g|png|svg|webp)$/i
13
-
14
- /**
15
- * @param {unknown} value
16
- * @returns {boolean} true when `value` is an author's address rather than the app's token
17
- */
18
- export const isAuthoredPreview = (value) =>
19
- typeof value === 'string' &&
20
- (/^https?:\/\//i.test(value) || /^\.{0,2}\//.test(value) || IMAGE_FILE.test(value.trim()))