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
@@ -29,3 +29,149 @@ asset_path: categories/category
29
29
  **Example:**
30
30
  - One-off page: `pages/categories.html` → `src/assets/css/pages/categories/index.scss`
31
31
  - Repeating layout: `_layouts/category.html` → `src/assets/css/pages/categories/category.scss` (set `asset_path: categories/category` in layout frontmatter)
32
+
33
+ ## Customizing Default Pages (blueprints)
34
+
35
+ Default pages live in `src/defaults/dist/_layouts/themes/[theme-id]/frontend/pages/`. Consumers customize them through frontmatter only — see [assets.md → Customizing Default Pages via Frontmatter](assets.md#customizing-default-pages-via-frontmatter) for the mechanism (`page.resolved`, reading the default layout first).
36
+
37
+ For each relevant default page:
38
+
39
+ 1. Create a page in the consuming project's `src/pages/` directory
40
+ 2. Use ONLY frontmatter to customize — NO HTML content needed
41
+ 3. Set `layout: blueprint/[page-name]` (e.g., `layout: blueprint/pricing`)
42
+ 4. Customize the frontmatter values to match the brand and purpose
43
+
44
+ ### File Extension Rules
45
+
46
+ - `.md` — Use for frontmatter-only customization (no HTML content)
47
+ - `.html` — Use only when adding custom HTML content beyond the layout
48
+
49
+ ### Frontmatter Rules
50
+
51
+ - Default pages (blueprint layouts): Do NOT include `meta.title` or `meta.description` — already set in the layout
52
+ - Do NOT include `theme` config (e.g., `theme.main.class`) unless explicitly changing from defaults
53
+ - `superheadline`: Homepage keeps default icon AND text; other pages may customize `text` but keep the default `icon`
54
+
55
+ ### Page Exclusions
56
+
57
+ Do NOT modify these pages:
58
+
59
+ - `auth/*` — Authentication pages (signin, signup, reset, oauth2)
60
+ - `payment/*` — Payment pages (checkout, confirmation)
61
+ - `account/*` — Account management pages
62
+ - `app.html` — App page
63
+ - `404.html` — Error page
64
+
65
+ ## Default Pages — Customization Levels
66
+
67
+ | Page | File | Layout | Level | Customize | Keep defaults |
68
+ |------|------|--------|-------|-----------|---------------|
69
+ | Homepage | `src/pages/index.md` | `blueprint/index` | Full | `hero`, `features`, `testimonials`, `stats`, `cta` | `superheadline` (icon AND text) |
70
+ | Pricing | `src/pages/pricing.md` | `blueprint/pricing` | Full | plan `features`/`pricing`/`definitions`/`price_per_unit`, `testimonials`, `faqs` | plan `id`/`name`/`tagline`, plan order |
71
+ | About | `src/pages/about.md` | `blueprint/about` | Full | `hero`, `mission`, `vision`, `story`, `values`, `team` | — |
72
+ | Contact | `src/pages/contact.md` | `blueprint/contact` | Minimal | `testimonials`, `faqs` ONLY | `hero`, `contact_methods`, `contact_form`, `stats` |
73
+ | Download | `src/pages/download.md` | `blueprint/download` | Minimal | `testimonials`, `faqs` ONLY | `hero`, `platforms`, `features`, `system_requirements` |
74
+
75
+ ### Homepage hero specifics
76
+
77
+ Always use the tagline format `tagline: "Introducing {{ site.brand.name }}"`. The hero supports optional display modes via `hero.display` — use only if the site benefits from an in-hero demo, CTA, or video:
78
+
79
+ ```yaml
80
+ hero:
81
+ display:
82
+ type: input # or: form, video, custom
83
+ view: side # or: bottom (default)
84
+ ```
85
+
86
+ - `bottom` (default) — content stacked vertically, display element below text; `side` — side-by-side, display element on the right
87
+ - Types: `input` (single field + button, e.g. email signup), `form` (multiple fields), `video` (embedded player), `custom` (only for something the others can't achieve)
88
+ - Reference examples: `src/defaults/dist/pages/test/components/hero-demo-{input,form,video,side,custom}.html`
89
+
90
+ ### Pricing plan features
91
+
92
+ Use YAML comments to separate feature types in each plan's `features` array:
93
+
94
+ - `# Common features` — listed in EVERY plan, displayed aligned across all plan cards
95
+ - `# Additional features` — features introduced in THIS plan only, appear in the "Everything in X, plus:" section
96
+
97
+ Do NOT repeat additional features that are unchanged from the previous plan (they're inherited automatically).
98
+
99
+ ### Testimonials (any page)
100
+
101
+ Each testimonial requires `quote` (1-2 sentences max), `author`, `role`, `company`, `initial` (first letter of name, shown in avatar). Use 3 for balanced display, keep quotes relevant to the page (pricing → value/ROI, contact → support experience), vary roles to show a broad customer base.
102
+
103
+ ### FAQs (any page)
104
+
105
+ Each FAQ requires `question` + `answer` (answers can include HTML like `<br><br>`). Use 3-5 per page, focused on the page's topic (pricing → billing/trials/refunds, contact → response times/channels), ordered by most commonly asked. `{{ site.brand.name }}` works in answers.
106
+
107
+ ## Dashboard Resource Pages (list/detail/edit)
108
+
109
+ For dashboard resource pages (e.g., forms, users, orders), use separate pages — do NOT use a single page with show/hide toggling.
110
+
111
+ **File structure:**
112
+
113
+ ```
114
+ src/pages/dashboard/items/
115
+ index.html ← list page
116
+ view.html ← detail page
117
+ edit.html ← edit page (optional)
118
+
119
+ src/assets/js/pages/dashboard/items/
120
+ index.js
121
+ view.js
122
+ edit.js ← (optional)
123
+ ```
124
+
125
+ **URL pattern:** `/dashboard/items` (list) · `/dashboard/items/view?id={id}` (detail) · `/dashboard/items/edit?id={id}` (edit, optional).
126
+
127
+ **Frontmatter:** the view page's breadcrumbs follow Home → Dashboard → Items (linked) → Details (active, no href):
128
+
129
+ ```yaml
130
+ meta:
131
+ breadcrumbs:
132
+ - label: Home
133
+ href: /
134
+ - label: Dashboard
135
+ href: /dashboard
136
+ - label: Items
137
+ href: /dashboard/items
138
+ - label: Details
139
+ active: true
140
+ ```
141
+
142
+ **JS conventions:**
143
+
144
+ - `view.js` must redirect to the list page if no `?id=` param is present:
145
+
146
+ ```js
147
+ const id = new URLSearchParams(window.location.search).get('id');
148
+ if (!id) {
149
+ window.location.href = '/dashboard/items';
150
+ return;
151
+ }
152
+ ```
153
+
154
+ - Links from the list page to detail use `/dashboard/items/view?id=${item.id}`
155
+ - After creating an item, redirect to `/dashboard/items/view?id=${item.id}`
156
+ - Delete on the view page redirects back to the list page
157
+
158
+ ## Creating Custom Pages (beyond blueprints)
159
+
160
+ When a page needs custom HTML (no blueprint fits):
161
+
162
+ 1. Place it in `src/pages/` with `.html` extension
163
+ 2. Use `layout: themes/[ site.theme.id ]/frontend/core/base`
164
+ 3. **Include `meta.title` and `meta.description`** in frontmatter (see [seo.md](seo.md))
165
+ 4. Follow the HTML patterns from default pages: `{% uj_icon %}` for icons, theme-adaptive classes (`bg-body`, `bg-body-secondary`, `text-body`, `btn-adaptive` — see [css.md](css.md)), `data-lazy="@class animation-slide-up"` animations, `page.resolved.[section]` for frontmatter data, the superheadline/headline/headline_accent/subheadline pattern
166
+ 5. Use frontmatter for configuration data, keep actual content in the body
167
+ 6. Forms: always FormManager with `onsubmit="return false"` + `novalidate` — see [page-loading.md](page-loading.md) for the full form protection standards
168
+ 7. Create `src/assets/css/pages/[page-name]/index.scss` / `src/assets/js/pages/[page-name]/index.js` only if needed ([assets.md](assets.md))
169
+
170
+ A good reference implementation to study: `src/defaults/dist/_layouts/themes/classy/frontend/pages/contact.html` (frontmatter structure, section organization, Bootstrap classes, `{% uj_icon %}`, `page.resolved`, form validation attributes, responsive grids, `data-lazy` animations).
171
+
172
+ ## See also
173
+
174
+ - [assets.md](assets.md) — frontmatter customization mechanism, page module/CSS auto-loading, nav/footer/account JSON
175
+ - [seo.md](seo.md) — content writing rules, services/solutions/alternatives page types, JSON-LD schema
176
+ - [themes.md](themes.md) — theme layouts, the classy fallback, authoring new themes
177
+ - [no-inline-scripts.md](no-inline-scripts.md) — where page JS goes (never inline)
@@ -4,14 +4,7 @@ The local development server URL is stored in `.temp/_config_browsersync.yml` in
4
4
 
5
5
  ## Log Files
6
6
 
7
- UJM tees every line of stdout/stderr from the gulp pipeline into a log file in the consumer project root, so you can `tail -f` it or `grep` through it after a run:
8
-
9
- - `npm start` → `logs/dev.log`
10
- - `npm run build` (i.e. `UJ_BUILD_MODE=true`) → `logs/build.log`
11
-
12
- Both files **truncate fresh on each run** — the most recent session only. ANSI color codes are stripped from the file (so it's grep-friendly); the terminal continues to receive colored output unchanged. Captures everything that flows through stdout/stderr: `Manager.logger(...)` output, raw `console.log` calls, gulp task names, jekyll's child output, webpack output, the works.
13
-
14
- **Skipped on CI/cloud.** When `UJ_IS_SERVER=true` (set by GitHub Actions workflows and other server contexts), the tee is bypassed entirely — no `logs/` directory is written. Implementation: [src/utils/attach-log-file.js](../src/utils/attach-log-file.js), attached at the top of [src/gulp/main.js](../src/gulp/main.js).
7
+ The gulp pipeline tees all output to `logs/dev.log` (`npm start`) / `logs/build.log` (`npm run build`), and `npx mgr test` tees to `logs/test.log`. Full reference file table, capture behavior, CI skip via `UJ_IS_SERVER`: [logging.md](logging.md).
15
8
 
16
9
  ## Connecting to Local Firebase Emulators
17
10
 
@@ -0,0 +1,30 @@
1
+ # Logging
2
+
3
+ UJM tees every line of CLI/pipeline output to log files in the consumer project root, so you can `tail -f` or `grep` a run instead of scrolling terminal scrollback. Frontend runtime logs live in the browser console — this doc covers the file logs UJM itself writes.
4
+
5
+ ## Log files
6
+
7
+ All in `<projectRoot>/logs/`:
8
+
9
+ | File | Source | Lifetime |
10
+ |---|---|---|
11
+ | `dev.log` | Gulp pipeline output on `npm start` | Truncated each run |
12
+ | `build.log` | Gulp pipeline output on `npm run build` (`UJ_BUILD_MODE=true`) | Truncated each run |
13
+ | `test.log` | `npx mgr test` runner output (suite names, pass/fail states, timings) | Truncated each run |
14
+
15
+ `dev.log` and `build.log` are the same gulp tee — which one it writes is chosen by `UJ_BUILD_MODE`, so they never both fill up in one run.
16
+
17
+ ## What gets captured
18
+
19
+ Everything that flows through stdout/stderr: `Manager.logger(...)` output, raw `console.log` calls, gulp task names, jekyll's child output, webpack output, the works. ANSI color codes are stripped from the file (grep-friendly); the terminal continues to receive colored output unchanged.
20
+
21
+ ## Controls
22
+
23
+ **Skipped on CI/cloud.** When `UJ_IS_SERVER=true` (set by GitHub Actions workflows and other server contexts), the tee is bypassed entirely — no `logs/` directory is written.
24
+
25
+ Implementation: [src/utils/attach-log-file.js](../src/utils/attach-log-file.js), attached at the top of [src/gulp/main.js](../src/gulp/main.js) — same pattern as EM's `dev.log`/`build.log` and BXM's.
26
+
27
+ ## See also
28
+
29
+ - [local-development.md](local-development.md) — dev server URL, emulator connection, PurgeCSS
30
+ - [test-framework.md](test-framework.md) — the test runner that feeds `test.log`
@@ -0,0 +1,131 @@
1
+ # Migration
2
+
3
+ Procedures for moving projects between UJ/UJM format versions. Three modes: **full migration** (old UJ → latest UJM base), **quick fix** (normalize `_config.yml` section order), and **revert posts** (back to the OLD pre-migration format).
4
+
5
+ ## Full Migration (old UJ → new UJM)
6
+
7
+ Three stages: repository migration to the latest UJM base (preserving content in `_legacy/`) → config YML re-mapping → cleanup.
8
+
9
+ ### Stage 1: Repository migration to the new UJM base
10
+
11
+ 1. **Sync latest changes:** `git fetch origin && git pull origin master`
12
+ 2. **Delete node_modules** before moving to legacy: `rm -rf node_modules`
13
+ 3. **Move everything to `_legacy/`** for a clean slate:
14
+ ```bash
15
+ mkdir -p _legacy
16
+ find . -maxdepth 1 ! -name '_legacy' ! -name '.' ! -name '.git' -exec mv {} _legacy/ \;
17
+ ```
18
+ 4. **Download the Ultimate Jekyll template** (https://github.com/itw-creative-works/ultimate-jekyll):
19
+ ```bash
20
+ git clone https://github.com/itw-creative-works/ultimate-jekyll.git /tmp/uj-fresh
21
+ rsync -av --exclude='.git' /tmp/uj-fresh/ ./
22
+ rm -rf /tmp/uj-fresh
23
+ ```
24
+ 5. **Setup and build:** `npm install && npx mgr setup && npm run build`
25
+ 6. **Migrate content from legacy.** **DO NOT copy default pages** — UJM provides standard pages through its template system (see `src/defaults/dist/pages` in the UJM package for the full list). Only migrate **content** (posts, images, collections):
26
+ ```bash
27
+ # Blog images
28
+ mkdir -p src/assets/images/blog
29
+ cp -r _legacy/assets/_src/images/blog/posts/* src/assets/images/blog/ 2>/dev/null || true
30
+ cp -r _legacy/assets/images/blog/posts/* src/assets/images/blog/ 2>/dev/null || true
31
+
32
+ # Blog posts
33
+ mkdir -p src/_posts
34
+ cp -r _legacy/_posts/* src/_posts/ 2>/dev/null || true
35
+
36
+ # Custom collections / images (if any)
37
+ cp -r _legacy/_themes src/_themes 2>/dev/null || true
38
+ cp -r _legacy/assets/images/themes src/assets/images/ 2>/dev/null || true
39
+ cp -r _legacy/assets/images/resources src/assets/images/ 2>/dev/null || true
40
+ ```
41
+ **Custom pages — DO NOT COPY, RECREATE:** reference `_legacy/pages/` for content/text only; rebuild the HTML with the current theme structure ([layouts-and-pages.md](layouts-and-pages.md), [themes.md](themes.md)).
42
+ 7. **Verify:** `src/_posts/` populated, `src/assets/images/blog/` populated, `_legacy/_config.yml` exists (used in stage 2), NO legacy pages copied.
43
+ 8. **Rename master → main:**
44
+ ```bash
45
+ git branch -m master main
46
+ git push -u origin main
47
+ gh api repos/OWNER/REPO -X PATCH -f default_branch=main
48
+ git push origin --delete master
49
+ ```
50
+
51
+ ### Stage 2: Config YML re-mapping
52
+
53
+ 1. Read the NEW standard template (`src/defaults/src/_config.yml` in the UJM package) and the OLD config (`_legacy/_config.yml`).
54
+ 2. Create a migration TODO list.
55
+ 3. **Map old keys to new format** — use the NEW format as the template (preserve order, comments, structure); map old keys to new (e.g. `contact.email-support` → `brand.contact.email`). **DO NOT import keys that don't exist in the NEW format**; only import deprecated keys when the user explicitly requests them.
56
+ 4. After initial mapping, tell the user: "Base migration complete. If you need any deprecated keys from `_legacy/_config.yml` imported, let me know which ones."
57
+ 5. Write the new `src/_config.yml` — NEW structure/order/comments, values filled from the old config where mappings exist.
58
+ 6. **Required adjustments:** replace hardcoded brand names with `{{ site.brand.name }}` in `meta.title`/`meta.description`; rewrite `brand.description` to 5-8 words max; set default colors for cookieConsent and chatsy (`#237afc` / `#fff`).
59
+ 7. Verify.
60
+
61
+ ### Stage 3: Cleanup
62
+
63
+ 1. Run `npx mgr migrate`.
64
+ 2. Tell the user: the `_legacy/` folder holds the original files for reference and can be deleted (`rm -rf _legacy/`) once confirmed.
65
+
66
+ ## Quick Fix (normalize `_config.yml` section order)
67
+
68
+ Ensure these keys exist in `src/_config.yml` in this order. Insert missing keys with these defaults — do NOT overwrite existing values:
69
+
70
+ ```yaml
71
+ # Tracking
72
+ tracking:
73
+ google-analytics: null
74
+ meta-pixel: null
75
+ tiktok-pixel: null
76
+
77
+ # reCAPTCHA
78
+ recaptcha:
79
+ site-key: null
80
+
81
+ # Cloudflare
82
+ cloudflare:
83
+ zone: null
84
+
85
+ # Download
86
+ download:
87
+ mac:
88
+ universal: ""
89
+ windows:
90
+ universal: ""
91
+ linux:
92
+ debian: ""
93
+ snap: ""
94
+ ios:
95
+ universal: ""
96
+ android:
97
+ universal: ""
98
+
99
+ # Extension
100
+ extension:
101
+ chrome: ""
102
+ firefox: ""
103
+ opera: ""
104
+ safari: ""
105
+ edge: ""
106
+ brave: ""
107
+
108
+ # Favicon
109
+ favicon:
110
+ path: "https://cdn.itwcreativeworks.com/assets/itw-creative-works/images/favicon"
111
+ safari-pinned-tab: "#5bbad5"
112
+ msapp-tile-color: "#da532c"
113
+ theme-color: "#ffffff"
114
+ ```
115
+
116
+ ## Revert Posts (back to the OLD format)
117
+
118
+ For sites that have NOT yet migrated to the new UJ structure. Verify `./src/_posts` exists before starting.
119
+
120
+ 1. **Move posts:** `./src/_posts` → `./_posts` (preserve folder structure)
121
+ 2. **Move blog images:** everything under `./src/assets/images/blog/` → `./assets/_src/images/blog/posts/` (create destination if needed)
122
+ 3. **Remove `./src`** after the moves complete
123
+ 4. **Update front matter** in every `./_posts/**/*.md`:
124
+ - `layout: blueprint/blog/post` → `layout: app/blog/post`
125
+ - rename key `post.description` → `post.excerpt`
126
+
127
+ ## See also
128
+
129
+ - [directory-structure.md](directory-structure.md) — where everything lives in the new format
130
+ - [layouts-and-pages.md](layouts-and-pages.md) — recreating custom pages with blueprints
131
+ - [themes.md](themes.md) — the theme structure recreated pages must use
@@ -0,0 +1,304 @@
1
+ # NO JavaScript in HTML Files — Hard Rule
2
+
3
+ **THIS IS NON-NEGOTIABLE.** No `<script>` tag with an inline JS body is ever allowed in any file under `src/` — not pages, not components (`_includes/`), not layouts (`_layouts/`). If you find one, you move it. If you write one, you are wrong.
4
+
5
+ ## Why this rule exists
6
+
7
+ 1. **UJM auto-loads page modules.** The framework already provides a first-class mechanism: `src/assets/js/pages/<pagePath>/index.js` runs automatically for each page based on `data-page-path`. Inline scripts bypass this system, fragment the codebase, and make code impossible to lint/test/bundle properly.
8
+ 2. **Inline scripts break module boundaries.** They can't `import` anything, can't be code-split, can't be tree-shaken, and have to re-declare every helper.
9
+ 3. **Inline scripts hide bugs.** They often reference globals (`Manager`, `firebase`, `webManager.uj`) that aren't exposed, breaking silently at runtime.
10
+ 4. **Inline scripts duplicate logic.** The same drop-down/demo/filter logic gets copy-pasted into many files instead of being shared from a single module.
11
+ 5. **Inline scripts are a historical accident.** Old UJM (pre-2.0) exposed `Manager` globally and encouraged inline scripts. That's gone. Any inline script you see in a repo is migration debt that must be paid.
12
+
13
+ ## The rule, stated precisely
14
+
15
+ For every `<script>` tag in `src/**/*.html` under a UJM project:
16
+
17
+ | Tag shape | Allowed? | Why |
18
+ |-----------|----------|-----|
19
+ | `<script>...body...</script>` with JS body | ❌ **NEVER** | Move the body to a JS module (see below) |
20
+ | `<script src="https://..."></script>` | ✅ OK | External loader, no inline code |
21
+ | `<script type="application/ld+json">...</script>` | ✅ OK | Structured data, not JS |
22
+ | `<script type="module" src="..."></script>` | ✅ OK | External ES module, no inline code |
23
+ | `<script>` with ≤10 lines of trivial first-paint display | ✅ OK with comment | Small helpers that must run before the bundle |
24
+
25
+ **The "small helper" exception must meet ALL of these:**
26
+
27
+ - Under ~10 lines of code
28
+ - Does a single cosmetic first-paint task (e.g. replacing a `--:--:--` placeholder with the current time)
29
+ - Would cause visible flash/flicker if deferred to the bundle
30
+ - Has an inline comment explaining why it's intentionally inline
31
+
32
+ If in doubt, move it. The exception is for ≤10-line display polyfills, NOT for 20-line "quick" handlers.
33
+
34
+ ## Where to move the script
35
+
36
+ ### Case 1: The script is inside a **page** file (`src/pages/<something>.html` or `.md`)
37
+
38
+ Move it to `src/assets/js/pages/<pagePath>/index.js` — the path is derived from the page's frontmatter `permalink:`.
39
+
40
+ | Page frontmatter | Page module path |
41
+ |------------------|------------------|
42
+ | `permalink: /` | `src/assets/js/pages/index.js` |
43
+ | `permalink: /contact` | `src/assets/js/pages/contact/index.js` |
44
+ | `permalink: /dashboard/history` | `src/assets/js/pages/dashboard/history/index.js` |
45
+ | `permalink: /tools/form-builder` | `src/assets/js/pages/tools/form-builder/index.js` |
46
+
47
+ **Always check if the target file already exists.** If it does, MERGE your logic into the existing `export default` function. DO NOT overwrite.
48
+
49
+ Module template:
50
+
51
+ ```javascript
52
+ /**
53
+ * <Page Name> Page JavaScript
54
+ */
55
+
56
+ import webManager from 'web-manager';
57
+
58
+ export default async () => {
59
+ await webManager.dom().ready();
60
+
61
+ // ... moved logic ...
62
+ };
63
+ ```
64
+
65
+ ### Case 2: The script is inside a **component** (`src/_includes/**/*.html`)
66
+
67
+ Components are Jekyll partials — they don't have a single "page path" because they can be included in many pages. Two strategies:
68
+
69
+ **Option A — Used by exactly one page:** Move the script to that one page's `src/assets/js/pages/<path>/index.js`. Simpler, more scoped.
70
+
71
+ **Option B — Used by multiple pages (or you can't tell):** Move the script to `src/assets/js/main.js` as an `init<ComponentName>()` function with an **element-existence guard** so it's a no-op on pages without the component.
72
+
73
+ ```javascript
74
+ // src/assets/js/main.js
75
+ import Manager from 'ultimate-jekyll-manager';
76
+ import webManager from 'web-manager';
77
+
78
+ const manager = new Manager();
79
+
80
+ manager.initialize().then(() => {
81
+ // Each component gets its own init function with a guard
82
+ initHeroDemo();
83
+ initChatDemo();
84
+ initScheduler();
85
+ });
86
+
87
+ function initHeroDemo() {
88
+ // Element-existence guard — MUST be the first thing in the function
89
+ const $toggle = document.getElementById('hero-demo-toggle');
90
+ if (!$toggle) return;
91
+
92
+ // ... moved logic ...
93
+ }
94
+ ```
95
+
96
+ **How to pick the guard element:** use the first `getElementById` / `querySelector` the original script called. If that element isn't on the page, the original script would have crashed anyway, so returning early is safe.
97
+
98
+ ### Case 3: The script is inside a **layout** (`src/_layouts/**/*.html`)
99
+
100
+ Same as components: grep for `layout: <layout-path>` across pages. If one page uses it, move to that page's module. If multiple, move to `main.js` with a guard.
101
+
102
+ ## Handling Liquid templating inside a script
103
+
104
+ This is the trickiest case, and the one that trips most people up. **Jekyll Liquid (`{{ ... }}`, `{% ... %}`) runs at BUILD TIME on HTML files. Webpack-bundled JS modules under `src/assets/js/` are NOT processed by Jekyll** — any Liquid you leave inside a `.js` file will appear as literal text in the output.
105
+
106
+ ### Strategy A: `data-*` attribute bridge (for Liquid values)
107
+
108
+ Use this when the script reads values from Liquid, e.g. `{{ site.url }}`, `{{ include.action1 | default: "..." }}`, `{{ page.items | jsonify }}`.
109
+
110
+ **Before (broken inline script):**
111
+
112
+ ```html
113
+ <!-- src/_includes/frontend/components/hero-demo.html -->
114
+ <script>
115
+ var platform = '{{ include.platform }}';
116
+ var actions = [
117
+ '{{ include.action1 | default: "Like" }}',
118
+ '{{ include.action2 | default: "Share" }}',
119
+ ];
120
+ var items = {{ page.items | jsonify }};
121
+ </script>
122
+ ```
123
+
124
+ **After:**
125
+
126
+ ```html
127
+ <!-- src/_includes/frontend/components/hero-demo.html -->
128
+ <!-- Hidden config element — Jekyll fills data attributes at build time -->
129
+ <div id="hero-demo-config"
130
+ data-platform="{{ include.platform }}"
131
+ data-action-1="{{ include.action1 | default: 'Like' }}"
132
+ data-action-2="{{ include.action2 | default: 'Share' }}"
133
+ data-items='{{ page.items | jsonify }}'
134
+ hidden></div>
135
+
136
+ <!-- ... rest of component markup ... -->
137
+
138
+ <!-- Logic moved to src/assets/js/main.js (initHeroDemo) -->
139
+ ```
140
+
141
+ ```javascript
142
+ // src/assets/js/main.js
143
+ function initHeroDemo() {
144
+ const $config = document.getElementById('hero-demo-config');
145
+ if (!$config) return;
146
+
147
+ const platform = $config.dataset.platform;
148
+ const actions = [
149
+ $config.dataset.action1,
150
+ $config.dataset.action2,
151
+ ];
152
+ const items = JSON.parse($config.dataset.items);
153
+
154
+ // ... moved logic ...
155
+ }
156
+ ```
157
+
158
+ **Notes on data attributes:**
159
+
160
+ - Use kebab-case in HTML (`data-action-1`), which maps to camelCase in JS (`.dataset.action1`).
161
+ - For `| jsonify` output, wrap the attribute value in single quotes: `data-items='{{ x | jsonify }}'` (jsonify emits double-quoted JSON).
162
+ - For complex objects, use `| jsonify` → `data-json='...'` → `JSON.parse($config.dataset.json)`.
163
+
164
+ ### Strategy B: `<template>` element cloning (for Liquid render tags)
165
+
166
+ Use this when the script interpolates Liquid render tags like `{% uj_icon "name" %}` or `{% include partials/x.html %}` into HTML strings it builds.
167
+
168
+ **Before (broken inline script):**
169
+
170
+ ```html
171
+ <!-- Inline script tries to build HTML string with Liquid render tag -->
172
+ <script>
173
+ el.innerHTML = '<span class="icon">{% uj_icon "arrow-pointer", "smaller" %}</span>';
174
+ </script>
175
+ ```
176
+
177
+ **After:**
178
+
179
+ ```html
180
+ <!-- Hidden templates — Jekyll renders the icon markup inside at build time -->
181
+ <template id="hero-demo-icon-arrow-pointer">{% uj_icon "arrow-pointer", "smaller" %}</template>
182
+ <template id="hero-demo-icon-eye">{% uj_icon "eye", "smaller" %}</template>
183
+
184
+ <!-- ... rest of component markup ... -->
185
+
186
+ <!-- Logic moved to src/assets/js/main.js (initHeroDemo) -->
187
+ ```
188
+
189
+ ```javascript
190
+ // src/assets/js/main.js
191
+ function getIcon(name) {
192
+ const $template = document.getElementById('hero-demo-icon-' + name);
193
+ return $template ? $template.content.cloneNode(true) : document.createTextNode('');
194
+ }
195
+
196
+ function initHeroDemo() {
197
+ const $container = document.getElementById('hero-demo');
198
+ if (!$container) return;
199
+
200
+ // Build DOM nodes programmatically — NO innerHTML string building
201
+ const $span = document.createElement('span');
202
+ $span.className = 'icon';
203
+ $span.appendChild(getIcon('arrow-pointer'));
204
+ $container.appendChild($span);
205
+ }
206
+ ```
207
+
208
+ **Why template cloning instead of innerHTML string concat:** Liquid render tags produce complex nested HTML (SVG with classes, etc.) that can't be easily stringified. Templates let Jekyll render the full HTML once, then JS clones it as needed. This also avoids XSS risk and the need for `escapeHTML()`.
209
+
210
+ ### Strategy C: Liquid conditional branching (`{% if %}` in scripts)
211
+
212
+ Rewrite as JS conditionals using data-attribute values.
213
+
214
+ **Before:**
215
+
216
+ ```html
217
+ <script>
218
+ {% if include.mode == "turbo" %}
219
+ setInterval(tick, 100);
220
+ {% else %}
221
+ setInterval(tick, 1000);
222
+ {% endif %}
223
+ </script>
224
+ ```
225
+
226
+ **After:**
227
+
228
+ ```html
229
+ <div id="demo-config" data-mode="{{ include.mode }}" hidden></div>
230
+ <!-- Logic moved to main.js (initDemo) -->
231
+ ```
232
+
233
+ ```javascript
234
+ function initDemo() {
235
+ const $config = document.getElementById('demo-config');
236
+ if (!$config) return;
237
+
238
+ const mode = $config.dataset.mode;
239
+ const intervalMs = mode === 'turbo' ? 100 : 1000;
240
+ setInterval(tick, intervalMs);
241
+ }
242
+ ```
243
+
244
+ ## Handling globals from inline scripts
245
+
246
+ Old inline scripts often defined functions that other code expected to find on `window` (hCaptcha callbacks, YouTube IFrame API callbacks, inline `onclick="..."` handlers, etc.). When moving to a module:
247
+
248
+ ```javascript
249
+ // In the page module / init function
250
+ function hCaptchaLoadCallback() { /* ... */ }
251
+ function hCaptchaCompleteCallback() { /* ... */ }
252
+
253
+ // Expose on window — required by external script that calls it globally
254
+ window.hCaptchaLoadCallback = hCaptchaLoadCallback;
255
+ window.hCaptchaCompleteCallback = hCaptchaCompleteCallback;
256
+ ```
257
+
258
+ Same applies to `onYouTubeIframeAPIReady`, inline `onclick="handleFoo(this)"`, and similar global-callback patterns. Leave the external `<script src="...">` loader in place (it's allowed), but put its callbacks in a module and explicitly assign them to `window`.
259
+
260
+ ## Playbook: moving an inline script step by step
261
+
262
+ 1. **Read the full HTML file** (not just the script block) — frontmatter, surrounding markup, all `<script>` tags.
263
+ 2. **Classify each `<script>` tag**:
264
+ - `type="application/ld+json"` → leave alone (structured data)
265
+ - `src="..."` → leave alone (external loader)
266
+ - Trivial ≤10-line first-paint helper → leave alone with a comment
267
+ - Everything else → **MOVE**
268
+ 3. **Grep the script body** for `{{` and `{%` to find Liquid references. Plan your data-attribute and template element bridges.
269
+ 4. **Determine the destination**:
270
+ - Page → `src/assets/js/pages/<pagePath>/index.js` (check if it exists; merge if so)
271
+ - Component/layout used by one page → that page's module
272
+ - Component/layout used by many pages → `src/assets/js/main.js` with `init<Name>()` guard
273
+ 5. **Add bridge elements to the HTML** (`<div data-*>` config, `<template>` icons) **before** removing the script. This way Jekyll still processes the Liquid.
274
+ 6. **Move the script body** to the destination. Unwrap any `(function() { ... })()` IIFE. Preserve `var`/`const`/`let` exactly. Add the element-existence guard as the first statement.
275
+ 7. **Rewrite Liquid references** in the moved JS to read from data attributes or clone templates.
276
+ 8. **Replace the `<script>...</script>` block** in HTML with `<!-- Logic moved to <destination path> -->`.
277
+ 9. **Verify with `git diff`** — confirm the HTML change is only the script removal + any bridge elements you added.
278
+ 10. **Do NOT refactor the script logic** while moving it. Move verbatim with only the changes required to work outside the HTML file. Refactoring and moving in the same pass is how bugs get introduced.
279
+
280
+ ## What NOT to do
281
+
282
+ - ❌ Don't leave `Manager.something()` — `Manager` is not globally exposed. Use `import webManager from 'web-manager'`.
283
+ - ❌ Don't leave `firebase.firestore()` — Firebase is not globally exposed either. Use `webManager.firestore()`.
284
+ - ❌ Don't build HTML strings with Liquid interpolation inside a JS module — it won't be processed.
285
+ - ❌ Don't use `window.showExitPopup = () => webManager.uj().showExitPopup()` shims as a way to avoid moving the rest of the logic. Move the whole script.
286
+ - ❌ Don't split one inline script across multiple modules "for organization" — keep it as one function in the destination file.
287
+ - ❌ Don't add the script to `main.js` without an element-existence guard. `main.js` runs on every page.
288
+ - ❌ Don't forget that `src/assets/js/pages/index.js` is the module for the root `permalink: /` — the file lives at `pages/index.js`, NOT `pages//index.js` or `pages/home/index.js`.
289
+
290
+ ## Verification
291
+
292
+ After moving an inline script, run this grep to confirm the file is clean:
293
+
294
+ ```bash
295
+ # Should find zero matches (excluding ld+json and external src)
296
+ rg -U '<script>[\s\S]*?</script>' src/**/*.html
297
+ ```
298
+
299
+ ## See also
300
+
301
+ - [assets.md](assets.md) — page module structure, webpack aliases, where each JS file lives
302
+ - [common-mistakes.md](common-mistakes.md) — inline scripts are mistake #1
303
+ - [xss-prevention.md](xss-prevention.md) — escaping rules for any HTML the moved JS builds
304
+ - [templating.md](templating.md) — what Liquid processes (and what it doesn't)