ultimate-jekyll-manager 1.7.2 → 1.8.0

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.
Files changed (85) hide show
  1. package/.claude/scheduled_tasks.lock +1 -0
  2. package/CHANGELOG.md +61 -1
  3. package/CLAUDE.md +36 -15
  4. package/README.md +4 -2
  5. package/TODO-AUTH-TESTING.md +1 -1
  6. package/dist/assets/themes/newsflash/README.md +58 -0
  7. package/dist/assets/themes/newsflash/_config.scss +138 -0
  8. package/dist/assets/themes/newsflash/_theme.js +27 -0
  9. package/dist/assets/themes/newsflash/_theme.scss +37 -0
  10. package/dist/assets/themes/newsflash/css/base/_mixins.scss +50 -0
  11. package/dist/assets/themes/newsflash/css/base/_root.scss +134 -0
  12. package/dist/assets/themes/newsflash/css/base/_typography.scss +49 -0
  13. package/dist/assets/themes/newsflash/css/base/_utilities.scss +58 -0
  14. package/dist/assets/themes/newsflash/css/components/_badges.scss +65 -0
  15. package/dist/assets/themes/newsflash/css/components/_buttons.scss +139 -0
  16. package/dist/assets/themes/newsflash/css/components/_cards.scss +52 -0
  17. package/dist/assets/themes/newsflash/css/components/_editorial.scss +182 -0
  18. package/dist/assets/themes/newsflash/css/components/_forms.scss +75 -0
  19. package/dist/assets/themes/newsflash/css/components/_infinite-scroll.scss +102 -0
  20. package/dist/assets/themes/newsflash/css/components/_panels.scss +91 -0
  21. package/dist/assets/themes/newsflash/css/components/_ticker.scss +70 -0
  22. package/dist/assets/themes/newsflash/css/layout/_general.scss +264 -0
  23. package/dist/assets/themes/newsflash/css/layout/_navigation.scss +164 -0
  24. package/dist/assets/themes/newsflash/js/initialize-tooltips.js +20 -0
  25. package/dist/assets/themes/newsflash/js/masthead-scroll.js +29 -0
  26. package/dist/assets/themes/newsflash/pages/404/index.scss +27 -0
  27. package/dist/assets/themes/newsflash/pages/about/index.scss +70 -0
  28. package/dist/assets/themes/newsflash/pages/blog/index.scss +17 -0
  29. package/dist/assets/themes/newsflash/pages/blog/post.js +29 -0
  30. package/dist/assets/themes/newsflash/pages/blog/post.scss +164 -0
  31. package/dist/assets/themes/newsflash/pages/index.scss +159 -0
  32. package/dist/assets/themes/newsflash/pages/pricing/index.scss +194 -0
  33. package/dist/assets/themes/newsflash/pages/test/libraries/layers/index.js +9 -0
  34. package/dist/assets/themes/newsflash/pages/test/libraries/layers/index.scss +7 -0
  35. package/dist/commands/blogify.js +6 -3
  36. package/dist/commands/test.js +34 -5
  37. package/dist/defaults/CLAUDE.md +17 -4
  38. package/dist/defaults/dist/_includes/core/pricing/resolve-plan.html +59 -0
  39. package/dist/defaults/dist/_includes/themes/classy/frontend/sections/footer.html +20 -3
  40. package/dist/defaults/dist/_layouts/themes/classy/admin/core/minimal-viewport-locked.html +1 -1
  41. package/dist/defaults/dist/_layouts/themes/classy/admin/core/minimal.html +1 -1
  42. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/pricing.html +5 -40
  43. package/dist/defaults/dist/_layouts/themes/neobrutalism/frontend/pages/pricing.html +33 -34
  44. package/dist/defaults/dist/_layouts/themes/newsflash/frontend/core/base.html +61 -0
  45. package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/404.html +86 -0
  46. package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/about.html +353 -0
  47. package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/blog/categories/category.html +105 -0
  48. package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/blog/categories/index.html +93 -0
  49. package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/blog/index.html +373 -0
  50. package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/blog/post.html +289 -0
  51. package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/blog/tags/index.html +90 -0
  52. package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/blog/tags/tag.html +107 -0
  53. package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/contact.html +340 -0
  54. package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/index.html +522 -0
  55. package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/pricing.html +485 -0
  56. package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/team/index.html +207 -0
  57. package/dist/defaults/dist/_layouts/themes/newsflash/frontend/pages/team/member.html +134 -0
  58. package/dist/defaults/test/README.md +4 -0
  59. package/dist/gulp/tasks/jekyll.js +4 -2
  60. package/dist/test/runner.js +50 -3
  61. package/dist/test/suites/build/attach-log-file.test.js +102 -0
  62. package/dist/test/suites/build/theme-contract.test.js +173 -0
  63. package/dist/test/utils/extended-mode-warning.js +13 -0
  64. package/dist/utils/attach-log-file.js +70 -43
  65. package/docs/appearance.md +1 -0
  66. package/docs/assets.md +9 -0
  67. package/docs/audit.md +27 -7
  68. package/docs/build-system.md +57 -0
  69. package/docs/common-mistakes.md +15 -0
  70. package/docs/{project-structure.md → directory-structure.md} +1 -1
  71. package/docs/environment-detection.md +1 -1
  72. package/docs/javascript-libraries.md +38 -1
  73. package/docs/layouts-and-pages.md +146 -0
  74. package/docs/local-development.md +1 -8
  75. package/docs/logging.md +30 -0
  76. package/docs/migration.md +131 -0
  77. package/docs/no-inline-scripts.md +304 -0
  78. package/docs/purgecss.md +164 -0
  79. package/docs/seo.md +131 -4
  80. package/docs/templating.md +23 -0
  81. package/docs/test-boot-layer.md +1 -1
  82. package/docs/test-framework.md +56 -8
  83. package/docs/themes.md +254 -13
  84. package/logs/test.log +111 -0
  85. package/package.json +1 -1
