How Instatic’s Super Import Feature Converts HTML/CSS to Editable Nodes

Instatic’s super import feature converts raw HTML and CSS into editable nodes by parsing markup into a sanitized DOM tree, walking it with walkAndMap, and mapping every element to the native PageNode format used by the visual editor.

The CoreBunch/Instatic repository implements this importer as a deterministic pipeline that can convert HTML/CSS to editable nodes when users paste or upload static markup. Because the output matches the editor’s internal tree structure, imported content requires no manual reconfiguration. The entire workflow is orchestrated by the importHtml function inside src/core/htmlImport/walkAndMap.ts.

The Pipeline to Convert HTML/CSS to Editable Nodes

The importer processes every input through discrete stages. Each stage is implemented as a dedicated routine in walkAndMap.ts, ensuring the final result is safe, complete, and editable.

Parse the Raw Markup

First, importHtml feeds the source string to parseHtml. This lightweight parser produces a DOM-like tree that can be traversed without a browser environment. All subsequent operations depend on this normalized representation.

Strip Unsafe Content

Next, stripUnsafe removes disallowed elements and attributes before they reach the editor. It eliminates <script> tags, on* event handlers, and dangerous URLs. If anything is removed, the resulting ImportResult sets the stripped flag to true, signaling that the input was sanitized.

Walk and Map to Page Nodes

The core conversion happens in walkAndMap. It recursively traverses the safe DOM tree and instantiates a PageNode for every element. The walker extracts:

  • HTML attributes → stored on props.htmlAttributes
  • Inline styles → stored on inlineStyles as camel-cased CSS properties
  • Class names → converted to classIds for the selector registry
  • Text nodes → become base.text nodes
  • Media and interactive elements → mapped to built-in modules such as base.image, base.link, and form components

Children are processed recursively, preserving the original nesting order in the final tree.

Collect Page-Scoped CSS

During the walk, the importer gathers all CSS from inline <style> blocks and <link rel="stylesheet"> references. These rules are concatenated into the styleCss field of the ImportResult. Once persisted, this CSS appears in the editor’s Style panel as a page-scoped stylesheet.

Return the Import Result

importHtml returns an ImportResult containing:

  • nodes: a Map<string, PageNode> of all created nodes
  • rootIds: identifiers for top-level roots, usually the <body> element
  • styleCss: the concatenated stylesheet string
  • stripped: boolean flag indicating whether unsafe content was removed

The caller can then commit these nodes through the standard editor mutation API.

Importing HTML Programmatically

You can invoke the importer directly from client or admin code. The snippet below demonstrates parsing a raw HTML string, inserting the resulting nodes into the active page tree, and persisting the extracted CSS.

import { importHtml } from '@core/htmlImport'
import { mutateActiveTree } from '@core/page-tree'
import { insertNode, addPageStyle } from 'src/admin/pages/site/store/slices/site/helpers'

const rawHtml = `
  <html>
    <head>
      <style>
        .hero { background: #f0f0f0; }
      </style>
    </head>
    <body class="hero">
      <h1>Hello world</h1>
      <p data-note="example">A paragraph with <a href="/page">a link</a>.</p>
    </body>
  </html>
`

const { nodes, rootIds, styleCss, stripped } = importHtml(rawHtml)

// Insert each root node into the current page
rootIds.forEach((rootId) => {
  const node = nodes[rootId]
  mutateActiveTree((s) => {
    insertNode({ parentId: s.activePageId, node })
  })
})

// Persist the collected CSS as a page-scoped stylesheet
mutateActiveTree((s) => {
  addPageStyle({ pageId: s.activePageId, css: styleCss })
})

After execution, the <body> becomes an editable base.body node, the <h1> becomes a base.text node, and the <a> becomes a base.link module. The .hero rule is stored as a page-scoped stylesheet, while any unsafe tags are automatically excluded.

Key Source Files

Understanding the import architecture requires familiarity with these modules:

Summary

  • Instatic’s super import feature centers on the importHtml function exported from src/core/htmlImport/walkAndMap.ts.
  • The pipeline runs through parse, sanitize, walk/map, and CSS collection stages in strict order.
  • Every HTML element is mapped to a PageNode with preserved attributes, inline styles, and child nesting.
  • Unsafe content is removed by stripUnsafe, and a stripped flag reports when sanitization occurs.
  • Extracted CSS is returned as styleCss and later surfaced in the editor’s Style panel as a page-scoped stylesheet.
  • Because the output uses the same node shape as the native editor tree, imported content is immediately editable.

Frequently Asked Questions

What is the entry point for Instatic’s HTML importer?

The public API is the importHtml function, which is the main export of src/core/htmlImport/walkAndMap.ts. External callers, including the site-import pipeline in src/core/siteImport/htmlPagePlan.ts, invoke this single function to transform a raw HTML string into editable nodes and extracted CSS.

How does Instatic prevent XSS or unsafe code during import?

Before any DOM traversal occurs, the stripUnsafe routine scrubs <script> tags, on* event handlers, and dangerous URLs from the parsed tree. If sanitization is required, the resulting ImportResult sets stripped to true, guaranteeing that the editor surface remains free of executable code.

What happens to CSS styles embedded in imported HTML?

The walker collects all rules from <style> tags and external stylesheet links into a single styleCss string. This CSS is returned alongside the node tree and is typically persisted as a page-scoped stylesheet, making it available in the editor’s Style panel for further customization.

Can imported nodes be edited right away without extra conversion?

Yes. Because walkAndMap emits nodes that conform exactly to the PageNode structure defined in src/core/page-tree/types.ts, the visual editor treats them identically to natively created content. Users can edit text, tweak attributes, and apply styles immediately after insertion.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →