ultimate-jekyll-manager 1.2.0 → 1.2.2

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.
@@ -1,1832 +0,0 @@
1
- # Ultimate Jekyll Manager
2
-
3
- ## Project Overview
4
-
5
- Ultimate Jekyll Manager 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.
6
-
7
- **Important:** This is NOT a standalone project. You cannot run `npm start` or `npm run build` directly in this repository.
8
-
9
- **DO NOT run `npm start`, `npm run build`, or any dev server commands.** The user already has a development server running in a consuming project. Running these commands here would either fail or create duplicate servers unnecessarily.
10
-
11
- ## Project Structure
12
-
13
- ### Directory Organization
14
- - `src/gulp/tasks` - Gulp tasks for building Jekyll sites
15
- - `src/defaults/src` - Default source files (editable by users, copied to consuming project's `src/`)
16
- - `src/defaults/dist` - Default distribution files (not editable by users, copied to consuming project's `dist/`)
17
- - `src/assets/css` - Stylesheets (global, pages, themes)
18
- - `src/assets/js` - JavaScript modules (core, pages, libraries)
19
- - `src/assets/themes` - Theme SCSS and JS files
20
-
21
- ### Consuming Project Structure
22
- - `src/` - Compiled to `dist/` via npm
23
- - `dist/` - Compiled to `_site/` via Jekyll
24
-
25
- ## Local Development
26
-
27
- The local development server URL is stored in `.temp/_config_browsersync.yml` in the consuming project's root directory. Read this file to determine the correct URL for browsing and testing. By default, use "https://192.168.86.69:4000".
28
-
29
- ### Connecting to Local Firebase Emulators
30
-
31
- Set the `FIREBASE_EMULATOR_CONNECT` environment variable to `true` to connect the frontend to local Firebase services (Auth, Firestore, Functions, etc.):
32
-
33
- ```bash
34
- FIREBASE_EMULATOR_CONNECT=true npm start
35
- ```
36
-
37
- This value is written to `.temp/_config_browsersync.yml` under `web_manager.env.FIREBASE_EMULATOR_CONNECT` and made available to the frontend at build time.
38
-
39
- ### PurgeCSS
40
-
41
- PurgeCSS runs automatically in production builds and can be enabled locally with `UJ_PURGECSS=true`. Consuming projects can add custom safelist patterns via `config/ultimate-jekyll-manager.json` under `sass.purgecss.safelist`:
42
-
43
- ```json5
44
- {
45
- sass: {
46
- purgecss: {
47
- safelist: {
48
- standard: [], // Matches against the full class name
49
- deep: [], // Matches including child selectors (e.g., pseudo-selectors like :checked)
50
- greedy: [], // Matches anywhere in the selector string
51
- keyframes: [], // Preserves @keyframes animations by name
52
- },
53
- },
54
- },
55
- }
56
- ```
57
-
58
- **All entries are regex strings** — each gets converted to `new RegExp(entry)`. This means:
59
-
60
- | Pattern | Matches | Does NOT match |
61
- |---------|---------|----------------|
62
- | `"^dot$"` | `dot` | `dotted`, `polkadot` |
63
- | `"^chat-"` | `chat-bubble`, `chat-input` | `live-chat` |
64
- | `"fw-semibold"` | `fw-semibold`, `fw-semibold-custom` | (matches loosely) |
65
-
66
- **Use `^` and `$` anchors for exact matches.** Without them, the pattern matches any class *containing* the string.
67
-
68
- **Example:**
69
- ```json5
70
- {
71
- sass: {
72
- purgecss: {
73
- safelist: {
74
- standard: ["^dot$", "^fw-semibold$", "^chat-"],
75
- deep: [":focus-within"],
76
- greedy: ["^chat-"],
77
- keyframes: ["chat-typing-bounce"],
78
- },
79
- },
80
- },
81
- }
82
- ```
83
-
84
- ## Asset Organization
85
-
86
- ### Ultimate Jekyll Manager Files (THIS project)
87
-
88
- **CSS:**
89
- - `src/assets/css/ultimate-jekyll-manager.scss` - Main UJ stylesheet (provides core styles)
90
- - `src/assets/css/global/` - Global UJ styles
91
- - `src/assets/css/pages/` - Page-specific styles provided by UJ
92
- - Format: `src/assets/css/pages/[page-name]/index.scss`
93
- - Example: `src/assets/css/pages/download/index.scss`
94
-
95
- **JavaScript:**
96
- - `src/assets/js/ultimate-jekyll-manager.js` - Main UJ JavaScript entry point (provides core functionality)
97
- - `src/assets/js/core/` - Core UJ modules
98
- - `src/assets/js/pages/` - Page-specific JavaScript provided by UJ
99
- - Format: `src/assets/js/pages/[page-name]/index.js`
100
- - Example: `src/assets/js/pages/download/index.js`
101
- - `src/assets/js/libs/` - UJ library modules (prerendered-icons, form-manager, authorized-fetch, etc.)
102
-
103
- **Default Pages & Layouts:**
104
-
105
- UJ provides default page templates and layouts in `src/defaults/dist/` that are copied to consuming projects. These are NOT meant to be edited by users.
106
-
107
- - Format: `src/defaults/dist/_layouts/themes/[theme-id]/frontend/pages/[page-name].html`
108
- - Examples:
109
- - `src/defaults/dist/_layouts/themes/classy/frontend/pages/download.html`
110
- - `src/defaults/dist/_layouts/themes/classy/frontend/pages/pricing.html`
111
- - `src/defaults/dist/_layouts/themes/classy/frontend/pages/payment/checkout.html`
112
- - `src/defaults/dist/_layouts/themes/classy/frontend/pages/payment/confirmation.html`
113
- - `src/defaults/dist/_layouts/themes/classy/frontend/pages/contact.html`
114
- - Core layouts:
115
- - `src/defaults/dist/_layouts/core/root.html` - Root HTML wrapper
116
- - `src/defaults/dist/_layouts/themes/[theme-id]/frontend/core/base.html` - Theme base layout
117
-
118
- **Complete UJ Page Example:**
119
- - **HTML:** `src/defaults/dist/_layouts/themes/classy/frontend/pages/download.html`
120
- - **CSS:** `src/assets/css/pages/download/index.scss`
121
- - **JS:** `src/assets/js/pages/download/index.js`
122
-
123
- These files serve as blueprints and reference implementations. When building custom pages in consuming projects, reference these for patterns and best practices.
124
-
125
- **IMPORTANT:** Consuming projects CAN create files with the same paths in their own `src/` directory to override UJ defaults, but this should ONLY be done when absolutely necessary. Prefer using `src/pages/` and `src/_layouts/` for custom pages instead of overriding UJ default files.
126
-
127
- ### Section Configuration Files (JSON)
128
-
129
- UJ provides JSON configuration files for common sections like navigation and footer. These JSON files are consumed by corresponding HTML templates during the build process.
130
-
131
- **Configuration Files:**
132
- - `src/defaults/src/_includes/frontend/sections/nav.json` - Navigation configuration
133
- - `src/defaults/src/_includes/frontend/sections/footer.json` - Footer configuration
134
- - `src/defaults/src/_includes/global/sections/account.json` - Account dropdown configuration (shared across frontend nav, backend topbar, admin topbar)
135
-
136
- **How It Works:**
137
- 1. JSON files contain structured data (links, labels, settings)
138
- 2. HTML templates in `src/defaults/dist/_includes/themes/[theme-id]/` read and render this data
139
- 3. The build process converts `.json` → data loaded by `.html` templates
140
-
141
- **Customizing Navigation/Footer:**
142
-
143
- Consuming projects should create their own JSON files in `src/_includes/frontend/sections/`:
144
- - `src/_includes/frontend/sections/nav.json`
145
- - `src/_includes/frontend/sections/footer.json`
146
-
147
- ### Account Dropdown (Shared Component)
148
-
149
- The account dropdown (avatar + user info + menu items) is a shared component used across the frontend nav, backend topbar, and admin topbar. It is defined once and included everywhere.
150
-
151
- **Data Source:** `src/defaults/src/_includes/global/sections/account.json`
152
-
153
- This is the single source of truth for account dropdown menu items. Consuming projects can override it by creating `src/_includes/global/sections/account.json`.
154
-
155
- **Example: account.json**
156
- ```json5
157
- {
158
- dropdown: [
159
- { label: 'Account', href: '/account#profile', icon: 'user-gear' },
160
- { label: 'Dashboard', href: '/dashboard', icon: 'gauge-high' },
161
- { divider: true, attributes: [['data-wm-bind', '@show auth.account.roles.admin']] },
162
- { label: 'Admin Panel', href: '/admin/dashboard', icon: 'shield-halved', attributes: [['data-wm-bind', '@show auth.account.roles.admin']] },
163
- { divider: true },
164
- { label: 'Sign Out', icon: 'arrow-right-from-bracket', class: 'auth-signout-btn text-danger' }
165
- ]
166
- }
167
- ```
168
-
169
- **Include:** `src/defaults/dist/_includes/themes/classy/global/sections/account.html`
170
-
171
- This renders the full account dropdown: avatar button with profile photo, user info header (displayName + email), and the menu items from `account.json`.
172
-
173
- **Parameters:**
174
-
175
- | Parameter | Default | Description |
176
- |-----------|---------|-------------|
177
- | `size` | `md` | Avatar size class (`sm`, `md`, `lg`) |
178
- | `attributes` | none | Array of `[name, value]` attribute pairs for the dropdown wrapper |
179
-
180
- **Usage in templates:**
181
- ```liquid
182
- {% include themes/classy/global/sections/account.html size="md" attributes=action.attributes %}
183
- ```
184
-
185
- **How it's wired into nav/topbar:**
186
-
187
- In `nav.json` or `topbar.json`, set `type: 'account'` on an action — the rendering templates detect this type and include the shared account dropdown automatically. No `dropdown` array is needed on the action:
188
-
189
- ```json5
190
- {
191
- type: 'account',
192
- attributes: [
193
- ['data-wm-bind', '@show auth.user'],
194
- ['hidden', '']
195
- ],
196
- }
197
- ```
198
-
199
- **File Locations:**
200
-
201
- | Purpose | Path |
202
- |---------|------|
203
- | Account data (SSOT) | `src/defaults/src/_includes/global/sections/account.json` |
204
- | Account include | `src/defaults/dist/_includes/themes/classy/global/sections/account.html` |
205
- | Frontend nav (uses include) | `src/defaults/dist/_includes/themes/classy/frontend/sections/nav.html` |
206
- | Backend topbar (uses include) | `src/defaults/dist/_includes/themes/classy/backend/sections/topbar.html` |
207
- | Admin topbar (wraps backend) | `src/defaults/dist/_includes/themes/classy/admin/sections/topbar.html` |
208
-
209
- **Example: Footer Configuration**
210
-
211
- ```json
212
- {
213
- logo: {
214
- href: '/',
215
- class: 'filter-adaptive',
216
- text: '{{ site.brand.name }}',
217
- description: '{{ site.meta.description }}',
218
- },
219
- links: [
220
- {
221
- label: 'Company',
222
- href: null,
223
- links: [
224
- {
225
- label: 'About Us',
226
- href: '/about',
227
- },
228
- {
229
- label: 'Pricing',
230
- href: '/pricing',
231
- },
232
- ],
233
- },
234
- ],
235
- socials: {
236
- enabled: true,
237
- },
238
- copyright: {
239
- enabled: true,
240
- text: null,
241
- },
242
- }
243
- ```
244
-
245
- **Note:** These are JSON5 files (support comments, trailing commas, unquoted keys). The corresponding HTML templates automatically process these files during the build.
246
-
247
- ### Customizing Default Pages via Frontmatter
248
-
249
- **BEST PRACTICE:** UJ default pages are designed to be customized through frontmatter WITHOUT writing any HTML. Consuming projects can create a simple page that includes ONLY frontmatter to configure the default page's behavior.
250
-
251
- **How It Works:**
252
- 1. UJ default pages use `page.resolved` to access merged frontmatter (site → layout → page)
253
- 2. **IMPORTANT:** Before customizing, READ the UJ default page in `src/defaults/dist/_layouts/` to understand available frontmatter options and how they're used
254
- 3. Consuming projects create a page in `src/pages/` with custom frontmatter
255
- 4. The page uses a UJ layout (e.g., `blueprint/pricing`)
256
- 5. Frontmatter overrides default values without any HTML
257
-
258
- **Example: Customizing the Pricing Page**
259
-
260
- **Step 1:** Read the UJ default pricing page to see available frontmatter options:
261
- - File: `src/defaults/dist/_layouts/themes/classy/frontend/pages/pricing.html`
262
- - Look for frontmatter at the top and how `page.resolved.pricing` is used in the HTML
263
-
264
- **Step 2:** In consuming project, create `src/pages/pricing.html`:
265
-
266
- ```yaml
267
- ---
268
- ### ALL PAGES ###
269
- layout: blueprint/pricing
270
- permalink: /pricing
271
-
272
- ### PAGE CONFIG ###
273
- pricing:
274
- price_per_unit:
275
- enabled: true
276
- feature_id: "credits"
277
- label: "credit"
278
- plans:
279
- - id: "basic"
280
- name: "Basic"
281
- tagline: "best for getting started"
282
- url: "/download"
283
- pricing:
284
- monthly: 0
285
- annually: 0
286
- features:
287
- - id: "credits"
288
- name: "Credits"
289
- value: 1
290
- icon: "sparkles"
291
- ...
292
- ---
293
- ```
294
-
295
- That's it! No HTML needed. The UJ pricing layout reads `page.resolved.pricing` and renders the plans accordingly.
296
-
297
- **When to Use Frontmatter Customization:**
298
- - ✅ Customizing UJ default pages (pricing, contact, download, etc.)
299
- - ✅ Changing configuration without touching HTML
300
- - ✅ Maintaining upgradability when UJ updates
301
-
302
- **When to Create Custom Pages:**
303
- - ❌ Building entirely new page types
304
- - ❌ Needing custom HTML structure
305
- - ❌ Pages with unique layouts not provided by UJ
306
-
307
- ### Consuming Project Files
308
-
309
- **CSS:**
310
- - `src/assets/css/main.scss` - Site-wide custom styles (runs on every page, edits by consuming project)
311
- - `src/assets/css/pages/` - Page-specific custom styles
312
- - Format: `src/assets/css/pages/[page-name]/index.scss`
313
-
314
- **JavaScript:**
315
- - `src/assets/js/main.js` - Site-wide custom JavaScript (runs on every page, edits by consuming project)
316
- - `src/assets/js/pages/` - Page-specific custom JavaScript
317
- - Format: `src/assets/js/pages/[page-name]/index.js`
318
-
319
- **Pages & Layouts:**
320
- - `src/pages/` - Individual page HTML/Markdown files
321
- - `src/_layouts/` - Custom layouts for the consuming project
322
-
323
- **Asset Loading:** Page-specific CSS/JS files are automatically included based on the page's canonical path. Override with `asset_path` frontmatter.
324
-
325
- ### Webpack Import Aliases
326
-
327
- UJM defines two webpack aliases (in `src/gulp/tasks/webpack.js`) for importing assets in JavaScript:
328
-
329
- | Alias | Resolves To | Purpose |
330
- |-------|------------|---------|
331
- | `__main_assets__` | `[UJM package]/dist/assets` | UJM's own built-in assets (core modules, libraries, pages) |
332
- | `__project_assets__` | `[consuming project]/src/assets` | The consuming project's custom assets |
333
-
334
- **`__main_assets__`** — Import UJM libraries and core modules:
335
- ```javascript
336
- import { FormManager } from '__main_assets__/js/libs/form-manager.js';
337
- import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
338
- import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';
339
- ```
340
-
341
- **`__project_assets__`** — Import consuming project's own assets:
342
- ```javascript
343
- // Used in src/index.js to load project-specific page modules
344
- import(`__project_assets__/js/pages/${pageModulePath}`)
345
- ```
346
-
347
- **How they work together:** `src/index.js` loads page modules from both aliases — first from `__main_assets__` (UJM defaults), then from `__project_assets__` (project overrides/extensions). If a project module doesn't exist, it gracefully skips. This enables a layered system where UJM provides defaults and consuming projects can extend or override page behavior.
348
-
349
- **When to use which:**
350
- - **`__main_assets__`** — When importing UJM-provided libraries, core modules, or referencing UJM's built-in page scripts
351
- - **`__project_assets__`** — When a consuming project needs to import its own custom assets from within UJM-managed code
352
-
353
- ### Page Module Structure
354
-
355
- All page modules must follow this standardized pattern:
356
-
357
- ```javascript
358
- /**
359
- * [Page Name] Page JavaScript
360
- */
361
-
362
- // Libraries
363
- import webManager from 'web-manager';
364
-
365
- // Module
366
- export default () => {
367
- return new Promise(async function (resolve) {
368
- // Initialize when DOM is ready
369
- await webManager.dom().ready();
370
-
371
- // Page initialization logic
372
- helper1();
373
-
374
- // Resolve after initialization
375
- return resolve();
376
- });
377
- };
378
-
379
- // Helper functions
380
- function helper1() {
381
- // Helper implementation
382
- }
383
- ```
384
-
385
- **Key Points:**
386
- - `web-manager` is a singleton — `import webManager from 'web-manager'` returns the same initialized instance everywhere. No need to receive it via params or store in module-level variables.
387
- - Helpers are defined outside the main export function
388
- - Always wait for DOM ready before manipulating elements
389
- - Use `webManager.utilities().escapeHTML()` for XSS prevention — do NOT write your own escape function
390
-
391
- ## XSS Prevention (ZERO TRUST — MANDATORY)
392
-
393
- **TREAT ALL DYNAMIC DATA AS UNTRUSTED.** This is a zero-trust policy: any value that did not come directly from a hardcoded literal in the source file MUST be escaped before being inserted into the DOM via `innerHTML` or attribute interpolation.
394
-
395
- This includes — but is not limited to:
396
- - Firestore document fields (user names, emails, IDs, descriptions, etc.)
397
- - API response data
398
- - URL parameters (`location.search`, `URLSearchParams`)
399
- - User input from form fields
400
- - OAuth-provided values (displayName, email from Google/GitHub)
401
- - Any variable whose origin is not a hardcoded source-code constant
402
-
403
- ### The Rule
404
- ```javascript
405
- // ✅ ALWAYS escape dynamic data before innerHTML
406
- $el.innerHTML = `<p>${webManager.utilities().escapeHTML(data.title)}</p>`;
407
- $el.innerHTML = `<a href="${webManager.utilities().escapeHTML(url)}">${webManager.utilities().escapeHTML(label)}</a>`;
408
-
409
- // ✅ textContent is always safe — no escaping needed
410
- $el.textContent = data.title;
411
-
412
- // ❌ NEVER inject dynamic data raw into innerHTML
413
- $el.innerHTML = `<p>${data.title}</p>`;
414
- $el.innerHTML = `<a href="${url}">${label}</a>`;
415
- ```
416
-
417
- ### NEVER Write Your Own Escape Function
418
- Do NOT create a local `escapeHtml` function or any variant. The ONLY allowed escape method is:
419
- ```javascript
420
- webManager.utilities().escapeHTML(str)
421
- ```
422
-
423
- ### When Building DOM Programmatically
424
- Prefer `document.createElement` + `textContent` for plain text nodes — it is inherently safe:
425
- ```javascript
426
- const $el = document.createElement('div');
427
- $el.textContent = data.message; // Safe — no escaping needed
428
- ```
429
-
430
- Only use `innerHTML` when you need actual HTML structure (tags, classes, etc.), and escape every dynamic value in it.
431
-
432
- ### Even "Safe" Values Must Be Escaped
433
- Even values that *seem* safe (like `Date.toLocaleDateString()` output, numeric calculations, or hardcoded config strings) MUST be escaped when inserted via `innerHTML`. This is defense-in-depth — if the data source ever changes, the escaping is already in place.
434
-
435
- ```javascript
436
- // ✅ CORRECT — escape even "safe" values in innerHTML
437
- $el.innerHTML = `<small>${webManager.utilities().escapeHTML(formatDate(timestamp))}</small>`;
438
- $el.innerHTML = `<span>${webManager.utilities().escapeHTML(reason)}</span>`;
439
-
440
- // ❌ WRONG — assuming the value is safe because it's from a date formatter
441
- $el.innerHTML = `<small>${formatDate(timestamp)}</small>`;
442
- ```
443
-
444
- ### Redirects Must Be Validated
445
- Never redirect to a URL from untrusted sources without validation:
446
-
447
- ```javascript
448
- // ✅ CORRECT — validate before redirect
449
- const url = urlParams.get('returnUrl');
450
- if (url && webManager.isValidRedirectUrl(url)) {
451
- window.location.href = url;
452
- }
453
-
454
- // ✅ CORRECT — validate API response URLs have safe scheme
455
- if (response.url && /^https?:\/\//i.test(response.url)) {
456
- window.location.href = response.url;
457
- }
458
-
459
- // ❌ WRONG — redirect to unvalidated input
460
- window.location.href = urlParams.get('returnUrl');
461
- ```
462
-
463
- ### postMessage Handlers Must Check Origin
464
- Always validate `event.origin` when handling `window.addEventListener('message', ...)`:
465
-
466
- ```javascript
467
- // ✅ CORRECT
468
- window.addEventListener('message', (event) => {
469
- if (event.origin !== window.location.origin && event.origin !== 'https://trusted-domain.com') {
470
- return;
471
- }
472
- // handle message
473
- });
474
-
475
- // ❌ WRONG — any origin can send messages
476
- window.addEventListener('message', (event) => {
477
- window.location.href = event.data.url; // attacker-controlled redirect
478
- });
479
- ```
480
-
481
- ### Never Use eval() or new Function()
482
- Do not use `eval()`, `new Function()`, `setTimeout(string)`, or `setInterval(string)`. These execute arbitrary code and violate CSP policies.
483
-
484
- ### Sanitize Markdown/Rich Text Output
485
- When rendering user-authored markdown or rich text, use DOMPurify to sanitize the output:
486
-
487
- ```javascript
488
- import DOMPurify from 'dompurify';
489
- const safeHTML = DOMPurify.sanitize(md.render(userContent), {
490
- ALLOWED_TAGS: ['h1', 'h2', 'h3', 'p', 'br', 'a', 'b', 'strong', 'i', 'em', 'ul', 'ol', 'li', 'img', 'code', 'pre'],
491
- ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class', 'target', 'rel'],
492
- });
493
- ```
494
-
495
- ### Do NOT Escape Values Passed to textContent-Based APIs
496
- `showNotification()`, `formManager.showSuccess()`, `formManager.showError()`, and `textContent` assignments use safe text insertion internally. Pre-escaping these causes double-encoding (e.g., `We'll` displays as `We&#039;ll`).
497
-
498
- ```javascript
499
- // ✅ CORRECT — these APIs use textContent internally, so they're already safe
500
- webManager.utilities().showNotification('Thank you! We\'ll be in touch.', 'success');
501
- formManager.showSuccess('Message sent successfully!');
502
-
503
- // ❌ WRONG — double-escapes
504
- webManager.utilities().showNotification(webManager.utilities().escapeHTML(message), 'success');
505
- ```
506
-
507
- ## Layouts and Pages
508
-
509
- ### Page Types
510
- - **One-off pages** (e.g., `/categories`, `/sitemap`) - Create as pages without custom layouts; use existing layouts
511
- - **Repeating page types** (e.g., blog posts, category pages) - Create a dedicated layout (e.g., `_layouts/category.html`)
512
-
513
- ### Layout Requirements
514
- All layouts and pages must eventually require a theme entry point:
515
- ```yaml
516
- layout: themes/[ site.theme.id ]/frontend/core/base
517
- ```
518
-
519
- **Note:** The `[ site.theme.id ]` syntax is correct and allows dynamic theme selection.
520
-
521
- ### Asset Path Configuration
522
-
523
- For pages sharing the same assets, use the `asset_path` frontmatter variable:
524
-
525
- ```yaml
526
- ---
527
- # Instead of deriving path from page.canonical.path
528
- asset_path: categories/category
529
- ---
530
- ```
531
-
532
- **Example:**
533
- - One-off page: `pages/categories.html` → `src/assets/css/pages/categories/index.scss`
534
- - Repeating layout: `_layouts/category.html` → `src/assets/css/pages/categories/category.scss` (set `asset_path: categories/category` in layout frontmatter)
535
-
536
- ## UJ Powertools (Jekyll Plugin)
537
-
538
- Ultimate Jekyll uses the `jekyll-uj-powertools` gem for custom Liquid functionality.
539
-
540
- **Documentation:** `/Users/ian/Developer/Repositories/ITW-Creative-works/jekyll-uj-powertools/README.md`
541
-
542
- ### Available Features
543
- - **Filters:** `uj_strip_ads`, `uj_json_escape`, `uj_title_case`, `uj_content_format`, `uj_hash`
544
- - **Tags:** `iftruthy`, `iffalsy`, `uj_icon`, `uj_logo`, `uj_image`, `uj_member`, `uj_post`, `uj_readtime`, `uj_social`, `uj_translation_url`, `uj_fake_comments`, `uj_language`
545
- - **Global Variables:** `site.uj.cache_breaker`
546
- - **Page Variables:** `page.random_id`, `page.extension`, `page.layout_data`, `page.resolved`
547
-
548
- **Always check the README before assuming functionality.**
549
-
550
- ### Key Liquid Functions
551
-
552
- #### `uj_content_format`
553
- Formats content by first liquifying it, then markdownifying it (if markdown file).
554
-
555
- #### `uj_hash`
556
- Returns a deterministic number between 0 and max (exclusive) based on the input string's MD5 hash. Same input always produces the same output.
557
-
558
- ```liquid
559
- {{ "some-string" | uj_hash: 1000 }} => 0-999 (stable across builds)
560
- {{ site.url | uj_hash: 2 }} => 0 or 1
561
- ```
562
-
563
- #### `iftruthy` / `iffalsy`
564
- Custom tags that check JavaScript truthiness (not null, undefined, or empty string).
565
-
566
- ```liquid
567
- {% iftruthy variable %}
568
- <!-- Content -->
569
- {% endiftruthy %}
570
- ```
571
-
572
- **Limitations:**
573
- - Does NOT support logical operators
574
- - Does NOT support `else` statements
575
- - CAN contain nested sub-statements
576
-
577
- #### `page.resolved`
578
- A deeply merged object containing all site, layout, and page variables. Precedence: page > layout > site. Enables a system of defaults with progressive overrides.
579
-
580
- #### `uj_icon`
581
- Inserts Font Awesome icons:
582
-
583
- ```liquid
584
- {% uj_icon icon-name, "fa-md" %}
585
- {% uj_icon "rocket", "fa-3xl" %}
586
- ```
587
-
588
- **Parameters:**
589
- 1. Icon name (string or variable, without "fa-" prefix)
590
- 2. CSS classes (optional, defaults to "fa-3xl")
591
-
592
- **Available Icon Sizes:**
593
- - `fa-2xs` - Extra extra small
594
- - `fa-xs` - Extra small
595
- - `fa-sm` - Small
596
- - `fa-md` - Medium (default base size)
597
- - `fa-lg` - Large
598
- - `fa-xl` - Extra large
599
- - `fa-2xl` - 2x extra large
600
- - `fa-3xl` - 3x extra large
601
- - `fa-4xl` - 4x extra large
602
- - `fa-5xl` - 5x extra large
603
-
604
- **Size Examples:**
605
- ```liquid
606
- {% uj_icon "check", "fa-sm" %} <!-- Small inline icon -->
607
- {% uj_icon "star", "fa-lg" %} <!-- Slightly larger -->
608
- {% uj_icon "rocket", "fa-2xl" %} <!-- Hero/feature icons -->
609
- {% uj_icon "chart-pie", "fa-4xl" %}<!-- Large placeholder icons -->
610
- ```
611
-
612
- #### `asset_path` Override
613
- Override default page-specific CSS/JS path derivation:
614
-
615
- ```yaml
616
- ---
617
- asset_path: blog/post
618
- ---
619
- ```
620
-
621
- Uses `/assets/css/pages/{{ asset_path }}.bundle.css` instead of deriving from `page.canonical.path`. Useful when multiple pages share assets (e.g., all blog posts).
622
-
623
- ## Blog Post Images
624
-
625
- ### Inline Images with `@post/` Shortcut
626
-
627
- Blog posts use standard markdown syntax for inline images. The `@post/` prefix provides a shortcut to reference images in the post's own image directory:
628
-
629
- ```markdown
630
- ![Alt text](@post/my-image.jpg)
631
- ```
632
-
633
- This resolves at build time to `/assets/images/blog/post-{id}/my-image.jpg`, where `{id}` comes from the post's `post.id` frontmatter value.
634
-
635
- **All image types work:**
636
-
637
- | Syntax | Result |
638
- |--------|--------|
639
- | `![alt](@post/file.jpg)` | Local post image (shortcut) |
640
- | `![alt](/assets/images/other.jpg)` | Absolute path (any image) |
641
- | `![alt](https://example.com/img.jpg)` | External URL |
642
-
643
- **How it works:** The `markdown-images.rb` hook in `jekyll-uj-powertools` intercepts `![alt](url)` patterns during `pre_render`, resolves `@post/` prefixes, then converts each image to a responsive `<picture>` element with WebP sources and lazy loading via `{% uj_image %}`.
644
-
645
- **Image directory structure:** Images for post ID `42` live at `src/assets/images/blog/post-42/`.
646
-
647
- **Image class customization:** Set via frontmatter:
648
- ```yaml
649
- ---
650
- theme:
651
- post:
652
- image:
653
- class: "img-fluid rounded-3 shadow my-5"
654
- ---
655
- ```
656
-
657
- ### BEM `admin/post` Image Handling
658
-
659
- When posts are created via BEM's `POST /admin/post` endpoint:
660
- 1. External image URLs in the markdown body (e.g., Unsplash) are downloaded
661
- 2. Images are uploaded to `src/assets/images/blog/post-{id}/` on GitHub
662
- 3. The body is rewritten to use `@post/{filename}` format
663
- 4. Failed downloads are skipped (original external URL preserved)
664
-
665
- ## Icon System
666
-
667
- Ultimate Jekyll uses Font Awesome icons but does NOT include the Font Awesome JavaScript or CSS library. All icons must be rendered server-side using Jekyll's `{% uj_icon %}` tag.
668
-
669
- ### Available Icons
670
-
671
- UJM ships with the **full Font Awesome Pro solid icon set** (4,600+ icons) at `assets/icons/font-awesome/solid/`, plus brand icons at `assets/icons/font-awesome/brands/`. Any Pro or Free solid/brand icon name can be used with `{% uj_icon %}` and prerendered icons. The icon style defaults to `solid` and can be configured via `site.config.icons.style`.
672
-
673
- Browse available icons at: https://fontawesome.com/icons
674
-
675
- ### When to Use `{% uj_icon %}` vs Prerendered Icons
676
-
677
- **IMPORTANT:** Use the correct method based on WHERE the icon will be used:
678
-
679
- #### Use `{% uj_icon %}` in HTML/Liquid Templates
680
-
681
- When icons are part of the static HTML template, use `{% uj_icon %}` directly:
682
-
683
- ```liquid
684
- <!-- Alerts -->
685
- <div class="alert alert-success">
686
- {% uj_icon "circle-check", "fa-sm" %} Success message
687
- </div>
688
-
689
- <!-- Buttons -->
690
- <button class="btn btn-primary">
691
- {% uj_icon "paper-plane", "fa-md me-2" %}
692
- Send
693
- </button>
694
-
695
- <!-- Labels -->
696
- <label>
697
- {% uj_icon "envelope", "fa-sm me-1 text-info" %}
698
- Email
699
- </label>
700
- ```
701
-
702
- **Use this when:**
703
- - The icon is in a Jekyll template (.html file)
704
- - The icon is static and known at build time
705
- - The icon is part of the page structure
706
-
707
- #### Use Prerendered Icons in JavaScript
708
-
709
- When icons need to be dynamically inserted via JavaScript, pre-render them in frontmatter and access them via the library:
710
-
711
- **1. Add icons to page frontmatter (names only, no classes):**
712
- ```yaml
713
- ---
714
- prerender_icons:
715
- - name: "mobile"
716
- - name: "envelope"
717
- - name: "bell"
718
- ---
719
- ```
720
-
721
- **2. Import the library in JavaScript:**
722
- ```javascript
723
- import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';
724
- ```
725
-
726
- **3. Use in your code (second argument works like uj_icon's second argument):**
727
- ```javascript
728
- // With size + classes (same as {% uj_icon "mobile", "fa-sm me-1" %})
729
- $badge.innerHTML = `${getPrerenderedIcon('mobile', 'fa-sm me-1')} Push Notification`;
730
-
731
- // Without classes (no size class on the <i> wrapper)
732
- $el.innerHTML = getPrerenderedIcon('bell');
733
- ```
734
-
735
- **Use this when:**
736
- - Icons are dynamically inserted via JavaScript
737
- - Icons are part of dynamically generated content
738
- - Icons are added to elements created with `document.createElement()`
739
-
740
- ### What NOT to Do
741
-
742
- **NEVER use manual icon HTML in JavaScript:**
743
- ```javascript
744
- // ❌ WRONG - Bootstrap Icons (we don't use Bootstrap Icons)
745
- $el.innerHTML = '<i class="bi bi-check-circle"></i> Text';
746
-
747
- // ❌ WRONG - Manual Font Awesome (we don't have FA JS/CSS)
748
- $el.innerHTML = '<i class="fa-solid fa-check"></i> Text';
749
-
750
- // ✅ CORRECT - Use prerendered icons
751
- $el.innerHTML = `${getPrerenderedIcon('circle-check', 'fa-sm me-1')} Text`;
752
- ```
753
-
754
- ### Benefits
755
- - Icons are rendered server-side with proper Font Awesome classes
756
- - No client-side icon generation overhead
757
- - Consistent icon styling across the application
758
- - No Font Awesome JavaScript/CSS library needed
759
-
760
- ## CSS Guidelines
761
-
762
- ### Section Padding in Custom Pages
763
-
764
- **DO NOT add padding classes to sections in custom frontend pages.**
765
-
766
- UJ handles section padding automatically via the theme's layout system. When creating or editing custom frontend pages:
767
-
768
- - ❌ DO NOT use `py-5`, `py-4`, `pt-5`, `pb-5`, `p-5`, etc. on `<section>` elements
769
- - ❌ DO NOT add vertical padding to sections manually
770
- - ✅ Let the UJ theme handle section spacing automatically
771
-
772
- **The ONLY exception:** Add padding if the user EXPLICITLY requests it for a specific section.
773
-
774
- ### Theme-Adaptive Classes
775
-
776
- **DO NOT USE:** `bg-light`, `bg-dark`, `text-light`, `text-dark`
777
-
778
- Ultimate Jekyll supports both light and dark modes. Use adaptive classes instead:
779
-
780
- **Backgrounds:**
781
- - `bg-body` - Primary background
782
- - `bg-body-secondary` - Secondary background
783
- - `bg-body-tertiary` - Tertiary background
784
-
785
- **Text:**
786
- - `text-body` - Body text color
787
-
788
- **Buttons:**
789
- - `btn-adaptive` - Adaptive button
790
- - `btn-outline-adaptive` - Adaptive outline button
791
-
792
- These classes automatically adapt to the current theme mode.
793
-
794
- ### Cards Inside Colored Sections
795
-
796
- When placing cards inside sections with `bg-body-secondary` or `bg-body-tertiary`, cards will blend in because they share the same background color by default.
797
-
798
- **Solution:** Add `bg-body` to cards to create visual contrast:
799
-
800
- ```html
801
- <!-- ❌ WRONG - Card blends with section background -->
802
- <section class="bg-body-secondary">
803
- <div class="card">...</div>
804
- </section>
805
-
806
- <!-- ✅ CORRECT - Card stands out with contrasting background -->
807
- <section class="bg-body-secondary">
808
- <div class="card bg-body">...</div>
809
- </section>
810
- ```
811
-
812
- **Rule:** When a section uses `bg-body-secondary` or `bg-body-tertiary`, always add `bg-body` to child cards to ensure proper visual hierarchy.
813
-
814
- ## Page Loading Protection System
815
-
816
- Ultimate Jekyll prevents race conditions by disabling buttons during JavaScript initialization.
817
-
818
- ### How It Works
819
- 1. HTML element starts with `data-page-loading="true"` and `aria-busy="true"` (`src/defaults/dist/_layouts/core/root.html`)
820
- 2. Protected elements are automatically disabled during this state
821
- 3. Attributes are removed when JavaScript completes (`src/assets/js/core/complete.js`)
822
-
823
- ### Protected Elements
824
- - All form buttons (`<button>`, `<input type="submit">`, `<input type="button">`, `<input type="reset">`)
825
- - Elements with `.btn` class (Bootstrap buttons)
826
- - Elements with `.btn-action` class (custom action triggers)
827
-
828
- ### The `.btn-action` Class
829
-
830
- Selectively protect non-standard elements that trigger important actions:
831
-
832
- ```html
833
- <!-- Protected during page load -->
834
- <a href="/api/delete" class="custom-link btn-action">Delete Item</a>
835
- <div class="card-action btn-action" onclick="processData()">Process</div>
836
-
837
- <!-- NOT protected (regular navigation/UI) -->
838
- <a href="/about" class="btn btn-primary">About Us</a>
839
- <button data-bs-toggle="modal">Show Info</button>
840
- ```
841
-
842
- **Use `.btn-action` for:**
843
- - API calls
844
- - Form submissions
845
- - Data modifications
846
- - Payment processing
847
- - Destructive actions
848
-
849
- **Don't use for:**
850
- - Navigation links
851
- - UI toggles (modals, accordions, tabs)
852
- - Harmless interactions
853
-
854
- ### Implementation
855
- - **CSS:** `src/assets/css/core/utilities.scss` - Disabled styling
856
- - **Click Prevention:** `src/defaults/dist/_includes/core/body.html` - Inline script
857
- - **State Removal:** `src/assets/js/core/complete.js` - Removes loading state
858
-
859
- ### Form Protection Standards
860
-
861
- All JS-managed forms use a layered protection strategy to prevent native form submission before JavaScript takes control:
862
-
863
- #### Layer 1: `onsubmit="return false"` on ALL JS-managed forms
864
-
865
- Every `<form>` that will be managed by FormManager MUST include `onsubmit="return false"`:
866
-
867
- ```html
868
- <form id="my-form" onsubmit="return false">
869
- ```
870
-
871
- This is a zero-cost safety net that prevents native form submission if a user clicks submit before FormManager attaches its `e.preventDefault()` handler. FormManager's own submit handling overrides this — there is no conflict.
872
-
873
- **Exception:** Traditional forms with an `action` attribute that intentionally navigate (e.g., search forms, external form submissions) should NOT include this.
874
-
875
- #### Layer 2: Button initial state based on use case
876
-
877
- | Use Case | Initial State | Mechanism |
878
- |----------|---------------|-----------|
879
- | Buttons dependent on async data (checkout payment methods) | `hidden` | `data-wm-bind="@show ..."` reveals when data loads |
880
- | Buttons on auth/sensitive forms | `disabled` | FormManager's `ready()` removes `disabled` |
881
- | Buttons on simple forms (contact, newsletter) | Default (visible) | FormManager's `autoReady: true` enables quickly |
882
-
883
- #### Layer 3: FormManager `autoReady` configuration
884
-
885
- | Scenario | `autoReady` | `ready()` call |
886
- |----------|-------------|----------------|
887
- | No async work before form init | `true` (default) | Automatic on DOM ready |
888
- | Async work before form init (API calls, redirects) | `false` | Explicit call after async completes |
889
-
890
- **Reference implementations:**
891
- - Simple form: `src/assets/js/pages/contact/index.js`
892
- - Auth form: `src/assets/js/libs/auth.js`
893
- - Async data form: `src/assets/js/pages/payment/checkout/index.js`
894
-
895
- ## Lazy Loading System
896
-
897
- Ultimate Jekyll uses a custom lazy loading system powered by web-manager.
898
-
899
- ### Syntax
900
- ```html
901
- data-lazy="@type value"
902
- ```
903
-
904
- ### Supported Types
905
-
906
- #### `@src` - Lazy load src attribute
907
- ```html
908
- <img data-lazy="@src /assets/images/hero.jpg" alt="Hero">
909
- <iframe data-lazy="@src https://example.com/embed"></iframe>
910
- ```
911
-
912
- #### `@srcset` - Lazy load srcset attribute
913
- ```html
914
- <img data-lazy="@srcset /img/small.jpg 480w, /img/large.jpg 1024w">
915
- ```
916
-
917
- #### `@bg` - Lazy load background images
918
- ```html
919
- <div data-lazy="@bg /assets/images/background.jpg"></div>
920
- ```
921
-
922
- #### `@class` - Lazy add CSS classes
923
- ```html
924
- <div data-lazy="@class animation-fade-in">Content</div>
925
- ```
926
-
927
- #### `@html` - Lazy inject HTML content
928
- ```html
929
- <div data-lazy="@html <p>Lazy loaded content</p>"></div>
930
- ```
931
-
932
- #### `@script` - Lazy load external scripts
933
- ```html
934
- <div data-lazy='@script {"src": "https://example.com/widget.js", "attributes": {"async": true}}'></div>
935
- ```
936
-
937
- ### Features
938
- - Automatic cache busting via `buildTime`
939
- - IntersectionObserver for performance (50px threshold)
940
- - Loading state CSS classes: `lazy-loading`, `lazy-loaded`, `lazy-error`
941
- - Intelligent handling of video/audio sources
942
- - Automatic DOM re-scanning for dynamic elements
943
-
944
- **Implementation:** `src/assets/js/core/lazy-loading.js`
945
-
946
- ## Ad Units (Verts)
947
-
948
- Ultimate Jekyll provides ad unit includes that display Google AdSense ads with automatic fallback to in-house ads served from promo-server when AdSense is blocked or unfilled.
949
-
950
- ### Include Files
951
-
952
- | Include | Purpose |
953
- |---------|---------|
954
- | `modules/adunits/adsense.html` | AdSense ad with promo-server fallback |
955
- | `modules/adunits/promo-server.html` | Direct promo-server ad (no AdSense) |
956
-
957
- ### AdSense Include
958
-
959
- ```liquid
960
- {% include /modules/adunits/adsense.html type="in-article" %}
961
- {% include /modules/adunits/adsense.html type="in-article" vert-size="rectangle" %}
962
- {% include /modules/adunits/adsense.html type="display" vert-size="banner" %}
963
- {% include /modules/adunits/adsense.html type="display" vert-size="300" %}
964
- ```
965
-
966
- **Parameters:**
967
-
968
- | Parameter | Required | Default | Description |
969
- |-----------|----------|---------|-------------|
970
- | `type` | No | `display` | Ad type: `display`, `in-article`, `in-feed`, `multiplex` |
971
- | `slot` | No | From site config | Override the ad slot ID |
972
- | `vert-size` | No | (unconstrained) | Max height preset or pixel value (cannot use `size` — conflicts with Liquid's built-in `size` filter) |
973
- | `style` | No | `""` | Custom inline CSS |
974
- | `layout` | No | `image-above` | Layout for `in-feed` type: `image-above`, `image-side` |
975
-
976
- ### Promo Server Include
977
-
978
- ```liquid
979
- {% include /modules/adunits/promo-server.html vert-id="/verts/units/test/google" %}
980
- {% include /modules/adunits/promo-server.html vert-id="/verts/units/test/google" vert-size="banner" %}
981
- ```
982
-
983
- **Parameters:**
984
-
985
- | Parameter | Required | Default | Description |
986
- |-----------|----------|---------|-------------|
987
- | `vert-id` | Yes | `""` | Path to the vert on promo-server |
988
- | `vert-size` | No | (unconstrained) | Max height preset or pixel value |
989
- | `style` | No | `""` | Custom inline CSS |
990
-
991
- ### Size Presets
992
-
993
- The `vert-size` parameter accepts preset names or raw pixel values. Presets constrain the ad unit's max-height:
994
-
995
- | Preset | Max Height | Typical Use |
996
- |--------|-----------|-------------|
997
- | `banner` | 150px | Horizontal banner ads |
998
- | `leaderboard` | 90px | Wide horizontal ads (alias for banner) |
999
- | `rectangle` | 250px | Medium rectangle, in-content ads |
1000
- | `large-rectangle` | 600px | Large rectangle, sidebar ads |
1001
- | `skyscraper` | 600px | Tall sidebar ads |
1002
-
1003
- Raw pixel values are also accepted: `vert-size="300"` → 300px max-height.
1004
-
1005
- When no `vert-size` is specified, the ad unit renders unconstrained.
1006
-
1007
- ### How It Works
1008
-
1009
- 1. The include renders a `data-lazy="@script ..."` div that lazy-loads `vert.bundle.js` when scrolled into view
1010
- 2. `vert.js` creates a `<vert-unit>` custom element with `max-height` + `overflow: hidden` (if `vert-size` is set)
1011
- 3. For AdSense types: loads the AdSense script, pushes the ad, and monitors fill status
1012
- 4. If AdSense is blocked or unfilled, falls back to a promo-server iframe
1013
- 5. The promo-server iframe content uses CSS container queries to adapt its layout to the available space
1014
- 6. Ad units are hidden for non-basic plan users via `data-wm-bind="@hide auth.account.subscription.product.id !== basic"`
1015
-
1016
- ### File Locations
1017
-
1018
- | Purpose | Path |
1019
- |---------|------|
1020
- | AdSense include | `src/defaults/dist/_includes/modules/adunits/adsense.html` |
1021
- | Promo Server include | `src/defaults/dist/_includes/modules/adunits/promo-server.html` |
1022
- | Vert JS module | `src/assets/js/modules/vert.js` |
1023
- | Vert CSS | `src/assets/css/core/_verts.scss` |
1024
-
1025
- ## Appearance Switching System
1026
-
1027
- Ultimate Jekyll supports dark/light/system theme switching with user preference persistence.
1028
-
1029
- ### Supported Modes
1030
- - `dark` - Force dark mode
1031
- - `light` - Force light mode
1032
- - `system` - Auto-detect from OS preference (`prefers-color-scheme`)
1033
-
1034
- ### JavaScript API
1035
-
1036
- ```javascript
1037
- // Get/set preference
1038
- webManager.uj().appearance.get(); // Returns 'dark', 'light', 'system', or null
1039
- webManager.uj().appearance.set('dark'); // Save and apply preference
1040
- webManager.uj().appearance.getResolved(); // Returns actual theme: 'dark' or 'light'
1041
-
1042
- // Utilities
1043
- webManager.uj().appearance.toggle(); // Toggle between dark/light
1044
- webManager.uj().appearance.cycle(); // Cycle: dark → light → system → dark
1045
- webManager.uj().appearance.clear(); // Clear saved preference
1046
- ```
1047
-
1048
- ### HTML Data Attributes
1049
-
1050
- ```html
1051
- <!-- Buttons to set appearance (auto-gets 'active' class) -->
1052
- <button data-appearance-set="light">Light</button>
1053
- <button data-appearance-set="dark">Dark</button>
1054
- <button data-appearance-set="system">System</button>
1055
-
1056
- <!-- Display current mode as text -->
1057
- <span data-appearance-current></span>
1058
-
1059
- <!-- Show/hide icons based on current mode -->
1060
- <span data-appearance-icon="light" hidden>☀️</span>
1061
- <span data-appearance-icon="dark" hidden>🌙</span>
1062
- <span data-appearance-icon="system" hidden>💻</span>
1063
- ```
1064
-
1065
- ### Dropdown Example
1066
-
1067
- ```html
1068
- <div class="dropdown">
1069
- <button class="btn dropdown-toggle" data-bs-toggle="dropdown">
1070
- <span data-appearance-icon="light" hidden>{% uj_icon "sun", "fa-md me-2" %}</span>
1071
- <span data-appearance-icon="dark" hidden>{% uj_icon "moon-stars", "fa-md me-2" %}</span>
1072
- <span data-appearance-icon="system" hidden>{% uj_icon "circle-half-stroke", "fa-md me-2" %}</span>
1073
- <span data-appearance-current></span>
1074
- </button>
1075
- <ul class="dropdown-menu">
1076
- <li><a class="dropdown-item" href="#" data-appearance-set="light">Light</a></li>
1077
- <li><a class="dropdown-item" href="#" data-appearance-set="dark">Dark</a></li>
1078
- <li><a class="dropdown-item" href="#" data-appearance-set="system">System</a></li>
1079
- </ul>
1080
- </div>
1081
- ```
1082
-
1083
- ### Implementation
1084
- - **Inline script:** `src/defaults/dist/_includes/core/body.html` - Runs immediately to prevent flash
1085
- - **Module:** `src/assets/js/core/appearance.js` - API and UI handling
1086
- - **Storage:** Saved under `_manager.appearance.preference` in localStorage
1087
- - **Test page:** `/test/libraries/appearance`
1088
-
1089
- ## JavaScript Libraries
1090
-
1091
- ### WebManager
1092
-
1093
- Custom library for site management functionality. **It's a singleton** — import it directly from any file:
1094
-
1095
- ```javascript
1096
- import webManager from 'web-manager';
1097
- ```
1098
-
1099
- This returns the same initialized instance everywhere. Do NOT pass it via params, store in module-level variables, or create new instances.
1100
-
1101
- **Documentation:** `/Users/ian/Developer/Repositories/ITW-Creative-Works/web-manager/README.md`
1102
-
1103
- **Available Utilities:**
1104
- - `webManager.auth()` - Authentication management
1105
- - `webManager.utilities()` - Utility functions (escapeHTML, clipboardCopy, etc.)
1106
- - `webManager.sentry()` - Error tracking
1107
- - `webManager.dom()` - DOM manipulation
1108
- - `webManager.utilities().escapeHTML(text)` - **XSS prevention** — use this instead of writing your own escape function
1109
-
1110
- **Important:** Always check the source code or README before assuming a function exists. Do not guess at API methods.
1111
-
1112
- #### Subscription Resolution
1113
-
1114
- Use `webManager.auth().resolveSubscription(account)` to derive calculated subscription state. This is the **single source of truth** for determining a user's effective plan — do NOT manually check `subscription.status`, `trial.claimed`, or `cancellation.pending` separately.
1115
-
1116
- ```javascript
1117
- const resolved = webManager.auth().resolveSubscription(account);
1118
- // Returns: { plan, active, trialing, cancelling }
1119
- ```
1120
-
1121
- | Field | Description |
1122
- |-------|-------------|
1123
- | `plan` | Effective plan ID right now (`'basic'` if cancelled/suspended) |
1124
- | `active` | Has active access (active, trialing, or cancelling) |
1125
- | `trialing` | In active trial |
1126
- | `cancelling` | Cancellation pending |
1127
-
1128
- Raw subscription data (product.id, status, trial, cancellation) is on `account.subscription` directly — `resolveSubscription()` returns only the calculated/derived fields.
1129
-
1130
- The same function exists in BEM as `User.resolveSubscription(account)` with identical return shape.
1131
-
1132
- ### Ultimate Jekyll Libraries
1133
-
1134
- Ultimate Jekyll provides helper libraries in `src/assets/js/libs/` that can be imported as needed.
1135
-
1136
- #### Prerendered Icons Library
1137
-
1138
- Provides access to icons defined in page frontmatter and rendered server-side.
1139
-
1140
- **Import:**
1141
- ```javascript
1142
- import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';
1143
- ```
1144
-
1145
- **Usage:**
1146
- ```javascript
1147
- // With classes (drop-in replacement for uj_icon)
1148
- getPrerenderedIcon('apple', 'fa-md me-2');
1149
-
1150
- // Without classes (no size class)
1151
- getPrerenderedIcon('apple');
1152
- ```
1153
-
1154
- **Reference:** `src/assets/js/libs/prerendered-icons.js`
1155
-
1156
- #### Authorized Fetch Library
1157
-
1158
- Simplifies authenticated API requests by automatically adding Firebase authentication tokens via Authorization Bearer header.
1159
-
1160
- **Import:**
1161
- ```javascript
1162
- import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
1163
- ```
1164
-
1165
- **Usage:**
1166
- ```javascript
1167
- const response = await authorizedFetch(url, options);
1168
- ```
1169
-
1170
- **Key Benefits:**
1171
- - No need to manually call `webManager.auth().getIdToken()`
1172
- - Automatic token injection as Authorization Bearer header
1173
- - Centralized authentication handling
1174
- - Automatic usage sync: extracts `bm-properties` header from every response and updates `webManager.bindings()` with fresh usage data under the `usage` key
1175
-
1176
- **Options pass-through:** All `wonderful-fetch` options (`response`, `output`, `body`, `timeout`, etc.) are passed through untouched. Internally, `authorizedFetch` uses `output: 'complete'` to read response headers, then returns only the body by default. If the caller passes `output: 'complete'`, they get the full `{ status, headers, body }` response.
1177
-
1178
- **Automatic Usage Binding Sync:**
1179
-
1180
- After every successful response, `authorizedFetch` reads the `bm-properties` header and updates the `usage` bindings key:
1181
- ```javascript
1182
- // After an API call, bindings are automatically updated:
1183
- // usage.credits = { monthly: 5, daily: 2, limit: 100 }
1184
- ```
1185
- This means any `data-wm-bind` elements bound to `usage.*` paths are automatically kept in sync without any manual work. See "Usage Bindings" below.
1186
-
1187
- **⚠️ IMPORTANT: Auth State Requirement**
1188
-
1189
- `authorizedFetch` requires Firebase Auth to have determined the current user's authentication state before being called. On fresh page loads (e.g., OAuth callback pages, deep links), Firebase Auth needs time to restore the session from IndexedDB/localStorage.
1190
-
1191
- **If called before auth state is determined, it will warn: `"No authenticated user found"`**
1192
-
1193
- **Solution:** Wait for auth state before calling `authorizedFetch`:
1194
-
1195
- ```javascript
1196
- // Wait for auth state to be determined (fires once auth is known)
1197
- webManager.auth().listen({ once: true }, async () => {
1198
- // Now safe to use authorizedFetch
1199
- const response = await authorizedFetch(url, options);
1200
- });
1201
- ```
1202
-
1203
- **When this matters:**
1204
- - Pages that load and immediately need to make authenticated API calls
1205
- - OAuth callback pages (user returns from external auth provider)
1206
- - Deep links that require authenticated requests on load
1207
-
1208
- **When NOT needed:**
1209
- - User-triggered actions (button clicks, form submissions) - by then auth state is always determined
1210
- - Pages that wait for user interaction before making API calls
1211
-
1212
- **Reference:** `src/assets/js/libs/authorized-fetch.js`
1213
-
1214
- #### Usage Bindings
1215
-
1216
- Usage data is available in the `usage` bindings key. It is populated from two sources:
1217
-
1218
- 1. **On page load (auth settle):** `web-manager` reads `account.usage` from Firestore and resolves plan limits from `config.payment.plans`, then sets `usage` bindings with the merged data.
1219
- 2. **After API calls:** `authorizedFetch` reads the `bm-properties` response header and merges fresh usage counters + limits into the existing `usage` bindings.
1220
-
1221
- **Bindings structure:**
1222
- ```javascript
1223
- // usage.credits = { monthly: 5, daily: 2, limit: 100 }
1224
- // usage.requests = { monthly: 20, limit: 500 }
1225
- ```
1226
-
1227
- **HTML usage:**
1228
- ```html
1229
- <!-- Show usage counter: "5/100" -->
1230
- <span data-wm-bind="@show usage.credits">
1231
- <span data-wm-bind="usage.credits.monthly">–</span>/<span data-wm-bind="usage.credits.limit">–</span>
1232
- </span>
1233
- ```
1234
-
1235
- **Config requirement:** Plan limits must be defined in `_config.yml` under `web_manager.payment.plans`:
1236
- ```yaml
1237
- web_manager:
1238
- payment:
1239
- plans:
1240
- - id: basic
1241
- limits:
1242
- credits: 100
1243
- - id: premium
1244
- limits:
1245
- credits: 500
1246
- ```
1247
-
1248
- #### Payment Config Library
1249
-
1250
- Reads payment configuration (products, processors, prices, limits) from `webManager.config.payment` — populated from `_config.yml` at build time. **Do NOT fetch `/backend-manager/brand` to get payment data.** It's already available instantly via this library.
1251
-
1252
- **Import:**
1253
- ```javascript
1254
- import { getPaymentConfig, getProcessors, getProducts, getProductById, getProductLimits, getCurrency } from '__main_assets__/js/libs/payment-config.js';
1255
- ```
1256
-
1257
- **Usage:**
1258
- ```javascript
1259
- // Get all products
1260
- const products = getProducts();
1261
-
1262
- // Find a specific product
1263
- const product = getProductById('plus');
1264
-
1265
- // Get product limits
1266
- const limits = getProductLimits('plus'); // { credits: 500, agents: 3, ... }
1267
-
1268
- // Get processors (stripe, paypal, etc.)
1269
- const processors = getProcessors();
1270
- ```
1271
-
1272
- **Config location in `_config.yml`:**
1273
- ```yaml
1274
- web_manager:
1275
- payment:
1276
- processors:
1277
- stripe:
1278
- publishableKey: pk_live_...
1279
- paypal:
1280
- clientId: ...
1281
- products:
1282
- - id: basic
1283
- name: Basic
1284
- limits:
1285
- credits: 100
1286
- - id: plus
1287
- name: Plus
1288
- limits:
1289
- credits: 500
1290
- prices:
1291
- monthly: 19
1292
- annually: 190
1293
- ```
1294
-
1295
- **How it works:** The `foot.html` Configuration injection serializes all `web_manager` properties into `window.Configuration`, which `webManager.initialize()` stores in `webManager.config`. The payment config is available immediately — no API call needed.
1296
-
1297
- **When to still use the brand API:**
1298
- - `oauth2` provider configuration (used by the connections section on the account page)
1299
- - Any data that is NOT in `_config.yml` and only exists server-side
1300
-
1301
- **Reference:** `src/assets/js/libs/payment-config.js`
1302
-
1303
- #### Pricing Page: Config-Resolved Values
1304
-
1305
- The pricing layout automatically resolves prices and feature limits from `_config.yml` when not explicitly set in frontmatter. This means consuming projects can define ONLY display metadata (name, tagline, icon, features list) and let prices/limits come from the single source of truth.
1306
-
1307
- **Resolution order (frontmatter wins):**
1308
- 1. `plan.pricing.monthly` / `plan.pricing.annually` from page frontmatter
1309
- 2. `site.web_manager.payment.products[matching_id].prices.monthly` / `.annually` from config
1310
- 3. `0` (default)
1311
-
1312
- **Feature value resolution:**
1313
- 1. `feature.value` from page frontmatter
1314
- 2. `site.web_manager.payment.products[matching_id].limits[feature.id]` from config (with `-1` → `"Unlimited"`)
1315
-
1316
- **Example: Minimal pricing.md (prices/limits come from config):**
1317
- ```yaml
1318
- ---
1319
- layout: blueprint/pricing
1320
- permalink: /pricing
1321
-
1322
- pricing:
1323
- plans:
1324
- - id: "basic"
1325
- name: "Basic"
1326
- tagline: "best for getting started"
1327
- url: "/dashboard"
1328
- features:
1329
- - id: "credits"
1330
- name: "Credits"
1331
- icon: "sparkles"
1332
- - id: "agents"
1333
- name: "Agents"
1334
- icon: "robot"
1335
- - id: "plus"
1336
- name: "Plus"
1337
- tagline: "best for small websites"
1338
- features:
1339
- - id: "credits"
1340
- name: "Credits"
1341
- icon: "sparkles"
1342
- - id: "agents"
1343
- name: "Agents"
1344
- icon: "robot"
1345
- ---
1346
- ```
1347
-
1348
- In this example, `credits` value of 100 and price of $19/mo come from `_config.yml`'s `web_manager.payment.products` — no hardcoding needed.
1349
-
1350
- #### FormManager Library
1351
-
1352
- Lightweight form state management library with built-in validation, state machine, and event system.
1353
-
1354
- **Import:**
1355
- ```javascript
1356
- import { FormManager } from '__main_assets__/js/libs/form-manager.js';
1357
- ```
1358
-
1359
- **Basic Usage:**
1360
- ```javascript
1361
- const formManager = new FormManager('#my-form', options);
1362
-
1363
- formManager.on('submit', async ({ data, $submitButton }) => {
1364
- const response = await fetch('/api', { body: JSON.stringify(data) });
1365
- if (!response.ok) throw new Error('Failed');
1366
- formManager.showSuccess('Form submitted!');
1367
- });
1368
- ```
1369
-
1370
- **State Machine:**
1371
- ```
1372
- initializing → ready ⇄ submitting → ready (or submitted)
1373
- ```
1374
-
1375
- **Configuration Options:**
1376
- ```javascript
1377
- {
1378
- autoReady: true, // Auto-transition to initialState when DOM ready
1379
- initialState: 'ready', // State after autoReady fires
1380
- allowResubmit: true, // Allow resubmission after success (false = 'submitted' state)
1381
- resetOnSuccess: false, // Clear form fields after successful submission
1382
- warnOnUnsavedChanges: true, // Warn user before leaving page with unsaved changes
1383
- submittingText: 'Processing...', // Text shown on submit button during submission
1384
- submittedText: 'Processed!', // Text shown on submit button after success (when allowResubmit: false)
1385
- inputGroup: null // Filter getData() by data-input-group attribute (null = all fields)
1386
- }
1387
- ```
1388
-
1389
- **Events:**
1390
-
1391
- | Event | Payload | Description |
1392
- |-------|---------|-------------|
1393
- | `submit` | `{ data, $submitButton }` | Form submission (throw error to show failure) |
1394
- | `validation` | `{ data, setError }` | Custom validation before submit |
1395
- | `change` | `{ field, name, value, data }` | Field value changed |
1396
- | `statechange` | `{ state, previousState }` | State transition |
1397
- | `honeypot` | `{ data }` | Honeypot triggered (for spam tracking) |
1398
-
1399
- **Validation System:**
1400
-
1401
- FormManager runs validation automatically before `submit`:
1402
- 1. **HTML5 validation** - Checks `required`, `minlength`, `maxlength`, `min`, `max`, `pattern`, `type="email"`, `type="url"`
1403
- 2. **Custom validation** - Use `validation` event for business logic
1404
-
1405
- ```javascript
1406
- fm.on('validation', ({ data, setError }) => {
1407
- if (data.age && parseInt(data.age) < 18) {
1408
- setError('age', 'You must be 18 or older');
1409
- }
1410
- });
1411
- ```
1412
-
1413
- Errors display with Bootstrap's `is-invalid` class and `.invalid-feedback` elements.
1414
-
1415
- **Autofocus:**
1416
-
1417
- When the form transitions to `ready` state, FormManager automatically focuses the field with the `autofocus` attribute (if present and not disabled).
1418
-
1419
- **Methods:**
1420
-
1421
- | Method | Description |
1422
- |--------|-------------|
1423
- | `on(event, callback)` | Register event listener (chainable) |
1424
- | `ready()` | Transition to ready state |
1425
- | `getData()` | Get form data as nested object (supports dot notation, respects input group filter) |
1426
- | `setData(obj)` | Set form values from nested object |
1427
- | `setInputGroup(group)` | Set input group filter (string, array, or null) |
1428
- | `getInputGroup()` | Get current input group filter |
1429
- | `showSuccess(msg)` | Show success notification |
1430
- | `showError(msg)` | Show error notification |
1431
- | `submit()` | Programmatically trigger form submission (fires native submit event) |
1432
- | `reset()` | Reset form and go to ready state |
1433
- | `isDirty()` | Check if form has unsaved changes |
1434
- | `setDirty(bool)` | Set dirty state |
1435
- | `clearFieldErrors()` | Clear all field validation errors |
1436
- | `throwFieldErrors({ field: msg })` | Set and display field errors, throw error |
1437
-
1438
- **Nested Field Names (Dot Notation):**
1439
-
1440
- Use dot notation in field names for nested data:
1441
- ```html
1442
- <input name="user.address.city" value="NYC">
1443
- ```
1444
-
1445
- Results in:
1446
- ```javascript
1447
- { user: { address: { city: 'NYC' } } }
1448
- ```
1449
-
1450
- **Input Groups:**
1451
-
1452
- Filter `getData()` to only return fields matching a specific group. Fields without `data-input-group` are "global" and always included.
1453
-
1454
- ```html
1455
- <!-- Global fields (no data-input-group) - always included -->
1456
- <input name="settings.theme" value="dark">
1457
-
1458
- <!-- Group-specific fields -->
1459
- <input name="options.url" data-input-group="url" value="https://example.com">
1460
- <input name="options.ssid" data-input-group="wifi" value="MyWiFi">
1461
- <input name="options.password" data-input-group="wifi" value="secret123">
1462
- ```
1463
-
1464
- ```javascript
1465
- // Set group filter (accepts string or array)
1466
- formManager.setInputGroup('url'); // Single group
1467
- formManager.setInputGroup(['url', 'wifi']); // Multiple groups
1468
- formManager.setInputGroup(null); // Clear filter (all fields)
1469
-
1470
- // Get current filter
1471
- formManager.getInputGroup(); // Returns ['url'] or null
1472
-
1473
- // getData() respects the filter
1474
- formManager.setInputGroup('wifi');
1475
- formManager.getData();
1476
- // Returns: { settings: { theme: 'dark' }, options: { ssid: 'MyWiFi', password: 'secret123' } }
1477
- // Note: 'url' field excluded, global 'settings.theme' included
1478
- ```
1479
-
1480
- Can also be set via config:
1481
- ```javascript
1482
- const fm = new FormManager('#form', { inputGroup: 'wifi' });
1483
- ```
1484
-
1485
- **Honeypot (Bot Detection):**
1486
-
1487
- FormManager automatically rejects submissions if a honeypot field is filled. Honeypot fields are hidden from users but bots fill them automatically.
1488
-
1489
- ```html
1490
- <!-- Hidden from users via CSS -->
1491
- <input type="text" name="honey" autocomplete="off" tabindex="-1"
1492
- style="position: absolute; left: -9999px;" aria-hidden="true">
1493
- ```
1494
-
1495
- Fields matching `[data-honey]` or `[name="honey"]` are:
1496
- - Excluded from `getData()` output
1497
- - Checked during validation — if filled, submission is rejected with generic error
1498
-
1499
- **Checkbox Handling:**
1500
- - **Single checkbox:** Returns `true`/`false`
1501
- - **Checkbox group (same name):** Returns object `{ value1: true, value2: false }`
1502
-
1503
- **Multiple Submit Buttons:**
1504
-
1505
- Access the clicked button via `$submitButton`:
1506
- ```html
1507
- <button type="submit" data-action="save">Save</button>
1508
- <button type="submit" data-action="draft">Save Draft</button>
1509
- ```
1510
-
1511
- ```javascript
1512
- fm.on('submit', async ({ data, $submitButton }) => {
1513
- const action = $submitButton?.dataset?.action; // 'save' or 'draft'
1514
- });
1515
- ```
1516
-
1517
- **Reference:** `src/assets/js/libs/form-manager.js`
1518
- **Test Page:** `src/assets/js/pages/test/libraries/form-manager/index.js`
1519
- **Example:** `src/assets/js/pages/contact/index.js`
1520
-
1521
- ## Analytics & Tracking
1522
-
1523
- Ultimate Jekyll uses three tracking platforms: Google Analytics (gtag), Facebook Pixel (fbq), and TikTok Pixel (ttq).
1524
-
1525
- ### ITM (Internal Tracking Medium)
1526
-
1527
- Internal tracking system modeled after UTM for cross-property user journey tracking.
1528
-
1529
- | Parameter | Purpose | Examples |
1530
- |-----------|---------|----------|
1531
- | `itm_source` | Platform/origin | `website`, `browser-extension`, `app`, `email` |
1532
- | `itm_medium` | Delivery mechanism | `modal`, `prompt`, `banner`, `tooltip` |
1533
- | `itm_campaign` | Specific campaign/feature | `exit-popup`, `premium-unlock`, `newsletter-signup` |
1534
- | `itm_content` | Specific context | Page path, feature ID, variant |
1535
-
1536
- **Examples:**
1537
- ```
1538
- # Website exit popup
1539
- ?itm_source=website&itm_medium=modal&itm_campaign=exit-popup&itm_content=/pricing
1540
-
1541
- # Extension premium unlock
1542
- ?itm_source=browser-extension&itm_medium=prompt&itm_campaign=premium-unlock&itm_content=bulk-export
1543
- ```
1544
-
1545
- ### Tracking Guidelines
1546
-
1547
- **IMPORTANT Rules:**
1548
- - Track important user events with `gtag()`, `fbq()`, and `ttq()` functions
1549
- - NEVER add conditional checks for tracking functions (e.g., `if (typeof gtag !== 'undefined')`)
1550
- - Always assume tracking functions exist - they're globally available or stubbed
1551
- - Reference standard events documentation before implementing custom tracking
1552
-
1553
- **Standard Events Documentation:**
1554
- - **Google Analytics GA4:** https://developers.google.com/analytics/devguides/collection/ga4/reference/events
1555
- - **Facebook Pixel:** https://www.facebook.com/business/help/402791146561655?id=1205376682832142
1556
- - **TikTok Pixel:** https://ads.tiktok.com/help/article/standard-events-parameters?redirected=2
1557
-
1558
- ### Platform-Specific Requirements
1559
-
1560
- #### TikTok Pixel Requirements
1561
- TikTok has strict validation requirements:
1562
-
1563
- **Required Parameters:**
1564
- - `content_id` - MUST be included in all events
1565
-
1566
- **Valid Content Types:**
1567
- - `"product"`
1568
- - `"product_group"`
1569
- - `"destination"`
1570
- - `"hotel"`
1571
- - `"flight"`
1572
- - `"vehicle"`
1573
-
1574
- Any other content type will generate a validation error.
1575
-
1576
- **Example:**
1577
- ```javascript
1578
- // ✅ CORRECT
1579
- ttq.track('ViewContent', {
1580
- content_id: 'product-123',
1581
- content_type: 'product'
1582
- });
1583
-
1584
- // ❌ WRONG - Missing content_id
1585
- ttq.track('ViewContent', {
1586
- content_type: 'product'
1587
- });
1588
-
1589
- // ❌ WRONG - Invalid content_type
1590
- ttq.track('ViewContent', {
1591
- content_id: 'product-123',
1592
- content_type: 'custom' // Not in approved list
1593
- });
1594
- ```
1595
-
1596
- ### Tracking Implementation
1597
-
1598
- **IMPORTANT:** Always track events to ALL THREE platforms in this order:
1599
- 1. Google Analytics (gtag)
1600
- 2. Facebook Pixel (fbq)
1601
- 3. TikTok Pixel (ttq)
1602
-
1603
- Track events directly without existence checks. All three tracking calls should be made together for every event.
1604
-
1605
- **Development Mode:**
1606
- In development mode, all tracking calls are intercepted and logged to the console for debugging. See `src/assets/js/libs/dev.js` for implementation.
1607
-
1608
- ## HTML Element Attributes
1609
-
1610
- The `<html>` element has data attributes for JavaScript/CSS targeting:
1611
-
1612
- | Attribute | Values |
1613
- |-----------|--------|
1614
- | `data-theme-id` | Theme ID (e.g., `classy`) |
1615
- | `data-theme-target` | `frontend`, `backend`, `docs` |
1616
- | `data-bs-theme` | `light`, `dark` |
1617
- | `data-page-path` | Page permalink (e.g., `/about`) |
1618
- | `data-asset-path` | Custom asset path or empty |
1619
- | `data-environment` | `development`, `production` |
1620
- | `data-platform` | `windows`, `mac`, `linux`, `ios`, `android`, `chromeos`, `unknown` |
1621
- | `data-browser` | `chrome`, `firefox`, `safari`, `edge`, `opera`, `brave` |
1622
- | `data-device` | `mobile` (<768px), `tablet` (768-1199px), `desktop` (>=1200px) |
1623
- | `data-runtime` | `web`, `browser-extension`, `electron`, `node` |
1624
- | `aria-busy` | `true` (loading), `false` (ready) |
1625
-
1626
- **Detection source:** `web-manager/src/modules/utilities.js`
1627
-
1628
- ## Alternatives Collection (SEO Competitor Comparison Pages)
1629
-
1630
- UJ provides an `alternatives` collection for SEO landing pages that target users searching for competitors (e.g., "ExampleApp alternatives"). These pages are entirely frontmatter-driven and designed to convert visitors who are comparing products.
1631
-
1632
- ### How It Works
1633
-
1634
- 1. The `alternatives` collection is registered in `src/config/_config_default.yml` (UJM-controlled)
1635
- 2. Each alternative is a markdown file in the consuming project's `_alternatives/` directory
1636
- 3. The layout chain: `blueprint/alternatives/alternative` → `themes/classy/frontend/pages/alternatives/alternative`
1637
- 4. An index page at `/alternatives` lists all alternatives automatically
1638
- 5. **Shared content lives in the layout** — the theme layout provides default testimonials, stats, FAQs, CTA, and why_switch content so competitor pages only need competitor-specific data
1639
- 6. The layout frontmatter uses `{{ page.resolved.alternative.competitor.name }}` to dynamically insert the competitor name into shared content (e.g., FAQ questions, CTA headlines)
1640
-
1641
- ### Creating an Alternative Page
1642
-
1643
- In the consuming project, create `src/_alternatives/competitor-name.md`. Only competitor-specific data is needed — shared sections are inherited from the layout:
1644
-
1645
- ```yaml
1646
- ---
1647
- layout: blueprint/alternatives/alternative
1648
- sitemap:
1649
- include: true
1650
-
1651
- alternative:
1652
- competitor:
1653
- name: "Competitor Name"
1654
- description: "Brief description of the competitor (shown on /alternatives listing)"
1655
- comparison:
1656
- features:
1657
- - name: "Feature Name"
1658
- icon: "sparkles"
1659
- ours:
1660
- value: true # or string like "Unlimited"
1661
- theirs:
1662
- value: false # or string like "Limited"
1663
- ---
1664
- ```
1665
-
1666
- That's it! The layout automatically generates:
1667
- - Hero with "Brand vs Competitor Name" headline
1668
- - Why Switch section with default differentiator items
1669
- - Testimonials, Stats, FAQs, and CTA with shared content
1670
- - All text dynamically references the competitor name via `{{ page.resolved.alternative.competitor.name }}`
1671
-
1672
- **To override any inherited section**, define it in the competitor's frontmatter — `page.resolved` merge gives page-level values highest priority.
1673
-
1674
- ### Available Sections
1675
-
1676
- All sections are **optional** — omit or leave empty to hide. Sections with `(shared)` have default content in the layout:
1677
-
1678
- | Section | Frontmatter Key | Description |
1679
- |---------|----------------|-------------|
1680
- | Hero | `alternative.hero` | Gradient animated hero with "Brand vs Competitor" headline (shared) |
1681
- | Comparison | `alternative.comparison` | Side-by-side feature table — **must be defined per competitor** |
1682
- | Why Switch | `alternative.why_switch` | Alternating image/text showcase blocks (shared) |
1683
- | Video | `alternative.video` | YouTube embed (default: hidden, set `youtube_id` to show) |
1684
- | Testimonials | `alternative.testimonials` | Reuses `testimonial-scroll.html` component (shared) |
1685
- | Stats | `alternative.stats` | Social proof numbers with icons (shared) |
1686
- | FAQs | `alternative.faqs` | Accordion with switching-related questions (shared) |
1687
- | CTA | `alternative.cta` | Final conversion card with buttons (shared) |
1688
-
1689
- ### Dynamic Competitor Name in Frontmatter
1690
-
1691
- The layout uses `{{ page.resolved.alternative.competitor.name }}` in its frontmatter defaults to dynamically reference the competitor. This works because the template pipes these values through `| uj_liquify` to resolve Liquid expressions.
1692
-
1693
- **Example:** The layout's default FAQ includes:
1694
- ```yaml
1695
- question: "Can I import my data from {{ page.resolved.alternative.competitor.name }}?"
1696
- ```
1697
- Which renders as "Can I import my data from ExampleApp?" for an ExampleApp competitor page.
1698
-
1699
- ### Reference Implementation
1700
-
1701
- - **Minimal competitor page:** `src/defaults/dist/_alternatives/example-competitor.md` — shows the minimum frontmatter needed (competitor name + comparison features)
1702
- - **Layout with all defaults:** `src/defaults/dist/_layouts/themes/classy/frontend/pages/alternatives/alternative.html` — contains shared content for all sections
1703
-
1704
- ### File Locations
1705
-
1706
- | Purpose | Path |
1707
- |---------|------|
1708
- | Theme layout (alternative page) | `src/defaults/dist/_layouts/themes/classy/frontend/pages/alternatives/alternative.html` |
1709
- | Theme layout (index/listing page) | `src/defaults/dist/_layouts/themes/classy/frontend/pages/alternatives/index.html` |
1710
- | Blueprint (alternative) | `src/defaults/dist/_layouts/blueprint/alternatives/alternative.html` |
1711
- | Blueprint (index) | `src/defaults/dist/_layouts/blueprint/alternatives/index.html` |
1712
- | Default page (index) | `src/defaults/dist/pages/alternatives/index.md` |
1713
- | Sample alternative | `src/defaults/dist/_alternatives/example-competitor.md` |
1714
- | CSS | `src/assets/css/pages/alternatives/alternative/index.scss` |
1715
- | JS | `src/assets/js/pages/alternatives/alternative/index.js` |
1716
-
1717
- ## Schema / Structured Data (JSON-LD)
1718
-
1719
- UJ automatically generates JSON-LD structured data in `foot.html`. The SoftwareApplication schema with AggregateRating is opt-in via frontmatter.
1720
-
1721
- ### SoftwareApplication Schema
1722
-
1723
- Renders a `SoftwareApplication` JSON-LD block with deterministic aggregate ratings. Enabled by blueprint layouts (index, pricing, download, alternatives/alternative) — consuming projects can override or disable per page.
1724
-
1725
- **How it works:**
1726
-
1727
- 1. **`_config.yml`** sets fallback defaults (no `enabled` key — just field defaults like `application_category`, `price`, etc.)
1728
- 2. **Blueprint layouts** set `schema.software_application.enabled: true` with page-appropriate `features`
1729
- 3. **Consuming projects** can override any value in their page frontmatter, or disable with `enabled: false`
1730
-
1731
- This follows the standard `page.resolved` merge: page > layout > site.
1732
-
1733
- **Deterministic ratings:** Uses the `uj_hash` filter (from jekyll-uj-powertools) seeded with `site.url` by default, producing stable values across builds:
1734
- - Rating: always `4.8` or `4.9` (deterministic per seed)
1735
- - Review count: 200,000–999,999 (deterministic per seed)
1736
- - Override seed per page with `hash_seed` to get different values
1737
-
1738
- **Blueprint frontmatter example:**
1739
- ```yaml
1740
- ### SCHEMA ###
1741
- schema:
1742
- software_application:
1743
- enabled: true
1744
- features:
1745
- - "Free to use"
1746
- - "24/7 availability"
1747
- - "User-friendly interface"
1748
- ```
1749
-
1750
- **Consuming project override example:**
1751
- ```yaml
1752
- schema:
1753
- software_application:
1754
- application_category: "EducationalApplication"
1755
- features:
1756
- - "AI-powered solutions"
1757
- - "24/7 availability"
1758
- ```
1759
-
1760
- **Consuming project disable example:**
1761
- ```yaml
1762
- schema:
1763
- software_application:
1764
- enabled: false
1765
- ```
1766
-
1767
- **Available fields:**
1768
-
1769
- | Field | Default | Description |
1770
- |-------|---------|-------------|
1771
- | `enabled` | (set by blueprint) | Enable/disable the schema block |
1772
- | `name` | `site.brand.name` | Application name |
1773
- | `description` | `page.resolved.meta.description` | Application description |
1774
- | `application_category` | `WebApplication` | Schema.org application category |
1775
- | `operating_system` | `Web-based` | Target OS |
1776
- | `price` | `0` | Price (string) |
1777
- | `price_currency` | `USD` | Currency code |
1778
- | `features` | `[]` | Feature list for `featureList` field |
1779
- | `hash_seed` | `site.url` | Seed for deterministic rating/count generation |
1780
-
1781
- **File locations:**
1782
-
1783
- | Purpose | Path |
1784
- |---------|------|
1785
- | Schema block (rendering) | `src/defaults/dist/_includes/core/foot.html` |
1786
- | Site-level defaults | `src/defaults/src/_config.yml` (under `schema:`) |
1787
- | Blueprint activation | `src/defaults/dist/_layouts/blueprint/{index,pricing,download}.html`, `blueprint/alternatives/alternative.html` |
1788
- | Hash filter | `jekyll-uj-powertools/lib/filters/main.rb` (`uj_hash`) |
1789
-
1790
- ### FAQPage Schema
1791
-
1792
- Renders a `FAQPage` JSON-LD block for pages with FAQ/accordion sections. Enabled by the alternatives blueprint — consuming projects can also enable it on any page with FAQ content.
1793
-
1794
- **How it works:**
1795
-
1796
- 1. **`_config.yml`** sets `faq_page.items: []` as fallback
1797
- 2. **Blueprint layouts** set `schema.faq_page.enabled: true`
1798
- 3. **Items source (fallback chain):** `schema.faq_page.items` → `page.resolved.faqs.items` → `page.resolved.alternative.faqs.items`. Pages with generic `faqs.items` (like pricing) and alternatives pages both get FAQPage schema automatically without duplicating content
1799
- 4. Questions/answers are processed through `uj_liquify` (supports Liquid expressions like competitor names) and `uj_json_escape`
1800
-
1801
- **Blueprint activation:** Enabled by default in `blueprint/pricing.html`, `blueprint/contact.html`, `blueprint/download.html`, `blueprint/extension/index.html`, and `blueprint/alternatives/alternative.html`.
1802
-
1803
- **Consuming project usage — provide items directly:**
1804
- ```yaml
1805
- schema:
1806
- faq_page:
1807
- enabled: true
1808
- items:
1809
- - question: "How do I get started?"
1810
- answer: "Sign up for free and follow the onboarding guide."
1811
- - question: "Is there a free plan?"
1812
- answer: "Yes, our basic plan is completely free."
1813
- ```
1814
-
1815
- **Available fields:**
1816
-
1817
- | Field | Default | Description |
1818
- |-------|---------|-------------|
1819
- | `enabled` | (set by blueprint) | Enable/disable the schema block |
1820
- | `items` | `[]` | Array of `{question, answer}` objects. Falls back to `alternative.faqs.items` if empty |
1821
-
1822
- ## Audit Workflow
1823
-
1824
- When fixing issues identified by the audit task (`src/gulp/tasks/audit.js`):
1825
-
1826
- 1. Review the audit file location provided
1827
- 2. Create a TODO list for each audit category
1828
- 3. Read the ENTIRE audit file and plan fixes for each category
1829
- 4. Tackle issues incrementally - DO NOT attempt to fix everything at once
1830
- 5. Work through one category at a time
1831
-
1832
- **Remember:** Audit files are large. Systematic, incremental fixes prevent errors and ensure thoroughness.