@uniweb/unipress 0.4.28 → 0.4.30

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.
@@ -0,0 +1,152 @@
1
+ # Size and layout
2
+
3
+ The physical shape of your book — its **trim size**, margins, and typography — is
4
+ set in `document.yml` under `book:`. Pick it once; every build uses it. This page
5
+ explains the choices and how to make them.
6
+
7
+ > Full field reference: [`book` template](../templates/book.md). This page is the
8
+ > *why* behind the settings.
9
+
10
+ ## Trim size
11
+
12
+ The trim size is the finished page dimensions after the printer cuts the sheet.
13
+ It's the single most defining physical choice — it sets how your book feels in the
14
+ hand and constrains everything else. Set it with `book.trim`:
15
+
16
+ ```yaml
17
+ book:
18
+ trim: trade-6x9
19
+ ```
20
+
21
+ ### Built-in presets
22
+
23
+ | Preset | Size | Good for |
24
+ |---|---|---|
25
+ | `trade-6x9` *(default)* | 6 × 9 in | The safe default. Most trade fiction and nonfiction. |
26
+ | `trade-7x10` | 7 × 10 in | Technical books, workbooks, anything with code or wide images. |
27
+ | `crown-octavo` | 7.44 × 9.69 in (189 × 246 mm) | Common UK trade hardcover. |
28
+ | `royal-octavo` | 6.14 × 9.21 in (156 × 234 mm) | Academic and scholarly hardcover. |
29
+ | `a5` | 148 × 210 mm | Compact European paperback; pocket guides. |
30
+
31
+ Two more sizes exist for the **article** genre — full-size paper for documents and
32
+ proofing copies rather than bound books:
33
+
34
+ | Preset | Size | Good for |
35
+ |---|---|---|
36
+ | `a4` | 210 × 297 mm | The world's standard paper. A roomy proofing copy. |
37
+ | `letter` | 8.5 × 11 in | US/Canada paper. A roomy proofing copy. |
38
+
39
+ ### A custom size
40
+
41
+ Any trim your printer supports. Give a width and height (and, if you like,
42
+ margins) instead of a preset name:
43
+
44
+ ```yaml
45
+ book:
46
+ trim:
47
+ width: 5in # a common "mass-market"-ish trade size
48
+ height: 8in
49
+ margins: { inside: 0.75in, outside: 0.5in, top: 0.75in, bottom: 0.75in }
50
+ ```
51
+
52
+ You can also start from a preset and nudge one value:
53
+
54
+ ```yaml
55
+ book:
56
+ trim:
57
+ preset: trade-6x9
58
+ margins: { inside: 0.9in } # more gutter for a thick book
59
+ ```
60
+
61
+ **Pick a size your print-on-demand service offers.** Each service lists its
62
+ supported trim sizes; matching one avoids a custom-size surcharge. Check
63
+ [KDP's list](https://kdp.amazon.com/help/topic/G201834180) or
64
+ [IngramSpark's](https://help.ingramspark.com/hc/en-us/articles/360010301911).
65
+
66
+ ## Margins and the gutter
67
+
68
+ A bound book needs an **asymmetric** margin: the **inside** (gutter) edge — where
69
+ the pages meet the spine — must be wider than the **outside**, or text disappears
70
+ into the binding. The presets already do this. The four margins:
71
+
72
+ ```yaml
73
+ book:
74
+ trim:
75
+ preset: trade-6x9
76
+ margins: { inside: 0.75in, outside: 0.5in, top: 0.75in, bottom: 0.75in }
77
+ ```
78
+
79
+ **The more pages, the more gutter you need** — a thick book's spine "steals" more
80
+ of the inside margin. Print-on-demand services publish a minimum inside margin by
81
+ page count; a ~300-page 6×9 usually wants at least 0.75–0.875 in inside. Confirm
82
+ against your service's table before you finalize, and widen `inside` if needed.
83
+
84
+ ## Bleed
85
+
86
+ **Bleed** is art that runs off the edge of the page. Because printers can't cut
87
+ perfectly, anything meant to reach the edge must extend ~0.125 in *past* the trim,
88
+ so the cut never leaves a white sliver.
89
+
90
+ - **Text interiors need no bleed.** Your words sit inside the margins; unipress
91
+ emits pages at the exact trim size, which is exactly what print-on-demand wants
92
+ for a text interior. This is the common case — you don't have to think about it.
93
+ - **Full-bleed interior *images*** (a photo running to the paper edge) are the
94
+ exception. unipress emits trim-size pages, so it isn't set up for edge-to-edge
95
+ interior art. Keep interior images within the text block, or ask your printer how
96
+ they want full-bleed interior pages supplied.
97
+ - **Your cover is where bleed really matters**, and that's handled separately by
98
+ your print service's cover template — see [covers-and-images.md](./covers-and-images.md#covers).
99
+
100
+ ## Front matter
101
+
102
+ The pages before Chapter 1 — title, copyright, contents. Toggle them under
103
+ `book.structure`:
104
+
105
+ ```yaml
106
+ book:
107
+ structure:
108
+ titlePage: true
109
+ copyrightPage: true
110
+ toc: true
111
+ tocDepth: 2 # how many heading levels the contents lists
112
+ frontMatterNumbering: roman # none | roman | arabic
113
+ ```
114
+
115
+ - **`titlePage`** — the title, subtitle, and author, typeset. If your cover art
116
+ already carries the title (many do), you may want this **off** to avoid repeating
117
+ it: `titlePage: false`.
118
+ - **`copyrightPage`** — the copyright/imprint page. Pulls from `book.rights`,
119
+ `book.publisher`, `book.isbn` (see [isbn-and-metadata.md](./isbn-and-metadata.md)).
120
+ - **`toc`** — a generated table of contents from your chapter titles.
121
+ - **`frontMatterNumbering: roman`** is the traditional convention: front matter is
122
+ numbered i, ii, iii…; Chapter 1 resets to page 1. Use `none` to leave front
123
+ matter unnumbered, or `arabic` to number straight through.
124
+
125
+ ## Typography
126
+
127
+ The defaults produce a clean, readable book — you can ship without touching them.
128
+ When you want control, set it under `book.typography`:
129
+
130
+ ```yaml
131
+ book:
132
+ typography:
133
+ bodySize: 11pt
134
+ leading: 0.72em # line spacing
135
+ firstLineIndent: 1.4em # paragraph indent
136
+ bodyFont: ["EB Garamond", "Georgia"] # first available wins
137
+ headingFont: ["EB Garamond", "Georgia"]
138
+ ```
139
+
140
+ - **Fonts are tried in order**; the first one installed on the machine running the
141
+ compile is used, and unipress embeds it in the PDF. Leave them unset to use the
142
+ compiler's clean built-in defaults. If you name a font, make sure it's installed
143
+ where you build.
144
+ - **Body size and leading** are the levers for page count and comfort. 10.5–11 pt
145
+ with roomy leading reads well in a 6×9.
146
+ - For a classical look with these choices preconfigured, scaffold the **monograph**
147
+ template instead of tuning by hand: `unipress create my-book --template monograph`.
148
+
149
+ ## Next
150
+
151
+ - Add your [covers and images](./covers-and-images.md).
152
+ - Produce your [ebook](./ebooks.md) or go to [print](./print.md).
@@ -53,7 +53,7 @@ with `paths: { pages: . }`. It's an ordinary config file; edit it freely.
53
53
  To write one by hand instead:
54
54
 
55
55
  ```yaml
56
- foundation: "@uniweb/book@0.4.1"
56
+ foundation: "@uniweb/book@0.4.2"
57
57
  format: pdf
58
58
  paths:
59
59
  pages: . # only needed when sections live at the root (no content/ folder)
@@ -200,6 +200,23 @@ The config file's default export is a string, number, array, or something else.
200
200
  export default { format: 'pdf' }
201
201
  ```
202
202
 
203
+ ## Cover image (or other config asset) doesn't appear in the output
204
+
205
+ Not an error — a silent drop. The book compiles, but the front/back cover (or a
206
+ banner/logo declared in config) is missing from the PDF/EPUB.
207
+
208
+ Config-declared asset paths — `book.covers.front`, `book.covers.back`, and the like —
209
+ are resolved from the top-level config unipress **reads for content collection**: the
210
+ default `document.yml`, or the file you pass to `--document`. They are *not* scanned from
211
+ a `unipress.config.js` supplied via `--config` (that file layers build settings — `out`,
212
+ `format`, `typst` — over the compile, after content is collected).
213
+
214
+ So if your covers live in `document-book.yml`, build it with `--variant document-book`
215
+ — not `--config document-book.yml`. Confirm with `--verbose`: the `Found N asset references`
216
+ line should count your covers (e.g. 2 more than the body images), and the source-bundle
217
+ `blob:` size should grow by roughly the covers' byte size. Also confirm the files exist at
218
+ the declared path relative to the content directory (e.g. `assets/front.png`).
219
+
203
220
  ## Internal error (exit code 2)
204
221
 
205
222
  Something unexpected threw — typically a library error or a code path unipress didn't wrap. Re-run with `--verbose` for a stack trace, then open an issue with the trace and a description of the content directory + foundation you're compiling.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/unipress",
3
- "version": "0.4.28",
3
+ "version": "0.4.30",
4
4
  "description": "Compile a content directory into a document (PDF, EPUB, Paged.js HTML, Typst source bundle, DOCX, XLSX) using a Uniweb foundation. Five built-in templates: book, monograph, report, data-report, directory.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -49,11 +49,11 @@
49
49
  "prompts": "^2.4.2",
50
50
  "react": "^19.0.0",
51
51
  "react-dom": "^19.0.0",
52
+ "@uniweb/build": "0.14.22",
52
53
  "@uniweb/content-reader": "1.1.12",
53
- "@uniweb/build": "0.14.20",
54
- "@uniweb/core": "0.7.14",
55
- "@uniweb/runtime": "0.8.20",
56
- "@uniweb/semantic-parser": "1.1.17"
54
+ "@uniweb/core": "0.7.15",
55
+ "@uniweb/semantic-parser": "1.1.17",
56
+ "@uniweb/runtime": "0.8.21"
57
57
  },
58
58
  "devDependencies": {
59
59
  "vitest": "^4.1.7"
package/src/cli.js CHANGED
@@ -25,6 +25,9 @@ Commands:
25
25
  latex emits a LaTeX source bundle (run latexmk yourself)
26
26
  --foundation <ref> override document.yml's foundation: field
27
27
  --out <path> output file (default: ./<dir>.<ext>)
28
+ --variant <name> build an alternate config <name>.yml in <dir>
29
+ (e.g. --variant document-book) instead of
30
+ document.yml — several cuts of one manuscript
28
31
  --config <path> explicit config file (default: <dir>/unipress.config.js)
29
32
  --typst-binary <p> path to a typst binary (skips managed download)
30
33
  --keep-temp keep the typst temp dir on failure (for debugging)
@@ -86,6 +89,7 @@ async function main(argv) {
86
89
  format: { type: 'string' },
87
90
  out: { type: 'string' },
88
91
  config: { type: 'string' },
92
+ variant: { type: 'string' },
89
93
  title: { type: 'string' },
90
94
  author: { type: 'string' },
91
95
  force: { type: 'boolean' },
@@ -131,6 +135,7 @@ async function main(argv) {
131
135
  foundation: values.foundation ?? null,
132
136
  out: values.out ?? null,
133
137
  config: values.config ?? null,
138
+ variant: values.variant ?? null,
134
139
  typstBinary: values['typst-binary'] ?? null,
135
140
  keepTemp: !!values['keep-temp'],
136
141
  yes: !!values.yes,
@@ -17,10 +17,18 @@ import { detectConfigFile, CONFIG_FILE_NAMES } from '../document-yml.js'
17
17
  import { detectBareContent, materializeDocumentYml } from '../materialize.js'
18
18
  import { findCatalogEntry } from '../catalog.js'
19
19
 
20
+ // `--variant book` → `book.yml`; `--variant print.yaml` → `print.yaml`.
21
+ // The name is used verbatim — no `document-` prefixing — with a `.yml`
22
+ // extension assumed when none is given. Returns null when unset.
23
+ export function variantToConfigFile(variant) {
24
+ if (!variant) return null
25
+ return /\.ya?ml$/i.test(variant) ? variant : `${variant}.yml`
26
+ }
27
+
20
28
  // Bare-folder default: the book foundation's `article` genre — a clean
21
29
  // single-column A4 paper, the natural shape for a loose folder of markdown.
22
30
  const DEFAULT_FOUNDATION =
23
- findCatalogEntry('article')?.foundation?.ref ?? '@uniweb/book@0.4.1'
31
+ findCatalogEntry('article')?.foundation?.ref ?? '@uniweb/book@0.4.2'
24
32
 
25
33
  // "durable-structured-systems" → "Durable Structured Systems"
26
34
  function titleize(slug) {
@@ -98,7 +106,7 @@ async function offerToMaterialize({ sitePath, format, foundation, yes }) {
98
106
  return true
99
107
  }
100
108
 
101
- export async function compileCommand({ dir, format = null, foundation = null, out = null, config = null, typstBinary = null, keepTemp = false, yes = false, verbose = false } = {}) {
109
+ export async function compileCommand({ dir, format = null, foundation = null, out = null, config = null, variant = null, typstBinary = null, keepTemp = false, yes = false, verbose = false } = {}) {
102
110
  if (!dir) {
103
111
  process.stderr.write('error: `compile` requires a directory argument\n')
104
112
  process.stderr.write('usage: unipress compile <dir> [--format <fmt>] [--foundation <ref>] [--out <path>] [--config <path>] [--typst-binary <path>] [--keep-temp] [--yes] [--verbose]\n')
@@ -106,7 +114,9 @@ export async function compileCommand({ dir, format = null, foundation = null, ou
106
114
  }
107
115
 
108
116
  const sitePath = resolve(dir)
109
- if (existsSync(sitePath) && !detectConfigFile(sitePath)) {
117
+ // `--variant <name>` names an explicit config; don't offer to generate
118
+ // one (loadContent throws a clear error if the named file is missing).
119
+ if (!variant && existsSync(sitePath) && !detectConfigFile(sitePath)) {
110
120
  const proceed = await offerToMaterialize({ sitePath, format, foundation, yes })
111
121
  if (!proceed) process.exit(1)
112
122
  }
@@ -121,6 +131,7 @@ export async function compileCommand({ dir, format = null, foundation = null, ou
121
131
  foundationRef: foundation,
122
132
  outPath: out,
123
133
  configPath: config,
134
+ documentConfig: variantToConfigFile(variant),
124
135
  typstBinaryPath: typstBinary,
125
136
  keepTemp,
126
137
  onProgress
package/src/compile.js CHANGED
@@ -52,10 +52,13 @@ export async function compile({
52
52
  typstBinaryPath: cliTypstBinaryPath = null,
53
53
  keepTemp = false,
54
54
  configPath: cliConfigPath = null,
55
+ documentConfig = null,
55
56
  onProgress = () => {}
56
57
  } = {}) {
57
58
  onProgress('loading content...')
58
- const { content, sitePath, configFile } = await loadContent(dir)
59
+ const { content, sitePath, configFile } = await loadContent(dir, {
60
+ configFile: documentConfig
61
+ })
59
62
  onProgress(` ${content.pages.length} page(s) from ${sitePath} (${configFile})`)
60
63
 
61
64
  // A config exists but nothing was collected — almost always the markdown
@@ -8,8 +8,8 @@
8
8
  // image asset-processor (which eagerly loads sharp's native binding and
9
9
  // blows up in a `bun build --compile` binary).
10
10
 
11
- import { existsSync } from 'node:fs'
12
- import { resolve, join } from 'node:path'
11
+ import { existsSync, readdirSync } from 'node:fs'
12
+ import { resolve, join, basename } from 'node:path'
13
13
  import { collectSiteContent, processCollections } from '@uniweb/build/content'
14
14
  import { detectConfigFile, CONFIG_FILE_NAMES } from './document-yml.js'
15
15
  import { ContentDirectoryError, DocumentYmlError } from './errors.js'
@@ -121,6 +121,21 @@ async function resolveLocalCollections(siteContent, sitePath) {
121
121
  }
122
122
  }
123
123
 
124
+ // Discoverability aid: when a named `--variant` config is missing, list the
125
+ // YAML configs that ARE present so a typo (or a forgotten `.yml`) is easy to
126
+ // spot and fix.
127
+ function listConfigsHint(sitePath) {
128
+ try {
129
+ const ymls = readdirSync(sitePath).filter(
130
+ (f) => f.endsWith('.yml') || f.endsWith('.yaml'),
131
+ )
132
+ if (!ymls.length) return ''
133
+ return `\navailable configs here: ${ymls.join(', ')}`
134
+ } catch {
135
+ return ''
136
+ }
137
+ }
138
+
124
139
  export async function loadContent(dir, options = {}) {
125
140
  const sitePath = resolve(dir)
126
141
 
@@ -128,17 +143,39 @@ export async function loadContent(dir, options = {}) {
128
143
  throw new ContentDirectoryError(`content directory does not exist: ${sitePath}`)
129
144
  }
130
145
 
131
- const configFile = detectConfigFile(sitePath)
132
- if (!configFile) {
133
- throw new DocumentYmlError(
134
- `no ${CONFIG_FILE_NAMES.PRIMARY} (or ${CONFIG_FILE_NAMES.FALLBACK}) found in ${sitePath}`
135
- )
146
+ // An explicit config name (from `--variant`) selects an alternate
147
+ // top-level config inside the content dir — e.g. a `document-book.yml`
148
+ // beside the default `document.yml`. It must exist; we don't fall back
149
+ // to auto-detection when the user named one. Otherwise auto-detect
150
+ // `document.yml` / `site.yml`.
151
+ let configFile
152
+ if (options.configFile) {
153
+ configFile = options.configFile
154
+ if (!existsSync(join(sitePath, configFile))) {
155
+ throw new DocumentYmlError(
156
+ `variant config not found: ${join(sitePath, configFile)}${listConfigsHint(sitePath)}`
157
+ )
158
+ }
159
+ } else {
160
+ configFile = detectConfigFile(sitePath)
161
+ if (!configFile) {
162
+ throw new DocumentYmlError(
163
+ `no ${CONFIG_FILE_NAMES.PRIMARY} (or ${CONFIG_FILE_NAMES.FALLBACK}) found in ${sitePath}`
164
+ )
165
+ }
136
166
  }
137
167
 
168
+ // unipress is a document tool: read any config that isn't the `site.yml`
169
+ // dogfood fallback with the document profile (content/ + folder mode +
170
+ // `content:` ordering), regardless of the file's name. So a variant can
171
+ // be named freely (book.yml, print.yml) and still build as a document.
172
+ const profile = basename(configFile).startsWith('site') ? 'site' : 'document'
173
+
138
174
  let content
139
175
  try {
140
176
  content = await collectSiteContent(sitePath, {
141
177
  configFile,
178
+ profile,
142
179
  foundationPath: options.foundationPath
143
180
  })
144
181
  } catch (err) {
@@ -72,8 +72,8 @@ function publicUrl(name, version) {
72
72
  }
73
73
 
74
74
  const BOOK_FOUNDATION = {
75
- ref: '@uniweb/book@0.4.1',
76
- source: { url: publicUrl('book', '0.4.1') },
75
+ ref: '@uniweb/book@0.4.2',
76
+ source: { url: publicUrl('book', '0.4.2') },
77
77
  }
78
78
 
79
79
  const DATA_FOUNDATION = {