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
@@ -0,0 +1,173 @@
1
+ // Theme contract — structural invariants every shipped theme must satisfy
2
+ // (docs/themes.md conventions turned into executable assertions). Globs the
3
+ // theme directories, so a new theme is covered the moment it lands:
4
+ // 1. Entry files exist ($avatar-sizes map, shared bootstrap overrides import)
5
+ // 2. Layouts are swappable (parent refs use [ site.theme.id ] brackets,
6
+ // no theme-prefixed classes in markup, no inline <script> bodies)
7
+ // 3. Cross-theme JS contracts hold (pricing data attributes, blog-post-content)
8
+ // 4. Page asset files use a shape the layouts' asset_path frontmatter declares
9
+ // (wrong shape = silent bundle skip)
10
+
11
+ const path = require('path');
12
+ const jetpack = require('fs-jetpack');
13
+ const glob = require('glob').globSync;
14
+
15
+ const ROOT = path.join(__dirname, '../../../..');
16
+ const ASSETS = path.join(ROOT, 'src/assets/themes');
17
+ const LAYOUTS = path.join(ROOT, 'src/defaults/dist/_layouts/themes');
18
+ const INCLUDES = path.join(ROOT, 'src/defaults/dist/_includes/themes');
19
+
20
+ // _template is held to the asset contract too — it's what theme authors copy.
21
+ // bootstrap is the shared Bootstrap source, not a theme.
22
+ const themes = jetpack.list(ASSETS).filter((t) => t !== 'bootstrap');
23
+
24
+ // Class tokens that would couple markup to one theme (markup must stay
25
+ // swappable; theme prefixes live only in SCSS internals)
26
+ const THEME_PREFIXES = ['nf-', 'nb-', 'classy-', 'newsflash-', 'neobrutalism-', 'broadsheet-', 'template-'];
27
+
28
+ // Markers the framework pricing JS reads — identical across themes by contract
29
+ // (see docs/themes.md "Pricing page JS contract")
30
+ const PRICING_MARKERS = ['data-plan-id', 'billing-info', 'price-per-unit', 'pricing-promo-banner', 'card-title', 'data-monthly'];
31
+
32
+ // All markup files (layouts + includes) for a theme
33
+ function markupFiles(theme) {
34
+ return [
35
+ ...glob(`${LAYOUTS}/${theme}/**/*.html`),
36
+ ...glob(`${INCLUDES}/${theme}/**/*.html`),
37
+ ];
38
+ }
39
+
40
+ // Inline <script> bodies are banned in theme markup (ld+json and src loaders OK)
41
+ function findInlineScript(html) {
42
+ const scripts = html.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/g);
43
+
44
+ for (const [, attrs, body] of scripts) {
45
+ if (attrs.includes('src=')) {
46
+ continue;
47
+ }
48
+ if (attrs.includes('application/ld+json')) {
49
+ continue;
50
+ }
51
+ if (body.trim() === '') {
52
+ continue;
53
+ }
54
+
55
+ return body.trim().slice(0, 80);
56
+ }
57
+
58
+ return null;
59
+ }
60
+
61
+ // Flat page-asset names declared by ANY theme layout's asset_path frontmatter
62
+ // (the fallback means classy's declarations apply to every theme)
63
+ function declaredAssetPaths() {
64
+ const paths = new Set();
65
+
66
+ for (const file of glob(`${LAYOUTS}/*/**/*.html`)) {
67
+ const match = jetpack.read(file).match(/^asset_path:\s*(\S+)/m);
68
+ if (match) {
69
+ paths.add(match[1].replace(/['"]/g, ''));
70
+ }
71
+ }
72
+
73
+ return paths;
74
+ }
75
+
76
+ module.exports = {
77
+ layer: 'build',
78
+ description: 'theme contract (structure, swappability, cross-theme JS contracts)',
79
+ type: 'group',
80
+ tests: [
81
+ ...themes.map((theme) => ({
82
+ name: `${theme}: entry files + config contract`,
83
+ run: (ctx) => {
84
+ const config = jetpack.read(`${ASSETS}/${theme}/_config.scss`);
85
+ const entry = jetpack.read(`${ASSETS}/${theme}/_theme.scss`);
86
+
87
+ ctx.expect(config).toBeTruthy();
88
+ ctx.expect(entry).toBeTruthy();
89
+ ctx.expect(jetpack.exists(`${ASSETS}/${theme}/_theme.js`)).toBeTruthy();
90
+
91
+ // Shared nav/account includes depend on the avatar size map
92
+ ctx.expect(config).toContain('$avatar-sizes');
93
+
94
+ // Universal Bootstrap overrides must close the cascade
95
+ ctx.expect(entry).toContain("@import '../bootstrap/overrides'");
96
+ },
97
+ })),
98
+
99
+ ...themes.map((theme) => ({
100
+ name: `${theme}: layouts swappable, markup clean`,
101
+ run: (ctx) => {
102
+ for (const file of markupFiles(theme)) {
103
+ const html = jetpack.read(file);
104
+ const rel = path.relative(ROOT, file);
105
+
106
+ // Parent layout refs resolve via bracket templating, never a
107
+ // hardcoded theme id (swappability: change theme.id, done)
108
+ const parent = html.match(/^layout:\s*themes\/(.+)$/m);
109
+ if (parent) {
110
+ ctx.expect(`${rel}: ${parent[1]}`).toMatch(/\[\s*site\.theme\.id\s*\]/);
111
+ }
112
+
113
+ // No theme-prefixed classes in markup
114
+ for (const [, classes] of html.matchAll(/class="([^"]*)"/g)) {
115
+ for (const token of classes.split(/\s+/)) {
116
+ const prefixed = THEME_PREFIXES.some((p) => token.startsWith(p));
117
+ ctx.expect(prefixed ? `${rel}: class "${token}"` : '').toBeFalsy();
118
+ }
119
+ }
120
+
121
+ // No inline script bodies
122
+ const inline = findInlineScript(html);
123
+ ctx.expect(inline ? `${rel}: inline <script> "${inline}"` : null).toBeFalsy();
124
+ }
125
+ },
126
+ })),
127
+
128
+ ...themes.map((theme) => ({
129
+ name: `${theme}: cross-theme JS contracts`,
130
+ run: (ctx) => {
131
+ // A theme that ships its own pricing/post layouts must keep the
132
+ // markers the framework JS targets; absent layouts inherit classy's,
133
+ // so the contract holds by definition.
134
+ const pricing = jetpack.read(`${LAYOUTS}/${theme}/frontend/pages/pricing.html`);
135
+ if (pricing) {
136
+ for (const marker of PRICING_MARKERS) {
137
+ ctx.expect(pricing).toContain(marker);
138
+ }
139
+ }
140
+
141
+ const post = jetpack.read(`${LAYOUTS}/${theme}/frontend/pages/blog/post.html`);
142
+ if (post) {
143
+ // Load-bearing for ad injection
144
+ ctx.expect(post).toContain('blog-post-content');
145
+ }
146
+ },
147
+ })),
148
+
149
+ {
150
+ name: 'page asset files match a declared asset_path shape',
151
+ run: (ctx) => {
152
+ const declared = declaredAssetPaths();
153
+
154
+ // Sanity: the derivation itself found the known flat shapes
155
+ ctx.expect(declared.has('blog/post')).toBeTruthy();
156
+
157
+ for (const theme of themes) {
158
+ for (const file of glob(`${ASSETS}/${theme}/pages/**/*.{scss,js}`)) {
159
+ const rel = path.relative(`${ASSETS}/${theme}/pages`, file);
160
+ const base = rel.replace(/\.(scss|js)$/, '');
161
+
162
+ // index.* shapes resolve by page path; flat shapes must be
163
+ // declared by some layout's asset_path or the bundle never loads
164
+ if (path.basename(base) === 'index') {
165
+ continue;
166
+ }
167
+ ctx.expect(declared.has(base) ? '' : `${theme}/pages/${rel}: no layout declares asset_path "${base}"`).toBeFalsy();
168
+ }
169
+ }
170
+ },
171
+ },
172
+ ],
173
+ };
@@ -0,0 +1,13 @@
1
+ // TEST_EXTENDED_MODE warning — SSOT for consistent messaging.
2
+ //
3
+ // Mirrors BEM/BXM/EM: `TEST_EXTENDED_MODE` is the shared, unprefixed env var that opts a
4
+ // test run into hitting REAL external services instead of skipping/stubbing them. Off by
5
+ // default so `npx mgr test` stays fast and offline-safe. Used by the test command (printed to
6
+ // console + teed to logs/test.log).
7
+ const EXTENDED_MODE_WARNING = [
8
+ '⚠️⚠️⚠️ WARNING: TEST_EXTENDED_MODE IS TRUE ⚠️⚠️⚠️',
9
+ 'Tests that hit real external services (network fetches, Firebase via web-manager, live APIs) are ENABLED!',
10
+ 'This will make real network calls against live backends.',
11
+ ];
12
+
13
+ module.exports = { EXTENDED_MODE_WARNING };
@@ -12,75 +12,102 @@
12
12
  // Truncates fresh on each call (O_TRUNC), so a new `npm start` doesn't accumulate stale
13
13
  // lines from the previous run.
14
14
  //
15
- // Idempotent: calling twice with the same path just returns the existing fd.
15
+ // Idempotent: calling twice with the same name on one tee just returns the existing fd.
16
16
  //
17
17
  // Uses synchronous fs.writeSync(fd, ...) rather than createWriteStream(). Reason: gulp tasks
18
18
  // crash via thrown errors that propagate to process.exit, and createWriteStream's internal
19
19
  // buffer was being dropped before the kernel could flush it — so the very lines describing
20
20
  // the crash (the most important ones) never made it to disk. Synchronous writes incur a
21
- // per-line syscall but guarantee the tail of the log survives an immediate exit.
21
+ // per-line syscall but guarantee the tail of the log survives an immediate exit. (This is the
22
+ // key behavioral difference from EM, whose tee is async/stream-based with an awaited detach.)
23
+ //
24
+ // The default export is a process-wide SINGLETON (the common case: a CLI command tees its
25
+ // whole run to one file). `attachLogFile.createTee()` returns an INDEPENDENT tee with its own
26
+ // state. Tees STACK: a later attach() captures the CURRENT `process.stdout.write` (which may
27
+ // already be an outer tee) as its "original", so writes fan out through every layer and
28
+ // detach() restores the exact prior writer in LIFO order. That stacking is what lets the
29
+ // attach-log-file unit test exercise attach/detach on a throwaway instance WITHOUT killing
30
+ // the live singleton tee that's capturing the actual test run — the bug that previously
31
+ // truncated `logs/test.log` to ~9 lines (the test detached the live tee mid-run).
22
32
 
23
33
  const fs = require('fs');
24
34
  const path = require('path');
25
35
 
26
36
  const ANSI_PATTERN = /\x1B\[[0-9;]*[a-zA-Z]/g;
27
37
 
28
- let activeFd = null;
29
- let activePath = null;
30
- let originalStdoutWrite = null;
31
- let originalStderrWrite = null;
38
+ function stripAnsi(s) {
39
+ return String(s).replace(ANSI_PATTERN, '');
40
+ }
32
41
 
33
- function attachLogFile(name) {
34
- // Skip on CI/cloud — controlled by UJ_IS_SERVER env var (set by workflows).
35
- const Manager = require('../build.js');
36
- if (Manager.isServer()) return null;
42
+ // Factory — each call returns an independent tee with its own closure state.
43
+ function createTee() {
44
+ let activeFd = null;
45
+ let activePath = null;
46
+ let originalStdoutWrite = null;
47
+ let originalStderrWrite = null;
37
48
 
38
- if (!name) return null;
49
+ function attach(name) {
50
+ // Skip on CI/cloud — controlled by UJ_IS_SERVER env var (set by workflows).
51
+ const Manager = require('../build.js');
52
+ if (Manager.isServer()) return null;
39
53
 
40
- const abs = path.resolve(process.cwd(), 'logs', `${name}.log`);
54
+ if (!name) return null;
41
55
 
42
- if (activeFd !== null && activePath === abs) return activeFd;
43
- if (activeFd !== null) detach();
56
+ const abs = path.resolve(process.cwd(), 'logs', `${name}.log`);
44
57
 
45
- fs.mkdirSync(path.dirname(abs), { recursive: true });
46
- const fd = fs.openSync(abs, 'w');
58
+ if (activeFd !== null && activePath === abs) return activeFd;
59
+ if (activeFd !== null) detach();
47
60
 
48
- fs.writeSync(fd, `# ujm log — ${new Date().toISOString()} pid=${process.pid}\n`);
61
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
62
+ const fd = fs.openSync(abs, 'w');
49
63
 
50
- originalStdoutWrite = process.stdout.write.bind(process.stdout);
51
- originalStderrWrite = process.stderr.write.bind(process.stderr);
64
+ fs.writeSync(fd, `# ujm log — ${new Date().toISOString()} — pid=${process.pid}\n`);
52
65
 
53
- process.stdout.write = function (chunk, ...rest) {
54
- try { fs.writeSync(fd, stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
55
- return originalStdoutWrite(chunk, ...rest);
56
- };
57
- process.stderr.write = function (chunk, ...rest) {
58
- try { fs.writeSync(fd, stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
59
- return originalStderrWrite(chunk, ...rest);
60
- };
66
+ // Capture whatever the CURRENT writer is — could be the raw stream OR an outer tee.
67
+ // Restoring this exact reference on detach() is what makes stacked tees safe.
68
+ originalStdoutWrite = process.stdout.write.bind(process.stdout);
69
+ originalStderrWrite = process.stderr.write.bind(process.stderr);
61
70
 
62
- activeFd = fd;
63
- activePath = abs;
71
+ process.stdout.write = function (chunk, ...rest) {
72
+ try { fs.writeSync(fd, stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
73
+ return originalStdoutWrite(chunk, ...rest);
74
+ };
75
+ process.stderr.write = function (chunk, ...rest) {
76
+ try { fs.writeSync(fd, stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
77
+ return originalStderrWrite(chunk, ...rest);
78
+ };
64
79
 
65
- return fd;
66
- }
80
+ activeFd = fd;
81
+ activePath = abs;
67
82
 
68
- function detach() {
69
- if (originalStdoutWrite) process.stdout.write = originalStdoutWrite;
70
- if (originalStderrWrite) process.stderr.write = originalStderrWrite;
71
- if (activeFd !== null) {
72
- try { fs.closeSync(activeFd); } catch (e) { /* ignore */ }
83
+ return fd;
73
84
  }
74
- activeFd = null;
75
- activePath = null;
76
- originalStdoutWrite = null;
77
- originalStderrWrite = null;
85
+
86
+ // Restores stdout/stderr and closes the fd. Synchronous — UJM writes synchronously, so the
87
+ // tail is already on disk by the time detach() runs; this just cleans up the handle.
88
+ function detach() {
89
+ if (originalStdoutWrite) process.stdout.write = originalStdoutWrite;
90
+ if (originalStderrWrite) process.stderr.write = originalStderrWrite;
91
+ if (activeFd !== null) {
92
+ try { fs.closeSync(activeFd); } catch (e) { /* ignore */ }
93
+ }
94
+ activeFd = null;
95
+ activePath = null;
96
+ originalStdoutWrite = null;
97
+ originalStderrWrite = null;
98
+ }
99
+
100
+ return { attach, detach };
78
101
  }
79
102
 
80
- function stripAnsi(s) {
81
- return String(s).replace(ANSI_PATTERN, '');
103
+ // Process-wide singleton — the production entry point.
104
+ const singleton = createTee();
105
+
106
+ function attachLogFile(name) {
107
+ return singleton.attach(name);
82
108
  }
83
109
 
84
110
  module.exports = attachLogFile;
85
- module.exports.detach = detach;
111
+ module.exports.detach = singleton.detach;
86
112
  module.exports.stripAnsi = stripAnsi;
113
+ module.exports.createTee = createTee;
@@ -63,3 +63,4 @@ webManager.uj().appearance.clear(); // Clear saved preference
63
63
  - **Module:** `src/assets/js/core/appearance.js` — API and UI handling
64
64
  - **Storage:** Saved under `_manager.appearance.preference` in localStorage
65
65
  - **Test page:** `/test/libraries/appearance`
66
+ - **Footer picker:** every theme footer ships the appearance dropdown next to the language dropdown (classy's footer provides it to fallback themes automatically; themes with custom footers include it themselves — see [docs/themes.md](themes.md#the-appearance-picker-is-required-in-every-footer)). The footer toggle button is **icon-only** — `data-appearance-icon` spans, no `data-appearance-current` text label (the mode words live in the menu items)
package/docs/assets.md CHANGED
@@ -151,6 +151,15 @@ This is the single source of truth for account dropdown menu items. Consuming pr
151
151
 
152
152
  This renders the full account dropdown: avatar button with profile photo, user info header (displayName + email), and the menu items from `account.json`.
153
153
 
154
+ Each dropdown item supports:
155
+
156
+ - `label` — Display text
157
+ - `href` — Link URL (omit for button behavior, e.g. Sign Out)
158
+ - `icon` — Font Awesome icon name
159
+ - `class` — Additional CSS classes
160
+ - `divider: true` — Renders a divider line
161
+ - `attributes` — Array of `[name, value]` pairs (e.g. `data-wm-bind` for visibility)
162
+
154
163
  **Parameters:**
155
164
 
156
165
  | Parameter | Default | Description |
package/docs/audit.md CHANGED
@@ -1,11 +1,31 @@
1
1
  # Audit Workflow
2
2
 
3
- When fixing issues identified by the audit task (`src/gulp/tasks/audit.js`):
3
+ Auditing a UJM site runs in two stages: an AI-powered content pass over the source files, then the automated `npx mgr audit` tool with a systematic fix loop.
4
4
 
5
- 1. Review the audit file location provided
6
- 2. Create a TODO list for each audit category
7
- 3. Read the ENTIRE audit file and plan fixes for each category
8
- 4. Tackle issues incrementally — DO NOT attempt to fix everything at once
9
- 5. Work through one category at a time
5
+ ## Stage 1: Content Audit (source files)
10
6
 
11
- **Remember:** Audit files are large. Systematic, incremental fixes prevent errors and ensure thoroughness.
7
+ 1. **Locate all page source files** `src/pages/**` and `src/_posts/**` (`.html` + `.md`).
8
+ 2. **Enforce content conventions** — headings start with action verbs, sentence case, headline/accent structure (see [seo.md](seo.md#content-writing-rules-applies-to-all-pages)). Skip front matter (`meta.title` etc. are controlled by blueprints/layouts), test pages, and blog posts.
9
+ 3. **Fix spelling and grammar** in body text — skip code blocks, attributes, URLs.
10
+ 4. **XSS / HTML escaping audit** — flag unsafe `innerHTML` assignments; fix with `webManager.utilities().escapeHTML()` (+ `sanitizeURL` for URL sinks). See [xss-prevention.md](xss-prevention.md).
11
+ 5. **Inline `<script>` audit (HARD RULE)** — scan all HTML under `src/` for inline script bodies and move them per the playbook in [no-inline-scripts.md](no-inline-scripts.md).
12
+ 6. **Summarize** — list files scanned and fixes applied before moving to stage 2.
13
+
14
+ ## Stage 2: Automated Audit (`npx mgr audit`)
15
+
16
+ 1. **Ask the user** whether to run the audit or whether they've already run it; run `npx mgr audit` if needed.
17
+ 2. **Locate results** in `.temp/audit/` and read every file COMPLETELY — audit files are large; don't plan from a skim.
18
+ 3. **Create a TODO list** — break fixes into atomic tasks, organized by category and priority.
19
+ 4. **Fix systematically** — one issue at a time: mark in-progress → navigate → understand root cause → fix → verify → mark complete. Work one category at a time; do NOT attempt to fix everything at once.
20
+ 5. **Re-run `npx mgr audit`** after each batch — confirm fixed issues are resolved and no new issues appeared.
21
+
22
+ ## Source
23
+
24
+ - Audit task implementation: [`src/gulp/tasks/audit.js`](../src/gulp/tasks/audit.js)
25
+ - Results land in `<projectRoot>/.temp/audit/`
26
+
27
+ ## See also
28
+
29
+ - [seo.md](seo.md) — the content conventions stage 1 enforces
30
+ - [xss-prevention.md](xss-prevention.md) — escaping rules
31
+ - [no-inline-scripts.md](no-inline-scripts.md) — the inline-script migration playbook
@@ -0,0 +1,57 @@
1
+ # Build System
2
+
3
+ UJM's build is a multi-stage gulp pipeline orchestrated by `src/gulp/main.js`, run from the consumer project (`npm start` for dev, `npm run build` for production).
4
+
5
+ ## Pipeline overview
6
+
7
+ Build sequence:
8
+
9
+ ```
10
+ defaults → distribute → parallel(webpack, sass, imagemin) → jsonToHtml → jekyll → audit → translation → minifyHtml
11
+ ```
12
+
13
+ Dev sequence:
14
+
15
+ ```
16
+ serve → build → developmentRebuild
17
+ ```
18
+
19
+ ## Gulp tasks
20
+
21
+ 15 tasks live in `src/gulp/tasks/`: `defaults` / `distribute` / `webpack` / `sass` / `imagemin` / `jekyll` / `jsonToHtml` / `preprocess` / `audit` / `translation` / `minifyHtml` / `serve` / `setup` / `developmentRebuild`.
22
+
23
+ Pure helpers are exposed under [src/gulp/tasks/utils/](../src/gulp/tasks/utils/) (`merge-jekyll-configs`, `_validate-yaml`, `template-transform`, `collectTextNodes`, `dictionary`, `github-cache`, `formatDocument`) — these are the highest-value test targets (zero I/O, callable directly in `build`-layer tests).
24
+
25
+ ## Config flow
26
+
27
+ Three config files in the consumer project feed the build:
28
+
29
+ 1. **`src/_config.yml`** — Jekyll config (brand, theme, meta, web_manager). Read by `Manager.getConfig('project')`.
30
+ 2. **`config/ultimate-jekyll-manager.json`** — UJM-specific config (purgecss safelist, webpack target, imagemin opts, distribute glob patterns). JSON5.
31
+ 3. **`package.json`** — read by `Manager.getPackage('project')`.
32
+
33
+ UJM ships defaults via `_config_default.yml` + `_config_development.yml` at `src/config/` — merged at Jekyll build time via the `--config` chain by [merge-jekyll-configs.js](../src/gulp/tasks/utils/merge-jekyll-configs.js).
34
+
35
+ ## Build modes
36
+
37
+ | Mode | Trigger | Effect |
38
+ |---|---|---|
39
+ | Development | `npm start` (default) | Serve + watch + incremental rebuild via `developmentRebuild` |
40
+ | Production | `npm run build` (`UJ_BUILD_MODE=true`) | Full pipeline incl. minifyHtml; PurgeCSS runs automatically |
41
+
42
+ PurgeCSS can be enabled locally with `UJ_PURGECSS=true`; consumer safelist patterns live in `config/ultimate-jekyll-manager.json` under `sass.purgecss.safelist`. See [local-development.md](local-development.md).
43
+
44
+ ## Serve / live reload
45
+
46
+ The dev server URL is stored in `.temp/_config_browsersync.yml` in the consuming project root — read it to determine the correct URL for browsing/testing. See [local-development.md](local-development.md) for emulator connection (`FIREBASE_EMULATOR_CONNECT=true`).
47
+
48
+ ## Log files
49
+
50
+ The gulp pipeline tees all output to `logs/dev.log` (`npm start`) / `logs/build.log` (`npm run build`). Full reference: [logging.md](logging.md).
51
+
52
+ ## See also
53
+
54
+ - [templating.md](templating.md) — node-powertools bracket conventions in the pipeline
55
+ - [css.md](css.md) — SCSS structure + PurgeCSS
56
+ - [local-development.md](local-development.md) — dev server, emulators, PurgeCSS toggles
57
+ - [test-framework.md](test-framework.md) — testing the pipeline's pure helpers
@@ -0,0 +1,15 @@
1
+ # Common Mistakes to Avoid
2
+
3
+ 1. **🚫 Inline `<script>` tags in HTML files** — the #1 worst mistake. Move ALL JS to page modules (`src/assets/js/pages/<path>/index.js`) or `main.js`. Component/layout scripts go in `main.js` with element-existence guards. Liquid-templated scripts bridge via `data-*` attributes or `<template>` elements. **Only exceptions:** `type="application/ld+json"`, external `<script src="...">` loaders, and ≤10-line first-paint display helpers with an explaining comment.
4
+ 2. **🚫 Reinventing Bootstrap — the #1 CSS mistake** — NEVER create custom classes for things Bootstrap already provides. No `.lm-btn` when `.btn .btn-primary` exists; no `.lm-wrap` when `.container` exists; no custom flex/gap/padding/margin/text-align classes when Bootstrap utilities do the same thing. Theme SCSS overrides how `.btn`/`.card`/`.navbar` LOOK — it doesn't create parallel replacements. Custom CSS is ONLY for genuinely novel components with no Bootstrap equivalent. Before writing ANY custom class, ask: "Does Bootstrap have this?" If yes, USE IT. See [themes.md](themes.md) and [css.md](css.md).
5
+ 3. **Creating duplicate CSS** — check Bootstrap and the active theme first.
6
+ 4. **Wrong imports** — FormManager needs curly braces: `import { FormManager } from ...`.
7
+ 5. **Assuming `Manager`, `firebase`, `webManager` are on `window`** — they are NOT. Use `import webManager from 'web-manager'` in a module. `firebase.firestore()` → `webManager.firestore()`. **Consumer code NEVER imports Firebase directly** — Firebase is web-manager's internal dependency. Same rule in EM and BXM.
8
+ 6. **Installing UJM's dependencies as direct consumer deps** — Consumer projects must NOT `npm install firebase`, `web-manager`, or any other UJM/web-manager transitive dep. UJM's webpack config includes `resolve.modules` pointing at the framework's own `node_modules/`. If a dependency isn't resolving, the fix is in UJM's webpack config — not the consumer's `package.json`. Mirrors EM and BXM.
9
+ 7. **Not using FormManager** — use it for ALL forms.
10
+ 8. **Calling `$form.requestSubmit()` directly** — use `formManager.submit()`.
11
+ 9. **Wrong dark mode classes** — use `bg-body` variants, not `bg-light`/`bg-dark`.
12
+ 10. **Not waiting for DOM** — always `await webManager.dom().ready()`.
13
+ 11. **Using native fetch** — always use `wonderful-fetch` or `authorized-fetch`.
14
+ 12. **XSS — unescaped dynamic data in innerHTML** — Use `webManager.utilities().escapeHTML()`. Dynamic URLs in `href`/`src`/`action`/`window.location`/`window.open` ALSO need `webManager.utilities().sanitizeURL()` — `escapeHTML` alone lets `javascript:` execute. See [xss-prevention.md](xss-prevention.md).
15
+ 13. **Leaving Liquid `{{ }}` or `{% %}` inside moved JS modules** — Jekyll does NOT process `src/assets/js/**/*.js`. Use `data-*` attribute bridges or `<template>` cloning.
@@ -1,4 +1,4 @@
1
- # Project Structure
1
+ # Directory Structure
2
2
 
3
3
  UJM is a template framework that consuming projects install as an NPM module to build Jekyll sites quickly and efficiently. It provides best-practice configurations, default components, themes, and build tools.
4
4
 
@@ -82,4 +82,4 @@ Write the function in [src/utils/mode-helpers.js](../src/utils/mode-helpers.js)
82
82
 
83
83
  ## See also
84
84
 
85
- - [test-framework.md](test-framework.md) — `UJ_TEST_MODE` is set automatically by the test runners; `--integration` gates real external APIs.
85
+ - [test-framework.md](test-framework.md) — `UJ_TEST_MODE` is set automatically by the test runners; `--extended` / `TEST_EXTENDED_MODE=true` gates real external APIs.
@@ -46,6 +46,43 @@ Raw subscription data (product.id, status, trial, cancellation) is on `account.s
46
46
 
47
47
  The same function exists in BEM as `User.resolveSubscription(account)` with identical return shape.
48
48
 
49
+ ### Reads vs Writes: When to Use What
50
+
51
+ | Operation | Method | Why |
52
+ |-----------|--------|-----|
53
+ | **Read** (list, fetch single) | `webManager.firestore()` | Faster, cheaper, no Cloud Function cold starts, no HTTP overhead |
54
+ | **Write** (create, update, delete) | `authorizedFetch` (Cloud Function) | Server-side validation, usage tracking, analytics |
55
+ | **Config/limits lookup** | `webManager.config.payment.products` | Already available client-side from `_config.yml`, no fetch needed |
56
+
57
+ **Dashboard and frontend reads MUST use the Firestore SDK directly** — never call a Cloud Function GET endpoint from the dashboard. Firestore reads are faster, cheaper, and don't incur cold starts. The backend GET routes (`routes/{resource}/get.js`) exist for **external API consumers only**, not for our own frontend. (Requires Firestore security rules that allow the read, e.g. `allow read: if isAuthenticated() && resource.data.owner == authUid()`.)
58
+
59
+ ```javascript
60
+ // Collection query (list user's items) — wait for auth first
61
+ webManager.auth().listen({ once: true }, async ({ user }) => {
62
+ if (!user) return;
63
+
64
+ const result = await webManager.firestore()
65
+ .collection('codes')
66
+ .where('owner', '==', user.uid)
67
+ .orderBy('meta.created.timestampUNIX', 'desc')
68
+ .get();
69
+
70
+ if (result.empty) return; // show empty state
71
+
72
+ result.docs.forEach((doc) => {
73
+ const data = doc.data();
74
+ // Render item...
75
+ });
76
+ });
77
+
78
+ // Single document read
79
+ const doc = await webManager.firestore().doc(`codes/${id}`).get();
80
+ if (!doc.exists()) return; // not found
81
+ const data = doc.data();
82
+ ```
83
+
84
+ Full Firestore query API (chainable `.where()`/`.orderBy()`/`.limit()`/`.startAt()`, response shapes): see `web-manager`'s [docs/modules.md](../../web-manager/docs/modules.md) — in consumer projects, `node_modules/web-manager/docs/modules.md`.
85
+
49
86
  ## Ultimate Jekyll Libraries
50
87
 
51
88
  Ultimate Jekyll provides helper libraries in `src/assets/js/libs/` that can be imported as needed.
@@ -314,7 +351,7 @@ initializing → ready ⇄ submitting → ready (or submitted)
314
351
  allowResubmit: true, // Allow resubmission after success (false = 'submitted' state)
315
352
  resetOnSuccess: false, // Clear form fields after successful submission
316
353
  warnOnUnsavedChanges: true, // Warn user before leaving page with unsaved changes
317
- submittingText: 'Processing...', // Text shown on submit button during submission
354
+ submittingText: 'Processing...', // Text shown on submit button during submission (use '' for icon-only buttons)
318
355
  submittedText: 'Processed!', // Text shown on submit button after success (when allowResubmit: false)
319
356
  inputGroup: null // Filter getData() by data-input-group attribute (null = all fields)
320
357
  }