{"id":20719906,"url":"https://github.com/itw-creative-works/ultimate-jekyll-manager","last_synced_at":"2026-05-24T22:02:09.004Z","repository":{"id":98695453,"uuid":"299744009","full_name":"itw-creative-works/ultimate-jekyll-manager","owner":"itw-creative-works","description":"All-in-one package for ultimate-jekyll","archived":false,"fork":false,"pushed_at":"2026-04-06T22:31:59.000Z","size":8416,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-07T00:31:38.575Z","etag":null,"topics":["css","framework","front-end","html","javascript","ultimate-jekyll","webpack","website"],"latest_commit_sha":null,"homepage":"https://itwcreativeworks.com","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/itw-creative-works.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2020-09-29T21:38:47.000Z","updated_at":"2026-04-06T22:31:34.000Z","dependencies_parsed_at":null,"dependency_job_id":"cfe776bc-307e-44bb-a394-8def52865ee0","html_url":"https://github.com/itw-creative-works/ultimate-jekyll-manager","commit_stats":null,"previous_names":[],"tags_count":18,"template":false,"template_full_name":null,"purl":"pkg:github/itw-creative-works/ultimate-jekyll-manager","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/itw-creative-works%2Fultimate-jekyll-manager","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/itw-creative-works%2Fultimate-jekyll-manager/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/itw-creative-works%2Fultimate-jekyll-manager/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/itw-creative-works%2Fultimate-jekyll-manager/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/itw-creative-works","download_url":"https://codeload.github.com/itw-creative-works/ultimate-jekyll-manager/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/itw-creative-works%2Fultimate-jekyll-manager/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31586410,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-08T14:31:17.711Z","status":"online","status_checked_at":"2026-04-09T02:00:06.848Z","response_time":112,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["css","framework","front-end","html","javascript","ultimate-jekyll","webpack","website"],"created_at":"2024-11-17T03:18:46.000Z","updated_at":"2026-05-19T00:09:28.201Z","avatar_url":"https://github.com/itw-creative-works.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://itwcreativeworks.com\"\u003e\n    \u003cimg src=\"https://cdn.itwcreativeworks.com/assets/itw-creative-works/images/logo/itw-creative-works-brandmark-black-x.svg\" width=\"100px\"\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://img.shields.io/github/package-json/v/itw-creative-works/ultimate-jekyll-manager.svg\"\u003e\n  \u003cbr\u003e\n  \u003cimg src=\"https://img.shields.io/librariesio/release/npm/ultimate-jekyll-manager.svg\"\u003e\n  \u003cimg src=\"https://img.shields.io/bundlephobia/min/ultimate-jekyll-manager.svg\"\u003e\n  \u003cimg src=\"https://img.shields.io/codeclimate/maintainability-percentage/itw-creative-works/ultimate-jekyll-manager.svg\"\u003e\n  \u003cimg src=\"https://img.shields.io/npm/dm/ultimate-jekyll-manager.svg\"\u003e\n  \u003cimg src=\"https://img.shields.io/node/v/ultimate-jekyll-manager.svg\"\u003e\n  \u003cimg src=\"https://img.shields.io/website/https/itwcreativeworks.com.svg\"\u003e\n  \u003cimg src=\"https://img.shields.io/github/license/itw-creative-works/ultimate-jekyll-manager.svg\"\u003e\n  \u003cimg src=\"https://img.shields.io/github/contributors/itw-creative-works/ultimate-jekyll-manager.svg\"\u003e\n  \u003cimg src=\"https://img.shields.io/github/last-commit/itw-creative-works/ultimate-jekyll-manager.svg\"\u003e\n  \u003cbr\u003e\n  \u003cbr\u003e\n  \u003ca href=\"https://itwcreativeworks.com\"\u003eSite\u003c/a\u003e | \u003ca href=\"https://www.npmjs.com/package/ultimate-jekyll-manager\"\u003eNPM Module\u003c/a\u003e | \u003ca href=\"https://github.com/itw-creative-works/ultimate-jekyll-manager\"\u003eGitHub Repo\u003c/a\u003e\n  \u003cbr\u003e\n  \u003cbr\u003e\n  \u003cstrong\u003eUltimate Jekyll\u003c/strong\u003e 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.\n\u003c/p\u003e\n\n## 🦄 Features\n* **SEO Optimized**: Ultimate Jekyll is fully SEO optimized.\n* **Blazingly Fast**: Ultimate Jekyll is blazingly fast.\n* **NPM \u0026 Gulp**: Ultimate Jekyll is fueled by an intuitive incorporation of npm and gulp.\n* **Built-in test framework**: three layers (`build` / `page` / `boot`) — plain Node, headless Chromium tab, headless Chromium against real `_site/` with SW registration verification.\n\n## 🚀 Getting started\n1. [Create a repo](https://github.com/itw-creative-works/ultimate-jekyll/generate) from the **Ultimate Jekyll** template.\n2. Clone the repo to your local machine.\n3. Run these commands to get everything setup and sync'd!\n```bash\nnpm start\n```\n\n## 🧪 Testing\n\nUJM ships a built-in three-layer test harness. Write tests under `test/\u003clayer\u003e/*.test.js` and run with:\n\n```bash\nnpx mgr test                   # all layers\nnpx mgr test --layer build     # plain Node, fast\nnpx mgr test --layer page      # headless Chromium tab against harness HTML\nnpx mgr test --layer boot      # headless Chromium against built _site/\n```\n\nTest files use Jest-compatible matchers:\n\n```js\n// test/build/config.test.js\nconst Manager = require('ultimate-jekyll-manager/build');\n\nmodule.exports = {\n  layer: 'build',\n  description: 'config has brand.id',\n  run: async (ctx) =\u003e {\n    const cfg = Manager.getConfig('project');\n    ctx.expect(cfg.brand.id).toBeTruthy();\n  },\n};\n```\n\nBoot tests run against your actually-built `_site/` after `npm run build`:\n\n```js\n// test/boot/site.test.js\nmodule.exports = {\n  layer: 'boot',\n  description: 'home renders + SW registers',\n  inspect: async ({ site, page, expect }) =\u003e {\n    await page.goto(site.baseUrl + '/');\n    expect((await page.title()).length).toBeGreaterThan(0);\n  },\n};\n```\n\nFull guide: [docs/test-framework.md](docs/test-framework.md). Boot layer deep-dive: [docs/test-boot-layer.md](docs/test-boot-layer.md).\n\n## 📦 How to sync with the template\n1. Simply run `npm start` in Terminal to get all the latest updates from the **Ultimate Jekyll template** and launch your website in the browser.\n\n## 🌎 Publishing your website\n1. Change the `url` in `_config.yml` to your domain.\n2. Push your changes to GitHub using `npm run dist` in Terminal.\n\n## ⛳️ Flags\n* `--browser=false` - Disables the browser from opening when running `npm start`.\n```bash\nnpm start -- --browser=false\n```\n* `--debug=true` - Enables logging of extra information when running `npm start`.\n```bash\nnpm start -- --debug=true\n```\n* `--ujPluginDevMode=true` - Enables the development mode for the [Ultimate Jekyll Ruby plugin](https://github.com/itw-creative-works/jekyll-uj-powertools).\n```bash\nnpm start -- --ujPluginDevMode=true\n```\n* `--profile` - Enables Jekyll build profiling to see how long each phase takes.\n```bash\nnpm start -- --profile\n```\n* `--all-posts` - Disables the development post limit (15 posts) and builds with all posts. Useful when you need to test with full blog content.\n```bash\nnpm start -- --all-posts\n```\n\n### Other ENV variables\n```bash\nUJ_PURGECSS=true # Enables PurgeCSS to remove unused CSS (normally only happens in production builds)\n```\n\n## Running Specific Tasks\nYou can run specific tasks using the `npm run gulp` command with the appropriate task name.\n\nSome of these require environment variables to be set and other tasks to be run first.\n\nHere are some examples:\n\n### Run the `audit` task:\n```bash\n# Run the audit task\nnpx mgr audit\n\n# Run with a Lighthouse URL (defaults to \"/\" if not provided)\nnpx mgr audit -- --lighthouseUrl=\"/contact\"\n\n# Add autoExit to continue developing and testing AFTER the audit\nnpx mgr audit -- --lighthouseUrl=\"/contact\" --autoExit=false\n```\n\n### Run the `translation` task:\n```bash\n# Test translation with GitHub cache (requires GH_TOKEN and GITHUB_REPOSITORY)\nGH_TOKEN=XXX \\\nGITHUB_REPOSITORY=XXX \\\nUJ_TRANSLATION_CACHE=true \\\nnpx mgr translation\n\n# Test with only 1 file\nUJ_TRANSLATION_ONLY=\"index.html\" \\\nGH_TOKEN=XXX \\\nGITHUB_REPOSITORY=XXX \\\nUJ_TRANSLATION_CACHE=true \\\nnpx mgr translation\n```\n\n### Run the `imagemin` task:\nTest image optimization with GitHub cache in development mode:\n```bash\n# Test with GitHub cache (requires GH_TOKEN and GITHUB_REPOSITORY)\nGH_TOKEN=XXX \\\nGITHUB_REPOSITORY=XXX \\\nUJ_IMAGEMIN_CACHE=true \\\nnpx mgr imagemin\n\n# Or run locally without cache\nnpx mgr imagemin\n```\nThe imagemin task will:\n- Process images from `src/assets/images/**/*.{jpg,jpeg,png}`\n- Generate multiple sizes (1024px, 425px) and WebP formats\n- Cache processed images in `cache-imagemin` branch (when using GitHub cache)\n- Skip already processed images on subsequent runs\n\n\u003c!-- Developing --\u003e\n## 🛠 Developing\n1. Clone the repo to your local machine.\n2. Run these commands\n```bash\nnpm install\nnpm run prepare:watch\n```\n\n### Run the `blogify` task:\nCreate 12 test blog posts in the `_posts` directory with the `blogify` task. This is useful for testing and development purposes.\n```bash\nnpx mgr blogify\n```\n\n## Page Frontmatter\nYou can add the following frontmatter to your pages to customize their behavior:\n\n### All pages\n```yaml\n---\n# Layout and Internals\nlayout: themes/[ site.theme.id ]/frontend/core/minimal # The layout to use for the page, usually 'default' or 'page'\npermalink: /path/to/page # The URL path for the page, can be relative\n\n# Control the page's meta tags\nmeta:\n  index: true # Set to false to disable indexing by search engines\n  title: 'Page Title' # Custom meta title for the page\n  description: 'Page description goes here.' # Custom meta description for the page\n  breadcrumb: '' # Custom breadcrumb for the page\n\n# Control the page's theme and layout\ntheme:\n  nav:\n    enabled: true # Enable theme's nav on the page\n  footer:\n    enabled: true # Enable theme's footer on the page\n  body:\n    class: '' # Add custom classes to the body tag\n    main:\n      class: '' # Add custom classes to the main tag\n  head:\n    content: '' # Injected at the end of the head tag\n  foot:\n    content: '' # Injected at the end of the foot tag (inside \u003cbody\u003e)\n---\n```\n\n### Post pages\n```yaml\n---\n# Post pages\npost:\n  title: \"Post Title\" # Custom post title for the page\n  description: \"Post description goes here.\" # Custom post description for the page\n  author: \"author-id\" # ID of the author from _data/authors.yml\n  id: 1689484669 # Unique ID for the post, used for permalink\n---\n```\n\n### Team Member pages\n```yaml\n---\n# Team Member pages\nmember:\n  id: \"member-id\" # ID of the team member from _data/team.yml\n  name: \"Member Name\" # Name of the team member\n---\n```\n\n### Special Class\n`uj-signin-btn`: Automatically handles signin (just add `data-provider=\"google.com\"` to the button)\n`uj-signup-btn`: Automatically handles signup (just add `data-provider=\"google.com\"` to the button)\n\n`uj-language-dropdown`:\n`uj-language-dropdown-item`\n\n### Utility Classes\n\n#### Max-Width Utilities\nUltimate 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:\n\n- `.mw-sm` - Sets max-width to 576px\n- `.mw-md` - Sets max-width to 768px\n- `.mw-lg` - Sets max-width to 992px\n- `.mw-xl` - Sets max-width to 1200px\n- `.mw-xxl` - Sets max-width to 1400px\n\n**Usage Examples:**\n```html\n\u003c!-- Constrain a form to medium width --\u003e\n\u003cform class=\"mw-md\"\u003e\n  \u003c!-- Form content stays readable at max 768px wide --\u003e\n\u003c/form\u003e\n\n\u003c!-- Limit content width for better readability --\u003e\n\u003cdiv class=\"container mw-lg\"\u003e\n  \u003c!-- Content won't exceed 992px even on larger screens --\u003e\n\u003c/div\u003e\n\n\u003c!-- Combine with margin utilities for centering --\u003e\n\u003cdiv class=\"mw-sm mx-auto\"\u003e\n  \u003c!-- Content is max 576px wide and centered --\u003e\n\u003c/div\u003e\n```\n\nThese utilities are particularly useful for:\n- Improving readability by preventing text from spanning too wide\n- Creating consistent content widths across different sections\n- Constraining forms, cards, and modals to reasonable sizes\n- Maintaining design consistency with Bootstrap's grid system\n\n### HTML Element Attributes\n\nThe `\u003chtml\u003e` element has data attributes for JavaScript/CSS targeting:\n\n| Attribute | Values |\n|-----------|--------|\n| `data-theme-id` | Theme ID (e.g., `classy`) |\n| `data-theme-target` | `frontend`, `backend`, `docs` |\n| `data-bs-theme` | `light`, `dark` |\n| `data-page-path` | Page permalink (e.g., `/about`) |\n| `data-asset-path` | Custom asset path or empty |\n| `data-environment` | `development`, `production` |\n| `data-platform` | `windows`, `mac`, `linux`, `ios`, `android`, `chromeos`, `unknown` |\n| `data-device` | `mobile` (\u003c768px), `tablet` (768-1199px), `desktop` (\u003e=1200px) |\n| `data-runtime` | `web`, `extension`, `electron`, `node` |\n| `aria-busy` | `true` (loading), `false` (ready) |\n\n### Appearance Switching\n\nUltimate Jekyll supports dark/light/system theme switching with user preference persistence.\n\n**JavaScript API:**\n```javascript\nwebManager.uj().appearance.get();        // Returns 'dark', 'light', 'system', or null\nwebManager.uj().appearance.set('dark');  // Save and apply preference\nwebManager.uj().appearance.toggle();     // Toggle dark/light\nwebManager.uj().appearance.cycle();      // Cycle: dark → light → system\n```\n\n**HTML Dropdown Example:**\n```html\n\u003cdiv class=\"dropdown\"\u003e\n  \u003cbutton class=\"btn dropdown-toggle\" data-bs-toggle=\"dropdown\"\u003e\n    \u003cspan data-appearance-icon=\"light\" hidden\u003e{% uj_icon \"sun\" %}\u003c/span\u003e\n    \u003cspan data-appearance-icon=\"dark\" hidden\u003e{% uj_icon \"moon-stars\" %}\u003c/span\u003e\n    \u003cspan data-appearance-icon=\"system\" hidden\u003e{% uj_icon \"circle-half-stroke\" %}\u003c/span\u003e\n    \u003cspan data-appearance-current\u003e\u003c/span\u003e\n  \u003c/button\u003e\n  \u003cul class=\"dropdown-menu\"\u003e\n    \u003cli\u003e\u003ca href=\"#\" data-appearance-set=\"light\"\u003eLight\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#\" data-appearance-set=\"dark\"\u003eDark\u003c/a\u003e\u003c/li\u003e\n    \u003cli\u003e\u003ca href=\"#\" data-appearance-set=\"system\"\u003eSystem\u003c/a\u003e\u003c/li\u003e\n  \u003c/ul\u003e\n\u003c/div\u003e\n```\n\n### Page Loading Protection System\n\nUltimate Jekyll includes an automatic protection system that prevents users from clicking buttons before JavaScript is fully loaded, eliminating race conditions and errors.\n\n#### How It Works\n1. Pages start with `data-page-loading=\"true\"` on the HTML element\n2. Certain buttons are automatically protected from clicks during this state\n3. When JavaScript finishes loading, the attribute is removed and buttons become clickable\n\n#### Protected Elements\nDuring page load, these elements are automatically protected:\n- All form buttons (`\u003cbutton\u003e`, `\u003cinput type=\"submit\"\u003e`, etc.)\n- Elements with `.btn` class (Bootstrap buttons)\n- Elements with `.btn-action` class (custom action triggers)\n\n#### Using `.btn-action` Class\nAdd the `.btn-action` class to protect custom elements that trigger important actions:\n\n```html\n\u003c!-- These will be protected during page load --\u003e\n\u003ca href=\"/api/delete\" class=\"custom-link btn-action\"\u003eDelete Item\u003c/a\u003e\n\u003cdiv onclick=\"saveData()\" class=\"btn-action\"\u003eSave\u003c/div\u003e\n\n\u003c!-- Regular navigation links are NOT protected --\u003e\n\u003ca href=\"/about\"\u003eAbout Us\u003c/a\u003e\n\u003cbutton data-bs-toggle=\"modal\"\u003eShow Modal\u003c/button\u003e\n```\n\n**Use `.btn-action` for:** API calls, form submissions, data modifications, payments, destructive actions\n**Don't use for:** Navigation, UI toggles, modals, accordions, harmless interactions\n\n#### Form Protection Standards\n\nAll JS-managed forms use a layered protection strategy:\n\n1. **`onsubmit=\"return false\"`** on every `\u003cform\u003e` managed by FormManager — prevents native submission before JS loads\n2. **Button initial state** — buttons dependent on async data start `hidden` (revealed by `data-wm-bind`); auth buttons start `disabled` (enabled by FormManager's `ready()`)\n3. **FormManager `autoReady`** — use `autoReady: false` when async work happens before form init, call `ready()` explicitly after\n\n**Exception:** Traditional forms with an `action` attribute that intentionally navigate should NOT include `onsubmit=\"return false\"`.\n\n### Ad Units (Verts)\n\nUJ provides ad unit includes that display Google AdSense ads with automatic fallback to in-house promo-server ads when AdSense is blocked or unfilled.\n\n#### AdSense Include (with fallback)\n```liquid\n{% include /modules/adunits/adsense.html type=\"in-article\" %}\n{% include /modules/adunits/adsense.html type=\"display\" vert-size=\"rectangle\" %}\n{% include /modules/adunits/adsense.html type=\"display\" vert-size=\"300\" %}\n```\n\n| Parameter | Default | Description |\n|-----------|---------|-------------|\n| `type` | `display` | Ad type: `display`, `in-article`, `in-feed`, `multiplex` |\n| `vert-size` | (unconstrained) | Max height preset or pixel value |\n| `slot` | From site config | Override the ad slot ID |\n| `style` | `\"\"` | Custom inline CSS |\n\n#### Promo Server Include (direct, no AdSense)\n```liquid\n{% include /modules/adunits/promo-server.html vert-id=\"/verts/units/test/google\" %}\n{% include /modules/adunits/promo-server.html vert-id=\"/verts/units/test/google\" vert-size=\"banner\" %}\n```\n\n| Parameter | Default | Description |\n|-----------|---------|-------------|\n| `vert-id` | `\"\"` | Path to the vert on promo-server |\n| `vert-size` | (unconstrained) | Max height preset or pixel value |\n| `style` | `\"\"` | Custom inline CSS |\n\n#### Size Presets\n\n| Preset | Max Height | Typical Use |\n|--------|-----------|-------------|\n| `banner` | 150px | Horizontal banner ads |\n| `leaderboard` | 90px | Wide horizontal ads |\n| `rectangle` | 250px | Medium rectangle, in-content ads |\n| `large-rectangle` | 600px | Large rectangle, sidebar ads |\n| `skyscraper` | 600px | Tall sidebar ads |\n\nRaw pixel values also accepted: `vert-size=\"300\"` → 300px max-height. Omit `vert-size` for unconstrained rendering.\n\n### Special Query Parameters\n\n#### Authentication\n* `authReturnUrl`: Redirects to this URL after authentication.\n\n#### Testing Parameters\n\n##### Account Page (`/account`)\n* `_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:\n  - `_dev_subscription=active`: Active paid subscription\n  - `_dev_subscription=trialing`: Free trial in progress\n  - `_dev_subscription=suspended`: Payment failed, access revoked\n  - `_dev_subscription=cancellation-requested`: Active but cancellation pending\n  - `_dev_subscription=cancelled`: Subscription ended\n* `_dev_prefill=true`: Adds fake test data for development:\n  - Inserts fake referral data in the Referrals section\n  - Inserts fake session data in the Security section (active sessions)\n\n##### Checkout Page (`/payment/checkout`)\n* `_dev_brandId`: Override the brand ID for testing (e.g., `_dev_brandId=test-app`)\n* `_dev_trialEligible`: Force trial eligibility status:\n  - `_dev_trialEligible=true`: User is eligible for trial\n  - `_dev_trialEligible=false`: User is not eligible for trial\n* `_dev_cardProcessor`: Force a specific card payment processor (e.g., `_dev_cardProcessor=stripe` or `_dev_cardProcessor=chargebee`)\n\n## JavaScript API\n\n### Ultimate Jekyll Libraries\n\nUltimate Jekyll provides helper libraries in `src/assets/js/libs/` that can be imported as needed in your page modules.\n\n#### Prerendered Icons Library\n\nThe prerendered icons library provides access to icons defined in page frontmatter. Icons are rendered server-side for optimal performance.\n\n**Import:**\n```javascript\nimport { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';\n```\n\n**Function: `getPrerenderedIcon(iconName, classes)`**\n\nA drop-in replacement for `uj_icon` in JavaScript contexts. The second argument works the same as `uj_icon`'s second argument.\n\n**Parameters:**\n- `iconName` (string) - Name of the icon to retrieve (matches `data-icon` attribute in frontmatter)\n- `classes` (string, optional) - CSS classes for the `\u003ci\u003e` wrapper (e.g. `\"fa-md me-2\"`). Without this, the icon has no size class.\n\n**Returns:**\n- (string) Icon HTML or empty string if not found\n\n**Example:**\n```javascript\nimport { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';\n\n// With size + classes (same as {% uj_icon \"apple\", \"fa-xl\" %})\n$el.innerHTML = getPrerenderedIcon('apple', 'fa-xl');\n\n// In a button (same as {% uj_icon \"play\", \"fa-md me-1\" %})\n$btn.innerHTML = `${getPrerenderedIcon('play', 'fa-md me-1')} Run Now`;\n\n// Without classes (no size class on the \u003ci\u003e wrapper)\n$el.innerHTML = getPrerenderedIcon('apple');\n```\n\n**Setup:**\nDefine icons in your page frontmatter (names only, no classes):\n```yaml\n---\nprerender_icons:\n  - name: \"apple\"\n  - name: \"android\"\n  - name: \"chrome\"\n---\n```\n\n**Available Icon Sizes (passed as second argument):**\n- `fa-2xs` - Extra extra small\n- `fa-xs` - Extra small\n- `fa-sm` - Small\n- `fa-md` - Medium (default base size)\n- `fa-lg` - Large\n- `fa-xl` - Extra large\n- `fa-2xl` - 2x extra large\n- `fa-3xl` - 3x extra large\n- `fa-4xl` - 4x extra large\n- `fa-5xl` - 5x extra large\n\nIcons are automatically rendered in the page HTML and can be retrieved by importing the library function.\n\n#### Authorized Fetch Library\n\nThe authorized fetch library simplifies authenticated API requests by automatically adding Firebase authentication tokens.\n\n**Import:**\n```javascript\nimport authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';\n```\n\n**Function: `authorizedFetch(url, options)`**\n\n**Parameters:**\n- `url` (string) - The API endpoint URL\n- `options` (Object) - Request options for wonderful-fetch (method, body, timeout, etc.)\n\n**Returns:**\n- (Promise) - The response from the API\n\n**Example:**\n```javascript\nimport authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';\n\n// Make an authenticated API call\nconst response = await authorizedFetch(serverApiURL, {\n  method: 'POST',\n  timeout: 30000,\n  response: 'json',\n  tries: 2,\n  body: {\n    command: 'user:get-data',\n    payload: { id: 'example' }\n  }\n});\n```\n\n**How It Works:**\n1. Retrieves the current Firebase user's ID token automatically\n2. Adds the token to the request as an `Authorization: Bearer \u003ctoken\u003e` header\n3. Makes the request using wonderful-fetch\n4. Throws an error if no authenticated user is found\n\n**Benefits:**\n- No need to manually call `webManager.auth().getIdToken()`\n- No need to add `authenticationToken` to request body\n- Centralized authentication handling\n- Consistent authentication across all API calls\n\n#### FormManager Library\n\nLightweight form state management library with built-in validation, state machine, and event system.\n\n**Import:**\n```javascript\nimport { FormManager } from '__main_assets__/js/libs/form-manager.js';\n```\n\n**Basic Usage:**\n```javascript\nconst formManager = new FormManager('#my-form', {\n  allowResubmit: true,      // Allow form to be submitted again after success\n  resetOnSuccess: false,    // Don't clear fields after success\n  warnOnUnsavedChanges: false // Don't warn on page leave\n});\n\n// Handle form submission\nformManager.on('submit', async ({ data, $submitButton }) =\u003e {\n  const response = await fetch('/api/submit', {\n    method: 'POST',\n    body: JSON.stringify(data)\n  });\n\n  if (!response.ok) {\n    throw new Error('Submission failed');\n  }\n\n  formManager.showSuccess('Form submitted successfully!');\n});\n```\n\n**State Machine:**\n```\ninitializing → ready ⇄ submitting → ready (or submitted)\n```\n\n**Events:**\n\n| Event | Payload | Description |\n|-------|---------|-------------|\n| `submit` | `{ data, $submitButton }` | Form submission. Throw an error to show failure message. |\n| `validation` | `{ data, setError }` | Custom validation before submit. Use `setError(fieldName, message)` to add errors. |\n| `change` | `{ field, name, value, data }` | Called when any field value changes. |\n| `statechange` | `{ state, previousState }` | Called when form state changes. |\n| `honeypot` | `{ data }` | Called when honeypot is triggered (for spam tracking). |\n\n**Validation:**\n\nFormManager runs validation automatically before the `submit` event:\n\n1. **HTML5 Validation** - Automatically checks `required`, `minlength`, `maxlength`, `min`, `max`, `pattern`, `type=\"email\"`, `type=\"url\"`\n2. **Custom Validation** - Use the `validation` event for business logic\n\n```javascript\nformManager.on('validation', ({ data, setError }) =\u003e {\n  // Custom validation runs AFTER HTML5 validation\n  if (data.age \u0026\u0026 parseInt(data.age) \u003c 18) {\n    setError('age', 'You must be 18 or older');\n  }\n\n  if (data.password !== data.confirmPassword) {\n    setError('confirmPassword', 'Passwords do not match');\n  }\n});\n```\n\nValidation errors are displayed using Bootstrap's `is-invalid` class and `.invalid-feedback` elements. The first field with an error is automatically focused.\n\n**Autofocus:**\n\nWhen the form transitions to `ready` state, FormManager automatically focuses any field with the `autofocus` attribute:\n\n```html\n\u003cinput type=\"text\" name=\"email\" autofocus\u003e\n```\n\n**Methods:**\n\n| Method | Description |\n|--------|-------------|\n| `on(event, callback)` | Register event listener (chainable) |\n| `ready()` | Manually transition to ready state (for `autoReady: false`) |\n| `getData()` | Get form data as nested object |\n| `setData(obj)` | Populate form from a nested object |\n| `showSuccess(msg)` | Show success notification |\n| `showError(msg)` | Show error notification |\n| `reset()` | Reset form and state |\n| `isDirty()` | Check if form has unsaved changes |\n| `clearFieldErrors()` | Clear all validation error displays |\n| `throwFieldErrors({ field: msg })` | Set errors and throw (for use in submit handler) |\n\n**Nested Field Names (Dot Notation):**\n\nUse dot notation in field `name` attributes for nested data structures:\n\n```html\n\u003cinput name=\"user.name\" value=\"John\"\u003e\n\u003cinput name=\"user.address.city\" value=\"NYC\"\u003e\n\u003cinput name=\"user.address.zip\" value=\"10001\"\u003e\n```\n\nProduces:\n```javascript\n{\n  user: {\n    name: 'John',\n    address: {\n      city: 'NYC',\n      zip: '10001'\n    }\n  }\n}\n```\n\n**Honeypot (Bot Detection):**\n\nFormManager 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.\n\n```html\n\u003c!-- Hidden from users via CSS --\u003e\n\u003cinput type=\"text\" name=\"honey\" autocomplete=\"off\" tabindex=\"-1\"\n       style=\"position: absolute; left: -9999px;\" aria-hidden=\"true\"\u003e\n```\n\n**Checkbox Handling:**\n\n- **Single checkbox:** Returns `true` or `false`\n- **Checkbox group (multiple with same name):** Returns object with each value as key\n\n```html\n\u003c!-- Single checkbox --\u003e\n\u003cinput type=\"checkbox\" name=\"subscribe\" checked\u003e\n\u003c!-- Result: { subscribe: true } --\u003e\n\n\u003c!-- Checkbox group --\u003e\n\u003cinput type=\"checkbox\" name=\"features\" value=\"darkmode\" checked\u003e\n\u003cinput type=\"checkbox\" name=\"features\" value=\"analytics\"\u003e\n\u003cinput type=\"checkbox\" name=\"features\" value=\"beta\" checked\u003e\n\u003c!-- Result: { features: { darkmode: true, analytics: false, beta: true } } --\u003e\n```\n\n**Multiple Submit Buttons:**\n\nAccess the clicked submit button to handle different actions:\n\n```html\n\u003cbutton type=\"submit\" data-action=\"save\"\u003eSave\u003c/button\u003e\n\u003cbutton type=\"submit\" data-action=\"draft\"\u003eSave as Draft\u003c/button\u003e\n```\n\n```javascript\nformManager.on('submit', async ({ data, $submitButton }) =\u003e {\n  const action = $submitButton?.dataset?.action;\n\n  if (action === 'draft') {\n    await saveDraft(data);\n    formManager.showSuccess('Draft saved!');\n  } else {\n    await saveAndPublish(data);\n    formManager.showSuccess('Published!');\n  }\n});\n```\n\n**Manual Ready Mode:**\n\nFor forms that need async initialization (e.g., loading data from API):\n\n```javascript\nconst formManager = new FormManager('#my-form', { autoReady: false });\n\n// Load data, then mark ready\nconst userData = await fetchUserData();\nformManager.setData(userData);\nformManager.ready(); // Now form is interactive\n```\n\n**Configuration Options:**\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `autoReady` | `true` | Auto-transition to ready when DOM is ready |\n| `initialState` | `'ready'` | State after autoReady fires |\n| `allowResubmit` | `true` | Allow form resubmission after success |\n| `resetOnSuccess` | `false` | Clear fields after successful submission |\n| `warnOnUnsavedChanges` | `true` | Show browser warning when leaving with unsaved changes |\n| `submittingText` | `'Processing...'` | Text shown on submit button during submission |\n\n**Test Page:** Navigate to `/test/libraries/form-manager` to see FormManager in action with various configurations.\n\n### ITM (Internal Tracking Medium)\n\nInternal tracking system modeled after UTM for cross-property user journey tracking.\n\n| Parameter | Purpose | Examples |\n|-----------|---------|----------|\n| `itm_source` | Platform/origin | `website`, `browser-extension`, `app`, `email` |\n| `itm_medium` | Delivery mechanism | `modal`, `prompt`, `banner`, `tooltip` |\n| `itm_campaign` | Specific campaign/feature | `exit-popup`, `premium-unlock`, `newsletter-signup` |\n| `itm_content` | Specific context | Page path, feature ID, variant |\n\n**Examples:**\n```\n# Website exit popup\n?itm_source=website\u0026itm_medium=modal\u0026itm_campaign=exit-popup\u0026itm_content=/pricing\n\n# Extension premium unlock\n?itm_source=browser-extension\u0026itm_medium=prompt\u0026itm_campaign=premium-unlock\u0026itm_content=bulk-export\n```\n\n### Icons\n* Fontawesome\n  * https://fontawesome.com/search\n* Flags\n  * https://www.freepik.com/icon/england_4720360#fromView=resource_detail\u0026position=1\n* More\n  * Language\n    * https://www.freepik.com/icon/language_484531#fromView=family\u0026page=1\u0026position=0\u0026uuid=651a2f0f-9023-4063-a495-af9a4ef72304\n\n### Webpack Import Aliases\n\nUJM defines two webpack aliases (in `src/gulp/tasks/webpack.js`) for importing assets in JavaScript:\n\n| Alias | Resolves To | Purpose |\n|-------|------------|---------|\n| `__main_assets__` | `[UJM package]/dist/assets` | UJM's own built-in assets (core modules, libraries, pages) |\n| `__project_assets__` | `[consuming project]/src/assets` | The consuming project's custom assets |\n\n**`__main_assets__`** — Import UJM libraries and core modules:\n```javascript\nimport { FormManager } from '__main_assets__/js/libs/form-manager.js';\nimport authorizedFetch from '__main_assets__/js/libs/authorized-fetch.js';\nimport { getPrerenderedIcon } from '__main_assets__/js/libs/prerendered-icons.js';\n```\n\n**`__project_assets__`** — Import consuming project's own assets:\n```javascript\n// Used in src/index.js to load project-specific page modules\nimport(`__project_assets__/js/pages/${pageModulePath}`)\n```\n\n**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.\n\n**When to use which:**\n- **`__main_assets__`** — When importing UJM-provided libraries, core modules, or referencing UJM's built-in page scripts\n- **`__project_assets__`** — When a consuming project needs to import its own custom assets from within UJM-managed code\n\n## Dev Flags\nAdd this to any js file to ONLY run in development mode (it will be excluded in production builds):\n```\n  /* @dev-only:start */\n  {\n    // Your development-only code goes here\n  }\n  /* @dev-only:end */\n```\n\n## 🧰 Sister projects\n\n- [Electron Manager (EM)](https://github.com/itw-creative-works/electron-manager) — same patterns, but for Electron desktop apps\n- [Browser Extension Manager (BXM)](https://github.com/itw-creative-works/browser-extension-manager) — same patterns, but for cross-browser MV3 extensions\n- [Backend Manager (BEM)](https://github.com/itw-creative-works/backend-manager) — Firebase Functions backend framework\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fitw-creative-works%2Fultimate-jekyll-manager","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fitw-creative-works%2Fultimate-jekyll-manager","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fitw-creative-works%2Fultimate-jekyll-manager/lists"}