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.
- package/CHANGELOG.md +15 -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
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
# JavaScript Libraries
|
|
2
|
+
|
|
3
|
+
UJM ships two layers of frontend JS infrastructure:
|
|
4
|
+
|
|
5
|
+
1. **WebManager** — the singleton site-management library (auth, utilities, sentry, dom).
|
|
6
|
+
2. **Ultimate Jekyll Libraries** at `src/assets/js/libs/` — helper modules importable via `__main_assets__/js/libs/<name>.js`.
|
|
7
|
+
|
|
8
|
+
## WebManager
|
|
9
|
+
|
|
10
|
+
Custom library for site management functionality. **It's a singleton** — import it directly from any file:
|
|
11
|
+
|
|
12
|
+
```javascript
|
|
13
|
+
import webManager from 'web-manager';
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This returns the same initialized instance everywhere. Do NOT pass it via params, store in module-level variables, or create new instances.
|
|
17
|
+
|
|
18
|
+
**Documentation:** `/Users/ian/Developer/Repositories/ITW-Creative-Works/web-manager/README.md`
|
|
19
|
+
|
|
20
|
+
**Available Utilities:**
|
|
21
|
+
- `webManager.auth()` — Authentication management
|
|
22
|
+
- `webManager.utilities()` — Utility functions (escapeHTML, clipboardCopy, etc.)
|
|
23
|
+
- `webManager.sentry()` — Error tracking
|
|
24
|
+
- `webManager.dom()` — DOM manipulation
|
|
25
|
+
- `webManager.utilities().escapeHTML(text)` — **XSS prevention** — use this instead of writing your own escape function. See [docs/xss-prevention.md](xss-prevention.md).
|
|
26
|
+
|
|
27
|
+
**Important:** Always check the source code or README before assuming a function exists. Do not guess at API methods.
|
|
28
|
+
|
|
29
|
+
### Subscription Resolution
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
const resolved = webManager.auth().resolveSubscription(account);
|
|
35
|
+
// Returns: { plan, active, trialing, cancelling }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
| Field | Description |
|
|
39
|
+
|-------|-------------|
|
|
40
|
+
| `plan` | Effective plan ID right now (`'basic'` if cancelled/suspended) |
|
|
41
|
+
| `active` | Has active access (active, trialing, or cancelling) |
|
|
42
|
+
| `trialing` | In active trial |
|
|
43
|
+
| `cancelling` | Cancellation pending |
|
|
44
|
+
|
|
45
|
+
Raw subscription data (product.id, status, trial, cancellation) is on `account.subscription` directly — `resolveSubscription()` returns only the calculated/derived fields.
|
|
46
|
+
|
|
47
|
+
The same function exists in BEM as `User.resolveSubscription(account)` with identical return shape.
|
|
48
|
+
|
|
49
|
+
## Ultimate Jekyll Libraries
|
|
50
|
+
|
|
51
|
+
Ultimate Jekyll provides helper libraries in `src/assets/js/libs/` that can be imported as needed.
|
|
52
|
+
|
|
53
|
+
### Prerendered Icons Library
|
|
54
|
+
|
|
55
|
+
Provides access to icons defined in page frontmatter and rendered server-side. See [docs/icons.md](icons.md) for when/why to use prerendered icons.
|
|
56
|
+
|
|
57
|
+
**Import:**
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Usage:**
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
// With classes (drop-in replacement for uj_icon)
|
|
67
|
+
getPrerenderedIcon('apple', 'fa-md me-2');
|
|
68
|
+
|
|
69
|
+
// Without classes (no size class)
|
|
70
|
+
getPrerenderedIcon('apple');
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Reference:** `src/assets/js/libs/prerendered-icons.js`
|
|
74
|
+
|
|
75
|
+
### Authorized Fetch Library
|
|
76
|
+
|
|
77
|
+
Simplifies authenticated API requests by automatically adding Firebase authentication tokens via Authorization Bearer header.
|
|
78
|
+
|
|
79
|
+
**Import:**
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Usage:**
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
const response = await authorizedFetch(url, options);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Key Benefits:**
|
|
92
|
+
- No need to manually call `webManager.auth().getIdToken()`
|
|
93
|
+
- Automatic token injection as Authorization Bearer header
|
|
94
|
+
- Centralized authentication handling
|
|
95
|
+
- Automatic usage sync: extracts `bm-properties` header from every response and updates `webManager.bindings()` with fresh usage data under the `usage` key
|
|
96
|
+
|
|
97
|
+
**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.
|
|
98
|
+
|
|
99
|
+
**Automatic Usage Binding Sync:**
|
|
100
|
+
|
|
101
|
+
After every successful response, `authorizedFetch` reads the `bm-properties` header and updates the `usage` bindings key:
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
// After an API call, bindings are automatically updated:
|
|
105
|
+
// usage.credits = { monthly: 5, daily: 2, limit: 100 }
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This means any `data-wm-bind` elements bound to `usage.*` paths are automatically kept in sync without any manual work. See "Usage Bindings" below.
|
|
109
|
+
|
|
110
|
+
**⚠️ IMPORTANT: Auth State Requirement**
|
|
111
|
+
|
|
112
|
+
`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.
|
|
113
|
+
|
|
114
|
+
**If called before auth state is determined, it will warn: `"No authenticated user found"`**
|
|
115
|
+
|
|
116
|
+
**Solution:** Wait for auth state before calling `authorizedFetch`:
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
// Wait for auth state to be determined (fires once auth is known)
|
|
120
|
+
webManager.auth().listen({ once: true }, async () => {
|
|
121
|
+
// Now safe to use authorizedFetch
|
|
122
|
+
const response = await authorizedFetch(url, options);
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**When this matters:**
|
|
127
|
+
- Pages that load and immediately need to make authenticated API calls
|
|
128
|
+
- OAuth callback pages (user returns from external auth provider)
|
|
129
|
+
- Deep links that require authenticated requests on load
|
|
130
|
+
|
|
131
|
+
**When NOT needed:**
|
|
132
|
+
- User-triggered actions (button clicks, form submissions) — by then auth state is always determined
|
|
133
|
+
- Pages that wait for user interaction before making API calls
|
|
134
|
+
|
|
135
|
+
**Reference:** `src/assets/js/libs/authorized-fetch.js`
|
|
136
|
+
|
|
137
|
+
### Usage Bindings
|
|
138
|
+
|
|
139
|
+
Usage data is available in the `usage` bindings key. It is populated from two sources:
|
|
140
|
+
|
|
141
|
+
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.
|
|
142
|
+
2. **After API calls:** `authorizedFetch` reads the `bm-properties` response header and merges fresh usage counters + limits into the existing `usage` bindings.
|
|
143
|
+
|
|
144
|
+
**Bindings structure:**
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
// usage.credits = { monthly: 5, daily: 2, limit: 100 }
|
|
148
|
+
// usage.requests = { monthly: 20, limit: 500 }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**HTML usage:**
|
|
152
|
+
|
|
153
|
+
```html
|
|
154
|
+
<!-- Show usage counter: "5/100" -->
|
|
155
|
+
<span data-wm-bind="@show usage.credits">
|
|
156
|
+
<span data-wm-bind="usage.credits.monthly">–</span>/<span data-wm-bind="usage.credits.limit">–</span>
|
|
157
|
+
</span>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Config requirement:** Plan limits must be defined in `_config.yml` under `web_manager.payment.plans`:
|
|
161
|
+
|
|
162
|
+
```yaml
|
|
163
|
+
web_manager:
|
|
164
|
+
payment:
|
|
165
|
+
plans:
|
|
166
|
+
- id: basic
|
|
167
|
+
limits:
|
|
168
|
+
credits: 100
|
|
169
|
+
- id: premium
|
|
170
|
+
limits:
|
|
171
|
+
credits: 500
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Payment Config Library
|
|
175
|
+
|
|
176
|
+
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.
|
|
177
|
+
|
|
178
|
+
**Import:**
|
|
179
|
+
|
|
180
|
+
```javascript
|
|
181
|
+
import { getPaymentConfig, getProcessors, getProducts, getProductById, getProductLimits, getCurrency } from '__main_assets__/js/libs/payment-config.js';
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Usage:**
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
// Get all products
|
|
188
|
+
const products = getProducts();
|
|
189
|
+
|
|
190
|
+
// Find a specific product
|
|
191
|
+
const product = getProductById('plus');
|
|
192
|
+
|
|
193
|
+
// Get product limits
|
|
194
|
+
const limits = getProductLimits('plus'); // { credits: 500, agents: 3, ... }
|
|
195
|
+
|
|
196
|
+
// Get processors (stripe, paypal, etc.)
|
|
197
|
+
const processors = getProcessors();
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Config location in `_config.yml`:**
|
|
201
|
+
|
|
202
|
+
```yaml
|
|
203
|
+
web_manager:
|
|
204
|
+
payment:
|
|
205
|
+
processors:
|
|
206
|
+
stripe:
|
|
207
|
+
publishableKey: pk_live_...
|
|
208
|
+
paypal:
|
|
209
|
+
clientId: ...
|
|
210
|
+
products:
|
|
211
|
+
- id: basic
|
|
212
|
+
name: Basic
|
|
213
|
+
limits:
|
|
214
|
+
credits: 100
|
|
215
|
+
- id: plus
|
|
216
|
+
name: Plus
|
|
217
|
+
limits:
|
|
218
|
+
credits: 500
|
|
219
|
+
prices:
|
|
220
|
+
monthly: 19
|
|
221
|
+
annually: 190
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**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.
|
|
225
|
+
|
|
226
|
+
**When to still use the brand API:**
|
|
227
|
+
- `oauth2` provider configuration (used by the connections section on the account page)
|
|
228
|
+
- Any data that is NOT in `_config.yml` and only exists server-side
|
|
229
|
+
|
|
230
|
+
**Reference:** `src/assets/js/libs/payment-config.js`
|
|
231
|
+
|
|
232
|
+
### Pricing Page: Config-Resolved Values
|
|
233
|
+
|
|
234
|
+
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.
|
|
235
|
+
|
|
236
|
+
**Resolution order (frontmatter wins):**
|
|
237
|
+
1. `plan.pricing.monthly` / `plan.pricing.annually` from page frontmatter
|
|
238
|
+
2. `site.web_manager.payment.products[matching_id].prices.monthly` / `.annually` from config
|
|
239
|
+
3. `0` (default)
|
|
240
|
+
|
|
241
|
+
**Feature value resolution:**
|
|
242
|
+
1. `feature.value` from page frontmatter
|
|
243
|
+
2. `site.web_manager.payment.products[matching_id].limits[feature.id]` from config (with `-1` → `"Unlimited"`)
|
|
244
|
+
|
|
245
|
+
**Example: Minimal pricing.md (prices/limits come from config):**
|
|
246
|
+
|
|
247
|
+
```yaml
|
|
248
|
+
---
|
|
249
|
+
layout: blueprint/pricing
|
|
250
|
+
permalink: /pricing
|
|
251
|
+
|
|
252
|
+
pricing:
|
|
253
|
+
plans:
|
|
254
|
+
- id: "basic"
|
|
255
|
+
name: "Basic"
|
|
256
|
+
tagline: "best for getting started"
|
|
257
|
+
url: "/dashboard"
|
|
258
|
+
features:
|
|
259
|
+
- id: "credits"
|
|
260
|
+
name: "Credits"
|
|
261
|
+
icon: "sparkles"
|
|
262
|
+
- id: "agents"
|
|
263
|
+
name: "Agents"
|
|
264
|
+
icon: "robot"
|
|
265
|
+
- id: "plus"
|
|
266
|
+
name: "Plus"
|
|
267
|
+
tagline: "best for small websites"
|
|
268
|
+
features:
|
|
269
|
+
- id: "credits"
|
|
270
|
+
name: "Credits"
|
|
271
|
+
icon: "sparkles"
|
|
272
|
+
- id: "agents"
|
|
273
|
+
name: "Agents"
|
|
274
|
+
icon: "robot"
|
|
275
|
+
---
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
In this example, `credits` value of 100 and price of $19/mo come from `_config.yml`'s `web_manager.payment.products` — no hardcoding needed.
|
|
279
|
+
|
|
280
|
+
### FormManager Library
|
|
281
|
+
|
|
282
|
+
Lightweight form state management library with built-in validation, state machine, and event system. See also [docs/page-loading.md → Form Protection Standards](page-loading.md#form-protection-standards) for the form-protection layering rules.
|
|
283
|
+
|
|
284
|
+
**Import:**
|
|
285
|
+
|
|
286
|
+
```javascript
|
|
287
|
+
import { FormManager } from '__main_assets__/js/libs/form-manager.js';
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Basic Usage:**
|
|
291
|
+
|
|
292
|
+
```javascript
|
|
293
|
+
const formManager = new FormManager('#my-form', options);
|
|
294
|
+
|
|
295
|
+
formManager.on('submit', async ({ data, $submitButton }) => {
|
|
296
|
+
const response = await fetch('/api', { body: JSON.stringify(data) });
|
|
297
|
+
if (!response.ok) throw new Error('Failed');
|
|
298
|
+
formManager.showSuccess('Form submitted!');
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**State Machine:**
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
initializing → ready ⇄ submitting → ready (or submitted)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Configuration Options:**
|
|
309
|
+
|
|
310
|
+
```javascript
|
|
311
|
+
{
|
|
312
|
+
autoReady: true, // Auto-transition to initialState when DOM ready
|
|
313
|
+
initialState: 'ready', // State after autoReady fires
|
|
314
|
+
allowResubmit: true, // Allow resubmission after success (false = 'submitted' state)
|
|
315
|
+
resetOnSuccess: false, // Clear form fields after successful submission
|
|
316
|
+
warnOnUnsavedChanges: true, // Warn user before leaving page with unsaved changes
|
|
317
|
+
submittingText: 'Processing...', // Text shown on submit button during submission
|
|
318
|
+
submittedText: 'Processed!', // Text shown on submit button after success (when allowResubmit: false)
|
|
319
|
+
inputGroup: null // Filter getData() by data-input-group attribute (null = all fields)
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Events:**
|
|
324
|
+
|
|
325
|
+
| Event | Payload | Description |
|
|
326
|
+
|-------|---------|-------------|
|
|
327
|
+
| `submit` | `{ data, $submitButton }` | Form submission (throw error to show failure) |
|
|
328
|
+
| `validation` | `{ data, setError }` | Custom validation before submit |
|
|
329
|
+
| `change` | `{ field, name, value, data }` | Field value changed |
|
|
330
|
+
| `statechange` | `{ state, previousState }` | State transition |
|
|
331
|
+
| `honeypot` | `{ data }` | Honeypot triggered (for spam tracking) |
|
|
332
|
+
|
|
333
|
+
**Validation System:**
|
|
334
|
+
|
|
335
|
+
FormManager runs validation automatically before `submit`:
|
|
336
|
+
1. **HTML5 validation** — Checks `required`, `minlength`, `maxlength`, `min`, `max`, `pattern`, `type="email"`, `type="url"`
|
|
337
|
+
2. **Custom validation** — Use `validation` event for business logic
|
|
338
|
+
|
|
339
|
+
```javascript
|
|
340
|
+
fm.on('validation', ({ data, setError }) => {
|
|
341
|
+
if (data.age && parseInt(data.age) < 18) {
|
|
342
|
+
setError('age', 'You must be 18 or older');
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Errors display with Bootstrap's `is-invalid` class and `.invalid-feedback` elements.
|
|
348
|
+
|
|
349
|
+
**Autofocus:**
|
|
350
|
+
|
|
351
|
+
When the form transitions to `ready` state, FormManager automatically focuses the field with the `autofocus` attribute (if present and not disabled).
|
|
352
|
+
|
|
353
|
+
**Methods:**
|
|
354
|
+
|
|
355
|
+
| Method | Description |
|
|
356
|
+
|--------|-------------|
|
|
357
|
+
| `on(event, callback)` | Register event listener (chainable) |
|
|
358
|
+
| `ready()` | Transition to ready state |
|
|
359
|
+
| `getData()` | Get form data as nested object (supports dot notation, respects input group filter) |
|
|
360
|
+
| `setData(obj)` | Set form values from nested object |
|
|
361
|
+
| `setInputGroup(group)` | Set input group filter (string, array, or null) |
|
|
362
|
+
| `getInputGroup()` | Get current input group filter |
|
|
363
|
+
| `showSuccess(msg)` | Show success notification |
|
|
364
|
+
| `showError(msg)` | Show error notification |
|
|
365
|
+
| `submit()` | Programmatically trigger form submission (fires native submit event) |
|
|
366
|
+
| `reset()` | Reset form and go to ready state |
|
|
367
|
+
| `isDirty()` | Check if form has unsaved changes |
|
|
368
|
+
| `setDirty(bool)` | Set dirty state |
|
|
369
|
+
| `clearFieldErrors()` | Clear all field validation errors |
|
|
370
|
+
| `throwFieldErrors({ field: msg })` | Set and display field errors, throw error |
|
|
371
|
+
|
|
372
|
+
**Nested Field Names (Dot Notation):**
|
|
373
|
+
|
|
374
|
+
Use dot notation in field names for nested data:
|
|
375
|
+
|
|
376
|
+
```html
|
|
377
|
+
<input name="user.address.city" value="NYC">
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Results in:
|
|
381
|
+
|
|
382
|
+
```javascript
|
|
383
|
+
{ user: { address: { city: 'NYC' } } }
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**Input Groups:**
|
|
387
|
+
|
|
388
|
+
Filter `getData()` to only return fields matching a specific group. Fields without `data-input-group` are "global" and always included.
|
|
389
|
+
|
|
390
|
+
```html
|
|
391
|
+
<!-- Global fields (no data-input-group) - always included -->
|
|
392
|
+
<input name="settings.theme" value="dark">
|
|
393
|
+
|
|
394
|
+
<!-- Group-specific fields -->
|
|
395
|
+
<input name="options.url" data-input-group="url" value="https://example.com">
|
|
396
|
+
<input name="options.ssid" data-input-group="wifi" value="MyWiFi">
|
|
397
|
+
<input name="options.password" data-input-group="wifi" value="secret123">
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
```javascript
|
|
401
|
+
// Set group filter (accepts string or array)
|
|
402
|
+
formManager.setInputGroup('url'); // Single group
|
|
403
|
+
formManager.setInputGroup(['url', 'wifi']); // Multiple groups
|
|
404
|
+
formManager.setInputGroup(null); // Clear filter (all fields)
|
|
405
|
+
|
|
406
|
+
// Get current filter
|
|
407
|
+
formManager.getInputGroup(); // Returns ['url'] or null
|
|
408
|
+
|
|
409
|
+
// getData() respects the filter
|
|
410
|
+
formManager.setInputGroup('wifi');
|
|
411
|
+
formManager.getData();
|
|
412
|
+
// Returns: { settings: { theme: 'dark' }, options: { ssid: 'MyWiFi', password: 'secret123' } }
|
|
413
|
+
// Note: 'url' field excluded, global 'settings.theme' included
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Can also be set via config:
|
|
417
|
+
|
|
418
|
+
```javascript
|
|
419
|
+
const fm = new FormManager('#form', { inputGroup: 'wifi' });
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
**Honeypot (Bot Detection):**
|
|
423
|
+
|
|
424
|
+
FormManager automatically rejects submissions if a honeypot field is filled. Honeypot fields are hidden from users but bots fill them automatically.
|
|
425
|
+
|
|
426
|
+
```html
|
|
427
|
+
<!-- Hidden from users via CSS -->
|
|
428
|
+
<input type="text" name="honey" autocomplete="off" tabindex="-1"
|
|
429
|
+
style="position: absolute; left: -9999px;" aria-hidden="true">
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Fields matching `[data-honey]` or `[name="honey"]` are:
|
|
433
|
+
- Excluded from `getData()` output
|
|
434
|
+
- Checked during validation — if filled, submission is rejected with generic error
|
|
435
|
+
|
|
436
|
+
**Checkbox Handling:**
|
|
437
|
+
- **Single checkbox:** Returns `true`/`false`
|
|
438
|
+
- **Checkbox group (same name):** Returns object `{ value1: true, value2: false }`
|
|
439
|
+
|
|
440
|
+
**Multiple Submit Buttons:**
|
|
441
|
+
|
|
442
|
+
Access the clicked button via `$submitButton`:
|
|
443
|
+
|
|
444
|
+
```html
|
|
445
|
+
<button type="submit" data-action="save">Save</button>
|
|
446
|
+
<button type="submit" data-action="draft">Save Draft</button>
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
```javascript
|
|
450
|
+
fm.on('submit', async ({ data, $submitButton }) => {
|
|
451
|
+
const action = $submitButton?.dataset?.action; // 'save' or 'draft'
|
|
452
|
+
});
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
**Reference:** `src/assets/js/libs/form-manager.js`
|
|
456
|
+
**Test Page:** `src/assets/js/pages/test/libraries/form-manager/index.js`
|
|
457
|
+
**Example:** `src/assets/js/pages/contact/index.js`
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# UJ Powertools (Jekyll Plugin)
|
|
2
|
+
|
|
3
|
+
Ultimate Jekyll uses the `jekyll-uj-powertools` gem for custom Liquid functionality.
|
|
4
|
+
|
|
5
|
+
**Documentation:** `/Users/ian/Developer/Repositories/ITW-Creative-works/jekyll-uj-powertools/README.md`
|
|
6
|
+
|
|
7
|
+
## Available Features
|
|
8
|
+
|
|
9
|
+
- **Filters:** `uj_strip_ads`, `uj_json_escape`, `uj_title_case`, `uj_content_format`, `uj_hash`
|
|
10
|
+
- **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`
|
|
11
|
+
- **Global Variables:** `site.uj.cache_breaker`
|
|
12
|
+
- **Page Variables:** `page.random_id`, `page.extension`, `page.layout_data`, `page.resolved`
|
|
13
|
+
|
|
14
|
+
**Always check the README before assuming functionality.**
|
|
15
|
+
|
|
16
|
+
## Key Liquid Functions
|
|
17
|
+
|
|
18
|
+
### `uj_content_format`
|
|
19
|
+
|
|
20
|
+
Formats content by first liquifying it, then markdownifying it (if markdown file).
|
|
21
|
+
|
|
22
|
+
### `uj_hash`
|
|
23
|
+
|
|
24
|
+
Returns a deterministic number between 0 and max (exclusive) based on the input string's MD5 hash. Same input always produces the same output.
|
|
25
|
+
|
|
26
|
+
```liquid
|
|
27
|
+
{{ "some-string" | uj_hash: 1000 }} => 0-999 (stable across builds)
|
|
28
|
+
{{ site.url | uj_hash: 2 }} => 0 or 1
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### `iftruthy` / `iffalsy`
|
|
32
|
+
|
|
33
|
+
Custom tags that check JavaScript truthiness (not null, undefined, or empty string).
|
|
34
|
+
|
|
35
|
+
```liquid
|
|
36
|
+
{% iftruthy variable %}
|
|
37
|
+
<!-- Content -->
|
|
38
|
+
{% endiftruthy %}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Limitations:**
|
|
42
|
+
- Does NOT support logical operators
|
|
43
|
+
- Does NOT support `else` statements
|
|
44
|
+
- CAN contain nested sub-statements
|
|
45
|
+
|
|
46
|
+
### `page.resolved`
|
|
47
|
+
|
|
48
|
+
A deeply merged object containing all site, layout, and page variables. Precedence: page > layout > site. Enables a system of defaults with progressive overrides.
|
|
49
|
+
|
|
50
|
+
### `uj_icon`
|
|
51
|
+
|
|
52
|
+
Inserts Font Awesome icons. See [docs/icons.md](icons.md) for the full reference (sizes, when to use vs prerendered icons in JS, etc.).
|
|
53
|
+
|
|
54
|
+
```liquid
|
|
55
|
+
{% uj_icon icon-name, "fa-md" %}
|
|
56
|
+
{% uj_icon "rocket", "fa-3xl" %}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### `asset_path` Override
|
|
60
|
+
|
|
61
|
+
Override default page-specific CSS/JS path derivation:
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
---
|
|
65
|
+
asset_path: blog/post
|
|
66
|
+
---
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
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). See also [docs/layouts-and-pages.md](layouts-and-pages.md).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Layouts and Pages
|
|
2
|
+
|
|
3
|
+
## Page Types
|
|
4
|
+
|
|
5
|
+
- **One-off pages** (e.g., `/categories`, `/sitemap`) — Create as pages without custom layouts; use existing layouts
|
|
6
|
+
- **Repeating page types** (e.g., blog posts, category pages) — Create a dedicated layout (e.g., `_layouts/category.html`)
|
|
7
|
+
|
|
8
|
+
## Layout Requirements
|
|
9
|
+
|
|
10
|
+
All layouts and pages must eventually require a theme entry point:
|
|
11
|
+
|
|
12
|
+
```yaml
|
|
13
|
+
layout: themes/[ site.theme.id ]/frontend/core/base
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Note:** The `[ site.theme.id ]` syntax is correct and allows dynamic theme selection.
|
|
17
|
+
|
|
18
|
+
## Asset Path Configuration
|
|
19
|
+
|
|
20
|
+
For pages sharing the same assets, use the `asset_path` frontmatter variable:
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
---
|
|
24
|
+
# Instead of deriving path from page.canonical.path
|
|
25
|
+
asset_path: categories/category
|
|
26
|
+
---
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Example:**
|
|
30
|
+
- One-off page: `pages/categories.html` → `src/assets/css/pages/categories/index.scss`
|
|
31
|
+
- Repeating layout: `_layouts/category.html` → `src/assets/css/pages/categories/category.scss` (set `asset_path: categories/category` in layout frontmatter)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Lazy Loading System
|
|
2
|
+
|
|
3
|
+
Ultimate Jekyll uses a custom lazy loading system powered by web-manager.
|
|
4
|
+
|
|
5
|
+
## Syntax
|
|
6
|
+
|
|
7
|
+
```html
|
|
8
|
+
data-lazy="@type value"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Supported Types
|
|
12
|
+
|
|
13
|
+
### `@src` — Lazy load src attribute
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<img data-lazy="@src /assets/images/hero.jpg" alt="Hero">
|
|
17
|
+
<iframe data-lazy="@src https://example.com/embed"></iframe>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### `@srcset` — Lazy load srcset attribute
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
<img data-lazy="@srcset /img/small.jpg 480w, /img/large.jpg 1024w">
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### `@bg` — Lazy load background images
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<div data-lazy="@bg /assets/images/background.jpg"></div>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### `@class` — Lazy add CSS classes
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<div data-lazy="@class animation-fade-in">Content</div>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### `@html` — Lazy inject HTML content
|
|
39
|
+
|
|
40
|
+
```html
|
|
41
|
+
<div data-lazy="@html <p>Lazy loaded content</p>"></div>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### `@script` — Lazy load external scripts
|
|
45
|
+
|
|
46
|
+
```html
|
|
47
|
+
<div data-lazy='@script {"src": "https://example.com/widget.js", "attributes": {"async": true}}'></div>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Features
|
|
51
|
+
|
|
52
|
+
- Automatic cache busting via `buildTime`
|
|
53
|
+
- IntersectionObserver for performance (50px threshold)
|
|
54
|
+
- Loading state CSS classes: `lazy-loading`, `lazy-loaded`, `lazy-error`
|
|
55
|
+
- Intelligent handling of video/audio sources
|
|
56
|
+
- Automatic DOM re-scanning for dynamic elements
|
|
57
|
+
|
|
58
|
+
**Implementation:** `src/assets/js/core/lazy-loading.js`
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Local Development
|
|
2
|
+
|
|
3
|
+
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`.
|
|
4
|
+
|
|
5
|
+
## Connecting to Local Firebase Emulators
|
|
6
|
+
|
|
7
|
+
Set the `FIREBASE_EMULATOR_CONNECT` environment variable to `true` to connect the frontend to local Firebase services (Auth, Firestore, Functions, etc.):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
FIREBASE_EMULATOR_CONNECT=true npm start
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## PurgeCSS
|
|
16
|
+
|
|
17
|
+
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`:
|
|
18
|
+
|
|
19
|
+
```json5
|
|
20
|
+
{
|
|
21
|
+
sass: {
|
|
22
|
+
purgecss: {
|
|
23
|
+
safelist: {
|
|
24
|
+
standard: [], // Matches against the full class name
|
|
25
|
+
deep: [], // Matches including child selectors (e.g., pseudo-selectors like :checked)
|
|
26
|
+
greedy: [], // Matches anywhere in the selector string
|
|
27
|
+
keyframes: [], // Preserves @keyframes animations by name
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**All entries are regex strings** — each gets converted to `new RegExp(entry)`. This means:
|
|
35
|
+
|
|
36
|
+
| Pattern | Matches | Does NOT match |
|
|
37
|
+
|---------|---------|----------------|
|
|
38
|
+
| `"^dot$"` | `dot` | `dotted`, `polkadot` |
|
|
39
|
+
| `"^chat-"` | `chat-bubble`, `chat-input` | `live-chat` |
|
|
40
|
+
| `"fw-semibold"` | `fw-semibold`, `fw-semibold-custom` | (matches loosely) |
|
|
41
|
+
|
|
42
|
+
**Use `^` and `$` anchors for exact matches.** Without them, the pattern matches any class *containing* the string.
|
|
43
|
+
|
|
44
|
+
**Example:**
|
|
45
|
+
|
|
46
|
+
```json5
|
|
47
|
+
{
|
|
48
|
+
sass: {
|
|
49
|
+
purgecss: {
|
|
50
|
+
safelist: {
|
|
51
|
+
standard: ["^dot$", "^fw-semibold$", "^chat-"],
|
|
52
|
+
deep: [":focus-within"],
|
|
53
|
+
greedy: ["^chat-"],
|
|
54
|
+
keyframes: ["chat-typing-bounce"],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
```
|