ultimate-jekyll-manager
Version:
Ultimate Jekyll dependency manager
817 lines (652 loc) โข 29.8 kB
Markdown
<p align="center">
<a href="https://itwcreativeworks.com">
<img src="https://cdn.itwcreativeworks.com/assets/itw-creative-works/images/logo/itw-creative-works-brandmark-black-x.svg" width="100px">
</a>
</p>
<p align="center">
<img src="https://img.shields.io/github/package-json/v/itw-creative-works/ultimate-jekyll-manager.svg">
<br>
<img src="https://img.shields.io/librariesio/release/npm/ultimate-jekyll-manager.svg">
<img src="https://img.shields.io/bundlephobia/min/ultimate-jekyll-manager.svg">
<img src="https://img.shields.io/codeclimate/maintainability-percentage/itw-creative-works/ultimate-jekyll-manager.svg">
<img src="https://img.shields.io/npm/dm/ultimate-jekyll-manager.svg">
<img src="https://img.shields.io/node/v/ultimate-jekyll-manager.svg">
<img src="https://img.shields.io/website/https/itwcreativeworks.com.svg">
<img src="https://img.shields.io/github/license/itw-creative-works/ultimate-jekyll-manager.svg">
<img src="https://img.shields.io/github/contributors/itw-creative-works/ultimate-jekyll-manager.svg">
<img src="https://img.shields.io/github/last-commit/itw-creative-works/ultimate-jekyll-manager.svg">
<br>
<br>
<a href="https://itwcreativeworks.com">Site</a> | <a href="https://www.npmjs.com/package/ultimate-jekyll-manager">NPM Module</a> | <a href="https://github.com/itw-creative-works/ultimate-jekyll-manager">GitHub Repo</a>
<br>
<br>
<strong>Ultimate Jekyll</strong> is a template that helps you jumpstart your Jekyll sites and is fueled by an intuitive incorporation of npm, gulp, and is fully SEO optimized and blazingly fast.
</p>
## ๐ฆ Features
* **SEO Optimized**: Ultimate Jekyll is fully SEO optimized.
* **Blazingly Fast**: Ultimate Jekyll is blazingly fast.
* **NPM & Gulp**: Ultimate Jekyll is fueled by an intuitive incorporation of npm and gulp.
* **Built-in test framework**: three layers (`build` / `page` / `boot`) โ plain Node, headless Chromium tab, headless Chromium against real `_site/` with SW registration verification.
## ๐ Getting started
1. [Create a repo](https://github.com/itw-creative-works/ultimate-jekyll/generate) from the **Ultimate Jekyll** template.
2. Clone the repo to your local machine.
3. Run these commands to get everything setup and sync'd!
```bash
npm start
```
## ๐งช Testing
UJM ships a built-in three-layer test harness. Write tests under `test/<layer>/*.test.js` and run with:
```bash
npx mgr test # all layers
npx mgr test --layer build # plain Node, fast
npx mgr test --layer page # headless Chromium tab against harness HTML
npx mgr test --layer boot # headless Chromium against built _site/
```
Test files use Jest-compatible matchers:
```js
// test/build/config.test.js
const Manager = require('ultimate-jekyll-manager/build');
module.exports = {
layer: 'build',
description: 'config has brand.id',
run: async (ctx) => {
const cfg = Manager.getConfig('project');
ctx.expect(cfg.brand.id).toBeTruthy();
},
};
```
Boot tests run against your actually-built `_site/` after `npm run build`:
```js
// test/boot/site.test.js
module.exports = {
layer: 'boot',
description: 'home renders + SW registers',
inspect: async ({ site, page, expect }) => {
await page.goto(site.baseUrl + '/');
expect((await page.title()).length).toBeGreaterThan(0);
},
};
```
Full guide: [docs/test-framework.md](docs/test-framework.md). Boot layer deep-dive: [docs/test-boot-layer.md](docs/test-boot-layer.md).
## ๐ฆ How to sync with the template
1. Simply run `npm start` in Terminal to get all the latest updates from the **Ultimate Jekyll template** and launch your website in the browser.
## ๐ Publishing your website
1. Change the `url` in `_config.yml` to your domain.
2. Push your changes to GitHub using `npm run dist` in Terminal.
## โณ๏ธ Flags
* `--browser=false` - Disables the browser from opening when running `npm start`.
```bash
npm start -- --browser=false
```
* `--debug=true` - Enables logging of extra information when running `npm start`.
```bash
npm start -- --debug=true
```
* `--ujPluginDevMode=true` - Enables the development mode for the [Ultimate Jekyll Ruby plugin](https://github.com/itw-creative-works/jekyll-uj-powertools).
```bash
npm start -- --ujPluginDevMode=true
```
* `--profile` - Enables Jekyll build profiling to see how long each phase takes.
```bash
npm start -- --profile
```
* `--all-posts` - Disables the development post limit (15 posts) and builds with all posts. Useful when you need to test with full blog content.
```bash
npm start -- --all-posts
```
### Other ENV variables
```bash
UJ_PURGECSS=true # Enables PurgeCSS to remove unused CSS (normally only happens in production builds)
UJ_IMAGEMIN_CACHE=false # Disables the GitHub-backed imagemin cache (forces local processing)
UJ_IMAGEMIN_REWRITE_SOURCES=true # One-off cleanup: shrinks oversized source images (>4096px) in place. See docs/images.md
```
## Running Specific Tasks
You can run specific tasks using the `npm run gulp` command with the appropriate task name.
Some of these require environment variables to be set and other tasks to be run first.
Here are some examples:
### Run the `audit` task:
```bash
# Run the audit task
npx mgr audit
# Run with a Lighthouse URL (defaults to "/" if not provided)
npx mgr audit -- --lighthouseUrl="/contact"
# Add autoExit to continue developing and testing AFTER the audit
npx mgr audit -- --lighthouseUrl="/contact" --autoExit=false
```
### Run the `translation` task:
```bash
# Test translation with GitHub cache (requires GH_TOKEN and GITHUB_REPOSITORY)
GH_TOKEN=XXX \
GITHUB_REPOSITORY=XXX \
UJ_TRANSLATION_CACHE=true \
npx mgr translation
# Test with only 1 file
UJ_TRANSLATION_ONLY="index.html" \
GH_TOKEN=XXX \
GITHUB_REPOSITORY=XXX \
UJ_TRANSLATION_CACHE=true \
npx mgr translation
```
### Run the `imagemin` task:
Test image optimization with GitHub cache in development mode:
```bash
# Test with GitHub cache (requires GH_TOKEN and GITHUB_REPOSITORY)
GH_TOKEN=XXX \
GITHUB_REPOSITORY=XXX \
UJ_IMAGEMIN_CACHE=true \
npx mgr imagemin
# Or run locally without cache
npx mgr imagemin
```
The imagemin task will:
- Process images from `src/assets/images/**/*.{jpg,jpeg,png}`
- Generate multiple sizes (1024px, 425px) and WebP formats
- Cache processed images in `cache-imagemin` branch (when using GitHub cache)
- Skip already processed images on subsequent runs
**Keep sources reasonably sized.** Source images larger than ~4096px on the longest side can stall `sharp`/`gulp-responsive-modern` in a way gulp can't detect, causing them to silently fail to land in `_site/`. Cap images at the upload step where possible. For one-off cleanup of an existing repo, run with `UJ_IMAGEMIN_REWRITE_SOURCES=true npm run build` โ see [docs/images.md](docs/images.md#cleanup-for-existing-oversized-sources-uj_imagemin_rewrite_sources) for details.
<!-- Developing -->
## ๐ Developing
1. Clone the repo to your local machine.
2. Run these commands
```bash
npm install
npm run prepare:watch
```
### Run the `blogify` task:
Create 12 test blog posts in the `_posts` directory with the `blogify` task. This is useful for testing and development purposes.
```bash
npx mgr blogify
```
## Page Frontmatter
You can add the following frontmatter to your pages to customize their behavior:
### All pages
```yaml
---
# Layout and Internals
layout: themes/[ site.theme.id ]/frontend/core/minimal # The layout to use for the page, usually 'default' or 'page'
permalink: /path/to/page # The URL path for the page, can be relative
# Control the page's meta tags
meta:
index: true # Set to false to disable indexing by search engines
title: 'Page Title' # Custom meta title for the page
description: 'Page description goes here.' # Custom meta description for the page
breadcrumb: '' # Custom breadcrumb for the page
# Control the page's theme and layout
theme:
nav:
enabled: true # Enable theme's nav on the page
footer:
enabled: true # Enable theme's footer on the page
body:
class: '' # Add custom classes to the body tag
main:
class: '' # Add custom classes to the main tag
head:
content: '' # Injected at the end of the head tag
foot:
content: '' # Injected at the end of the foot tag (inside <body>)
---
```
### Post pages
```yaml
---
# Post pages
post:
title: "Post Title" # Custom post title for the page
description: "Post description goes here." # Custom post description for the page
author: "author-id" # ID of the author from _data/authors.yml
id: 1689484669 # Unique ID for the post, used for permalink
---
```
### Team Member pages
```yaml
---
# Team Member pages
member:
id: "member-id" # ID of the team member from _data/team.yml
name: "Member Name" # Name of the team member
---
```
### Special Class
`uj-signin-btn`: Automatically handles signin (just add `data-provider="google.com"` to the button)
`uj-signup-btn`: Automatically handles signup (just add `data-provider="google.com"` to the button)
`uj-language-dropdown`:
`uj-language-dropdown-item`
### Utility Classes
#### Max-Width Utilities
Ultimate Jekyll includes max-width utility classes based on Bootstrap's breakpoint sizes. These classes constrain an element's maximum width to match Bootstrap's standard responsive breakpoints:
- `.mw-sm` - Sets max-width to 576px
- `.mw-md` - Sets max-width to 768px
- `.mw-lg` - Sets max-width to 992px
- `.mw-xl` - Sets max-width to 1200px
- `.mw-xxl` - Sets max-width to 1400px
**Usage Examples:**
```html
<!-- Constrain a form to medium width -->
<form class="mw-md">
<!-- Form content stays readable at max 768px wide -->
</form>
<!-- Limit content width for better readability -->
<div class="container mw-lg">
<!-- Content won't exceed 992px even on larger screens -->
</div>
<!-- Combine with margin utilities for centering -->
<div class="mw-sm mx-auto">
<!-- Content is max 576px wide and centered -->
</div>
```
These utilities are particularly useful for:
- Improving readability by preventing text from spanning too wide
- Creating consistent content widths across different sections
- Constraining forms, cards, and modals to reasonable sizes
- Maintaining design consistency with Bootstrap's grid system
### HTML Element Attributes
The `<html>` element has data attributes for JavaScript/CSS targeting:
| Attribute | Values |
|-----------|--------|
| `data-theme-id` | Theme ID (e.g., `classy`) |
| `data-theme-target` | `frontend`, `backend`, `docs` |
| `data-bs-theme` | `light`, `dark` |
| `data-page-path` | Page permalink (e.g., `/about`) |
| `data-asset-path` | Custom asset path or empty |
| `data-environment` | `development`, `production` |
| `data-platform` | `windows`, `mac`, `linux`, `ios`, `android`, `chromeos`, `unknown` |
| `data-device` | `mobile` (<768px), `tablet` (768-1199px), `desktop` (>=1200px) |
| `data-runtime` | `web`, `extension`, `electron`, `node` |
| `aria-busy` | `true` (loading), `false` (ready) |
### Appearance Switching
Ultimate Jekyll supports dark/light/system theme switching with user preference persistence.
**JavaScript API:**
```javascript
webManager.uj().appearance.get(); // Returns 'dark', 'light', 'system', or null
webManager.uj().appearance.set('dark'); // Save and apply preference
webManager.uj().appearance.toggle(); // Toggle dark/light
webManager.uj().appearance.cycle(); // Cycle: dark โ light โ system
```
**HTML Dropdown Example:**
```html
<div class="dropdown">
<button class="btn dropdown-toggle" data-bs-toggle="dropdown">
<span data-appearance-icon="light" hidden>{% uj_icon "sun" %}</span>
<span data-appearance-icon="dark" hidden>{% uj_icon "moon-stars" %}</span>
<span data-appearance-icon="system" hidden>{% uj_icon "circle-half-stroke" %}</span>
<span data-appearance-current></span>
</button>
<ul class="dropdown-menu">
<li><a href="#" data-appearance-set="light">Light</a></li>
<li><a href="#" data-appearance-set="dark">Dark</a></li>
<li><a href="#" data-appearance-set="system">System</a></li>
</ul>
</div>
```
### Page Loading Protection System
Ultimate Jekyll includes an automatic protection system that prevents users from clicking buttons before JavaScript is fully loaded, eliminating race conditions and errors.
#### How It Works
1. Pages start with `data-page-loading="true"` on the HTML element
2. Certain buttons are automatically protected from clicks during this state
3. When JavaScript finishes loading, the attribute is removed and buttons become clickable
#### Protected Elements
During page load, these elements are automatically protected:
- All form buttons (`<button>`, `<input type="submit">`, etc.)
- Elements with `.btn` class (Bootstrap buttons)
- Elements with `.btn-action` class (custom action triggers)
#### Using `.btn-action` Class
Add the `.btn-action` class to protect custom elements that trigger important actions:
```html
<!-- These will be protected during page load -->
<a href="/api/delete" class="custom-link btn-action">Delete Item</a>
<div onclick="saveData()" class="btn-action">Save</div>
<!-- Regular navigation links are NOT protected -->
<a href="/about">About Us</a>
<button data-bs-toggle="modal">Show Modal</button>
```
**Use `.btn-action` for:** API calls, form submissions, data modifications, payments, destructive actions
**Don't use for:** Navigation, UI toggles, modals, accordions, harmless interactions
#### Form Protection Standards
All JS-managed forms use a layered protection strategy:
1. **`onsubmit="return false"`** on every `<form>` managed by FormManager โ prevents native submission before JS loads
2. **Button initial state** โ buttons dependent on async data start `hidden` (revealed by `data-wm-bind`); auth buttons start `disabled` (enabled by FormManager's `ready()`)
3. **FormManager `autoReady`** โ use `autoReady: false` when async work happens before form init, call `ready()` explicitly after
**Exception:** Traditional forms with an `action` attribute that intentionally navigate should NOT include `onsubmit="return false"`.
### Ad Units (Verts)
UJ provides ad unit includes that display Google AdSense ads with automatic fallback to in-house promo-server ads when AdSense is blocked or unfilled.
#### AdSense Include (with fallback)
```liquid
{% include /modules/adunits/adsense.html type="in-article" %}
{% include /modules/adunits/adsense.html type="display" vert-size="rectangle" %}
{% include /modules/adunits/adsense.html type="display" vert-size="300" %}
```
| Parameter | Default | Description |
|-----------|---------|-------------|
| `type` | `display` | Ad type: `display`, `in-article`, `in-feed`, `multiplex` |
| `vert-size` | (unconstrained) | Max height preset or pixel value |
| `slot` | From site config | Override the ad slot ID |
| `style` | `""` | Custom inline CSS |
#### Promo Server Include (direct, no AdSense)
```liquid
{% include /modules/adunits/promo-server.html vert-id="/verts/units/test/google" %}
{% include /modules/adunits/promo-server.html vert-id="/verts/units/test/google" vert-size="banner" %}
```
| Parameter | Default | Description |
|-----------|---------|-------------|
| `vert-id` | `""` | Path to the vert on promo-server |
| `vert-size` | (unconstrained) | Max height preset or pixel value |
| `style` | `""` | Custom inline CSS |
#### Size Presets
| Preset | Max Height | Typical Use |
|--------|-----------|-------------|
| `banner` | 150px | Horizontal banner ads |
| `leaderboard` | 90px | Wide horizontal ads |
| `rectangle` | 250px | Medium rectangle, in-content ads |
| `large-rectangle` | 600px | Large rectangle, sidebar ads |
| `skyscraper` | 600px | Tall sidebar ads |
Raw pixel values also accepted: `vert-size="300"` โ 300px max-height. Omit `vert-size` for unconstrained rendering.
### Special Query Parameters
#### Authentication
* `authReturnUrl`: Redirects to this URL after authentication.
#### Testing Parameters
##### Account Page (`/account`)
* `_dev_subscription`: Override subscription data for testing billing states. The product ID is automatically patched to match a real product from the backend. Available values:
- `_dev_subscription=active`: Active paid subscription
- `_dev_subscription=trialing`: Free trial in progress
- `_dev_subscription=suspended`: Payment failed, access revoked
- `_dev_subscription=cancellation-requested`: Active but cancellation pending
- `_dev_subscription=cancelled`: Subscription ended
* `_dev_prefill=true`: Adds fake test data for development:
- Inserts fake referral data in the Referrals section
- Inserts fake session data in the Security section (active sessions)
##### Checkout Page (`/payment/checkout`)
* `_dev_brandId`: Override the brand ID for testing (e.g., `_dev_brandId=test-app`)
* `_dev_trialEligible`: Force trial eligibility status:
- `_dev_trialEligible=true`: User is eligible for trial
- `_dev_trialEligible=false`: User is not eligible for trial
* `_dev_cardProcessor`: Force a specific card payment processor (e.g., `_dev_cardProcessor=stripe` or `_dev_cardProcessor=chargebee`)
## JavaScript API
### Ultimate Jekyll Libraries
Ultimate Jekyll provides helper libraries in `src/assets/js/libs/` that can be imported as needed in your page modules.
#### Prerendered Icons Library
The prerendered icons library provides access to icons defined in page frontmatter. Icons are rendered server-side for optimal performance.
**Import:**
```javascript
import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';
```
**Function: `getPrerenderedIcon(iconName, classes)`**
A drop-in replacement for `uj_icon` in JavaScript contexts. The second argument works the same as `uj_icon`'s second argument.
**Parameters:**
- `iconName` (string) - Name of the icon to retrieve (matches `data-icon` attribute in frontmatter)
- `classes` (string, optional) - CSS classes for the `<i>` wrapper (e.g. `"fa-md me-2"`). Without this, the icon has no size class.
**Returns:**
- (string) Icon HTML or empty string if not found
**Example:**
```javascript
import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';
// With size + classes (same as {% uj_icon "apple", "fa-xl" %})
$el.innerHTML = getPrerenderedIcon('apple', 'fa-xl');
// In a button (same as {% uj_icon "play", "fa-md me-1" %})
$btn.innerHTML = `${getPrerenderedIcon('play', 'fa-md me-1')} Run Now`;
// Without classes (no size class on the <i> wrapper)
$el.innerHTML = getPrerenderedIcon('apple');
```
**Setup:**
Define icons in your page frontmatter (names only, no classes):
```yaml
---
prerender_icons:
- name: "apple"
- name: "android"
- name: "chrome"
---
```
**Available Icon Sizes (passed as second argument):**
- `fa-2xs` - Extra extra small
- `fa-xs` - Extra small
- `fa-sm` - Small
- `fa-md` - Medium (default base size)
- `fa-lg` - Large
- `fa-xl` - Extra large
- `fa-2xl` - 2x extra large
- `fa-3xl` - 3x extra large
- `fa-4xl` - 4x extra large
- `fa-5xl` - 5x extra large
Icons are automatically rendered in the page HTML and can be retrieved by importing the library function.
#### Authorized Fetch Library
The authorized fetch library simplifies authenticated API requests by automatically adding Firebase authentication tokens.
**Import:**
```javascript
import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
```
**Function: `authorizedFetch(url, options)`**
**Parameters:**
- `url` (string) - The API endpoint URL
- `options` (Object) - Request options for wonderful-fetch (method, body, timeout, etc.)
**Returns:**
- (Promise) - The response from the API
**Example:**
```javascript
import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
// Make an authenticated API call
const response = await authorizedFetch(serverApiURL, {
method: 'POST',
timeout: 30000,
response: 'json',
tries: 2,
body: {
command: 'user:get-data',
payload: { id: 'example' }
}
});
```
**How It Works:**
1. Retrieves the current Firebase user's ID token automatically
2. Adds the token to the request as an `Authorization: Bearer <token>` header
3. Makes the request using wonderful-fetch
4. Throws an error if no authenticated user is found
**Benefits:**
- No need to manually call `webManager.auth().getIdToken()`
- No need to add `authenticationToken` to request body
- Centralized authentication handling
- Consistent authentication across all API calls
#### FormManager Library
Lightweight form state management library with built-in validation, state machine, and event system.
**Import:**
```javascript
import { FormManager } from '__main_assets__/js/libs/form-manager.js';
```
**Basic Usage:**
```javascript
const formManager = new FormManager('#my-form', {
allowResubmit: true, // Allow form to be submitted again after success
resetOnSuccess: false, // Don't clear fields after success
warnOnUnsavedChanges: false // Don't warn on page leave
});
// Handle form submission
formManager.on('submit', async ({ data, $submitButton }) => {
const response = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error('Submission failed');
}
formManager.showSuccess('Form submitted successfully!');
});
```
**State Machine:**
```
initializing โ ready โ submitting โ ready (or submitted)
```
**Events:**
| Event | Payload | Description |
|-------|---------|-------------|
| `submit` | `{ data, $submitButton }` | Form submission. Throw an error to show failure message. |
| `validation` | `{ data, setError }` | Custom validation before submit. Use `setError(fieldName, message)` to add errors. |
| `change` | `{ field, name, value, data }` | Called when any field value changes. |
| `statechange` | `{ state, previousState }` | Called when form state changes. |
| `honeypot` | `{ data }` | Called when honeypot is triggered (for spam tracking). |
**Validation:**
FormManager runs validation automatically before the `submit` event:
1. **HTML5 Validation** - Automatically checks `required`, `minlength`, `maxlength`, `min`, `max`, `pattern`, `type="email"`, `type="url"`
2. **Custom Validation** - Use the `validation` event for business logic
```javascript
formManager.on('validation', ({ data, setError }) => {
// Custom validation runs AFTER HTML5 validation
if (data.age && parseInt(data.age) < 18) {
setError('age', 'You must be 18 or older');
}
if (data.password !== data.confirmPassword) {
setError('confirmPassword', 'Passwords do not match');
}
});
```
Validation errors are displayed using Bootstrap's `is-invalid` class and `.invalid-feedback` elements. The first field with an error is automatically focused.
**Autofocus:**
When the form transitions to `ready` state, FormManager automatically focuses any field with the `autofocus` attribute:
```html
<input type="text" name="email" autofocus>
```
**Methods:**
| Method | Description |
|--------|-------------|
| `on(event, callback)` | Register event listener (chainable) |
| `ready()` | Manually transition to ready state (for `autoReady: false`) |
| `getData()` | Get form data as nested object |
| `setData(obj)` | Populate form from a nested object |
| `showSuccess(msg)` | Show success notification |
| `showError(msg)` | Show error notification |
| `reset()` | Reset form and state |
| `isDirty()` | Check if form has unsaved changes |
| `clearFieldErrors()` | Clear all validation error displays |
| `throwFieldErrors({ field: msg })` | Set errors and throw (for use in submit handler) |
**Nested Field Names (Dot Notation):**
Use dot notation in field `name` attributes for nested data structures:
```html
<input name="user.name" value="John">
<input name="user.address.city" value="NYC">
<input name="user.address.zip" value="10001">
```
Produces:
```javascript
{
user: {
name: 'John',
address: {
city: 'NYC',
zip: '10001'
}
}
}
```
**Honeypot (Bot Detection):**
FormManager automatically rejects submissions if a honeypot field is filled. Fields matching `[data-honey]` or `[name="honey"]` are excluded from `getData()` and trigger rejection if filled.
```html
<!-- Hidden from users via CSS -->
<input type="text" name="honey" autocomplete="off" tabindex="-1"
style="position: absolute; left: -9999px;" aria-hidden="true">
```
**Checkbox Handling:**
- **Single checkbox:** Returns `true` or `false`
- **Checkbox group (multiple with same name):** Returns object with each value as key
```html
<!-- Single checkbox -->
<input type="checkbox" name="subscribe" checked>
<!-- Result: { subscribe: true } -->
<!-- Checkbox group -->
<input type="checkbox" name="features" value="darkmode" checked>
<input type="checkbox" name="features" value="analytics">
<input type="checkbox" name="features" value="beta" checked>
<!-- Result: { features: { darkmode: true, analytics: false, beta: true } } -->
```
**Multiple Submit Buttons:**
Access the clicked submit button to handle different actions:
```html
<button type="submit" data-action="save">Save</button>
<button type="submit" data-action="draft">Save as Draft</button>
```
```javascript
formManager.on('submit', async ({ data, $submitButton }) => {
const action = $submitButton?.dataset?.action;
if (action === 'draft') {
await saveDraft(data);
formManager.showSuccess('Draft saved!');
} else {
await saveAndPublish(data);
formManager.showSuccess('Published!');
}
});
```
**Manual Ready Mode:**
For forms that need async initialization (e.g., loading data from API):
```javascript
const formManager = new FormManager('#my-form', { autoReady: false });
// Load data, then mark ready
const userData = await fetchUserData();
formManager.setData(userData);
formManager.ready(); // Now form is interactive
```
**Configuration Options:**
| Option | Default | Description |
|--------|---------|-------------|
| `autoReady` | `true` | Auto-transition to ready when DOM is ready |
| `initialState` | `'ready'` | State after autoReady fires |
| `allowResubmit` | `true` | Allow form resubmission after success |
| `resetOnSuccess` | `false` | Clear fields after successful submission |
| `warnOnUnsavedChanges` | `true` | Show browser warning when leaving with unsaved changes |
| `submittingText` | `'Processing...'` | Text shown on submit button during submission |
**Test Page:** Navigate to `/test/libraries/form-manager` to see FormManager in action with various configurations.
### ITM (Internal Tracking Medium)
Internal tracking system modeled after UTM for cross-property user journey tracking.
| Parameter | Purpose | Examples |
|-----------|---------|----------|
| `itm_source` | Platform/origin | `website`, `browser-extension`, `app`, `email` |
| `itm_medium` | Delivery mechanism | `modal`, `prompt`, `banner`, `tooltip` |
| `itm_campaign` | Specific campaign/feature | `exit-popup`, `premium-unlock`, `newsletter-signup` |
| `itm_content` | Specific context | Page path, feature ID, variant |
**Examples:**
```
# Website exit popup
?itm_source=website&itm_medium=modal&itm_campaign=exit-popup&itm_content=/pricing
# Extension premium unlock
?itm_source=browser-extension&itm_medium=prompt&itm_campaign=premium-unlock&itm_content=bulk-export
```
### Icons
* Fontawesome
* https://fontawesome.com/search
* Flags
* https://www.freepik.com/icon/england_4720360#fromView=resource_detail&position=1
* More
* Language
* https://www.freepik.com/icon/language_484531#fromView=family&page=1&position=0&uuid=651a2f0f-9023-4063-a495-af9a4ef72304
### Webpack Import Aliases
UJM defines two webpack aliases (in `src/gulp/tasks/webpack.js`) for importing assets in JavaScript:
| Alias | Resolves To | Purpose |
|-------|------------|---------|
| `__main_assets__` | `[UJM package]/dist/assets` | UJM's own built-in assets (core modules, libraries, pages) |
| `__project_assets__` | `[consuming project]/src/assets` | The consuming project's custom assets |
**`__main_assets__`** โ Import UJM libraries and core modules:
```javascript
import { FormManager } from '__main_assets__/js/libs/form-manager.js';
import authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';
import { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';
```
**`__project_assets__`** โ Import consuming project's own assets:
```javascript
// Used in src/index.js to load project-specific page modules
import(`__project_assets__/js/pages/${pageModulePath}`)
```
**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.
**When to use which:**
- **`__main_assets__`** โ When importing UJM-provided libraries, core modules, or referencing UJM's built-in page scripts
- **`__project_assets__`** โ When a consuming project needs to import its own custom assets from within UJM-managed code
## Dev Flags
Add this to any js file to ONLY run in development mode (it will be excluded in production builds):
```
/* @dev-only:start */
{
// Your development-only code goes here
}
/* @dev-only:end */
```
## ๐งฐ Sister projects
- [Electron Manager (EM)](https://github.com/itw-creative-works/electron-manager) โ same patterns, but for Electron desktop apps
- [Browser Extension Manager (BXM)](https://github.com/itw-creative-works/browser-extension-manager) โ same patterns, but for cross-browser MV3 extensions
- [Backend Manager (BEM)](https://github.com/itw-creative-works/backend-manager) โ Firebase Functions backend framework