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.
@@ -0,0 +1,560 @@
1
+ # HUBSPOT_RULES.md — HubSpot CMS Design Manager Gotchas & Validation Rules
2
+
3
+ > These rules prevent the most common errors when generating HubSpot CMS templates, modules, and theme files. Every rule here comes from real validation errors, community forum pain, or undocumented behavior that breaks silently.
4
+
5
+ ---
6
+
7
+ ## 1. Module Field Naming Rules
8
+
9
+ ### Reserved Field Names (will collide with dnd_module parameters)
10
+ These names are used by drag-and-drop tags as built-in parameters. If you name a module field any of these, it will conflict when used inside `dnd_module` tags. The Design Manager will block new fields with these names, but the error is cryptic.
11
+
12
+ ```
13
+ NEVER use these as field names:
14
+ - width
15
+ - offset
16
+ - label
17
+ - path
18
+ - module_id
19
+ - css_class
20
+ - css_id
21
+ - styles
22
+ - style
23
+ - flexbox_positioning
24
+ - definition_id
25
+ - smart_type
26
+ - smart_objects
27
+ - wrap
28
+ - extra_classes
29
+
30
+ If you inherit a legacy module that already uses one of these names,
31
+ use the `fields` parameter to pass values instead:
32
+
33
+ ✅ {% dnd_module path="@hubspot/divider", fields={width: "90"} %}
34
+ ❌ {% dnd_module path="@hubspot/divider", width="90" %}
35
+ ```
36
+
37
+ ### Field Name Format Rules
38
+ ```
39
+ - Use snake_case for field names: "hero_heading" not "heroHeading" or "Hero Heading"
40
+ - Field names must be unique within a module (including across groups)
41
+ - Field names cannot contain spaces, dashes, or special characters
42
+ - Field names are case-sensitive
43
+ - The "name" in fields.json is the HubL variable name (module.field_name)
44
+ - The "label" in fields.json is the human-readable name shown in the editor
45
+ - "name" and "label" should NEVER be identical short strings like "title" or "text"
46
+ → Use descriptive names: "hero_title", "section_heading", "cta_button_text"
47
+ ```
48
+
49
+ ### Field Label Rules (for Marketplace / quality)
50
+ ```
51
+ - Labels must be descriptive: "Job Title" not just "Title"
52
+ - Labels must NOT contain numbers: "Hero Banner" not "Hero Banner 01"
53
+ - Labels must NOT contain underscores: "Hero Banner" not "Hero_Banner"
54
+ - Labels must NOT contain abbreviations: "Column" not "Col"
55
+ - Default field values must NOT contain Lorem Ipsum text
56
+ - Module labels must not duplicate default HubSpot module names
57
+ ```
58
+
59
+ ---
60
+
61
+ ## 2. Module File Structure
62
+
63
+ ### Local Development (.module folder)
64
+ ```
65
+ my-module.module/
66
+ ├── fields.json ← Field definitions (array of objects)
67
+ ├── meta.json ← Module configuration (label, icon, categories)
68
+ ├── module.html ← HubL + HTML template
69
+ ├── module.css ← Scoped CSS (loaded once per page)
70
+ └── module.js ← JavaScript (loaded once per page, NOT per instance)
71
+ ```
72
+
73
+ ### Critical: Module Folder Naming
74
+ ```
75
+ ✅ my-module.module/ ← MUST end with .module
76
+ ❌ my-module-module/ ← WRONG — causes "text fields not supported in theme" errors
77
+ ❌ my-module/ ← WRONG — not recognized as a module
78
+
79
+ This is a VERY common mistake. The folder MUST have the .module suffix.
80
+ The CLI error message when this is wrong is misleading — it says things like
81
+ "text fields are not supported in theme fields.json" because HubSpot thinks
82
+ your module's fields.json IS the theme's fields.json.
83
+ ```
84
+
85
+ ### meta.json Required Structure
86
+ ```json
87
+ {
88
+ "label": "My Custom Module",
89
+ "css_assets": [],
90
+ "external_js": [],
91
+ "global": false,
92
+ "help_text": "",
93
+ "host_template_types": ["PAGE", "BLOG_POST", "BLOG_LISTING"],
94
+ "is_available_for_new_content": true,
95
+ "js_assets": [],
96
+ "other_assets": [],
97
+ "smart_type": "NOT_SMART",
98
+ "categories": [],
99
+ "content_tags": []
100
+ }
101
+ ```
102
+
103
+ ---
104
+
105
+ ## 3. fields.json Rules
106
+
107
+ ### Structure
108
+ ```
109
+ - fields.json is a JSON ARRAY of objects: [ { ... }, { ... } ]
110
+ - NOT an object: { fields: [ ... ] } ← WRONG
111
+ - Every field needs at minimum: "name", "label", "type"
112
+ - "id" is optional in local dev (auto-generated on upload)
113
+ - "default" should always be provided — never leave fields without defaults
114
+ ```
115
+
116
+ ### Field Types Available in Modules vs Themes
117
+ ```
118
+ MODULES can use ALL field types including:
119
+ text, richtext, image, link, url, number, boolean, choice, color,
120
+ font, alignment, spacing, backgroundimage, border, gradient, icon,
121
+ cta, form, blog, hubdbtable, hubdbrow, crmobject, menu, date,
122
+ datetime, payment, video, embed, audioplayer
123
+
124
+ THEMES can only use a LIMITED subset:
125
+ color, font, image, boolean, choice, number, spacing, border,
126
+ backgroundimage, gradient, alignment, group
127
+
128
+ ❌ "text" fields are NOT supported in theme fields.json
129
+ ❌ "richtext" fields are NOT supported in theme fields.json
130
+ ❌ "url" / "link" fields are NOT supported in theme fields.json
131
+
132
+ If you see "text fields are not supported in theme fields.json"
133
+ you either:
134
+ 1. Put a text field in your THEME'S fields.json (not allowed), OR
135
+ 2. Your module folder is missing the .module suffix (see above)
136
+ ```
137
+
138
+ ### Color Field Format (CRITICAL)
139
+ ```
140
+ Color fields MUST use this exact default format:
141
+ { "color": "#rrggbb", "opacity": 100 }
142
+
143
+ RULES:
144
+ - "color" MUST be a 6-digit hex string starting with "#" (e.g. "#ffffff")
145
+ - "opacity" MUST be an integer from 0 to 100 (NOT a float like 0.7)
146
+ - NEVER use rgba(), rgb(), hsl(), or named colors (e.g. "red", "white")
147
+
148
+ ✅ { "color": "#ffffff", "opacity": 70 }
149
+ ✅ { "color": "#1a2e0d", "opacity": 100 }
150
+ ❌ { "color": "rgba(255,255,255,0.7)", "opacity": 100 } ← INVALID
151
+ ❌ { "color": "white", "opacity": 100 } ← INVALID
152
+ ❌ { "color": "#fff", "opacity": 100 } ← INVALID (3-digit)
153
+ ❌ { "color": "#ffffff", "opacity": 0.7 } ← INVALID (float)
154
+
155
+ HubSpot error: "The format for the color value is invalid"
156
+ This error means you used a non-hex color format in a color field default.
157
+ Convert rgba to hex + opacity:
158
+ rgba(255,255,255,0.85) → { "color": "#ffffff", "opacity": 85 }
159
+ rgba(0,0,0,0.5) → { "color": "#000000", "opacity": 50 }
160
+ ```
161
+
162
+ ### Style Fields (tab: "STYLE")
163
+ ```
164
+ - Style fields MUST be wrapped in a group with "tab": "STYLE"
165
+ - The group must be named "styles"
166
+ - Style fields render in the Style tab of the page editor
167
+ - Only certain field types can be style fields:
168
+ color, font, alignment, spacing, backgroundimage, border, gradient,
169
+ number, boolean, choice
170
+
171
+ Example:
172
+ [
173
+ {
174
+ "type": "group",
175
+ "name": "styles",
176
+ "tab": "STYLE",
177
+ "children": [
178
+ {
179
+ "name": "background_color",
180
+ "label": "Background Color",
181
+ "type": "color",
182
+ "default": { "color": "#ffffff", "opacity": 100 }
183
+ }
184
+ ]
185
+ }
186
+ ]
187
+ ```
188
+
189
+ ### Field Groups
190
+ ```
191
+ - Groups are fields with "type": "group" and a "children" array
192
+ - Groups CAN be nested (groups inside groups)
193
+ - When a module has at least one control in a group, ALL controls
194
+ should be organized into labeled groups (Marketplace requirement)
195
+ - Repeater fields need "occurrence" with "min" and optionally "max" / "default"
196
+ ```
197
+
198
+ ### Visibility / Display Conditions
199
+ ```json
200
+ {
201
+ "name": "headline_text",
202
+ "label": "Headline",
203
+ "type": "text",
204
+ "default": "Welcome",
205
+ "visibility": {
206
+ "controlling_field": "show_headline",
207
+ "controlling_value_regex": "true",
208
+ "operator": "EQUAL"
209
+ }
210
+ }
211
+
212
+ - "controlling_field" references another field's "name" (not label)
213
+ - For nested fields, use "controlling_field_path": "group_name.field_name"
214
+ - Operators: "EQUAL", "NOT_EQUAL", "EMPTY", "NOT_EMPTY", "MATCHES_REGEX"
215
+ - controlling_value_regex for booleans: use "true" or "false" (as strings)
216
+ ```
217
+
218
+ ---
219
+
220
+ ## 4. HubL Template Rules
221
+
222
+ ### Template Annotations (required at top of .html template files)
223
+ ```html
224
+ <!--
225
+ templateType: page
226
+ label: My Landing Page
227
+ isAvailableForNewContent: true
228
+ screenshotPath: ../images/template-previews/landing-page.png
229
+ -->
230
+ ```
231
+
232
+ Valid templateType values:
233
+ ```
234
+ page, blog_base, blog_listing, blog_post, email, section,
235
+ global_partial, search_results, membership_login,
236
+ membership_register, membership_reset, password_prompt, error_page
237
+ ```
238
+
239
+ ### Section Templates (the most error-prone area)
240
+ ```
241
+ Section template annotations:
242
+ <!--
243
+ templateType: section
244
+ label: Hero Section
245
+ isAvailableForNewContent: true
246
+ screenshotPath: ../images/section-previews/hero.png
247
+ description: "A hero section with headline and CTA"
248
+ -->
249
+
250
+ CRITICAL RULES for section templates:
251
+ 1. Multiple dnd_modules in a single dnd_section REQUIRE width and offset
252
+ 2. Width + offset values use a 12-column grid
253
+ 3. Missing width/offset causes: "Cannot resolve property [missing {{ token }} value]"
254
+
255
+ ✅ CORRECT (two modules in one section):
256
+ {% dnd_section %}
257
+ {% dnd_column %}
258
+ {% dnd_row %}
259
+ {% dnd_module "rich_text_1" path="@hubspot/rich_text" width=6, offset=0 %}
260
+ {% end_dnd_module %}
261
+ {% dnd_module "rich_text_2" path="@hubspot/rich_text" width=6, offset=6 %}
262
+ {% end_dnd_module %}
263
+ {% end_dnd_row %}
264
+ {% end_dnd_column %}
265
+ {% end_dnd_section %}
266
+
267
+ ❌ WRONG (missing width/offset):
268
+ {% dnd_section %}
269
+ {% dnd_module "rich_text_1" path="@hubspot/rich_text" %}{% end_dnd_module %}
270
+ {% dnd_module "rich_text_2" path="@hubspot/rich_text" %}{% end_dnd_module %}
271
+ {% end_dnd_section %}
272
+ ```
273
+
274
+ ### Drag-and-Drop Nesting Order
275
+ ```
276
+ The nesting MUST follow this exact hierarchy:
277
+ dnd_area → dnd_section → dnd_column → dnd_row → dnd_module
278
+
279
+ - dnd_area: top-level container, only ONE per template
280
+ - dnd_section: full-width horizontal band
281
+ - dnd_column: vertical division inside a section
282
+ - dnd_row: horizontal division inside a column
283
+ - dnd_module: the actual content module
284
+
285
+ Every opening tag MUST have a matching closing tag:
286
+ {% dnd_section %} ... {% end_dnd_section %}
287
+ {% dnd_column %} ... {% end_dnd_column %}
288
+ {% dnd_row %} ... {% end_dnd_row %}
289
+ {% dnd_module "name" path="..." %} {% end_dnd_module %}
290
+
291
+ Common mistake: forgetting {% end_dnd_column %} or mismatching nesting.
292
+ ```
293
+
294
+ ### Module References in HubL
295
+ ```
296
+ CURRENT (v2): {{ module.field_name }}
297
+ LEGACY (v1): {{ widget.field_name }} ← still works but deprecated
298
+
299
+ - URL fields return OBJECTS, not strings:
300
+ ✅ {{ module.my_link.href }}
301
+ ❌ {{ module.my_link }} ← prints the entire object as text
302
+
303
+ - Image fields:
304
+ ✅ {{ module.my_image.src }}
305
+ ✅ {{ module.my_image.alt }}
306
+ ❌ {{ module.my_image }} ← prints object
307
+
308
+ - Icon fields have inconsistent naming:
309
+ The "style" property on {% icon %} tag reads from "type" in the field data
310
+ ✅ {% icon name="{{ module.icon_field.name }}"
311
+ style="{{ module.icon_field.type }}"
312
+ unicode="{{ module.icon_field.unicode }}" %}
313
+ ❌ {% icon style="{{ module.icon_field.style }}" %} ← "style" doesn't exist on the field
314
+ ```
315
+
316
+ ### Module Names (unique identifiers in templates)
317
+ ```
318
+ - Must be in quotes: {% module "my_module" ... %}
319
+ - Must use underscores, not spaces or dashes: "my_module" not "my-module"
320
+ - Must be unique within the template
321
+ - If two modules share the same name, editing one edits both
322
+ - This is sometimes intentional (for synced content) but usually a bug
323
+ ```
324
+
325
+ ---
326
+
327
+ ## 5. CSS Rules
328
+
329
+ ### Do NOT Style HubSpot's Generated Classes
330
+ ```
331
+ These classes are auto-generated and WILL change without notice:
332
+
333
+ /* IDs — never target these */
334
+ #hs_cos_wrapper_*
335
+ #hs_form_target_dnd*
336
+
337
+ /* Classes — never target these */
338
+ .heading-container-wrapper
339
+ .heading-container
340
+ .body-container-wrapper
341
+ .body-container
342
+ .footer-container-wrapper
343
+ .footer-container
344
+ .container-fluid
345
+ .row-fluid
346
+ .row-fluid-wrapper
347
+ .row-depth-*
348
+ .row-number-*
349
+ .span*
350
+ .hs-cos-wrapper
351
+ .hs-cos-wrapper-widget
352
+ .dnd-section
353
+ .dnd-column
354
+ .dnd-row
355
+ .dnd-module
356
+ .dnd_area*
357
+
358
+ Instead: use custom classes assigned via the class parameter in templates
359
+ or through field-driven dynamic classes in modules.
360
+ ```
361
+
362
+ ### CSS Scoping in Modules
363
+ ```
364
+ - module.css loads ONCE per page, even if the module appears 10 times
365
+ - Use {{ name }} in module.html to get a unique instance class
366
+ - Scope your CSS:
367
+
368
+ module.html:
369
+ <div class="{{ name }} my-custom-module">...</div>
370
+
371
+ module.css:
372
+ .my-custom-module { ... }
373
+
374
+ - For dynamic styles, use require_css block:
375
+ {% require_css %}
376
+ <style>
377
+ {% scope_css %}
378
+ .my-module { color: {{ module.styles.text_color.color }}; }
379
+ {% end_scope_css %}
380
+ </style>
381
+ {% end_require_css %}
382
+ ```
383
+
384
+ ### Inline Styles
385
+ ```
386
+ - Hardcoded inline styles are discouraged (Marketplace rejection)
387
+ - Use field-driven dynamic inline styles instead:
388
+
389
+ ❌ <div style="background-color: #ff0000;">
390
+ ✅ <div style="background-color: {{ module.styles.bg_color.color }};">
391
+ ✅ <div style="{{ module.styles.bg_image.css }}"> ← .css property on backgroundimage fields
392
+ ```
393
+
394
+ ---
395
+
396
+ ## 6. JavaScript Rules
397
+
398
+ ### Module JS Behavior
399
+ ```
400
+ - module.js loads ONCE per page, regardless of how many instances exist
401
+ - You CANNOT assume your module appears only once on the page
402
+ - Use class-based selectors scoped to the module, not IDs
403
+
404
+ ❌ document.getElementById('my-module')
405
+ ✅ document.querySelectorAll('.my-module-class')
406
+
407
+ - For module-specific targeting, use the {{ name }} variable in module.html
408
+ to generate a unique wrapper class, then target that
409
+
410
+ - Scripts in module.js load BEFORE any require_js files
411
+ - Defer module.js execution:
412
+ var defined_name = (function() { /* your code */ })();
413
+ ```
414
+
415
+ ### jQuery
416
+ ```
417
+ - Prefer vanilla JS — adding jQuery to a site not using it causes conflicts
418
+ - If jQuery is needed, use require_js() to include it:
419
+ {{ require_js("https://code.jquery.com/jquery-3.x.min.js") }}
420
+ - This ensures it only loads if not already present
421
+ ```
422
+
423
+ ---
424
+
425
+ ## 7. Theme Inheritance Rules
426
+
427
+ ### Font and Color Inheritance (Marketplace requirement)
428
+ ```
429
+ Modules with font or color fields MUST inherit from theme settings.
430
+ Without this, you get: "Module needs to inherit from standard field names"
431
+
432
+ Use "inherited_value" with "default_value_path":
433
+
434
+ {
435
+ "name": "heading_font",
436
+ "label": "Heading Font",
437
+ "type": "font",
438
+ "inherited_value": {
439
+ "default_value_path": "theme.heading_font"
440
+ },
441
+ "default": {
442
+ "font": "Poppins",
443
+ "fallback": "sans-serif",
444
+ "variant": "600",
445
+ "font_set": "GOOGLE",
446
+ "size": 48,
447
+ "size_unit": "px"
448
+ }
449
+ }
450
+
451
+ Standard theme field names to inherit from:
452
+ - theme.primary_color
453
+ - theme.secondary_color
454
+ - theme.heading_font
455
+ - theme.body_font
456
+
457
+ If your theme uses different names, use alternate_names in theme fields.json:
458
+ {
459
+ "name": "brand_color",
460
+ "type": "color",
461
+ "alternate_names": ["primary_color"],
462
+ "default": { "color": "#516747" }
463
+ }
464
+ ```
465
+
466
+ ---
467
+
468
+ ## 8. Common Error Messages → Causes
469
+
470
+ | Error | Likely Cause |
471
+ |-------|-------------|
472
+ | `The format for the color value is invalid` | Color field default uses rgba/rgb/named color instead of hex `{ "color": "#rrggbb", "opacity": 100 }` |
473
+ | `Cannot resolve property "[missing {{ token }} value]"` | Multiple dnd_modules in a section without width/offset |
474
+ | `"text" fields are not supported in theme fields.json` | Module folder missing .module suffix, OR text field in theme fields.json |
475
+ | `Module inherits standard fields` | Font/color field missing `inherited_value` with `default_value_path` |
476
+ | `The template cannot contain "dnd_area" modules` | Using dnd_area in a blog post template (not supported) or wrong templateType |
477
+ | Field shows entire object instead of value | URL field: use `.href`, Image: use `.src`, not just `module.field_name` |
478
+ | Icon not displaying | Icon field "style" property reads from `.type` not `.style` (HubSpot inconsistency) |
479
+ | Nested module error | Modules cannot be nested inside other modules — use sections/groups instead |
480
+ | Module JS not working for multiple instances | module.js loads once — use class selectors, not ID selectors |
481
+ | Validation errors on upload but not in UI | Use `hs cms convert-fields` to get detailed errors from JS field format |
482
+ | Changes not appearing | Saving is NOT publishing — you must click Publish |
483
+
484
+ ---
485
+
486
+ ## 9. HubL Syntax Quick Reference
487
+
488
+ ### Variables and Expressions
489
+ ```
490
+ {{ variable }} ← Output a value
491
+ {% statement %} ← Logic (if, for, set, etc.)
492
+ {# comment #} ← Comment (not rendered)
493
+
494
+ {{ module.field_name }} ← Access module field
495
+ {{ content.meta_description }} ← Access page settings
496
+ {{ request.path }} ← Access request info
497
+ ```
498
+
499
+ ### Useful Filters
500
+ ```
501
+ {{ variable|pprint }} ← Debug: print variable type
502
+ {{ variable|tojson }} ← Debug: output as JSON
503
+ {{ module.field|escape }} ← HTML escape
504
+ {{ module.text|truncatewords(20) }} ← Truncate text
505
+ ```
506
+
507
+ ### Require CSS/JS (proper asset loading)
508
+ ```
509
+ {# External CSS — loads in <head> #}
510
+ {{ require_css(get_asset_url("../css/module.css")) }}
511
+
512
+ {# Inline CSS — loads in <head> via <style> tag #}
513
+ {% require_css %}
514
+ <style>
515
+ .my-class { color: red; }
516
+ </style>
517
+ {% end_require_css %}
518
+
519
+ {# External JS — loads after CSS #}
520
+ {{ require_js(get_asset_url("../js/module.js")) }}
521
+ ```
522
+
523
+ ### CRM Functions Limits
524
+ ```
525
+ - crm_object() and crm_objects(): max 10 calls per page
526
+ - crm_objects(): max 100 objects returned, default 10
527
+ - blog_popular_posts(): max 200 posts, max 10 calls per page
528
+ - Standard CRM objects (contacts, deals, etc.) require password-protected pages
529
+ - Only products and marketing_events can be shown on public pages
530
+ - Custom objects CAN be shown on public pages
531
+ ```
532
+
533
+ ---
534
+
535
+ ## 10. Pre-Upload Checklist
536
+
537
+ Before deploying any HubSpot CMS code, verify:
538
+
539
+ - [ ] All module folders end with `.module` suffix
540
+ - [ ] fields.json is a JSON array `[...]` not an object `{...}`
541
+ - [ ] No field names collide with dnd reserved parameters (width, offset, label, path, styles)
542
+ - [ ] Field names use snake_case, no spaces or dashes
543
+ - [ ] All fields have `default` values (no empty defaults for required fields)
544
+ - [ ] Color field defaults use hex format `{ "color": "#rrggbb", "opacity": 100 }` — no rgba/rgb/named colors
545
+ - [ ] No Lorem Ipsum in any default values
546
+ - [ ] URL fields accessed via `.href`, image fields via `.src`
547
+ - [ ] Multiple dnd_modules in sections have `width` and `offset` defined
548
+ - [ ] dnd nesting follows: area → section → column → row → module
549
+ - [ ] Every opening dnd tag has its closing `end_` counterpart
550
+ - [ ] Module unique names use underscores: `"my_module"` not `"my-module"`
551
+ - [ ] CSS does not target HubSpot generated classes (.dnd-section, .hs-cos-wrapper, etc.)
552
+ - [ ] module.js handles multiple instances (class selectors, not ID selectors)
553
+ - [ ] Font/color fields inherit from theme via `inherited_value` if in a theme
554
+ - [ ] No `text` or `richtext` fields in theme fields.json (module-only types)
555
+ - [ ] Template annotations present at top of .html files (templateType, label, etc.)
556
+ - [ ] No errors in Design Manager OR browser console before publishing
557
+
558
+ ---
559
+
560
+ *These rules will prevent 90% of the cryptic errors that HubSpot CMS throws at you. The remaining 10% will be undocumented edge cases — when in doubt, build a minimal reproduction in a sandbox account and test before deploying.*
@@ -0,0 +1,116 @@
1
+ # HubSpot Page Types — AI Context
2
+
3
+ Each page type has specific template requirements, HubL variables, and annotation rules.
4
+
5
+ ---
6
+
7
+ ## Landing Page
8
+
9
+ Template annotation:
10
+ ```
11
+ templateType: page
12
+ isAvailableForNewContent: true
13
+ ```
14
+
15
+ Rules:
16
+ - Self-contained single page — no navigation menu required
17
+ - Full creative freedom with modules (hero, features, testimonials, CTA, footer, etc.)
18
+ - Use `{% dnd_area %}` for drag-and-drop content areas
19
+ - All modules referenced via `{% dnd_module path="../modules/ModuleName.module" %}`
20
+ - Focus on conversion: clear CTAs, social proof, benefit-driven copy
21
+ - Anchor-link navigation is fine (link to #section-id within the same page)
22
+
23
+ ---
24
+
25
+ ## Blog Post
26
+
27
+ Template annotation:
28
+ ```
29
+ templateType: blog_post
30
+ isAvailableForNewContent: true
31
+ ```
32
+
33
+ Required HubL variables — these MUST be used in the template:
34
+ - `{{ content.name }}` — post title
35
+ - `{{ content.post_body }}` — post body content (the main article)
36
+ - `{{ content.featured_image }}` — featured image URL
37
+ - `{{ content.featured_image_alt_text }}` — featured image alt text
38
+ - `{{ content.publish_date }}` — publication date (use `|datetimeformat('%B %d, %Y')`)
39
+ - `{{ content.author.display_name }}` — author name
40
+ - `{{ content.author.avatar }}` — author avatar URL
41
+ - `{{ content.tag_list }}` — list of tags
42
+ - `{{ content.topic_list }}` — list of topics
43
+ - `{{ content.comment_count }}` — number of comments
44
+ - `{{ content.absolute_url }}` — canonical URL
45
+
46
+ Optional HubL:
47
+ - `{% blog_social_sharing %}` — social share buttons
48
+ - `{% blog_comments %}` — comment section
49
+ - `{% related_blog_posts %}` — related posts widget
50
+
51
+ Rules:
52
+ - The `{{ content.post_body }}` is where blog content goes — don't replace it with modules
53
+ - Modules are for surrounding layout: header, sidebar, author bio, related posts, share buttons
54
+ - Always include a blog listing template alongside (templateType: blog_listing)
55
+ - The listing template uses `{% for content in contents %}` to loop over posts
56
+ - Blog listing required: `{{ group.public_title }}`, `{{ next_page_url }}`
57
+
58
+ ---
59
+
60
+ ## Website Page
61
+
62
+ Template annotation:
63
+ ```
64
+ templateType: page
65
+ isAvailableForNewContent: true
66
+ ```
67
+
68
+ Rules:
69
+ - MUST include navigation using `{% menu "main_nav" %}` HubL tag
70
+ - The menu reads from the portal's menu settings — links are managed in HubSpot, not hardcoded
71
+ - Footer should include `{% menu "footer_nav" %}` for footer links
72
+ - Design the menu module visually (layout, colors, hover states, mobile hamburger)
73
+ - The user wires actual page links in the HubSpot portal after publishing pages
74
+ - Include consistent header and footer across all website page templates
75
+
76
+ Navigation module pattern:
77
+ ```html
78
+ <nav class="nav">
79
+ {% menu "main_nav" %}
80
+ </nav>
81
+ ```
82
+
83
+ For a custom-styled menu (when {% menu %} styling is insufficient):
84
+ ```html
85
+ <nav class="nav">
86
+ {% set menu = menu("main_nav") %}
87
+ {% for item in menu.children %}
88
+ <a href="{{ item.url }}" class="nav__link{% if item.active %} nav__link--active{% endif %}">
89
+ {{ item.label }}
90
+ </a>
91
+ {% endfor %}
92
+ </nav>
93
+ ```
94
+
95
+ Menu field type for modules:
96
+ ```json
97
+ {
98
+ "name": "navigation_menu",
99
+ "label": "Navigation Menu",
100
+ "type": "menu",
101
+ "default": "main_nav"
102
+ }
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Module Only
108
+
109
+ No template annotation needed — no template is generated.
110
+
111
+ Rules:
112
+ - Only module files are generated: fields.json, meta.json, module.html, module.css, module.js
113
+ - Used for adding standalone modules to existing HubSpot themes
114
+ - The module can be used in any template via `{% dnd_module path="..." %}`
115
+ - Set `host_template_types: ["PAGE"]` in meta.json (or `["BLOG_POST"]`, `["PAGE", "BLOG_POST"]` as needed)
116
+ - Focus on making the module self-contained and reusable
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+
3
+ import("../dist/index.js").catch((err) => {
4
+ if (err.code === "ERR_MODULE_NOT_FOUND") {
5
+ console.error(
6
+ "vibeSpot has not been built yet. Run `npm run build` first."
7
+ );
8
+ process.exit(1);
9
+ }
10
+ throw err;
11
+ });