vibespot 0.4.0
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/LICENSE +33 -0
- package/README.md +118 -0
- package/assets/content-guide.md +445 -0
- package/assets/conversion-guide.md +693 -0
- package/assets/design-guide.md +380 -0
- package/assets/hubspot-rules.md +560 -0
- package/assets/page-types.md +116 -0
- package/bin/vibespot.mjs +11 -0
- package/dist/index.js +6552 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
- package/ui/chat.js +803 -0
- package/ui/dashboard.js +383 -0
- package/ui/dialog.js +117 -0
- package/ui/field-editor.js +292 -0
- package/ui/index.html +393 -0
- package/ui/preview.js +132 -0
- package/ui/settings.js +927 -0
- package/ui/setup.js +830 -0
- package/ui/styles.css +2552 -0
- package/ui/upload-panel.js +554 -0
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
# React/TypeScript to HubSpot CMS — Conversion Guide
|
|
2
|
+
|
|
3
|
+
This guide documents the process of converting a React/Vite/Tailwind single-page application into native HubSpot CMS modules. It is designed as reusable instructional context for performing this conversion at scale — whether the source is Lovable, v0, Bolt, or any React-based page builder.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [Overview](#overview)
|
|
10
|
+
2. [Architecture Mapping](#architecture-mapping)
|
|
11
|
+
3. [Module Structure](#module-structure)
|
|
12
|
+
4. [Field Types & Gotchas](#field-types--gotchas)
|
|
13
|
+
5. [CSS Conversion](#css-conversion)
|
|
14
|
+
6. [JavaScript Conversion](#javascript-conversion)
|
|
15
|
+
7. [Template Creation](#template-creation)
|
|
16
|
+
8. [Style Tab Integration](#style-tab-integration)
|
|
17
|
+
9. [Common Pitfalls](#common-pitfalls)
|
|
18
|
+
10. [Checklist](#checklist)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Overview
|
|
23
|
+
|
|
24
|
+
### What Gets Converted
|
|
25
|
+
|
|
26
|
+
| React Concept | HubSpot Equivalent |
|
|
27
|
+
|---------------|-------------------|
|
|
28
|
+
| React component | HubSpot module (`.module/` directory) |
|
|
29
|
+
| Props / state | `fields.json` (editable fields) |
|
|
30
|
+
| JSX template | `module.html` (HubL template) |
|
|
31
|
+
| Component CSS | `module.css` (vanilla CSS) |
|
|
32
|
+
| Component JS (hooks, effects) | `module.js` or shared JS file |
|
|
33
|
+
| Tailwind utilities | Vanilla CSS with BEM naming |
|
|
34
|
+
| React Router pages | HubSpot page templates |
|
|
35
|
+
| Context / global state | Theme-level `fields.json` |
|
|
36
|
+
| npm packages (shadcn, Radix, Embla) | Vanilla JS replacements |
|
|
37
|
+
|
|
38
|
+
### What Gets Skipped
|
|
39
|
+
|
|
40
|
+
- **Cookie consent**: HubSpot provides this natively
|
|
41
|
+
- **Password gates**: Use HubSpot's membership system
|
|
42
|
+
- **Client-side routing**: Not applicable (each page is server-rendered)
|
|
43
|
+
- **Build tooling**: No Vite, Webpack, or bundler needed
|
|
44
|
+
- **Type definitions**: HubL is untyped
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Architecture Mapping
|
|
49
|
+
|
|
50
|
+
### 1:1 Component → Module Mapping
|
|
51
|
+
|
|
52
|
+
Each visual section of the React page becomes one HubSpot module. A typical landing page maps like this:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
React Component → HubSpot Module
|
|
56
|
+
─────────────────────────────────────────────────
|
|
57
|
+
Header.tsx → Header.module/
|
|
58
|
+
HeroSection.tsx → Hero.module/
|
|
59
|
+
FeaturesSection.tsx → Features.module/
|
|
60
|
+
TestimonialsSection.tsx → Testimonials.module/
|
|
61
|
+
PricingSection.tsx → Pricing.module/
|
|
62
|
+
ContactSection.tsx → Contact.module/
|
|
63
|
+
Footer.tsx → Footer.module/
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Shared Files
|
|
67
|
+
|
|
68
|
+
| File | Purpose |
|
|
69
|
+
|------|---------|
|
|
70
|
+
| `css/<theme>.css` | Shared design system: CSS variables, utilities, animations, form overrides |
|
|
71
|
+
| `js/<theme>-animations.js` | Shared vanilla JS: scroll animations, carousels, accordions, etc. |
|
|
72
|
+
| `templates/<page>.html` | Page template that assembles modules in a DnD area |
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Module Structure
|
|
77
|
+
|
|
78
|
+
Each module lives in `modules/<Name>.module/` with these files:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
MyModule.module/
|
|
82
|
+
├── fields.json # Editable content & style fields
|
|
83
|
+
├── meta.json # Module metadata
|
|
84
|
+
├── module.html # HubL template (converted from JSX)
|
|
85
|
+
├── module.css # Module-specific styles (converted from Tailwind)
|
|
86
|
+
└── module.js # Optional: module-specific JavaScript
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### meta.json
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"label": "My Module",
|
|
94
|
+
"css_assets": [],
|
|
95
|
+
"external_js": [],
|
|
96
|
+
"global": false,
|
|
97
|
+
"host_template_types": ["PAGE"],
|
|
98
|
+
"content_types": ["LANDING_PAGE"],
|
|
99
|
+
"is_available_for_new_content": true
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- `content_types`: Use `["LANDING_PAGE"]` for landing pages, `["PAGE"]` for website pages, or both.
|
|
104
|
+
- `global`: Set `true` only for modules that appear on every page (rare).
|
|
105
|
+
|
|
106
|
+
### fields.json
|
|
107
|
+
|
|
108
|
+
This is where React props become HubSpot-editable fields. Every piece of text, image, link, or repeating group that an editor should be able to change goes here.
|
|
109
|
+
|
|
110
|
+
**Converting React props/hardcoded strings to fields:**
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
// React: hardcoded content
|
|
114
|
+
<h2>Our Features</h2>
|
|
115
|
+
<p>We offer the best solutions for your business.</p>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
// fields.json
|
|
120
|
+
[
|
|
121
|
+
{
|
|
122
|
+
"name": "headline",
|
|
123
|
+
"label": "Headline",
|
|
124
|
+
"type": "text",
|
|
125
|
+
"default": "Our Features"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"name": "subtitle",
|
|
129
|
+
"label": "Subtitle",
|
|
130
|
+
"type": "text",
|
|
131
|
+
"default": "We offer the best solutions for your business."
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```html
|
|
137
|
+
<!-- module.html -->
|
|
138
|
+
<h2>{{ module.headline }}</h2>
|
|
139
|
+
<p>{{ module.subtitle }}</p>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Converting Repeating Content (map → repeater group)
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
// React: array.map()
|
|
146
|
+
{features.map((feature) => (
|
|
147
|
+
<div key={feature.title}>
|
|
148
|
+
<h3>{feature.title}</h3>
|
|
149
|
+
<p>{feature.description}</p>
|
|
150
|
+
</div>
|
|
151
|
+
))}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
// fields.json — repeater group
|
|
156
|
+
{
|
|
157
|
+
"name": "features",
|
|
158
|
+
"label": "Features",
|
|
159
|
+
"type": "group",
|
|
160
|
+
"occurrence": {
|
|
161
|
+
"min": 1,
|
|
162
|
+
"max": 10,
|
|
163
|
+
"default": 3
|
|
164
|
+
},
|
|
165
|
+
"default": [
|
|
166
|
+
{ "feature_title": "Feature 1", "feature_desc": "Description 1" },
|
|
167
|
+
{ "feature_title": "Feature 2", "feature_desc": "Description 2" },
|
|
168
|
+
{ "feature_title": "Feature 3", "feature_desc": "Description 3" }
|
|
169
|
+
],
|
|
170
|
+
"children": [
|
|
171
|
+
{
|
|
172
|
+
"name": "feature_title",
|
|
173
|
+
"label": "Title",
|
|
174
|
+
"type": "text",
|
|
175
|
+
"default": "Feature"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"name": "feature_desc",
|
|
179
|
+
"label": "Description",
|
|
180
|
+
"type": "text",
|
|
181
|
+
"default": "Description"
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
```html
|
|
188
|
+
<!-- module.html — HubL for loop -->
|
|
189
|
+
{%- for item in module.features -%}
|
|
190
|
+
<div>
|
|
191
|
+
<h3>{{ item.feature_title }}</h3>
|
|
192
|
+
<p>{{ item.feature_desc }}</p>
|
|
193
|
+
</div>
|
|
194
|
+
{%- endfor -%}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Field Types & Gotchas
|
|
200
|
+
|
|
201
|
+
### Supported Field Types
|
|
202
|
+
|
|
203
|
+
| HubSpot Type | Use For | React Equivalent |
|
|
204
|
+
|-------------|---------|-----------------|
|
|
205
|
+
| `text` | Single-line or multi-line text | `string` prop |
|
|
206
|
+
| `richtext` | Formatted HTML content | `dangerouslySetInnerHTML` |
|
|
207
|
+
| `image` | Image with src + alt | `<img>` props |
|
|
208
|
+
| `link` | URL with target options | `href` prop |
|
|
209
|
+
| `color` | Color picker with opacity | CSS color values |
|
|
210
|
+
| `choice` | Dropdown/radio selection | Enum prop |
|
|
211
|
+
| `boolean` | Toggle switch | Boolean prop |
|
|
212
|
+
| `number` | Numeric input | Number prop |
|
|
213
|
+
| `group` | Container for child fields | Object prop |
|
|
214
|
+
| `group` + `occurrence` | Repeater (array of items) | Array prop |
|
|
215
|
+
| `form` | HubSpot form selector | Form embed |
|
|
216
|
+
|
|
217
|
+
### Critical Gotchas
|
|
218
|
+
|
|
219
|
+
| Issue | Error | Fix |
|
|
220
|
+
|-------|-------|-----|
|
|
221
|
+
| `"type": "textarea"` | `'unknown' is not a valid field type` | Use `"type": "text"` instead — `textarea` is deprecated |
|
|
222
|
+
| `"name": "name"` | `missing field name` | `name` is reserved — use `item_name`, `link_label`, etc. |
|
|
223
|
+
| `{% module %}` in module.html | `'module' is disabled in this context` | Cannot nest modules — use `{% form %}` for forms |
|
|
224
|
+
| `{{ now() }}` | `Could not resolve function 'now'` | Use `{{ local_dt }}` for current date/time |
|
|
225
|
+
| Partially uploaded module | Re-upload still fails | Run `hs remove <path>` first, then re-upload |
|
|
226
|
+
| SVG in text field | SVG renders as escaped text | SVG markup in text fields is auto-escaped by HubL |
|
|
227
|
+
|
|
228
|
+
### Image Fields
|
|
229
|
+
|
|
230
|
+
```json
|
|
231
|
+
{
|
|
232
|
+
"name": "logo",
|
|
233
|
+
"label": "Logo",
|
|
234
|
+
"type": "image",
|
|
235
|
+
"default": {
|
|
236
|
+
"src": "",
|
|
237
|
+
"alt": "Company Logo"
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
```html
|
|
243
|
+
{%- if module.logo.src -%}
|
|
244
|
+
<img src="{{ module.logo.src }}" alt="{{ module.logo.alt }}" />
|
|
245
|
+
{%- else -%}
|
|
246
|
+
<span>Fallback Text</span>
|
|
247
|
+
{%- endif -%}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Form Embedding
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
{
|
|
254
|
+
"name": "form_field",
|
|
255
|
+
"label": "Form",
|
|
256
|
+
"type": "form",
|
|
257
|
+
"default": {
|
|
258
|
+
"form_id": "your-form-guid-here",
|
|
259
|
+
"portal_id": "your-portal-id"
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
```html
|
|
265
|
+
{% form
|
|
266
|
+
form_to_use="{{ module.form_field.form_id }}"
|
|
267
|
+
response_response_type="redirect"
|
|
268
|
+
response_redirect_url=""
|
|
269
|
+
no_title=true
|
|
270
|
+
%}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## CSS Conversion
|
|
276
|
+
|
|
277
|
+
### From Tailwind to Vanilla CSS
|
|
278
|
+
|
|
279
|
+
1. **Extract the design system** from `tailwind.config.ts` and `index.css`:
|
|
280
|
+
- Color palette → CSS custom properties (`--prefix-*`)
|
|
281
|
+
- Font families → `@import` Google Fonts + CSS properties
|
|
282
|
+
- Spacing scale → Hardcoded `rem` values
|
|
283
|
+
- Breakpoints → `@media` queries
|
|
284
|
+
|
|
285
|
+
2. **Convert utility classes to BEM-named classes**:
|
|
286
|
+
|
|
287
|
+
```tsx
|
|
288
|
+
// React + Tailwind
|
|
289
|
+
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800 hover:shadow-lg transition">
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
```css
|
|
293
|
+
/* Vanilla CSS with BEM */
|
|
294
|
+
.my-module__card {
|
|
295
|
+
background: hsl(var(--dark-900));
|
|
296
|
+
border-radius: 1rem;
|
|
297
|
+
padding: 1.5rem;
|
|
298
|
+
border: 1px solid hsl(var(--border));
|
|
299
|
+
transition: box-shadow 0.3s;
|
|
300
|
+
}
|
|
301
|
+
.my-module__card:hover {
|
|
302
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
3. **Prefix ALL classes** with a unique namespace (e.g., `ai-`, `wco-`) to avoid conflicts with the HubSpot theme's existing CSS.
|
|
307
|
+
|
|
308
|
+
### CSS Load Order in HubSpot
|
|
309
|
+
|
|
310
|
+
The base template loads CSS in this order (each can override the previous):
|
|
311
|
+
|
|
312
|
+
```
|
|
313
|
+
1. main.css ← Theme reset, base styles
|
|
314
|
+
2. style.css ← Theme custom styles
|
|
315
|
+
3. template_css ← Your shared CSS (e.g., ai-theme.css)
|
|
316
|
+
4. theme-overrides ← Theme settings (colors, fonts, spacing from theme.json)
|
|
317
|
+
5. module.css ← Module-specific styles
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Critical**: `theme-overrides.css` loads AFTER your template CSS. It sets:
|
|
321
|
+
- `body { color: ...; font-size: ...; }` — overrides text color
|
|
322
|
+
- `h1, h2, h3... { color: ...; }` — overrides heading colors
|
|
323
|
+
- `.dnd-section { padding: ...; }` — adds section padding
|
|
324
|
+
- `.dnd-section > .row-fluid { max-width: ...; }` — constrains width
|
|
325
|
+
- `a { color: ...; }` — overrides link colors
|
|
326
|
+
- Button, form, table styles
|
|
327
|
+
|
|
328
|
+
**Solution**: Add scoped overrides in your shared CSS:
|
|
329
|
+
|
|
330
|
+
```css
|
|
331
|
+
/* Override theme-overrides.css within your page scope */
|
|
332
|
+
.my-page h1, .my-page h2, .my-page h3,
|
|
333
|
+
.my-page h4, .my-page h5, .my-page h6 {
|
|
334
|
+
color: hsl(var(--my-fg)) !important;
|
|
335
|
+
font-family: 'My Font', sans-serif !important;
|
|
336
|
+
}
|
|
337
|
+
.my-page p {
|
|
338
|
+
color: hsl(var(--my-fg)) !important;
|
|
339
|
+
}
|
|
340
|
+
.my-page .dnd-section {
|
|
341
|
+
padding: 0 !important;
|
|
342
|
+
}
|
|
343
|
+
.my-page .dnd-section > .row-fluid {
|
|
344
|
+
max-width: 100% !important;
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Body Background Problem
|
|
349
|
+
|
|
350
|
+
The page wrapper structure is:
|
|
351
|
+
```html
|
|
352
|
+
<div class="body-wrapper">
|
|
353
|
+
<main class="body-container-wrapper">
|
|
354
|
+
<div class="my-page"> ← your wrapper
|
|
355
|
+
<!-- modules -->
|
|
356
|
+
</div>
|
|
357
|
+
</main>
|
|
358
|
+
</div>
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
`.my-page` is a CHILD of `.body-wrapper`. CSS cannot target a parent from a child selector. Solutions:
|
|
362
|
+
|
|
363
|
+
```css
|
|
364
|
+
/* Modern browsers: :has() selector */
|
|
365
|
+
.body-wrapper:has(.my-page) {
|
|
366
|
+
background: #0d1117 !important;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* JS fallback class (added via your animations.js) */
|
|
370
|
+
.body-wrapper.my-page-active {
|
|
371
|
+
background: #0d1117 !important;
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
```js
|
|
376
|
+
// In your shared JS
|
|
377
|
+
var page = document.querySelector('.my-page');
|
|
378
|
+
if (page) {
|
|
379
|
+
var wrapper = page.closest('.body-wrapper');
|
|
380
|
+
if (wrapper) wrapper.classList.add('my-page-active');
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## JavaScript Conversion
|
|
387
|
+
|
|
388
|
+
### React Hooks → Vanilla JS
|
|
389
|
+
|
|
390
|
+
| React Pattern | Vanilla JS Replacement |
|
|
391
|
+
|--------------|----------------------|
|
|
392
|
+
| `useEffect` + IntersectionObserver | `IntersectionObserver` in IIFE |
|
|
393
|
+
| `useState` for toggle | `classList.toggle()` |
|
|
394
|
+
| `useRef` | `document.getElementById()` / `querySelector()` |
|
|
395
|
+
| `setTimeout` / `setInterval` in effect | Direct `setTimeout` / `setInterval` |
|
|
396
|
+
| Embla Carousel | Custom carousel with `translateX` |
|
|
397
|
+
| Radix Accordion | Custom accordion with `maxHeight` toggle |
|
|
398
|
+
| Framer Motion | CSS transitions + IntersectionObserver `.visible` class |
|
|
399
|
+
|
|
400
|
+
### Scroll Animation Pattern
|
|
401
|
+
|
|
402
|
+
```tsx
|
|
403
|
+
// React hook (useScrollAnimation.ts)
|
|
404
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
405
|
+
useEffect(() => {
|
|
406
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
407
|
+
if (entry.isIntersecting) setIsVisible(true);
|
|
408
|
+
}, { threshold: 0.1 });
|
|
409
|
+
if (ref.current) observer.observe(ref.current);
|
|
410
|
+
return () => observer.disconnect();
|
|
411
|
+
}, []);
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
```js
|
|
415
|
+
// Vanilla JS equivalent
|
|
416
|
+
function initScrollAnimations() {
|
|
417
|
+
var els = document.querySelectorAll('.scroll-animate');
|
|
418
|
+
var observer = new IntersectionObserver(function(entries) {
|
|
419
|
+
entries.forEach(function(entry) {
|
|
420
|
+
if (entry.isIntersecting) {
|
|
421
|
+
entry.target.classList.add('visible');
|
|
422
|
+
observer.unobserve(entry.target);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}, { threshold: 0.1 });
|
|
426
|
+
els.forEach(function(el) { observer.observe(el); });
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
```css
|
|
431
|
+
/* CSS for the animation */
|
|
432
|
+
.scroll-animate {
|
|
433
|
+
opacity: 0;
|
|
434
|
+
transform: translateY(30px);
|
|
435
|
+
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
|
|
436
|
+
}
|
|
437
|
+
.scroll-animate.visible {
|
|
438
|
+
opacity: 1;
|
|
439
|
+
transform: translateY(0);
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### JS Loading in Templates
|
|
444
|
+
|
|
445
|
+
JavaScript must be loaded from the **base template context**, not from child template blocks. HubSpot's `get_asset_url()` resolves paths relative to the file where it's called.
|
|
446
|
+
|
|
447
|
+
```html
|
|
448
|
+
<!-- WRONG: require_js in child template block resolves relative to child -->
|
|
449
|
+
{% block body %}
|
|
450
|
+
{{ require_js(get_asset_url("../../js/animations.js")) }}
|
|
451
|
+
{% endblock %}
|
|
452
|
+
|
|
453
|
+
<!-- CORRECT: use a variable, resolved in base template -->
|
|
454
|
+
<!-- Child template: -->
|
|
455
|
+
{% set template_js = "../../js/animations.js" %}
|
|
456
|
+
|
|
457
|
+
<!-- Base template (templates/layouts/base.html): -->
|
|
458
|
+
{% if template_js %}
|
|
459
|
+
{{ require_js(get_asset_url(template_js)) }}
|
|
460
|
+
{% endif %}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## Template Creation
|
|
466
|
+
|
|
467
|
+
### Page Template Structure
|
|
468
|
+
|
|
469
|
+
```html
|
|
470
|
+
<!--
|
|
471
|
+
templateType: page
|
|
472
|
+
isAvailableForNewContent: true
|
|
473
|
+
label: My Landing Page
|
|
474
|
+
screenshotPath: ../images/template-previews/my-page.png
|
|
475
|
+
-->
|
|
476
|
+
{% extends "./layouts/base.html" %}
|
|
477
|
+
|
|
478
|
+
{% set template_css = "../../css/my-theme.css" %}
|
|
479
|
+
{% set template_js = "../../js/my-animations.js" %}
|
|
480
|
+
|
|
481
|
+
{% block header %}
|
|
482
|
+
{# Custom header module replaces global header #}
|
|
483
|
+
{% endblock header %}
|
|
484
|
+
|
|
485
|
+
{% block body %}
|
|
486
|
+
<div class="my-page">
|
|
487
|
+
{% dnd_area "main_content" label="Main Content" %}
|
|
488
|
+
|
|
489
|
+
{% dnd_section
|
|
490
|
+
padding={"top":"0","bottom":"0","left":"0","right":"0"},
|
|
491
|
+
full_width=true
|
|
492
|
+
%}
|
|
493
|
+
{% dnd_module path="../modules/My Hero.module" %}
|
|
494
|
+
{% end_dnd_module %}
|
|
495
|
+
{% end_dnd_section %}
|
|
496
|
+
|
|
497
|
+
{# Repeat for each section... #}
|
|
498
|
+
|
|
499
|
+
{% end_dnd_area %}
|
|
500
|
+
</div>
|
|
501
|
+
{% endblock body %}
|
|
502
|
+
|
|
503
|
+
{% block footer %}
|
|
504
|
+
{# Custom footer module replaces global footer #}
|
|
505
|
+
{% endblock footer %}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### DnD Section Rules
|
|
509
|
+
|
|
510
|
+
Every `dnd_section` MUST have:
|
|
511
|
+
|
|
512
|
+
```
|
|
513
|
+
padding={"top":"0","bottom":"0","left":"0","right":"0"}, full_width=true
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
Without these:
|
|
517
|
+
- HubSpot applies default padding from `theme-overrides.css`
|
|
518
|
+
- Content is constrained to `max-width` from theme settings
|
|
519
|
+
- Your full-width designs break
|
|
520
|
+
|
|
521
|
+
Do NOT add `dnd_column` or `dnd_row` wrappers — HubSpot creates these automatically:
|
|
522
|
+
|
|
523
|
+
```html
|
|
524
|
+
<!-- WRONG -->
|
|
525
|
+
{% dnd_section %}
|
|
526
|
+
{% dnd_column %}
|
|
527
|
+
{% dnd_row %}
|
|
528
|
+
{% dnd_module path="..." %}{% end_dnd_module %}
|
|
529
|
+
{% end_dnd_row %}
|
|
530
|
+
{% end_dnd_column %}
|
|
531
|
+
{% end_dnd_section %}
|
|
532
|
+
|
|
533
|
+
<!-- CORRECT -->
|
|
534
|
+
{% dnd_section padding={"top":"0","bottom":"0","left":"0","right":"0"}, full_width=true %}
|
|
535
|
+
{% dnd_module path="..." %}{% end_dnd_module %}
|
|
536
|
+
{% end_dnd_section %}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
## Style Tab Integration
|
|
542
|
+
|
|
543
|
+
HubSpot modules have a **Content tab** and a **Style tab** in the page editor. To place color pickers and other styling options in the Style tab:
|
|
544
|
+
|
|
545
|
+
```json
|
|
546
|
+
{
|
|
547
|
+
"name": "styles",
|
|
548
|
+
"label": "Styles",
|
|
549
|
+
"type": "group",
|
|
550
|
+
"tab": "STYLE",
|
|
551
|
+
"children": [
|
|
552
|
+
{
|
|
553
|
+
"name": "section_bg",
|
|
554
|
+
"label": "Section Background",
|
|
555
|
+
"type": "color",
|
|
556
|
+
"default": { "color": "#0d1117", "opacity": 100 }
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
"name": "heading_color",
|
|
560
|
+
"label": "Heading Color",
|
|
561
|
+
"type": "color",
|
|
562
|
+
"default": { "color": "#eef1f5", "opacity": 100 }
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
"name": "text_color",
|
|
566
|
+
"label": "Text Color",
|
|
567
|
+
"type": "color",
|
|
568
|
+
"default": { "color": "#8a95a5", "opacity": 100 }
|
|
569
|
+
}
|
|
570
|
+
]
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
Apply in module.html with inline styles:
|
|
575
|
+
|
|
576
|
+
```html
|
|
577
|
+
<section style="background-color: {{ module.styles.section_bg.color }};">
|
|
578
|
+
<h2 style="color: {{ module.styles.heading_color.color }};">{{ module.headline }}</h2>
|
|
579
|
+
<p style="color: {{ module.styles.text_color.color }};">{{ module.subtitle }}</p>
|
|
580
|
+
</section>
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
For backgrounds with transparency (e.g., glassmorphism cards):
|
|
584
|
+
|
|
585
|
+
```html
|
|
586
|
+
<div style="background-color: rgba({{ module.styles.card_bg.color|convert_rgb }}, {{ module.styles.card_bg.opacity / 100 }});">
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
**Key rules:**
|
|
590
|
+
- `"tab": "STYLE"` goes on the **group**, not on individual children
|
|
591
|
+
- Children inherit the tab placement from the parent group
|
|
592
|
+
- Defaults should match your current CSS design so the page looks correct out of the box
|
|
593
|
+
- Inline styles override CSS class styles, giving editors direct control
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
## Common Pitfalls
|
|
598
|
+
|
|
599
|
+
### 1. Page Appears Empty After Upload
|
|
600
|
+
|
|
601
|
+
**Cause**: JavaScript not loading. Elements with `.scroll-animate` class start at `opacity: 0` and rely on JS to add `.visible`. If JS path is wrong, everything except CSS-animated elements stays invisible.
|
|
602
|
+
|
|
603
|
+
**Fix**: Use the `template_js` variable pattern (see [JS Loading](#js-loading-in-templates)).
|
|
604
|
+
|
|
605
|
+
### 2. White Page / Light Text Invisible
|
|
606
|
+
|
|
607
|
+
**Cause**: `theme-overrides.css` sets light-theme colors on `body`, headings, and paragraphs. Your dark-theme text becomes invisible on the white body background.
|
|
608
|
+
|
|
609
|
+
**Fix**: Add scoped overrides with `!important` and fix the body-wrapper background (see [CSS Conversion](#css-conversion)).
|
|
610
|
+
|
|
611
|
+
### 3. Sections Constrained to Narrow Width
|
|
612
|
+
|
|
613
|
+
**Cause**: Missing `padding` and `full_width` on `dnd_section` tags.
|
|
614
|
+
|
|
615
|
+
**Fix**: Add `padding={"top":"0","bottom":"0","left":"0","right":"0"}, full_width=true` to every `dnd_section`.
|
|
616
|
+
|
|
617
|
+
### 4. Module Upload Fails After Fix
|
|
618
|
+
|
|
619
|
+
**Cause**: Partially uploaded module with invalid `fields.json` is cached on HubSpot.
|
|
620
|
+
|
|
621
|
+
**Fix**: `hs remove my-theme/modules/MyModule.module` then re-upload.
|
|
622
|
+
|
|
623
|
+
### 5. Repeater Group Content Missing
|
|
624
|
+
|
|
625
|
+
**Cause**: The `default` array in the group doesn't match the `children` structure, or `occurrence.default` is 0.
|
|
626
|
+
|
|
627
|
+
**Fix**: Ensure `default` array items have all child field names, and `occurrence.default` is > 0.
|
|
628
|
+
|
|
629
|
+
### 6. HubSpot Form Renders with Light Theme
|
|
630
|
+
|
|
631
|
+
**Cause**: HubSpot forms load in iframes with their own styling.
|
|
632
|
+
|
|
633
|
+
**Fix**: Add aggressive CSS overrides scoped to your page wrapper:
|
|
634
|
+
```css
|
|
635
|
+
.my-page .hs-form-frame input,
|
|
636
|
+
.my-page .hs-form-frame select,
|
|
637
|
+
.my-page .hs-form-frame textarea {
|
|
638
|
+
background-color: #0d1117 !important;
|
|
639
|
+
color: #eef1f5 !important;
|
|
640
|
+
border: 1px solid #2a2f3a !important;
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
---
|
|
645
|
+
|
|
646
|
+
## Checklist
|
|
647
|
+
|
|
648
|
+
### Per Module
|
|
649
|
+
|
|
650
|
+
- [ ] `meta.json` created with correct `content_types`
|
|
651
|
+
- [ ] `fields.json` created — no `textarea` type, no `name` as field name
|
|
652
|
+
- [ ] All hardcoded React content extracted to field defaults
|
|
653
|
+
- [ ] Repeater groups have `occurrence.default` > 0 and matching `default` array
|
|
654
|
+
- [ ] `module.html` converts JSX to HubL (`{{ module.field }}`, `{% for %}`)
|
|
655
|
+
- [ ] `module.css` converts Tailwind utilities to BEM vanilla CSS
|
|
656
|
+
- [ ] All CSS classes use a unique prefix (e.g., `ai-`, `wco-`)
|
|
657
|
+
- [ ] Style fields wrapped in a `styles` group with `"tab": "STYLE"`
|
|
658
|
+
- [ ] Inline styles applied from `module.styles.*` fields
|
|
659
|
+
|
|
660
|
+
### Per Template
|
|
661
|
+
|
|
662
|
+
- [ ] Extends `base.html`
|
|
663
|
+
- [ ] Sets `template_css` and `template_js` variables
|
|
664
|
+
- [ ] Empty `{% block header %}` and `{% block footer %}` (if using custom header/footer modules)
|
|
665
|
+
- [ ] All `dnd_section` tags have `padding` zeroed and `full_width=true`
|
|
666
|
+
- [ ] No `dnd_column`/`dnd_row` wrappers
|
|
667
|
+
- [ ] Wrapper div with page-specific class (e.g., `.my-page`)
|
|
668
|
+
|
|
669
|
+
### Shared CSS
|
|
670
|
+
|
|
671
|
+
- [ ] CSS custom properties for the design system
|
|
672
|
+
- [ ] Scoped overrides to defeat `theme-overrides.css`
|
|
673
|
+
- [ ] Body-wrapper background fix (`:has()` + JS fallback)
|
|
674
|
+
- [ ] HubSpot form dark theme overrides (if applicable)
|
|
675
|
+
- [ ] Mobile performance rules (disable `backdrop-filter`, reduce blur)
|
|
676
|
+
|
|
677
|
+
### Shared JS
|
|
678
|
+
|
|
679
|
+
- [ ] Scroll animations (IntersectionObserver)
|
|
680
|
+
- [ ] Body-wrapper class fallback
|
|
681
|
+
- [ ] Any interactive features (carousel, accordion, typing animation, etc.)
|
|
682
|
+
- [ ] `DOMContentLoaded` / readyState check for initialization
|
|
683
|
+
|
|
684
|
+
### Upload & Test
|
|
685
|
+
|
|
686
|
+
- [ ] `hs cms upload` succeeds for all modules
|
|
687
|
+
- [ ] Template uploads without errors
|
|
688
|
+
- [ ] New page created from template shows all sections
|
|
689
|
+
- [ ] Scroll animations trigger on scroll
|
|
690
|
+
- [ ] Interactive features work (carousel, accordion, etc.)
|
|
691
|
+
- [ ] Style tab fields appear and change colors
|
|
692
|
+
- [ ] Mobile responsive layout works
|
|
693
|
+
- [ ] HubSpot form submits correctly (if applicable)
|