ultimate-jekyll-manager 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/CLAUDE.md +50 -23
- package/dist/defaults/CHANGELOG.md +15 -0
- package/dist/defaults/CLAUDE.md +17 -5
- package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/extension/index.html +0 -26
- package/dist/defaults/docs/README.md +17 -0
- package/dist/defaults/test/README.md +31 -0
- package/docs/ads.md +78 -0
- package/docs/analytics.md +90 -0
- package/docs/appearance.md +65 -0
- package/docs/assets.md +314 -0
- package/docs/audit.md +11 -0
- package/docs/css.md +73 -0
- package/docs/icons.md +125 -0
- package/docs/images.md +42 -0
- package/docs/javascript-libraries.md +457 -0
- package/docs/jekyll-plugin.md +69 -0
- package/docs/layouts-and-pages.md +31 -0
- package/docs/lazy-loading.md +58 -0
- package/docs/local-development.md +59 -0
- package/docs/page-loading.md +85 -0
- package/docs/project-structure.md +23 -0
- package/docs/seo.md +206 -0
- package/docs/xss-prevention.md +126 -0
- package/package.json +1 -1
- package/docs/_legacy-claude-md.md +0 -1832
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Page Loading Protection System
|
|
2
|
+
|
|
3
|
+
Ultimate Jekyll prevents race conditions by disabling buttons during JavaScript initialization.
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
1. HTML element starts with `data-page-loading="true"` and `aria-busy="true"` (`src/defaults/dist/_layouts/core/root.html`)
|
|
8
|
+
2. Protected elements are automatically disabled during this state
|
|
9
|
+
3. Attributes are removed when JavaScript completes (`src/assets/js/core/complete.js`)
|
|
10
|
+
|
|
11
|
+
## Protected Elements
|
|
12
|
+
|
|
13
|
+
- All form buttons (`<button>`, `<input type="submit">`, `<input type="button">`, `<input type="reset">`)
|
|
14
|
+
- Elements with `.btn` class (Bootstrap buttons)
|
|
15
|
+
- Elements with `.btn-action` class (custom action triggers)
|
|
16
|
+
|
|
17
|
+
## The `.btn-action` Class
|
|
18
|
+
|
|
19
|
+
Selectively protect non-standard elements that trigger important actions:
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<!-- Protected during page load -->
|
|
23
|
+
<a href="/api/delete" class="custom-link btn-action">Delete Item</a>
|
|
24
|
+
<div class="card-action btn-action" onclick="processData()">Process</div>
|
|
25
|
+
|
|
26
|
+
<!-- NOT protected (regular navigation/UI) -->
|
|
27
|
+
<a href="/about" class="btn btn-primary">About Us</a>
|
|
28
|
+
<button data-bs-toggle="modal">Show Info</button>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Use `.btn-action` for:**
|
|
32
|
+
- API calls
|
|
33
|
+
- Form submissions
|
|
34
|
+
- Data modifications
|
|
35
|
+
- Payment processing
|
|
36
|
+
- Destructive actions
|
|
37
|
+
|
|
38
|
+
**Don't use for:**
|
|
39
|
+
- Navigation links
|
|
40
|
+
- UI toggles (modals, accordions, tabs)
|
|
41
|
+
- Harmless interactions
|
|
42
|
+
|
|
43
|
+
## Implementation
|
|
44
|
+
|
|
45
|
+
- **CSS:** `src/assets/css/core/utilities.scss` — Disabled styling
|
|
46
|
+
- **Click Prevention:** `src/defaults/dist/_includes/core/body.html` — Inline script
|
|
47
|
+
- **State Removal:** `src/assets/js/core/complete.js` — Removes loading state
|
|
48
|
+
|
|
49
|
+
## Form Protection Standards
|
|
50
|
+
|
|
51
|
+
All JS-managed forms use a layered protection strategy to prevent native form submission before JavaScript takes control:
|
|
52
|
+
|
|
53
|
+
### Layer 1: `onsubmit="return false"` on ALL JS-managed forms
|
|
54
|
+
|
|
55
|
+
Every `<form>` that will be managed by FormManager MUST include `onsubmit="return false"`:
|
|
56
|
+
|
|
57
|
+
```html
|
|
58
|
+
<form id="my-form" onsubmit="return false">
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This is a zero-cost safety net that prevents native form submission if a user clicks submit before FormManager attaches its `e.preventDefault()` handler. FormManager's own submit handling overrides this — there is no conflict.
|
|
62
|
+
|
|
63
|
+
**Exception:** Traditional forms with an `action` attribute that intentionally navigate (e.g., search forms, external form submissions) should NOT include this.
|
|
64
|
+
|
|
65
|
+
### Layer 2: Button initial state based on use case
|
|
66
|
+
|
|
67
|
+
| Use Case | Initial State | Mechanism |
|
|
68
|
+
|----------|---------------|-----------|
|
|
69
|
+
| Buttons dependent on async data (checkout payment methods) | `hidden` | `data-wm-bind="@show ..."` reveals when data loads |
|
|
70
|
+
| Buttons on auth/sensitive forms | `disabled` | FormManager's `ready()` removes `disabled` |
|
|
71
|
+
| Buttons on simple forms (contact, newsletter) | Default (visible) | FormManager's `autoReady: true` enables quickly |
|
|
72
|
+
|
|
73
|
+
### Layer 3: FormManager `autoReady` configuration
|
|
74
|
+
|
|
75
|
+
| Scenario | `autoReady` | `ready()` call |
|
|
76
|
+
|----------|-------------|----------------|
|
|
77
|
+
| No async work before form init | `true` (default) | Automatic on DOM ready |
|
|
78
|
+
| Async work before form init (API calls, redirects) | `false` | Explicit call after async completes |
|
|
79
|
+
|
|
80
|
+
**Reference implementations:**
|
|
81
|
+
- Simple form: `src/assets/js/pages/contact/index.js`
|
|
82
|
+
- Auth form: `src/assets/js/libs/auth.js`
|
|
83
|
+
- Async data form: `src/assets/js/pages/payment/checkout/index.js`
|
|
84
|
+
|
|
85
|
+
See also [docs/javascript-libraries.md → FormManager](javascript-libraries.md#formmanager-library) for the FormManager API itself.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Project Structure
|
|
2
|
+
|
|
3
|
+
UJM is a template framework that consuming projects install as an NPM module to build Jekyll sites quickly and efficiently. It provides best-practice configurations, default components, themes, and build tools.
|
|
4
|
+
|
|
5
|
+
> **UJM is NOT a standalone project.** You cannot run `npm start` or `npm run build` directly in this repository. The user already has a development server running in a consuming project — running those commands here would either fail or create duplicate servers unnecessarily. See [the Identity section in CLAUDE.md](../CLAUDE.md#identity) for what IS safe to run inside UJM itself.
|
|
6
|
+
|
|
7
|
+
## Directory Organization (UJM repo)
|
|
8
|
+
|
|
9
|
+
| Path | Purpose |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `src/gulp/tasks` | Gulp tasks for building Jekyll sites |
|
|
12
|
+
| `src/defaults/src` | Default source files (editable by users, copied to consuming project's `src/`) |
|
|
13
|
+
| `src/defaults/dist` | Default distribution files (not editable by users, copied to consuming project's `dist/`) |
|
|
14
|
+
| `src/assets/css` | Stylesheets (global, pages, themes) |
|
|
15
|
+
| `src/assets/js` | JavaScript modules (core, pages, libraries) |
|
|
16
|
+
| `src/assets/themes` | Theme SCSS and JS files |
|
|
17
|
+
|
|
18
|
+
## Consuming Project Structure
|
|
19
|
+
|
|
20
|
+
| Path | Purpose |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `src/` | Compiled to `dist/` via npm |
|
|
23
|
+
| `dist/` | Compiled to `_site/` via Jekyll |
|
package/docs/seo.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# SEO — Alternatives Pages & Structured Data
|
|
2
|
+
|
|
3
|
+
This doc covers two SEO-focused subsystems:
|
|
4
|
+
|
|
5
|
+
1. **Alternatives Collection** — competitor comparison landing pages.
|
|
6
|
+
2. **Schema / Structured Data (JSON-LD)** — `SoftwareApplication` and `FAQPage` JSON-LD blocks.
|
|
7
|
+
|
|
8
|
+
## Alternatives Collection (SEO Competitor Comparison Pages)
|
|
9
|
+
|
|
10
|
+
UJ provides an `alternatives` collection for SEO landing pages that target users searching for competitors (e.g., "ExampleApp alternatives"). These pages are entirely frontmatter-driven and designed to convert visitors who are comparing products.
|
|
11
|
+
|
|
12
|
+
### How It Works
|
|
13
|
+
|
|
14
|
+
1. The `alternatives` collection is registered in `src/config/_config_default.yml` (UJM-controlled)
|
|
15
|
+
2. Each alternative is a markdown file in the consuming project's `_alternatives/` directory
|
|
16
|
+
3. The layout chain: `blueprint/alternatives/alternative` → `themes/classy/frontend/pages/alternatives/alternative`
|
|
17
|
+
4. An index page at `/alternatives` lists all alternatives automatically
|
|
18
|
+
5. **Shared content lives in the layout** — the theme layout provides default testimonials, stats, FAQs, CTA, and why_switch content so competitor pages only need competitor-specific data
|
|
19
|
+
6. The layout frontmatter uses `{{ page.resolved.alternative.competitor.name }}` to dynamically insert the competitor name into shared content (e.g., FAQ questions, CTA headlines)
|
|
20
|
+
|
|
21
|
+
### Creating an Alternative Page
|
|
22
|
+
|
|
23
|
+
In the consuming project, create `src/_alternatives/competitor-name.md`. Only competitor-specific data is needed — shared sections are inherited from the layout:
|
|
24
|
+
|
|
25
|
+
```yaml
|
|
26
|
+
---
|
|
27
|
+
layout: blueprint/alternatives/alternative
|
|
28
|
+
sitemap:
|
|
29
|
+
include: true
|
|
30
|
+
|
|
31
|
+
alternative:
|
|
32
|
+
competitor:
|
|
33
|
+
name: "Competitor Name"
|
|
34
|
+
description: "Brief description of the competitor (shown on /alternatives listing)"
|
|
35
|
+
comparison:
|
|
36
|
+
features:
|
|
37
|
+
- name: "Feature Name"
|
|
38
|
+
icon: "sparkles"
|
|
39
|
+
ours:
|
|
40
|
+
value: true # or string like "Unlimited"
|
|
41
|
+
theirs:
|
|
42
|
+
value: false # or string like "Limited"
|
|
43
|
+
---
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
That's it! The layout automatically generates:
|
|
47
|
+
- Hero with "Brand vs Competitor Name" headline
|
|
48
|
+
- Why Switch section with default differentiator items
|
|
49
|
+
- Testimonials, Stats, FAQs, and CTA with shared content
|
|
50
|
+
- All text dynamically references the competitor name via `{{ page.resolved.alternative.competitor.name }}`
|
|
51
|
+
|
|
52
|
+
**To override any inherited section**, define it in the competitor's frontmatter — `page.resolved` merge gives page-level values highest priority.
|
|
53
|
+
|
|
54
|
+
### Available Sections
|
|
55
|
+
|
|
56
|
+
All sections are **optional** — omit or leave empty to hide. Sections with `(shared)` have default content in the layout:
|
|
57
|
+
|
|
58
|
+
| Section | Frontmatter Key | Description |
|
|
59
|
+
|---------|----------------|-------------|
|
|
60
|
+
| Hero | `alternative.hero` | Gradient animated hero with "Brand vs Competitor" headline (shared) |
|
|
61
|
+
| Comparison | `alternative.comparison` | Side-by-side feature table — **must be defined per competitor** |
|
|
62
|
+
| Why Switch | `alternative.why_switch` | Alternating image/text showcase blocks (shared) |
|
|
63
|
+
| Video | `alternative.video` | YouTube embed (default: hidden, set `youtube_id` to show) |
|
|
64
|
+
| Testimonials | `alternative.testimonials` | Reuses `testimonial-scroll.html` component (shared) |
|
|
65
|
+
| Stats | `alternative.stats` | Social proof numbers with icons (shared) |
|
|
66
|
+
| FAQs | `alternative.faqs` | Accordion with switching-related questions (shared) |
|
|
67
|
+
| CTA | `alternative.cta` | Final conversion card with buttons (shared) |
|
|
68
|
+
|
|
69
|
+
### Dynamic Competitor Name in Frontmatter
|
|
70
|
+
|
|
71
|
+
The layout uses `{{ page.resolved.alternative.competitor.name }}` in its frontmatter defaults to dynamically reference the competitor. This works because the template pipes these values through `| uj_liquify` to resolve Liquid expressions.
|
|
72
|
+
|
|
73
|
+
**Example:** The layout's default FAQ includes:
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
question: "Can I import my data from {{ page.resolved.alternative.competitor.name }}?"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Which renders as "Can I import my data from ExampleApp?" for an ExampleApp competitor page.
|
|
80
|
+
|
|
81
|
+
### Reference Implementation
|
|
82
|
+
|
|
83
|
+
- **Minimal competitor page:** `src/defaults/dist/_alternatives/example-competitor.md` — shows the minimum frontmatter needed (competitor name + comparison features)
|
|
84
|
+
- **Layout with all defaults:** `src/defaults/dist/_layouts/themes/classy/frontend/pages/alternatives/alternative.html` — contains shared content for all sections
|
|
85
|
+
|
|
86
|
+
### File Locations
|
|
87
|
+
|
|
88
|
+
| Purpose | Path |
|
|
89
|
+
|---------|------|
|
|
90
|
+
| Theme layout (alternative page) | `src/defaults/dist/_layouts/themes/classy/frontend/pages/alternatives/alternative.html` |
|
|
91
|
+
| Theme layout (index/listing page) | `src/defaults/dist/_layouts/themes/classy/frontend/pages/alternatives/index.html` |
|
|
92
|
+
| Blueprint (alternative) | `src/defaults/dist/_layouts/blueprint/alternatives/alternative.html` |
|
|
93
|
+
| Blueprint (index) | `src/defaults/dist/_layouts/blueprint/alternatives/index.html` |
|
|
94
|
+
| Default page (index) | `src/defaults/dist/pages/alternatives/index.md` |
|
|
95
|
+
| Sample alternative | `src/defaults/dist/_alternatives/example-competitor.md` |
|
|
96
|
+
| CSS | `src/assets/css/pages/alternatives/alternative/index.scss` |
|
|
97
|
+
| JS | `src/assets/js/pages/alternatives/alternative/index.js` |
|
|
98
|
+
|
|
99
|
+
## Schema / Structured Data (JSON-LD)
|
|
100
|
+
|
|
101
|
+
UJ automatically generates JSON-LD structured data in `foot.html`. The SoftwareApplication schema with AggregateRating is opt-in via frontmatter.
|
|
102
|
+
|
|
103
|
+
### SoftwareApplication Schema
|
|
104
|
+
|
|
105
|
+
Renders a `SoftwareApplication` JSON-LD block with deterministic aggregate ratings. Enabled by blueprint layouts (index, pricing, download, alternatives/alternative) — consuming projects can override or disable per page.
|
|
106
|
+
|
|
107
|
+
**How it works:**
|
|
108
|
+
|
|
109
|
+
1. **`_config.yml`** sets fallback defaults (no `enabled` key — just field defaults like `application_category`, `price`, etc.)
|
|
110
|
+
2. **Blueprint layouts** set `schema.software_application.enabled: true` with page-appropriate `features`
|
|
111
|
+
3. **Consuming projects** can override any value in their page frontmatter, or disable with `enabled: false`
|
|
112
|
+
|
|
113
|
+
This follows the standard `page.resolved` merge: page > layout > site.
|
|
114
|
+
|
|
115
|
+
**Deterministic ratings:** Uses the `uj_hash` filter (from jekyll-uj-powertools) seeded with `site.url` by default, producing stable values across builds:
|
|
116
|
+
- Rating: always `4.8` or `4.9` (deterministic per seed)
|
|
117
|
+
- Review count: 200,000–999,999 (deterministic per seed)
|
|
118
|
+
- Override seed per page with `hash_seed` to get different values
|
|
119
|
+
|
|
120
|
+
**Blueprint frontmatter example:**
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
### SCHEMA ###
|
|
124
|
+
schema:
|
|
125
|
+
software_application:
|
|
126
|
+
enabled: true
|
|
127
|
+
features:
|
|
128
|
+
- "Free to use"
|
|
129
|
+
- "24/7 availability"
|
|
130
|
+
- "User-friendly interface"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Consuming project override example:**
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
schema:
|
|
137
|
+
software_application:
|
|
138
|
+
application_category: "EducationalApplication"
|
|
139
|
+
features:
|
|
140
|
+
- "AI-powered solutions"
|
|
141
|
+
- "24/7 availability"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Consuming project disable example:**
|
|
145
|
+
|
|
146
|
+
```yaml
|
|
147
|
+
schema:
|
|
148
|
+
software_application:
|
|
149
|
+
enabled: false
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Available fields:**
|
|
153
|
+
|
|
154
|
+
| Field | Default | Description |
|
|
155
|
+
|-------|---------|-------------|
|
|
156
|
+
| `enabled` | (set by blueprint) | Enable/disable the schema block |
|
|
157
|
+
| `name` | `site.brand.name` | Application name |
|
|
158
|
+
| `description` | `page.resolved.meta.description` | Application description |
|
|
159
|
+
| `application_category` | `WebApplication` | Schema.org application category |
|
|
160
|
+
| `operating_system` | `Web-based` | Target OS |
|
|
161
|
+
| `price` | `0` | Price (string) |
|
|
162
|
+
| `price_currency` | `USD` | Currency code |
|
|
163
|
+
| `features` | `[]` | Feature list for `featureList` field |
|
|
164
|
+
| `hash_seed` | `site.url` | Seed for deterministic rating/count generation |
|
|
165
|
+
|
|
166
|
+
**File locations:**
|
|
167
|
+
|
|
168
|
+
| Purpose | Path |
|
|
169
|
+
|---------|------|
|
|
170
|
+
| Schema block (rendering) | `src/defaults/dist/_includes/core/foot.html` |
|
|
171
|
+
| Site-level defaults | `src/defaults/src/_config.yml` (under `schema:`) |
|
|
172
|
+
| Blueprint activation | `src/defaults/dist/_layouts/blueprint/{index,pricing,download}.html`, `blueprint/alternatives/alternative.html` |
|
|
173
|
+
| Hash filter | `jekyll-uj-powertools/lib/filters/main.rb` (`uj_hash`) |
|
|
174
|
+
|
|
175
|
+
### FAQPage Schema
|
|
176
|
+
|
|
177
|
+
Renders a `FAQPage` JSON-LD block for pages with FAQ/accordion sections. Enabled by the alternatives blueprint — consuming projects can also enable it on any page with FAQ content.
|
|
178
|
+
|
|
179
|
+
**How it works:**
|
|
180
|
+
|
|
181
|
+
1. **`_config.yml`** sets `faq_page.items: []` as fallback
|
|
182
|
+
2. **Blueprint layouts** set `schema.faq_page.enabled: true`
|
|
183
|
+
3. **Items source (fallback chain):** `schema.faq_page.items` → `page.resolved.faqs.items` → `page.resolved.alternative.faqs.items`. Pages with generic `faqs.items` (like pricing) and alternatives pages both get FAQPage schema automatically without duplicating content
|
|
184
|
+
4. Questions/answers are processed through `uj_liquify` (supports Liquid expressions like competitor names) and `uj_json_escape`
|
|
185
|
+
|
|
186
|
+
**Blueprint activation:** Enabled by default in `blueprint/pricing.html`, `blueprint/contact.html`, `blueprint/download.html`, `blueprint/extension/index.html`, and `blueprint/alternatives/alternative.html`.
|
|
187
|
+
|
|
188
|
+
**Consuming project usage — provide items directly:**
|
|
189
|
+
|
|
190
|
+
```yaml
|
|
191
|
+
schema:
|
|
192
|
+
faq_page:
|
|
193
|
+
enabled: true
|
|
194
|
+
items:
|
|
195
|
+
- question: "How do I get started?"
|
|
196
|
+
answer: "Sign up for free and follow the onboarding guide."
|
|
197
|
+
- question: "Is there a free plan?"
|
|
198
|
+
answer: "Yes, our basic plan is completely free."
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Available fields:**
|
|
202
|
+
|
|
203
|
+
| Field | Default | Description |
|
|
204
|
+
|-------|---------|-------------|
|
|
205
|
+
| `enabled` | (set by blueprint) | Enable/disable the schema block |
|
|
206
|
+
| `items` | `[]` | Array of `{question, answer}` objects. Falls back to `alternative.faqs.items` if empty |
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# XSS Prevention (ZERO TRUST — MANDATORY)
|
|
2
|
+
|
|
3
|
+
**TREAT ALL DYNAMIC DATA AS UNTRUSTED.** This is a zero-trust policy: any value that did not come directly from a hardcoded literal in the source file MUST be escaped before being inserted into the DOM via `innerHTML` or attribute interpolation.
|
|
4
|
+
|
|
5
|
+
This includes — but is not limited to:
|
|
6
|
+
- Firestore document fields (user names, emails, IDs, descriptions, etc.)
|
|
7
|
+
- API response data
|
|
8
|
+
- URL parameters (`location.search`, `URLSearchParams`)
|
|
9
|
+
- User input from form fields
|
|
10
|
+
- OAuth-provided values (displayName, email from Google/GitHub)
|
|
11
|
+
- Any variable whose origin is not a hardcoded source-code constant
|
|
12
|
+
|
|
13
|
+
## The Rule
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
// ✅ ALWAYS escape dynamic data before innerHTML
|
|
17
|
+
$el.innerHTML = `<p>${webManager.utilities().escapeHTML(data.title)}</p>`;
|
|
18
|
+
$el.innerHTML = `<a href="${webManager.utilities().escapeHTML(url)}">${webManager.utilities().escapeHTML(label)}</a>`;
|
|
19
|
+
|
|
20
|
+
// ✅ textContent is always safe — no escaping needed
|
|
21
|
+
$el.textContent = data.title;
|
|
22
|
+
|
|
23
|
+
// ❌ NEVER inject dynamic data raw into innerHTML
|
|
24
|
+
$el.innerHTML = `<p>${data.title}</p>`;
|
|
25
|
+
$el.innerHTML = `<a href="${url}">${label}</a>`;
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## NEVER Write Your Own Escape Function
|
|
29
|
+
|
|
30
|
+
Do NOT create a local `escapeHtml` function or any variant. The ONLY allowed escape method is:
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
webManager.utilities().escapeHTML(str)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## When Building DOM Programmatically
|
|
37
|
+
|
|
38
|
+
Prefer `document.createElement` + `textContent` for plain text nodes — it is inherently safe:
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
const $el = document.createElement('div');
|
|
42
|
+
$el.textContent = data.message; // Safe — no escaping needed
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Only use `innerHTML` when you need actual HTML structure (tags, classes, etc.), and escape every dynamic value in it.
|
|
46
|
+
|
|
47
|
+
## Even "Safe" Values Must Be Escaped
|
|
48
|
+
|
|
49
|
+
Even values that *seem* safe (like `Date.toLocaleDateString()` output, numeric calculations, or hardcoded config strings) MUST be escaped when inserted via `innerHTML`. This is defense-in-depth — if the data source ever changes, the escaping is already in place.
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
// ✅ CORRECT — escape even "safe" values in innerHTML
|
|
53
|
+
$el.innerHTML = `<small>${webManager.utilities().escapeHTML(formatDate(timestamp))}</small>`;
|
|
54
|
+
$el.innerHTML = `<span>${webManager.utilities().escapeHTML(reason)}</span>`;
|
|
55
|
+
|
|
56
|
+
// ❌ WRONG — assuming the value is safe because it's from a date formatter
|
|
57
|
+
$el.innerHTML = `<small>${formatDate(timestamp)}</small>`;
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Redirects Must Be Validated
|
|
61
|
+
|
|
62
|
+
Never redirect to a URL from untrusted sources without validation:
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
// ✅ CORRECT — validate before redirect
|
|
66
|
+
const url = urlParams.get('returnUrl');
|
|
67
|
+
if (url && webManager.isValidRedirectUrl(url)) {
|
|
68
|
+
window.location.href = url;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ✅ CORRECT — validate API response URLs have safe scheme
|
|
72
|
+
if (response.url && /^https?:\/\//i.test(response.url)) {
|
|
73
|
+
window.location.href = response.url;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ❌ WRONG — redirect to unvalidated input
|
|
77
|
+
window.location.href = urlParams.get('returnUrl');
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## postMessage Handlers Must Check Origin
|
|
81
|
+
|
|
82
|
+
Always validate `event.origin` when handling `window.addEventListener('message', ...)`:
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
// ✅ CORRECT
|
|
86
|
+
window.addEventListener('message', (event) => {
|
|
87
|
+
if (event.origin !== window.location.origin && event.origin !== 'https://trusted-domain.com') {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// handle message
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ❌ WRONG — any origin can send messages
|
|
94
|
+
window.addEventListener('message', (event) => {
|
|
95
|
+
window.location.href = event.data.url; // attacker-controlled redirect
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Never Use eval() or new Function()
|
|
100
|
+
|
|
101
|
+
Do not use `eval()`, `new Function()`, `setTimeout(string)`, or `setInterval(string)`. These execute arbitrary code and violate CSP policies.
|
|
102
|
+
|
|
103
|
+
## Sanitize Markdown/Rich Text Output
|
|
104
|
+
|
|
105
|
+
When rendering user-authored markdown or rich text, use DOMPurify to sanitize the output:
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
import DOMPurify from 'dompurify';
|
|
109
|
+
const safeHTML = DOMPurify.sanitize(md.render(userContent), {
|
|
110
|
+
ALLOWED_TAGS: ['h1', 'h2', 'h3', 'p', 'br', 'a', 'b', 'strong', 'i', 'em', 'ul', 'ol', 'li', 'img', 'code', 'pre'],
|
|
111
|
+
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class', 'target', 'rel'],
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Do NOT Escape Values Passed to textContent-Based APIs
|
|
116
|
+
|
|
117
|
+
`showNotification()`, `formManager.showSuccess()`, `formManager.showError()`, and `textContent` assignments use safe text insertion internally. Pre-escaping these causes double-encoding (e.g., `We'll` displays as `We'll`).
|
|
118
|
+
|
|
119
|
+
```javascript
|
|
120
|
+
// ✅ CORRECT — these APIs use textContent internally, so they're already safe
|
|
121
|
+
webManager.utilities().showNotification('Thank you! We\'ll be in touch.', 'success');
|
|
122
|
+
formManager.showSuccess('Message sent successfully!');
|
|
123
|
+
|
|
124
|
+
// ❌ WRONG — double-escapes
|
|
125
|
+
webManager.utilities().showNotification(webManager.utilities().escapeHTML(message), 'success');
|
|
126
|
+
```
|