package/docs/themes.md CHANGED
@@ -16,6 +16,14 @@ Shipped themes live in [src/assets/themes/](../src/assets/themes/):
16
16
  fallback source** (see below).
17
17
  - **`neobrutalism/`** — bold high-contrast theme (hard borders, offset shadows,
18
18
  zero radius). A worked example of a second theme.
19
+ - **`newsflash/`** — editorial news-site theme (paper + ink + vermilion, serif
20
+ headlines, live ticker, reading progress). The worked example of a
21
+ **genre-specific** theme: news-native frontmatter defaults, news-purposed
22
+ homepage sections (`latest`, `rundown`, `desks`, `more_stories`), membership-tier pricing, desk/topic
23
+ archives (blog categories + tags), and a newsroom masthead (team + reporter
24
+ profile pages). Functional pages (download, feedback, updates, auth, account)
25
+ intentionally ride the classy fallback — their structure is the feature, and
26
+ the theme CSS restyles them.
19
27
  - **`_template/`** — a copy-paste starter for new themes (the `_` prefix excludes
20
28
  it from selection).
21
29
 
@@ -89,13 +97,147 @@ adopts your look. Override a layout only when the **structure itself** must diff
89
97
  The `neobrutalism` theme demonstrates both. It restyles classy's markup everywhere
90
98
  EXCEPT the homepage and pricing page, where it ships genuinely different structure
91
99
  (asymmetric split hero, offset showcase rows, oversized color-block stats) at
