ultimate-jekyll-manager 1.2.0 → 1.2.1
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.
- package/CHANGELOG.md +7 -0
- package/CLAUDE.md +50 -23
- package/dist/defaults/CHANGELOG.md +15 -0
- package/dist/defaults/CLAUDE.md +17 -5
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/extension/index.html +0 -26
- package/dist/defaults/docs/README.md +17 -0
- package/dist/defaults/test/README.md +31 -0
- package/docs/ads.md +78 -0
- package/docs/analytics.md +90 -0
- package/docs/appearance.md +65 -0
- package/docs/assets.md +314 -0
- package/docs/audit.md +11 -0
- package/docs/css.md +73 -0
- package/docs/icons.md +125 -0
- package/docs/images.md +42 -0
- package/docs/javascript-libraries.md +457 -0
- package/docs/jekyll-plugin.md +69 -0
- package/docs/layouts-and-pages.md +31 -0
- package/docs/lazy-loading.md +58 -0
- package/docs/local-development.md +59 -0
- package/docs/page-loading.md +85 -0
- package/docs/project-structure.md +23 -0
- package/docs/seo.md +206 -0
- package/docs/xss-prevention.md +126 -0
- package/package.json +1 -1
- package/docs/_legacy-claude-md.md +0 -1832
|
@@ -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'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
|
-

|
|
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
|
-
| `` | Local post image (shortcut) |
|
|
640
|
-
| `` | Absolute path (any image) |
|
|
641
|
-
| `` | External URL |
|
|
642
|
-
|
|
643
|
-
**How it works:** The `markdown-images.rb` hook in `jekyll-uj-powertools` intercepts `` 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.
|