# MultiPortal SaaS — Template Integration Skill
## How to integrate a new template and produce a `blocks.json`

**Version:** 2.1  
**System:** MultiPortal SaaS  
**Files involved:** `admin.html`, `multiportal.html`, `templates/{id}/template.html`, `templates/{id}/blocks.json`, `data/db.json`

---

## 1. System Overview

MultiPortal is a SaaS portal builder where each "portal" (website) is assigned a **template** — a self-contained HTML file that provides the visual shell (navbar, footer, colour scheme, typography). Page content is injected into the shell via `{{TAG}}` placeholders.

A **block** is a reusable HTML section (Hero, Features, Team, etc.) that a portal admin drags onto a page using the Block Builder. Blocks are stored per-template in `templates/{id}/blocks.json`.

The **Block Editor** (`blockeditor.html`) is a standalone tool for creating and editing `blocks.json` files visually.

### Data Flow

```
User picks template → admin.html fetches templates/{id}/blocks.json
                     → Block Builder shows available blocks
                     → User drags blocks onto a page
                     → Page HTML = concatenated block HTML
                     → multiportal.html fetches template.html
                       + fills {{TAGS}} → renders portal in iframe
```

### Two Types of Template CSS

Understanding this distinction is critical before starting any integration:

| Type | CSS lives in | Applied to |
|------|-------------|------------|
| **Native template** (you built it) | `template.html` `<style>` block | Entire portal shell — navbar, footer, page sections, all blocks share it automatically |
| **Third-party / bought template** | Separate `.css` file(s) | Blocks are injected as `{{SECTIONS_HTML}}` — the shell does NOT automatically load the separate CSS files. CSS must travel with the blocks. |

For **native templates**: CSS in `template.html` is always available to all blocks.  
For **third-party templates**: the CSS must be embedded directly into a dedicated "styles foundation" block (see §4.5).

---

## 2. File Structure

```
project-root/
├── admin.html                  ← Admin dashboard + Visual Editor + Block Builder
├── multiportal.html            ← Public portal viewer
├── blockeditor.html            ← Block JSON authoring tool (standalone)
├── data/
│   └── db.json                 ← Schema reference + seed data (localStorage in dev)
└── templates/
    ├── README.md               ← Template authoring guide (basic)
    ├── t001/
    │   ├── template.html       ← Template HTML shell
    │   └── blocks.json         ← Block library for this template
    ├── t002/ ... t008/         ← Same structure
    └── tNNN/                   ← Your new template goes here
```

**Template IDs:** `t001`–`t008` are built-in. Use `t009` onward for custom templates. IDs are lowercase strings like `t009`, `t010`, `t011`, etc.

---

## 3. Template Shell (`template.html`)

### 3.1 Minimum Required Structure

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{{PORTAL_NAME}}</title>

  <!-- Required: Bootstrap 5 -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

  <!-- Optional but common -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet">

  <!-- Dynamic font from portal settings -->
  <link href="https://fonts.googleapis.com/css2?family={{FONT_FAMILY_URL}}:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">

  <style>
    /* REQUIRED: page section visibility control */
    .page-section { display: none; }
    .page-section.active { display: block; }

    /* Use YOUR template prefix for ALL classes — avoid clashes */
    :root {
      --t9-primary:   {{PRIMARY_COLOR}};
      --t9-secondary: {{SECONDARY_COLOR}};
      --t9-font:      '{{FONT_FAMILY}}', sans-serif;
    }
    * { font-family: var(--t9-font); }
    /* ... rest of your CSS using .t9-* class names ... */
  </style>
</head>
<body>

  <!-- NAVBAR -->
  <nav class="t9-nav">
    <a class="t9-brand" href="#" onclick="showPage('home');return false;">
      {{PORTAL_LOGO_HTML}}{{PORTAL_NAME}}
    </a>
    <ul class="t9-nav-list">
      {{NAV_ITEMS_HTML}}
    </ul>
  </nav>

  <!-- PAGE SECTIONS (injected by renderer) -->
  {{SECTIONS_HTML}}

  <!-- FOOTER -->
  <footer class="t9-footer">
    <p>&copy; {{CURRENT_YEAR}} {{PORTAL_NAME}}. {{FOOTER_TEXT}}</p>
    <p>{{CONTACT_EMAIL}} | {{CONTACT_PHONE}}</p>
    <ul>{{SOCIAL_LINKS_HTML}}</ul>
  </footer>

  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
  <script>
    /* REQUIRED: page switcher */
    function showPage(slug) {
      document.querySelectorAll('.page-section').forEach(s => s.classList.remove('active'));
      var el = document.getElementById('page-' + slug);
      if (el) el.classList.add('active');
      document.querySelectorAll('.t9-nav-list a').forEach(a => {
        a.classList.toggle('active', a.dataset.slug === slug);
      });
      window.scrollTo(0, 0);
    }
  </script>
</body>
</html>
```

### 3.2 All Available `{{TAGS}}`

| Tag | Type | Description |
|-----|------|-------------|
| `{{PORTAL_NAME}}` | string | Portal display name, e.g. `"SMK Bukit Jambul"` |
| `{{PORTAL_TAGLINE}}` | string | Short description / tagline |
| `{{PRIMARY_COLOR}}` | hex | e.g. `#4361ee` — use in CSS variables |
| `{{SECONDARY_COLOR}}` | hex | e.g. `#10b981` |
| `{{FONT_FAMILY}}` | string | Google Font name, e.g. `Poppins` |
| `{{FONT_FAMILY_URL}}` | string | Same name with `+` not spaces, e.g. `DM+Sans` |
| `{{PORTAL_LOGO_HTML}}` | HTML | `<img src="..." style="height:36px;...">` or empty string |
| `{{NAV_ITEMS_HTML}}` | HTML | Rendered `<li><a>` elements for navigation |
| `{{SECTIONS_HTML}}` | HTML | All page sections as `<div class="page-section ...">` |
| `{{FOOTER_TEXT}}` | string | Copyright text |
| `{{CONTACT_EMAIL}}` | string | Contact email |
| `{{CONTACT_PHONE}}` | string | Contact phone |
| `{{CONTACT_ADDRESS}}` | string | Physical address |
| `{{SOCIAL_LINKS_HTML}}` | HTML | Social media `<li><a>` elements |
| `{{CURRENT_YEAR}}` | number | 4-digit year |

**Tags in block HTML**: Blocks can also use `{{PORTAL_NAME}}`, `{{PORTAL_TAGLINE}}`, `{{PRIMARY_COLOR}}`, `{{SECONDARY_COLOR}}` etc. — they are resolved at render time. In the Block Editor they display as **orange badge pills** rather than being replaced, so authors can see them.

### 3.3 How `{{SECTIONS_HTML}}` renders

The renderer wraps each published page's content HTML in:

```html
<!-- Home page (type="home" or slug="home") -->
<div class="page-section tN-hero active" id="page-home">
  ...content blocks HTML...
</div>

<!-- All other pages -->
<div class="page-section tN-section" id="page-about">
  ...content blocks HTML...
</div>
```

The first section gets `active`. Style both `.tN-hero` and `.tN-section` in your CSS.

### 3.4 How `{{NAV_ITEMS_HTML}}` renders

```html
<li>
  <a href="#page-home" class="active" data-slug="home"
     onclick="showPage('home');return false;"
     style="...">Home</a>
</li>
```

### 3.5 CSS Class Naming Convention

**CRITICAL for native templates:** Every CSS class must use a unique prefix based on the template ID to prevent style bleed between templates in the admin previewer.

| Template ID | Required prefix |
|-------------|-----------------|
| `t009` | `.t9-` |
| `t010` | `.t10-` |
| `t011` | `.t11-` |

Examples: `.t9-nav`, `.t9-hero`, `.t9-card`, `.t9-footer`

**Third-party templates** already have their own class names (e.g. `.hero`, `.about-thumb`, `.projects-image`). Do NOT rename these — they are tied to the CSS. Handle CSS via the styles foundation block (see §4.5).

---

## 4. Block Library (`blocks.json`)

### 4.1 File Location

```
templates/{templateId}/blocks.json
```

Loaded by `admin.html` via `fetch('templates/{id}/blocks.json')`. Must be accessible over HTTP (not `file://`).

### 4.2 Block Object Schema