92
- `_layouts/themes/neobrutalism/frontend/pages/{index,pricing}.html`. **Critically,
93
- those overrides reuse the SAME `page.resolved.*` frontmatter data contract as
94
- classy's versions** same `hero`, `pricing`, `stats`, `cta` keys, same
95
- price-resolution Liquid so a consumer's existing page frontmatter keeps working;
96
- only the HTML that renders the data changes. When you override a page layout, copy
97
- classy's frontmatter block and preserve any data-resolution Liquid; restructure
98
- only the markup below the front matter.
100
+ `_layouts/themes/neobrutalism/frontend/pages/{index,pricing}.html`. When you
101
+ override a page layout, **preserve any data-resolution Liquid** (the pricing
102
+ product-matching, paginator loops, `uj_post`/`uj_member` resolution) and keep the
103
+ **universal section keys** (`hero`, `cta`, `stats`, `faqs`, `testimonials`,
104
+ `newsletter`, `trusted_by`, `pricing.plans`) so a consumer's existing page
105
+ frontmatter keeps working across theme swaps but write your own defaults and
106
+ structure for everything else (next section).
107
+
108
+ #### Frontmatter defaults are part of the theme's identity
109
+
110
+ Every theme page layout ships **default frontmatter** that renders when a
111
+ consumer page doesn't override it. Those defaults are not filler — they are the
112
+ theme's out-of-the-box voice, and **they MUST be written for the theme's genre,
113
+ NOT copied from classy.** Themes serve different purposes: classy/neobrutalism
114
+ are SaaS-product themes, `newsflash` is a news-site theme. A new theme's default
115
+ copy, section names, and demo data should read like the kind of site the theme
116
+ is for.
117
+
118
+ Concretely, when authoring a theme's page layouts:
119
+
120
+ 1. **Write genre-native default values for every key.** A news theme's homepage
121
+ hero pitches the publication ("News with a pulse", "Read the latest" →
122
+ `/blog`), its pricing page sells reader memberships (`Reader` / `Supporter` /
123
+ `Insider` tiers funding the journalism), its contact page has a tips line and
124
+ a corrections subject — not "Technical support" and "API access". See
125
+ `_layouts/themes/newsflash/frontend/pages/*.html` for the reference example.
126
+ 2. **Keep universal keys universal.** Concepts that exist on any site keep the
127
+ shared names — `hero`, `cta`, `stats`, `faqs`, `testimonials`, `newsletter`,
128
+ `trusted_by`, and the `pricing` engine block — so consumer overrides survive
129
+ a theme swap.
130
+ 3. **Genre-specific sections get genre-specific keys.** When a section only
131
+ makes sense for the theme's genre, name the key for what it means there
132
+ instead of force-fitting classy's vocabulary: newsflash's homepage replaces
133
+ classy's `showcase`/`features` with `latest` (the front-page post feed),
134
+ `rundown` (the newsroom's numbered playbook), `desks` (coverage areas), and
135
+ `more_stories` (extra story tiles). Consumers customizing those sections
136
+ write frontmatter against the active theme's contract — document the keys
137
+ in the layout's frontmatter comments.
138
+ 4. **Never invent parallel resolution logic.** Whatever the keys are called,
139
+ the Liquid that resolves posts, members, and pagination is copied from
140
+ classy verbatim — and the plan-pricing math is not even copied: every
141
+ theme's pricing layout calls the shared
142
+ `{% include core/pricing/resolve-plan.html plan=plan %}` (product lookup,
143
+ monthly/annual precedence, per-unit math) and renders the variables it
144
+ assigns (`_plan_monthly`, `_plan_annually`, `monthly_price_per_unit`,
145
+ `annual_price_per_unit`, `_config_product`). Only the data defaults and
146
+ presentation change per theme.
147
+
148
+ #### The pricing page has a JS contract too
149
+
150
+ The pricing page is the one page whose content is **dynamically driven at
151
+ runtime**: the framework page module `src/assets/js/pages/pricing/index.js`
152
+ runs on every theme's `/pricing` and queries the DOM for the billing toggle,
153
+ price swapping, checkout routing, the current-plan indicator, and the
154
+ flash-sale promo banner. A theme that overrides the pricing layout MUST keep
155
+ these hooks (restyle them freely — the ids, classes, and data attributes are
156
+ the contract):
157
+
158
+ | Hook | What the framework JS does with it |
159
+ |---|---|
160
+ | `input[name="billing"]` radios with `data-billing="monthly"` / `"annually"` (one `checked`) | source of truth for the billing toggle |
161
+ | `.amount`, `.billing-info`, `.price-per-unit` — each carrying `data-monthly` + `data-annually` | text content swapped when the toggle changes |
162
+ | `button[data-plan-id="<plan id>"]` inside a `.card` | click → `/payment/checkout?product=<id>` (`enterprise` → `/contact`); the current-plan indicator disables + relabels the signed-in user's active plan button |
163
+ | plan name element matching `.card-title`, `.h2`, or `.h3` inside the card | plan name for add-to-cart analytics (falls back to the plan id) |
164
+ | `#pricing-promo-banner` (shipped with the `hidden` attribute) containing `#pricing-promo-badge` / `#pricing-promo-text` / `#pricing-promo-countdown` / `#pricing-promo-code` | flash-sale banner: the JS reveals it, fills in the rotating sale name + countdown, and pushes `.navbar-wrapper` + `main > section:first-of-type` down to make room |
165
+
166
+ Two behaviors worth knowing:
167
+
168
+ - The JS swaps active/inactive **button classes** on the toggle only when the
169
+ radios live inside a `.btn-group` (classy's structure). A custom toggle
170
+ (newsflash's `.billing-toggle`) skips that gracefully — style the active
171
+ state in CSS via `.btn-check:checked + label` instead.
172
+ - Omitting a hook fails **silently**, not loudly — a missing promo banner just
173
+ never appears, a missing `.card-title` quietly degrades analytics. Diff your
174
+ pricing layout against classy's hooks before calling the theme done.
175
+
176
+ The hooks (and the rest of the theme conventions: entry files, `$avatar-sizes`,
177
+ `[ site.theme.id ]` bracket parents, no theme-prefixed classes, no inline
178
+ scripts, page-asset shapes) are enforced by the build-layer **theme-contract
179
+ test** — `npx mgr test mgr:build/theme-contract` — which globs every theme, so
180
+ a new theme is covered the moment it lands. It caught neobrutalism's missing
181
+ promo banner + `.card-title` the day it was written.
182
+
183
+ #### Theme chrome: inherit classy's nav + footer, restyle via CSS
184
+
185
+ The site chrome (masthead/nav + footer) resolves automatically: the
186
+ `jsonToHtml` task generates wrapper includes that dispatch to
187
+ `themes/<active-id>/frontend/sections/*.html` with the consumer's
188
+ `nav.json`/`footer.json` data, and `copyFallbackThemeFiles()` supplies classy's
189
+ version (with `themes/classy/` paths rewritten to your namespace) when the
190
+ theme doesn't ship one.
191
+
192
+ **Inherit by default — do NOT fork chrome includes.** The chrome's *identity*
193
+ comes from theme CSS, not from forked markup: newsflash's sticky blurred-paper
194
+ masthead and editorial ink-slab footer are achieved entirely in
195
+ `css/layout/_navigation.scss` + `_general.scss` against classy's inherited
196
+ markup (serif wordmark sizing, `.avatar { display: none }`, panel-color
197
+ repaints of the `.link-muted`/`.text-body` utilities, volt column heads). A
198
+ forked include that only re-skins is a copy that silently drifts every time
199
+ classy's chrome gets a fix — newsflash's nav fork was deleted for exactly this
200
+ reason after diverging from classy by nothing but a comment.
201
+
202
+ Fork a chrome include ONLY when the *structure* genuinely diverges (different
203
+ element order, removed/added blocks that CSS cannot express). If you do:
204
+
205
+ 1. **Keep the data contract** — render the same `data.logo` / `data.links` /
206
+ `data.actions` / `data.socials` / `data.legal` / `data.copyright` shapes from
207
+ `nav.json`/`footer.json` so consumer config works across theme swaps.
208
+ 2. **Reference your own theme namespace** for nested includes (e.g.
209
+ `{% include themes/<id>/global/sections/account.html %}`) — the fallback
210
+ copies classy's file into your namespace when you don't ship one.
211
+ 3. **The footer MUST include the appearance picker** (see below) and the
212
+ language dropdown.
213
+
214
+ #### The appearance picker is required in every footer
215
+
216
+ Every theme footer includes the appearance dropdown — a pure drop-in block; all
217
+ logic is handled framework-side via `data-appearance-*` attributes (see
218
+ [docs/appearance.md](appearance.md)). The toggle button is **icon-only**: the
219
+ mode icons swap via `data-appearance-icon`, and there is deliberately NO
220
+ `data-appearance-current` text label in the button (the words live in the menu
221
+ items):
222
+
223
+ ```html
224
+ <div class="dropup uj-appearance-dropdown">
225
+ <button class="btn btn-sm btn-outline-adaptive dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-label="Appearance">
226
+ <span data-appearance-icon="light" hidden>{% uj_icon "sun", "fa-sm" %}</span>
227
+ <span data-appearance-icon="dark" hidden>{% uj_icon "moon-stars", "fa-sm" %}</span>
228
+ <span data-appearance-icon="system" hidden>{% uj_icon "circle-half-stroke", "fa-sm" %}</span>
229
+ </button>
230
+ <ul class="dropdown-menu">
231
+ <li><button class="dropdown-item" type="button" data-appearance-set="light">Light</button></li>
232
+ <li><button class="dropdown-item" type="button" data-appearance-set="dark">Dark</button></li>
233
+ <li><button class="dropdown-item" type="button" data-appearance-set="system">System</button></li>
234
+ </ul>
235
+ </div>
236
+ ```
237
+
238
+ Classy's footer ships it next to the language dropdown, so themes inheriting
239
+ the fallback footer get it for free; themes with custom footers must include
240
+ it themselves.
99
241
 
100
242
  #### No theme-prefixed classes — use universal class names
101
243
 
@@ -263,7 +405,8 @@ Get this distinction right or you'll either duplicate plumbing or fight override
263
405
  |---|---|---|
264
406
  | Core behavior CSS | [src/assets/css/core/](../src/assets/css/core/) | animations, alerts, lazy-loading shimmer, cookie consent, bindings skeletons, social sharing. Injected for **every** theme by `ultimate-jekyll-manager.scss`. |
265
407
  | Bootstrap extensions | [src/assets/themes/bootstrap/overrides/](../src/assets/themes/bootstrap/overrides/) | avatars, color-shades, soft-colors, adaptive buttons, spacing, link/typography utilities. Each theme pulls these in via `@import '../bootstrap/overrides'` at the end of its `_theme.scss`. |
266
- | Page layouts/includes | classy theme + fallback copy | the ~40 frontend/backend/admin layouts and nav/footer/account includes. |
408
+ | Page layouts/includes (fallback) | classy theme + fallback copy | the ~40 frontend/backend/admin layouts and ALL includes a theme doesn't define — including the nav + footer chrome, which themes inherit and restyle via CSS (see [Theme chrome](#theme-chrome-inherit-classys-nav--footer-restyle-via-css)). |
409
+ | Pricing math | [_includes/core/pricing/resolve-plan.html](../src/defaults/dist/_includes/core/pricing/resolve-plan.html) | the plan price-resolution Liquid (product lookup, monthly/annual precedence, per-unit math). Every theme's pricing layout calls `{% include core/pricing/resolve-plan.html plan=plan %}` and renders the assigned variables — never re-implement the math. |
267
410
  | Bootstrap class contract | `bootstrap/scss` | the markup + class names (`.btn`, `.card`, `.navbar`, `.form-control`). Themes **restyle** these classes; they don't invent new markup. |
268
411
 
269
412
  ### Per-theme — this IS the theme's job (and SHOULD differ between themes)
@@ -272,8 +415,12 @@ Get this distinction right or you'll either duplicate plumbing or fight override
272
415
  - `_root.scss` — SCSS → CSS-variable bridge for light/dark.
273
416
  - Component SCSS — how `.btn`/`.card`/`.form-control`/`.navbar` actually look.
274
417
  - `_theme.js` — expose Bootstrap + run theme behaviors on DOM ready.
418
+ - Chrome LOOK via CSS — masthead/footer restyling in `css/layout/` against the
419
+ inherited classy chrome markup (fork the include itself only on real
420
+ structural divergence — see [Theme chrome](#theme-chrome-inherit-classys-nav--footer-restyle-via-css)).
275
421
  - *(optional)* Page-layout overrides under `_layouts/themes/<id>/...` — for pages
276
- whose structure must differ (reuse classy's frontmatter data contract).
422
+ whose structure must differ (keep the resolution Liquid + universal keys, write
423
+ genre-native defaults).
277
424
  - *(optional)* Theme page CSS at `pages/<path>/index.scss` — additive per-page
278
425
  styles for those overridden layouts.
279
426
  - *(optional)* Theme page JS at `pages/<path>/index.js` — additive per-page behavior
@@ -343,6 +490,80 @@ One more: a generic `a:hover { color: … }` also paints **button** labels (butt
343
490
  `<a>`). Guard it with `a.btn:hover { color: var(--bs-btn-color); }` so buttons keep
344
491
  their own (frozen) text color. Nav/dropdown/footer links already win on specificity.
345
492
 
493
+ ### Page bundles re-emit Bootstrap — double your selectors (gotcha)
494
+
495
+ Every theme **page** bundle (`pages/<path>/index.<id>.bundle.css`) compiles
496
+ standalone via `@use 'config'`, so it contains a full copy of Bootstrap — and it
497
+ loads AFTER `main.bundle.css`. Any main-bundle rule that ties Bootstrap's
498
+ specificity loses on pages that ship page CSS: Bootstrap's re-emitted
499
+ `:root`/`[data-bs-theme=dark]` variable blocks clobber a theme's single-selector
500
+ `_root.scss` bridge (dark mode reverts to Bootstrap gray), and re-emitted
501
+ `.btn`/`.btn-outline-*` rules clobber single-class button overrides.
502
+
503
+ **The fix is doubled selectors** in the theme's structural rules so they win on
504
+ specificity regardless of load order:
505
+
506
+ ```scss
507
+ :root:root, [data-bs-theme="light"][data-bs-theme="light"] { /* light vars */ }
508
+ [data-bs-theme="dark"][data-bs-theme="dark"] { /* dark vars */ }
509
+ .btn.btn { /* press/lift system */ }
510
+ [class*="btn-outline-"][class*="btn-outline-"] { /* ghost buttons */ }
511
+ .dropdown-menu.dropdown-menu { /* panel inset (Bootstrap re-emits padding-x: 0) */ }
512
+ ```
513
+
514
+ See newsflash's `_root.scss` + `_buttons.scss` (and neobrutalism's `.btn.btn`)
515
+ for reference treatments.
516
+
517
+ The same trap applies to **type metrics**: Bootstrap's re-emitted `.display-*`
518
+ (`font-weight: 300`), `.lead` (`font-weight: 300`), and `body` rules clobber
519
+ main-bundle element-rule overrides on any page that ships page CSS — headings
520
+ silently go thin on exactly those pages. For values Bootstrap owns a variable
521
+ for, **set the variable in the config `@forward ... with (...)` block instead
522
+ of writing an element rule** (`$display-font-weight`, `$display-line-height`,
523
+ `$lead-font-weight`, `$headings-line-height`, `$line-height-base`, …) — then
524
+ every Bootstrap copy compiles the right value natively and there is no
525
+ specificity war at all. Element rules in `_typography.scss` are only for
526
+ props Bootstrap has no variable for (optical sizing, letter-spacing,
527
+ `text-wrap`, font smoothing).
528
+
529
+ ### Remap the `--bs-*-rgb` companions too (gotcha)
530
+
531
+ Bootstrap's `.bg-body`, `.bg-body-secondary`, `.bg-body-tertiary`,
532
+ `.text-body`, `.text-body-secondary` utilities paint from
533
+ `rgba(var(--bs-*-rgb), opacity)` — NOT the hex variables. A theme that remaps
534
+ `--bs-secondary-bg` but not `--bs-secondary-bg-rgb` gets Bootstrap's default
535
+ gray triplets bleeding through every `.bg-body-*` surface (most visibly in dark
536
+ mode). When the `_root.scss` bridge remaps a surface/color var, **always remap
537
+ its `-rgb` companion** in the same block:
538
+
539
+ ```scss
540
+ --bs-secondary-bg: #{$nf-paper-2};
541
+ --bs-secondary-bg-rgb: #{red($nf-paper-2)}, #{green($nf-paper-2)}, #{blue($nf-paper-2)};
542
+ ```
543
+
544
+ Also note: shared includes may put `!important` utilities (e.g.
545
+ `.bg-body-secondary` on the footer) on elements a theme wants to restyle — the
546
+ override needs `!important` AND equal-or-higher specificity
547
+ (`footer.bg-body-secondary`, not bare `footer`).
548
+
549
+ ### Derive dark-mode brand remaps from `$primary` (gotcha)
550
+
551
+ When a dark block remaps `--bs-primary` / `--bs-link-color` (e.g. to brighten
552
+ the brand color for contrast on dark surfaces), **derive the value from the
553
+ compile-time `$primary`** — never hardcode the theme's stock color. Consumers
554
+ override `$primary` via `main.scss`'s `with (...)` block; a hardcoded dark
555
+ remap would snap their brand back to the theme's color the moment dark mode
556
+ engages (light honors the override, dark ignores it). newsflash's pattern:
557
+
558
+ ```scss
559
+ // Stock vermilion keeps its hand-tuned brightening; any other brand color
560
+ // gets a generic white-mix lift.
561
+ $nf-primary-dark-mode: if($primary == $nf-vermilion, $nf-vermilion-dark-mode, mix(white, $primary, 15%));
562
+ ```
563
+
564
+ The theme's own identity accents (newsflash's `--nf-vermilion` used in cover-art
565
+ gradients) are exempt — those ARE the theme, not the consumer's brand.
566
+
346
567
  ---
347
568
 
348
569
  ## 🚨 BOOTSTRAP-FIRST — NEVER reinvent the wheel
@@ -408,7 +629,14 @@ The GOOD version uses zero custom CSS for layout/buttons — the theme's `_butto
408
629
  6. **Fonts** load via the base layout's `theme.head.content`, NOT a CSS `@import`
409
630
  (avoids render-blocking duplicate loads). To use custom fonts, override
410
631
  `frontend/core/base.html` (see below).
411
- 7. **Validate live, then document.** UJM can't run a dev server build in a
632
+ 7. **Develop in ONE appearance mode; ship with BOTH.** Pick a primary mode
633
+ while building (the consumer's `theme.appearance` default is the natural
634
+ choice) and get the design right there first — splitting attention across
635
+ both modes mid-build doubles every iteration. But a theme is only **done**
636
+ when light AND dark are both validated: the `_root.scss` bridge makes dark
637
+ mode a single remap block, so build the token bridge correctly, then do a
638
+ dedicated both-modes screenshot pass at the end.
639
+ 8. **Validate live, then document.** UJM can't run a dev server — build in a
412
640
  consumer and screenshot (see [Validating](#validating-a-theme)).
413
641
 
414
642
  ---
@@ -436,8 +664,15 @@ Use this for first-party themes like `neobrutalism`.
436
664
  classy fallback supplies the rest. The most common override is
437
665
  `frontend/core/base.html` to load your fonts (neobrutalism overrides just this
438
666
  one file).
439
- 5. **Add a `README.md`** in the theme folder (customization quickstart).
440
- 6. `npm run prepare` (copies `src/`→`dist/`) so consumers see it. Then test in a
667
+ 5. **Write genre-native frontmatter defaults** in every layout you override
668
+ the default copy/sections must match the theme's purpose, not classy's SaaS
669
+ demo data. See [Frontmatter defaults are part of the theme's
670
+ identity](#frontmatter-defaults-are-part-of-the-themes-identity).
671
+ 6. **Restyle the inherited nav + footer chrome via CSS** (`css/layout/`) — do
672
+ NOT fork the chrome includes unless the structure genuinely diverges. See
673
+ [Theme chrome](#theme-chrome-inherit-classys-nav--footer-restyle-via-css).
674
+ 7. **Add a `README.md`** in the theme folder (customization quickstart).
675
+ 8. `npm run prepare` (copies `src/`→`dist/`) so consumers see it. Then test in a
441
676
  consumer (Path: [Validating](#validating-a-theme)).
442
677
 
443
678
  ## Path B — author a theme IN A CONSUMER PROJECT (that project only)
@@ -481,7 +716,10 @@ UJM cannot run a dev server itself (it runs inside a consumer). To verify a them
481
716
  3. Screenshot the key pages (home, pricing, signin, signup) in **both** light and
482
717
  dark (`document.documentElement.setAttribute('data-bs-theme','dark')`) — e.g.
483
718
  via the chrome-devtools MCP. Check the console for errors and that your theme's
484
- "loaded" log appears.
719
+ "loaded" log appears. Developing in one mode is fine (and encouraged — see
720
+ [Authoring conventions](#authoring-conventions-both-paths)); **shipping
721
+ requires both modes validated**, plus a click-through of the footer
722
+ appearance picker to confirm live switching looks right.
485
723
  4. Iterate on SCSS — the consumer's gulp watcher recompiles when UJM's `dist/`
486
724
  changes (if UJM is running `npm start`), or re-run `npm run prepare`.
487
725
 
@@ -495,6 +733,9 @@ all four pages in light + dark.
495
733
 
496
734
  - Theme tokens example: [src/assets/themes/neobrutalism/_config.scss](../src/assets/themes/neobrutalism/_config.scss)
497
735
  - CSS-variable bridge example: [src/assets/themes/neobrutalism/css/base/_root.scss](../src/assets/themes/neobrutalism/css/base/_root.scss)
736
+ - Genre-native frontmatter defaults example: [src/defaults/dist/_layouts/themes/newsflash/frontend/pages/index.html](../src/defaults/dist/_layouts/themes/newsflash/frontend/pages/index.html)
737
+ - Posts-driven theme sections (ticker in base.html, cover-story hero, most-read rail — all guarded for empty `site.posts`): [src/defaults/dist/_layouts/themes/newsflash/](../src/defaults/dist/_layouts/themes/newsflash/)
738
+ - Theme page JS example (blog post reading-progress, flat `asset_path` shape): [src/assets/themes/newsflash/pages/blog/post.js](../src/assets/themes/newsflash/pages/blog/post.js)
498
739
  - Starter: [src/assets/themes/_template/](../src/assets/themes/_template/)
499
740
  - Fallback mechanism: [src/gulp/tasks/distribute.js](../src/gulp/tasks/distribute.js) (`copyFallbackThemeFiles`)
500
741
  - Resolution: [src/gulp/tasks/sass.js](../src/gulp/tasks/sass.js), [src/gulp/tasks/webpack.js](../src/gulp/tasks/webpack.js)
package/logs/test.log ADDED
@@ -0,0 +1,111 @@
1
+ # ujm log — 2026-06-11T08:01:19.096Z — pid=72007
2
+ [01:01:19] 'test': Running tests (layer=all)
3
+ [01:01:19] 'test': Test mode: normal (external APIs skipped)
4
+
5
+ Ultimate Jekyll Manager Tests
6
+
7
+ Framework Tests
8
+ ⤷ attach-log-file — tee stdout/stderr to a file
9
+ ✓ exports the expected surface (0ms)
10
+ ✓ stripAnsi removes color escape codes (0ms)
11
+ hello world
12
+ colored line
13
+ ✓ attach + stdout.write + detach: file contains the writes (1ms)
14
+ ✓ idempotent: attaching twice with same name returns same fd (0ms)
15
+ ✓ attach with falsy name returns null and does nothing (0ms)
16
+ ⤷ CLI alias resolution
17
+ ✓ cli.js exports a Main class (0ms)
18
+ ✓ all expected commands exist on disk (0ms)
19
+ ✓ each command module exports an async function (6ms)
20
+ ⤷ collectTextNodes (utils/collectTextNodes.js)
21
+ ✓ extracts page title (145ms)
22
+ ✓ skips <script> and <style> (1ms)
23
+ ✓ spellcheck dictionary (utils/dictionary.js) (1ms)
24
+ ⤷ expect() matcher set
25
+ ✓ toBe + toEqual basics (0ms)
26
+ ✓ .not negates (0ms)
27
+ ✓ toContain works on arrays and strings (0ms)
28
+ ✓ toThrow catches sync + async throws (0ms)
29
+ ✓ toBeGreaterThan / toBeLessThan (0ms)
30
+ ✓ failing assertions throw AssertionError (0ms)
31
+ ✓ package.json exports resolve to real files in dist/ (0ms)
32
+ ⤷ Logger (src/lib/logger.js)
33
+ ✓ Logger constructor stores name (0ms)
34
+ ✓ Logger exposes log/error/warn/info methods (0ms)
35
+ ✓ Logger.format is chalk (0ms)
36
+ ✓ Logger output goes through console with prefix (0ms)
37
+ ⤷ Manager (build.js) public surface
38
+ ✓ Manager constructor is a function (0ms)
39
+ ✓ static methods match prototype methods (0ms)
40
+ ✓ isBuildMode reflects UJ_BUILD_MODE env (0ms)
41
+ ✓ isQuickMode reflects UJ_QUICK env (0ms)
42
+ ✓ isServer reflects UJ_IS_SERVER env (0ms)
43
+ ✓ getEnvironment maps to development/testing/production (0ms)
44
+ ✓ actLikeProduction is true when isBuildMode OR UJ_AUDIT_FORCE (0ms)
45
+ ✓ getRootPath("package") points at UJM root (1ms)
46
+ ✓ getMemoryUsage returns shape with MB-sized numbers (0ms)
47
+ ✓ getArguments returns object with _ array + boolean defaults (0ms)
48
+ ✓ logger returns object with log/error/warn/info methods (0ms)
49
+ ✓ processBatches processes items in chunks and returns flat results (0ms)
50
+ ⤷ mergeJekyllConfigs (utils/merge-jekyll-configs.js)
51
+ ✓ merges collections from both configs (project additions win) (6ms)
52
+ ✓ dedups defaults by scope key (project wins) (2ms)
53
+ ✓ returns null when there is nothing to merge (1ms)
54
+ ⤷ mode-helpers (isTesting / isDevelopment / isProduction / getVersion)
55
+ ✓ helpers attach to Manager statically AND on prototype (0ms)
56
+ ✓ isTesting reflects UJ_TEST_MODE env (0ms)
57
+ ✓ isDevelopment false / isProduction true when UJ_BUILD_MODE=true (and not testing) (0ms)
58
+ ✓ environments are mutually exclusive — testing wins under UJ_TEST_MODE (0ms)
59
+ ✓ invariant: is*() exactly matches getEnvironment() + mutually exclusive (every scenario) (1ms)
60
+ ✓ getVersion returns a non-empty string when run from a package (0ms)
61
+ ⤷ createTemplateTransform (utils/template-transform.js)
62
+ ✓ replaces [site.theme.id] with config value in .html files (2ms)
63
+ ✓ leaves non-matching extensions untouched (e.g. .css) (0ms)
64
+ ✓ passes directories through untouched (0ms)
65
+ ⤷ node-powertools templating brackets ({} and [])
66
+ ✓ default { } brackets resolve nested keys (0ms)
67
+ ✓ [ ] brackets resolve nested keys when explicitly configured (1ms)
68
+ ✓ [ ] brackets leave Jekyll {{ }} placeholders alone (0ms)
69
+ ⤷ theme contract (structure, swappability, cross-theme JS contracts)
70
+ ✓ _template: entry files + config contract (0ms)
71
+ ✓ classy: entry files + config contract (0ms)
72
+ ✓ neobrutalism: entry files + config contract (0ms)
73
+ ✓ newsflash: entry files + config contract (1ms)
74
+ ✓ _template: layouts swappable, markup clean (1ms)
75
+ ✓ classy: layouts swappable, markup clean (12ms)
76
+ ✓ neobrutalism: layouts swappable, markup clean (2ms)
77
+ ✓ newsflash: layouts swappable, markup clean (3ms)
78
+ ✓ _template: cross-theme JS contracts (0ms)
79
+ ✓ classy: cross-theme JS contracts (1ms)
80
+ ✓ neobrutalism: cross-theme JS contracts (1ms)
81
+ ✓ newsflash: cross-theme JS contracts (0ms)
82
+ ✓ page asset files match a declared asset_path shape (7ms)
83
+ ⤷ validateYAMLFrontMatter (utils/_validate-yaml.js)
84
+ ✓ returns { valid: true } for a file with valid frontmatter (3ms)
85
+ ✓ returns { valid: true } when no frontmatter present (0ms)
86
+ ✓ flags malformed YAML frontmatter as invalid with error message (1ms)
87
+ ⤷ page-layer baseline (DOM + fetch + storage)
88
+ ✓ document is interactive or complete (0ms)
89
+ ✓ fetch() works against the local harness server (7ms)
90
+ ✓ localStorage is available (1ms)
91
+ ⤷ harness globals (window.Configuration + dataset)
92
+ ✓ window.Configuration has brand + theme + web_manager (0ms)
93
+ ✓ document.documentElement.dataset.pagePath is set (0ms)
94
+ ✓ UJ_TEST_MODE is signalled on globalThis (0ms)
95
+ ⤷ prerendered icons template lookup
96
+ ✓ template#prerendered-icons exists and has the test icon (1ms)
97
+ ✓ looking up a missing icon returns null (0ms)
98
+ ⤷ boot tests (consumer _site/)
99
+ ✓ /service-worker.js served with javascript content type (105ms)
100
+ ✓ index.html registers SW and reaches activated state (202ms)
101
+ ✓ SW responds to get-cache-name message with brand-id pattern (116ms)
102
+ ✓ home page renders with title + body content (210ms)
103
+ ✓ /about resolves via Jekyll-style .html fallback (93ms)
104
+ ✓ build.json is served with brand metadata (109ms)
105
+ ✓ CSS bundle served with text/css content type (94ms)
106
+
107
+ Results
108
+ 80 passing
109
+
110
+ Total: 80 tests in 24960ms
111
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-jekyll-manager",
3
- "version": "1.7.2",
3
+ "version": "1.8.0",
4
4
  "description": "Ultimate Jekyll dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "exports": {