---
title: "Convert HTML into BuildWithHQ/Puck pages"
section: "Builder Guide"
canonical_url: "https://support.buildwithhq.com/builder-guide/html-to-puck-converter.html"
reviewed_at: "2026-09-14"
authority: Official
---

# Convert HTML into BuildWithHQ/Puck pages

Turn static HTML or a frontend build entry into deterministic, validated Professional Foundation page JSON without importing arbitrary code or authority.

## In brief

- **Input:** HTML file or dist/index.html
- **Output:** Canonical Puck page JSON
- **Security:** No scripts, secrets, or inferred bindings

Developer Kit 1.20.71 includes `tools/html_to_puck.py`, an offline converter for bringing the useful structure of an existing static design into the current BuildWithHQ page system. It accepts one HTML file or a directory whose entry is `index.html`, then emits real page JSON made only from registered Professional Foundation component keys.

[Download Developer Kit 1.20.71](../downloads/BuildWithHQ-API43-Developer-Kit-1.20.71.zip) [Edit bindings in local Puck](local-puck-binding-editor.html) [Component and theme guide](professional-template-foundation.html) [Page editor guide](page-editor.html)

Note

This is a design migration helper, not a compiled-application loader. A Vite dist directory supplies its index.html as input; JavaScript chunks, source maps, CSS bundles, credentials and asset files are never embedded in page JSON.

## Run the converter

### Windows PowerShell

```
# From the extracted Developer Kit 1.20.71 directory
python .\tools\html_to_puck.py .\dist `
  --output .\converted\home.page.json `
  --report .\converted\home.report.json

# Reject the conversion if any executable/unsupported content was discarded.
python .\tools\html_to_puck.py .\source.html `
  --output .\converted\source.page.json `
  --report .\converted\source.report.json `
  --strict
```

### Linux or macOS

```
# From the extracted Developer Kit 1.20.71 directory
python3 ./tools/html_to_puck.py ./dist \
  --output ./converted/home.page.json \
  --report ./converted/home.report.json

python3 ./tools/html_to_puck.py ./source.html \
  --output ./converted/source.page.json \
  --report ./converted/source.report.json \
  --strict
```

Use `--entry another.html` when a directory has a different entry filename. The converter uses only Python's standard library and makes no network, API or database connection.

## What becomes a Puck component

| HTML intent | BuildWithHQ output | Behavior |
| --- | --- | --- |
| First `h1` | `core.page-header` | Page title with a stable generated block ID. |
| Other headings and paragraphs | `core.text` | Escaped display text; markup does not become executable HTML. |
| `section`, `article`, `aside` | `core.card` | First heading/paragraph become title/description; remaining content stays nested. |
| Grid/flex structural containers | `core.container` | Preserves stack, row or automatic-grid intent and simple gaps. |
| Lists | `core.list-row` | Bounded display rows inside a stack. |
| HTML tables | `core.data-table` | At most 24 columns and 500 static preview rows. |

Repeated conversion with the same seed and source order produces the same block IDs. This makes source review and page diffs meaningful. Input is capped at 2 MiB, output at 5,000 blocks, nesting at 32 levels and charts at 60 finite values.

## Annotate HTML when you want an exact component

Use `data-bwhq-component` with a public registry key and allowlisted `data-bwhq-*` properties. Unknown components, icons, tones, enum values, properties and responsive spans fail closed.

```
<div class="grid gap-4" data-bwhq-component="core.container">
  <div
    data-bwhq-component="core.stat-tile"
    data-bwhq-label="Open work"
    data-bwhq-value="42"
    data-bwhq-delta="6 today"
    data-bwhq-direction="up"
    data-bwhq-icon-name="work-order"
    data-bwhq-tone="primary"
    data-bwhq-desktop-span="3"
    data-bwhq-tablet-span="6"
    data-bwhq-mobile-span="12">
  </div>