```json
[
  {
    "id":          "t9-hero",
    "name":        "Hero Banner",
    "desc":        "Full-width hero with background, heading, tagline and CTA button.",
    "icon":        "🏠",
    "category":    "Hero",
    "thumbnail":   "data:image/jpeg;base64,...",
    "html":        "<div class=\"t9-hero\">...</div>"
  }
]
```

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `id` | string | ✓ | Unique, kebab-case, prefixed: `t9-hero`, `t9-about` |
| `name` | string | ✓ | Human-readable, shown in block library |
| `desc` | string | ✓ | One sentence description for block library card |
| `icon` | string | ✓ | Single emoji, shown if no thumbnail |
| `category` | string | ✓ | See Category List §4.3 |
| `thumbnail` | string | recommended | Base64 JPEG data URL of a preview screenshot |
| `html` | string | ✓ | The block's raw HTML (see §4.4 and §4.5) |

### 4.3 Category List

Use exactly these strings (case-sensitive):

```
Hero          Features      About         Services
Content       People        Social Proof  Media
Contact       Stats         Menu          Courses
Listings      News          Layout        CTA
```

### 4.4 Block HTML Rules

#### Rule 1: Self-contained, Bootstrap 5 compatible

Each block must render correctly assuming Bootstrap 5 CSS and JS are already loaded. Do not include `<html>`, `<head>`, `<body>`, or `<link>` tags. Do not embed `<style>` blocks inside content blocks — the **one exception** is the styles foundation block (§4.5).

#### Rule 2: Single root element

Each block has exactly **one root element**.

```html
<!-- CORRECT -->
<div class="t9-hero"><h1>Heading</h1><p>Subtext</p></div>

<!-- WRONG: multiple root elements -->
<div class="t9-hero-wrap">...</div>
<div class="t9-hero-overlay">...</div>
```

#### Rule 3: Use template CSS classes

Blocks reference classes defined in `template.html` (native) or the styles foundation block (third-party).

#### Rule 4: Inline styles for block-specific overrides

Use inline styles for background images, specific heights/padding, and per-block colour overrides.

#### Rule 5: Template tokens

Use `{{PORTAL_NAME}}`, `{{PORTAL_TAGLINE}}`, `{{PRIMARY_COLOR}}` etc. where the block displays portal-specific data.

#### Rule 6: data-bm-id attribute

The system stamps `data-bm-id="{block.id}"` on the root element automatically. Do not include it yourself.

#### Rule 7: Accessible image alt text

All `<img>` tags must have `alt` attributes. Use empty `alt=""` for decorative images.

#### Rule 8: Background images must be absolute URLs

Replace all relative image paths with absolute CDN URLs (Unsplash, Cloudinary, etc.). Relative paths do not resolve from the portal iframe context.

---

### 4.5 CSS Embedding for Third-Party Templates ⭐

This section applies when integrating a **bought or third-party template** that ships its own `.css` file(s).

#### Why blocks need the CSS

When blocks are placed on a page, they are injected as `{{SECTIONS_HTML}}` into the template shell. The shell loads Bootstrap but **does not automatically load the third-party CSS files** — those are referenced via `<link>` tags in the original `index.html` that are absent from the MultiPortal shell.

Without the CSS, all the custom classes (`.hero`, `.about-thumb`, `.projects-image`, etc.) have no styling and the blocks look completely broken.

#### The Styles Foundation Block pattern

Create a dedicated block — always the **first entry** in `blocks.json` and the **first block added to every page** — whose entire purpose is to carry the template CSS. It renders invisibly but injects the CSS into the live page DOM.

```html
<div class="xxx-template-styles" style="display:none;" aria-hidden="true">
<style>
  /* Full template CSS goes here */
</style>
</div>
```

**Why `display:none` works:** A `<style>` tag is valid and processed anywhere in the DOM — inside a hidden `div`, inside `body`, inside a section. The browser applies the CSS rules regardless of the parent element's visibility.

**`aria-hidden="true"`** ensures screen readers skip the invisible container.

#### Step-by-step: embedding a third-party CSS file

**Step 1 — Identify the CSS files**

Find which CSS files the template loads (in `<head>`):
```html
<link href="css/template-name.css" rel="stylesheet">
<link href="css/plugin.css" rel="stylesheet">
```
Gather each file you need. **Exclude `bootstrap.min.css`** — Bootstrap is already loaded by the MultiPortal shell.

**Step 2 — Replace CSS variables with MultiPortal tokens**

The template's `:root` block defines its colour palette and font. Replace those hardcoded values with `{{TOKEN}}` placeholders so portal settings take effect:

```css
/* BEFORE */
:root {
  --primary-color:    #f9c10b;
  --secondary-color:  #dc3545;
  --body-font-family: 'DM Sans', sans-serif;
}

/* AFTER — MultiPortal tokens injected */
:root {
  --primary-color:    {{PRIMARY_COLOR}};
  --secondary-color:  {{SECONDARY_COLOR}};
  --body-font-family: '{{FONT_FAMILY}}', 'DM Sans', sans-serif;
}
```

**Variable mapping guide:**

| CSS variable purpose | Replace with |
|---------------------|--------------|
| Main brand / accent colour | `{{PRIMARY_COLOR}}` |
| Secondary / highlight colour | `{{SECONDARY_COLOR}}` |
| Body font family | `'{{FONT_FAMILY}}', [original-font-as-fallback]` |

Only replace variables where the intent clearly matches. Leave utility colours like `--dark-color: #000000` unchanged.

**Step 3 — Add the Google Fonts import**

Most third-party templates load their font via a `<link>` tag in `<head>`. Since the styles block cannot use `<link>`, use a CSS `@import` instead — place it as the very first line of the `<style>` block:

```css
@import url('https://fonts.googleapis.com/css2?family={{FONT_FAMILY_URL}}:wght@400;500;700&family=DM+Sans:wght@400;500;700&display=swap');
```

This imports both the portal's chosen font (`{{FONT_FAMILY_URL}}`) and the template's original font as a fallback. The `{{FONT_FAMILY_URL}}` token uses `+` instead of spaces (e.g. `DM+Sans`).

**Step 4 — Fix relative asset paths in CSS**

CSS files often reference images via relative paths that break when the CSS is embedded in a different context:

```css
/* BREAKS in portal context */
.section-hero {
  background-image: url('../images/hero-bg.jpg');
}
```

Fix options:
- Replace with an absolute CDN URL: `url('https://images.unsplash.com/...')`
- Remove the declaration entirely if the block always overrides it with an inline `style="background-image:..."`

**Step 5 — Handle multiple CSS files**

If the template requires multiple CSS files, concatenate them all into the single `<style>` block in the correct load order (plugins first, main stylesheet last):

```html
<div class="xxx-styles" style="display:none;" aria-hidden="true">
<style>
@import url('https://fonts.googleapis.com/css2?family={{FONT_FAMILY_URL}}...');

/* === magnific-popup.css === */
.mfp-bg { top:0;left:0;width:100%;height:100%;z-index:1042;position:fixed;background:#0b0b0b;opacity:.8 }
/* ... rest of plugin CSS ... */

/* === tooplate-main.css === */
:root {
  --primary-color: {{PRIMARY_COLOR}};
  --secondary-color: {{SECONDARY_COLOR}};
  --body-font-family: '{{FONT_FAMILY}}', 'DM Sans', sans-serif;
}
/* ... rest of main CSS ... */
</style>
</div>
```

**Step 6 — Assemble the foundation block**

```json
{
  "id":       "xxx-styles",
  "name":     "⚠ Template Styles (Required)",
  "desc":     "MUST be the first block on every page. Contains all [Template Name] CSS. Add this once before all other blocks.",
  "icon":     "🎨",
  "category": "Layout",
  "thumbnail": null,
  "html":     "<div class=\"xxx-styles\" style=\"display:none;\" aria-hidden=\"true\">\n<style>\n@import url('...');\n:root { --primary-color: {{PRIMARY_COLOR}}; }\n/* ... */\n</style>\n</div>"
}
```

#### What NOT to embed

| Do NOT include | Reason |
|----------------|--------|
| `bootstrap.min.css` | Already loaded by shell — doubles CSS, causes specificity conflicts |
| `bootstrap.min.js` | Already loaded by shell |
| Bootstrap Icons / Font Awesome CSS | Already loaded by shell |
| JavaScript | Use a separate hidden `<div><script>...</script></div>` block only if a plugin JS is genuinely needed |

#### Critical usage rule

The `xxx-styles` block **must be the first block on every page** that uses this template's blocks. Without it, all other blocks render as completely unstyled HTML. Make this unmissable in the block name and description.

---

## 5. Standard Block Set

For **native templates**, standard content blocks:

| # | Block ID | Category | Key tokens used |
|---|----------|----------|-----------------|
| 1 | `tN-hero` | Hero | `{{PORTAL_NAME}}`, `{{PORTAL_TAGLINE}}`, `{{PRIMARY_COLOR}}` |
| 2 | `tN-about` | About | `{{PORTAL_NAME}}`, `{{PORTAL_TAGLINE}}` |
| 3 | `tN-services` | Features | `{{PRIMARY_COLOR}}`, `{{SECONDARY_COLOR}}` |
| 4 | `tN-contact` | Contact | `{{CONTACT_EMAIL}}`, `{{CONTACT_PHONE}}`, `{{CONTACT_ADDRESS}}` |
| 5 | `tN-cta` | CTA | `{{PORTAL_NAME}}`, `{{PRIMARY_COLOR}}` |
| 6 | `tN-stats` | Stats | `{{PRIMARY_COLOR}}` |
| 7 | `tN-team` | People | — |
| 8 | `tN-testimonials` | Social Proof | — |

For **third-party templates**, add a block #0 before all content:

| # | Block ID | Category | Key tokens used | Notes |
|---|----------|----------|-----------------|-------|
| 0 | `xxx-styles` | Layout | `{{PRIMARY_COLOR}}`, `{{SECONDARY_COLOR}}`, `{{FONT_FAMILY}}`, `{{FONT_FAMILY_URL}}` | Required first block on every page |

---

## 6. Complete `blocks.json` Examples

### Native template block

```json
{
  "id": "t9-hero",
  "name": "Hero — Dark Overlay",
  "desc": "Full-width hero with dark background image, large heading, tagline and two CTA buttons.",
  "icon": "🌌",
  "category": "Hero",
  "thumbnail": null,
  "html": "<div class=\"t9-hero\" style=\"position:relative;min-height:85vh;display:flex;align-items:center;background:url('https://images.unsplash.com/photo-1521737711867?w=1400&auto=format&fit=crop&q=80') center/cover;\">\n  <div style=\"position:absolute;inset:0;background:rgba(10,15,30,.62);\"></div>\n  <div class=\"container\" style=\"position:relative;z-index:2;padding:80px 0;\">\n    <h1 class=\"t9-hero-title\">{{PORTAL_NAME}}</h1>\n    <p class=\"t9-hero-sub\">{{PORTAL_TAGLINE}}</p>\n    <a href=\"#\" style=\"background:{{PRIMARY_COLOR}};color:#fff;padding:12px 28px;border-radius:50px;font-weight:700;text-decoration:none;\">Get Started</a>\n  </div>\n</div>"
}
```

### Third-party template — styles foundation block

```json
{
  "id": "waso-styles",
  "name": "⚠ Template Styles (Required)",
  "desc": "MUST be the first block on every page. Contains all Waso Strategy CSS. Add once before any other Waso blocks.",
  "icon": "🎨",
  "category": "Layout",
  "thumbnail": null,
  "html": "<div class=\"waso-template-styles\" style=\"display:none;\" aria-hidden=\"true\">\n<style>\n@import url('https://fonts.googleapis.com/css2?family={{FONT_FAMILY_URL}}:wght@400;500;700&family=DM+Sans:wght@400;500;700&display=swap');\n:root {\n  --primary-color: {{PRIMARY_COLOR}};\n  --secondary-color: {{SECONDARY_COLOR}};\n  --body-font-family: '{{FONT_FAMILY}}', 'DM Sans', sans-serif;\n  --white-color: #ffffff;\n  --dark-color: #000000;\n  --p-color: #717275;\n  --section-bg-color: #f5f5f5;\n}\nbody { background: var(--white-color); font-family: var(--body-font-family); }\n/* ... all other CSS rules ... */\n</style>\n</div>"
}
```

---

## 7. Step-by-Step Integration Checklist

### Step 1 — Assign a template ID

Pick the next available ID. Check `data/db.json` under `templates.seed` for existing IDs. Example: `t009`.

### Step 2 — Create the folder

```
templates/t009/
├── template.html
└── blocks.json
```

### Step 3 — Adapt `template.html`

**If building from scratch (native):**
1. Write HTML shell with `{{TAGS}}`, CSS with `.tN-` prefixes.
2. Add `showPage(slug)` and `.page-section` visibility CSS.

**If working from a purchased/third-party template:**
1. Keep original HTML structure and class names.
2. Replace navbar content with `{{PORTAL_NAME}}`, `{{NAV_ITEMS_HTML}}`.
3. Replace main content area with `{{SECTIONS_HTML}}`.
4. Replace footer with tokens.
5. Add `showPage(slug)` function and `.page-section { display:none; }` CSS.
6. Add Bootstrap 5 CDN link if not present.
7. **Also embed the template CSS in a `<style>` block inside `template.html`** — this ensures the Visual Editor in admin renders correctly without needing the foundation block.

### Step 4 — Author `blocks.json`

**For third-party templates — create styles foundation block first:**
1. Read all provided `.css` files (exclude Bootstrap and icon fonts).
2. Replace `:root` colour/font variables with `{{TOKEN}}` placeholders.
3. Add `@import` for Google Fonts using `{{FONT_FAMILY_URL}}`.
4. Fix or remove relative `url('../images/...')` asset paths.
5. Concatenate all CSS into one `<style>` block inside a hidden `<div>`.
6. Create the `xxx-styles` block entry, place it **first** in the array.

**For every content section:**
1. Extract HTML with exactly one root element.
2. Replace hardcoded portal text with `{{TOKEN}}` placeholders.
3. Replace relative image paths with CDN URLs.
4. Remove section anchor `id` attributes (e.g. `id="section_2"`) — they conflict across pages.
5. Remove `href="page-name.html"` links — replace with `href="#"`.
6. Remove email-obfuscation markup — replace with `{{CONTACT_EMAIL}}`.
7. Remove `<html>`, `<head>`, `<body>`, `<link>` tags.
8. Assign `id`, `name`, `desc`, `icon`, `category`, set `thumbnail: null`.

### Step 5 — Capture thumbnails

1. Open `blockeditor.html`.
2. Load your `blocks.json`.
3. Skip the styles foundation block (it renders invisible).
4. For each content block: Properties tab → **Capture Preview**.
5. Save JSON → copy to `templates/{id}/blocks.json`.

### Step 6 — Register in `data/db.json`

```json
{
  "id": "t009", "name": "My Custom Template",
  "slug": "my-custom-template",
  "description": "A bold, modern template for...",
  "thumbnail_color": "#1a1a2e", "thumbnail_icon": "✨",
  "path": "templates/t009/", "category": "custom",
  "tags": ["modern", "dark"], "is_free": true, "price": 0,
  "status": "active", "created_at": "2026-01-01T00:00:00Z"
}
```

### Step 7 — Register in `admin.html`

Find `SEED_TEMPLATES` array and add the same fields.

### Step 8 — Register in `multiportal.html` (offline fallback)

Find `const TEMPLATES = {` and add:
```js
t009: `<!DOCTYPE html>...full template.html...`,
```
Escape backticks as `` \` `` and `</script>` as `<\/script>`.

### Step 9 — Test

1. Create a portal in admin, select the new template.
2. Open Block Builder for a page.
3. **Third-party only:** add `xxx-styles` block first, then content blocks.
4. Confirm blocks render with correct styling.
5. Save page, open `multiportal.html?portal=your-slug`.
6. Test on multiple screen sizes.

---

## 8. Block Editor (`blockeditor.html`) Usage

The Block Editor is a dedicated tool for creating and editing `blocks.json` without writing raw JSON.

### Workflow

```
1. Open blockeditor.html
2. Load JSON → load blocks.json (or paste it)
3. Select a block from the left panel
4. Visual Editor shows the live preview
5. Click any element → green pencil → click to edit content
   OR Hierarchy tab → click nodes → edit in Styles panel