</div>
```

For chart arrays or other richer preview properties, `data-bwhq-props` accepts a JSON object and applies the same property allowlist. It cannot introduce binding identity, authorization identity or executable code.

## Review the loss report

The report counts stripped scripts/styles/iframes, removed event handlers, display-only links/buttons, omitted media, ignored asset URLs, rejected authority attributes, component totals and any bounded text/table shortening. Credential-shaped `bwk_` and `bwu_` tokens are redacted from visible text and attributes and counted by location without copying the secret into the report. With `--strict`, a lossy conversion returns exit code 2 and does not write the page file; the report is still written for review.

Important

Links, buttons and form controls do not become working writes. Add interactions after conversion through registered components and closed action keys mapped to reviewed APIs. Never recreate an onclick handler or infer an API route from HTML.

## Set data bindings outside Puck

Puck is optional for binding assignment. Open the generated page JSON in a text editor, source-controlled build step or custom authoring tool. Put `bindingKey` and optional `dataKey` directly on the target block, beside `_id`, `_type` and `props`.

```
{
  "_id": "home-work-table",
  "_type": "core.presentation-grid",
  "bindingKey": "work-orders.presentation",
  "dataKey": "table",
  "props": {}
}
```

The deterministic `_id` gives automation a stable target. Several blocks may share one registered binding and select bounded children such as `metrics`, `recent`, `table` or `timeline` with `dataKey`. A data key is only a safe path into returned JSON; it is not SQL, a filter expression or authorization.

The binding must already be installed and registered for that SaaS. Adding an unknown key does not create a binding: server validation rejects the page and runtime resolution fails closed. Get the current `pageVersion`, save the edited document with `BuildWithHQDesignClient.saveDraft` and `expectedPageVersion`, then call `validatePage`. Keep the `bwk_draft_` credential on a trusted server.

Note

The converter refuses binding attributes in source HTML on purpose. Assigning bindings after conversion prevents downloaded markup from selecting a data source or carrying tenant, account, user, role or location identity into the page.

## Make the converted design live

1. Open the generated JSON in Puck or an external JSON workflow and review desktop, tablet, mobile, keyboard, empty and long-content behavior.
2. Replace static preview values with registered binding keys. The server must still derive the SaaS app, AppAccount, user, DataRoles and Locations.
3. Add forms, checkboxes, uploads and actions using existing first-class module components and reviewed API handlers.
4. Save the page as an unpublished draft with the server-only `BuildWithHQDesignClient` (or `bwhq page save`) and a `bwk_draft_` credential.
5. Run server validation. Publish the exact draft history only with a separate `bwk_live_` credential carrying `pages.publish`.

The static page works immediately in Puck. Live data and writes work when explicitly connected to the platform's real secured contracts—there are no generated placeholder routes or fake bindings.

## Complete annotated example

**HTML input included in the ZIP**

```
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Operations control panel</title>
  <style>body { font-family: system-ui; }</style>
</head>
<body>
  <main class="flex flex-col gap-4">
    <h1>Operations control panel</h1>
    <p>Review current workload, service health, and team activity.</p>

    <div class="grid gap-4" data-bwhq-component="core.container">
      <div data-bwhq-component="core.stat-tile" data-bwhq-label="Open work" data-bwhq-value="42" data-bwhq-delta="6 today" data-bwhq-direction="up" data-bwhq-icon-name="work-order" data-bwhq-tone="primary" data-bwhq-desktop-span="4" data-bwhq-tablet-span="6" data-bwhq-mobile-span="12"></div>
      <div data-bwhq-component="core.stat-tile" data-bwhq-label="Within service level" data-bwhq-value="96%" data-bwhq-delta="2% this week" data-bwhq-direction="up" data-bwhq-icon-name="check-circle" data-bwhq-tone="success" data-bwhq-desktop-span="4" data-bwhq-tablet-span="6" data-bwhq-mobile-span="12"></div>
      <div data-bwhq-component="core.stat-tile" data-bwhq-label="Needs attention" data-bwhq-value="7" data-bwhq-delta="2 overdue" data-bwhq-direction="down" data-bwhq-icon-name="bell" data-bwhq-tone="warning" data-bwhq-desktop-span="4" data-bwhq-tablet-span="12" data-bwhq-mobile-span="12"></div>
    </div>

    <section class="primary">
      <h2>Priority work</h2>
      <p>A bounded preview that can be replaced with a reviewed live binding.</p>
      <table>
        <caption>Today's work queue</caption>
        <thead><tr><th>Reference</th><th>Summary</th><th>Owner</th><th>Status</th></tr></thead>
        <tbody>
          <tr><td>WO-1048</td><td>Inspect cooling system</td><td>Morgan Lee</td><td>In progress</td></tr>
          <tr><td>TK-318</td><td>Confirm access window</td><td>Avery Patel</td><td>Waiting</td></tr>
          <tr><td>AP-092</td><td>Approve completion proof</td><td>Riley Chen</td><td>Review</td></tr>
        </tbody>
      </table>
    </section>

    <section>
      <h2>Next actions</h2>
      <p>Display-only list imported from HTML.</p>
      <ul>
        <li>Review two overdue tickets</li>
        <li>Confirm tomorrow's technician schedule</li>
        <li>Approve completed checklist evidence</li>
      </ul>
    </section>
  </main>