6. HTML Code tab → edit raw HTML + Format button for indenting
7. Save JSON → replace templates/{id}/blocks.json
```

### Block Properties

```
Block ID       → tN-hero (unique, kebab-case, prefixed)
Name           → "Hero Banner"
Description    → One sentence
Category       → "Hero"
Icon (emoji)   → 🏠 (shown if no thumbnail)
Thumbnail      → Capture Preview button or Upload
```

### Saving Thumbnails

- Captured via **Capture Preview** (uses `html2canvas`)
- Or uploaded manually via **Upload**
- Saved in `thumbnail` field of each block object
- Displayed in admin Block Library cards and Page Blocks list
- **Styles foundation block:** renders invisible — skip capture, leave `thumbnail: null`

---

## 9. localStorage Data Structure Reference

All portal data uses the `mp2_` prefix:

| Key | Type | Description |
|-----|------|-------------|
| `mp2_portals` | Array | Portal records including `template_id` and `settings` |
| `mp2_pages` | Array | Page records with `content` field (concatenated block HTML) |
| `mp2_templates` | Array | Template registry |

**Portal settings object:**

```json
{
  "primary_color": "#4361ee", "secondary_color": "#10b981",
  "font_family": "Inter", "contact_email": "info@example.com",
  "contact_phone": "+60 12-345 6789", "address": "No. 1 Jalan Utama, KL",
  "footer_text": "All rights reserved.", "nav_menu": "main-menu",
  "social_links": { "facebook": "", "instagram": "", "twitter": "",
                    "youtube": "", "whatsapp": "", "telegram": "" }
}
```

---

## 10. Common Mistakes & How to Avoid Them

### ❌ Missing styles foundation block (third-party templates)

**Problem:** Blocks render as completely unstyled plain HTML.  
**Cause:** Template CSS lives in a separate file not loaded by the MultiPortal shell.  
**Fix:** Create `xxx-styles` foundation block following §4.5. Make it the first block on every page.

---

### ❌ CSS variables not replaced with tokens

**Problem:** Changing the portal's primary colour in settings has no effect on blocks.  
**Fix:** Replace `:root` colour and font values in the embedded CSS with `{{PRIMARY_COLOR}}`, `{{SECONDARY_COLOR}}`, `{{FONT_FAMILY}}` tokens.

---

### ❌ Embedding Bootstrap CSS in the styles block

**Problem:** Specificity conflicts and doubled CSS.  
**Fix:** Exclude `bootstrap.min.css` — the shell already loads it.

---

### ❌ Relative CSS asset paths breaking

**Problem:** `url('../images/bg.jpg')` doesn't resolve from the portal iframe context.  
**Fix:** Replace with absolute CDN URLs, or remove if the block overrides it with an inline style.

---

### ❌ CSS class conflicts (native templates)

**Problem:** Generic class names like `.hero` clash between templates.  
**Fix:** Always prefix every class with `tN-`.

---

### ❌ Multiple root elements in a block

**Problem:** `<section>...</section><div>...</div>` — two roots.  
**Fix:** Wrap in one root: `<div class="xxx-wrap"><section>...</section><div>...</div></div>`

---

### ❌ Hardcoded portal data

**Problem:** `<h1>Sekolah Menengah Kebangsaan Bukit Jambul</h1>`  
**Fix:** `<h1>{{PORTAL_NAME}}</h1>`

---

### ❌ `thumbnail` field missing

**Problem:** Thumbnails disappear after reloading blocks.json.  
**Fix:** Map `thumbnail: b.thumbnail || null` in the `lB()` load function.

---

### ❌ `</script>` inside a template literal

**Problem:** Breaks the JS template string in `multiportal.html`.  
**Fix:** Escape it: `<\/script>`

---

### ❌ Fetching blocks.json fails locally

**Problem:** `file://` protocol blocks `fetch()`.  
**Fix:** `python3 -m http.server 8887` → open `http://localhost:8887/admin.html`

---

### ❌ Section anchor IDs left in blocks

**Problem:** `id="section_2"` on multiple pages causes duplicate ID conflicts in the DOM.  
**Fix:** Remove all `id="section_N"` attributes from block HTML.

---

## 11. Quick Reference Card

```
Template ID format:    t009, t010, t011 ...
CSS class prefix:      .t9-, .t10-, .t11- ... (native templates only)
Folder:                templates/t009/
Template file:         templates/t009/template.html
Block library:         templates/t009/blocks.json
Block ID format:       t9-hero, t9-about, t9-cta

Required template tags (minimum):
  {{PORTAL_NAME}}   {{NAV_ITEMS_HTML}}
  {{SECTIONS_HTML}} {{CURRENT_YEAR}}

Required template JS:
  showPage(slug) — toggles .active on .page-section divs

Required CSS in template:
  .page-section { display: none; }
  .page-section.active { display: block; }

Block JSON schema:
  id, name, desc, icon, category, thumbnail, html

⚠ OUTPUT FORMAT — CRITICAL:
  The file MUST be a plain JSON array: [{...}, {...}, ...]
  NEVER wrap it in an object: {"blocks": [...]}  ← WRONG
  The block editor rejects anything that is not Array.isArray()

Block HTML rules:
  • Single root element
  • Bootstrap 5 classes allowed
  • Template CSS classes used (defined in shell or foundation block)
  • Inline styles for block-specific overrides
  • {{TOKEN}} placeholders for dynamic portal data
  • No <html>, <head>, <body>, <link> tags in content blocks
  • <style> ONLY in the styles foundation block

Third-party template extra (REQUIRED):
  • Create xxx-styles block: hidden <div> + <style> tag
  • Embed full template CSS inside it (exclude Bootstrap)
  • @import Google Fonts using {{FONT_FAMILY_URL}}
  • Replace :root colour/font vars with {{TOKEN}}
  • Fix relative url() paths in CSS to absolute CDN URLs
  • Place xxx-styles FIRST in blocks.json array
  • Users MUST add it as first block on every page

Categories:
  Hero, Features, About, Services, Content,
  People, Social Proof, Media, Contact, Stats,
  Menu, Courses, Listings, News, Layout, CTA
```

---

## 12. Checklist for AI Integration of a New Template

### Preliminary assessment

- [ ] Is this a **native template** (CSS inside `template.html`) or a **third-party template** (separate `.css` file)?
- [ ] If third-party: are the CSS files provided? If not, **ask for them before proceeding** — they are required
- [ ] Read the full `template.html` to identify all distinct sections
- [ ] Note the CSS class naming convention (prefixed or unprefixed)
- [ ] Identify navbar and footer — these stay in the shell, not in blocks
- [ ] Identify all sections between navbar and footer — each becomes a block

### If third-party template — create styles foundation block FIRST

- [ ] Read all provided `.css` files (exclude Bootstrap, Bootstrap Icons, Font Awesome)
- [ ] Note which `:root` variables control primary colour, secondary colour, and font
- [ ] Replace those `:root` values with `{{PRIMARY_COLOR}}`, `{{SECONDARY_COLOR}}`, `{{FONT_FAMILY}}`
- [ ] Add `@import url('https://fonts.googleapis.com/css2?family={{FONT_FAMILY_URL}}...')` as first line
- [ ] Scan CSS for `url('../images/...')` paths — replace with CDN URLs or remove
- [ ] Concatenate all CSS files in correct order into one `<style>` block
- [ ] Wrap in `<div style="display:none;" aria-hidden="true">...</div>`
- [ ] Create foundation block: id=`xxx-styles`, category=`Layout`, name=`"⚠ Template Styles (Required)"`, desc warns it must be first on every page
- [ ] Place as **first item** in `blocks.json`

### For each content section

- [ ] Give it a unique `id` (kebab-case, template-prefixed)
- [ ] Choose descriptive `name` and one-sentence `desc`
- [ ] Pick correct `category` from §4.3 list
- [ ] Pick relevant `icon` emoji
- [ ] Set `thumbnail: null`
- [ ] Extract HTML with exactly **one root element**
- [ ] Replace hardcoded portal text with `{{TOKEN}}` placeholders
- [ ] Replace hardcoded primary colour hex values with `{{PRIMARY_COLOR}}`
- [ ] Replace hardcoded secondary colour values with `{{SECONDARY_COLOR}}`
- [ ] Replace ALL relative image paths `src="images/..."` with absolute CDN URLs
- [ ] Remove section anchor IDs like `id="section_2"`
- [ ] Replace `href="page-name.html"` with `href="#"`
- [ ] Remove Cloudflare or other email-obfuscation markup — replace with `{{CONTACT_EMAIL}}`
- [ ] Remove `<style>`, `<link>`, `<script>`, `<html>`, `<head>`, `<body>` tags
- [ ] Keep all Bootstrap classes (`container`, `row`, `col-*`, etc.)
- [ ] Keep all template-specific CSS classes
- [ ] Keep all inline styles

### Final validation

- [ ] **Output is a raw JSON array `[...]` at the root — NEVER wrap in `{"blocks":[...]}` or any object.** The block editor checks `Array.isArray(data)` and rejects anything that is not a plain top-level array.
- [ ] Array parses as valid JSON
- [ ] No duplicate `id` values
- [ ] All `html` values start with a single HTML tag
- [ ] No `src="images/..."` local paths remain
- [ ] No hardcoded company names, phone numbers, or addresses
- [ ] Styles foundation block is first in array (third-party only)
- [ ] Foundation block description clearly warns it must be first on every page