</body>
</html>
```

**Exact generated Puck page included in the ZIP**

```
{
  "blocks": [
    {
      "_id": "html-control-panel-0014-c80bd7a308",
      "_type": "core.container",
      "props": {
        "layout": "stack",
        "gap": "normal"
      },
      "children": [
        {
          "_id": "html-control-panel-0001-5b3859c95a",
          "_type": "core.page-header",
          "props": {
            "title": "Operations control panel"
          }
        },
        {
          "_id": "html-control-panel-0002-aa94387df2",
          "_type": "core.text",
          "props": {
            "text": "Review current workload, service health, and team activity.",
            "variant": "body",
            "align": "left"
          }
        },
        {
          "_id": "html-control-panel-0006-9d2e403adf",
          "_type": "core.container",
          "props": {
            "layout": "grid",
            "gridMode": "auto-fit",
            "gap": "normal"
          },
          "children": [
            {
              "_id": "html-control-panel-0003-c53609701a",
              "_type": "core.stat-tile",
              "props": {
                "tone": "primary",
                "label": "Open work",
                "value": "42",
                "direction": "up",
                "delta": "6 today",
                "iconName": "work-order",
                "responsive": {
                  "desktop": { "span": 4 },
                  "tablet": { "span": 6 },
                  "mobile": { "span": 12 }
                }
              }
            },
            {
              "_id": "html-control-panel-0004-53f4f0b16d",
              "_type": "core.stat-tile",
              "props": {
                "tone": "success",
                "label": "Within service level",
                "value": "96%",
                "direction": "up",
                "delta": "2% this week",
                "iconName": "check-circle",
                "responsive": {
                  "desktop": { "span": 4 },
                  "tablet": { "span": 6 },
                  "mobile": { "span": 12 }
                }
              }
            },
            {
              "_id": "html-control-panel-0005-51eb9b8be4",
              "_type": "core.stat-tile",
              "props": {
                "tone": "warning",
                "label": "Needs attention",
                "value": "7",
                "direction": "down",
                "delta": "2 overdue",
                "iconName": "bell",
                "responsive": {
                  "desktop": { "span": 4 },
                  "tablet": { "span": 12 },
                  "mobile": { "span": 12 }
                }
              }
            }
          ]
        },
        {
          "_id": "html-control-panel-0008-a56413e43e",
          "_type": "core.card",
          "props": {
            "tone": "primary",
            "title": "Priority work",
            "description": "A bounded preview that can be replaced with a reviewed live binding."
          },
          "children": [
            {
              "_id": "html-control-panel-0007-bda86f9048",
              "_type": "core.data-table",
              "props": {
                "caption": "Today's work queue",
                "columns": [
                  { "key": "reference", "label": "Reference", "align": "left" },
                  { "key": "summary", "label": "Summary", "align": "left" },
                  { "key": "owner", "label": "Owner", "align": "left" },
                  { "key": "status", "label": "Status", "align": "left" }
                ],
                "rows": [
                  { "reference": "WO-1048", "summary": "Inspect cooling system", "owner": "Morgan Lee", "status": "In progress" },
                  { "reference": "TK-318", "summary": "Confirm access window", "owner": "Avery Patel", "status": "Waiting" },
                  { "reference": "AP-092", "summary": "Approve completion proof", "owner": "Riley Chen", "status": "Review" }
                ]
              }
            }
          ]
        },
        {
          "_id": "html-control-panel-0013-76a243792d",
          "_type": "core.card",
          "props": {
            "tone": "neutral",
            "title": "Next actions",
            "description": "Display-only list imported from HTML."
          },
          "children": [
            {
              "_id": "html-control-panel-0012-b612166a0a",
              "_type": "core.container",
              "props": {
                "layout": "stack",
                "gap": "compact"
              },
              "children": [
                {
                  "_id": "html-control-panel-0009-c28519306d",
                  "_type": "core.list-row",
                  "props": { "primary": "Review two overdue tickets", "tone": "neutral" }
                },
                {
                  "_id": "html-control-panel-0010-0bc1e38bf4",
                  "_type": "core.list-row",
                  "props": { "primary": "Confirm tomorrow's technician schedule", "tone": "neutral" }
                },
                {
                  "_id": "html-control-panel-0011-a53fc93bed",
                  "_type": "core.list-row",
                  "props": { "primary": "Approve completed checklist evidence", "tone": "neutral" }
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}
```

## Verify the tool

```
python .\tools\html_to_puck.py --self-test
python -m pytest templates\professional-foundation\tests\test_html_to_puck.py -q
```

Tip

Use this converter to accelerate a migration, then let Puck and the component registry own the page. Keep the original HTML as design reference, not as a second production renderer.

---

Capability review: 2026-09-14. [View the canonical guide](https://support.buildwithhq.com/builder-guide/html-to-puck-converter.html).
