ScreenshotNeo

BlogHow-to

How to Build a Visual HTML Template Editor

Build a visual HTML template editor with reusable components, editable project state, deliberate exports, and sandboxed previews. Includes a runnable GrapesJS starter.

By the ScreenshotNeo team4 October 202610 min read

A visual HTML template editor is a browser application where people assemble reusable page structures from blocks, edit their properties and styles, save the editable project, and export HTML and CSS. Build it around a structured component model rather than a rich-text field: the canvas should be a view of the same document that you save and export.

GrapesJS is a practical framework to evaluate for this job. Its official documentation covers HTML-like structures, components, blocks, webpage and newsletter presets, and customizable editor managers. It provides the editing framework; you still build the product UI, persistence, security policy, validation, and publishing flow. See the GrapesJS documentation.

1. Decide what the editor produces

Write an output contract before designing the canvas. A static webpage, a multi-page site, an email newsletter, and a server-rendered template have different rules for allowed markup, CSS, assets, variables, and scripts.

  • Web page: decide whether users can add arbitrary sections or only approved components, and how responsive styles are represented.
  • Email: constrain the markup and CSS to what your downstream email workflow supports. A browser preview alone does not establish how a template renders in email clients.
  • Application template: define how dynamic values such as titles, links, and image URLs are represented and escaped at render time.
  • Multiple pages: decide how users navigate pages, share assets, and export one page versus the complete project.

Make explicit whether export includes scripts, external stylesheets, images, and fonts. Do not assume editor-only dependencies become part of the exported artifact.

2. Build a small editor with GrapesJS

The following minimal page loads GrapesJS from its documented CDN assets, mounts the editor, and adds a few starter blocks. For a production application, pin and bundle the version you have reviewed, add your own application shell, and define your content and security policies.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Template editor</title>
  <link rel="stylesheet" href="https://unpkg.com/grapesjs/dist/css/grapes.min.css">
  <style>
    html, body { margin: 0; min-height: 100%; }
    #editor { min-height: 100vh; }
  </style>
</head>
<body>
  <div id="editor"></div>
  <script src="https://unpkg.com/grapesjs"></script>
  <script>
    const editor = grapesjs.init({
      container: '#editor',
      height: '100vh',
      fromElement: false,
      storageManager: false,
      components: '<main class="page"><h1>Your headline</h1><p>Add a message here.</p></main>',
      style: '.page { max-width: 720px; margin: 48px auto; padding: 24px; font-family: sans-serif; }'
    });

    const blocks = editor.BlockManager;
    blocks.add('section', {
      label: 'Section',
      content: '<section style="padding: 32px;"><h2>Section heading</h2><p>Section content</p></section>'
    });
    blocks.add('text', {
      label: 'Text',
      content: '<p>Write a paragraph</p>'
    });
    blocks.add('image', {
      label: 'Image',
      select: true,
      content: { type: 'image' },
      activate: true
    });
    blocks.add('button', {
      label: 'Button',
      content: '<a href="#" style="display:inline-block;padding:12px 18px;background:#2457d6;color:white;text-decoration:none">Button label</a>'
    });

    // Export the selected page's HTML and CSS for a simple single-page project.
    function exportCurrentPage() {
      return { html: editor.getHtml(), css: editor.getCss() };
    }
    window.templateEditor = { editor, exportCurrentPage };
  </script>
</body>
</html>

Save this as editor.html and serve it from a local web server so the browser loads the external assets. This is a starting canvas, not a complete editor product: add labeled save and export actions, a clear selection state, responsive controls, error handling, and an asset workflow.

3. Design the component vocabulary and controls

Blocks are the reusable items users drag into the canvas. Keep the initial palette small: section, heading, text, image, button, and perhaps a two-column section. Add specialized blocks only when they match the output contract. Each block should create a meaningful component, not arbitrary markup that your application cannot validate or migrate.

For each component, decide which properties are editable and how they are stored. Typical controls include:

  • Text: content, heading level, and alignment.
  • Link or button: label, destination, and whether it opens in a new tab.
  • Image: source, alternative text, dimensions, and crop behavior.
  • Section: spacing, background, and content width.
  • Columns: stacking behavior at narrow viewport widths.

Use component types and traits or custom controls to expose these properties. Provide a few useful style controls instead of exposing every CSS property by default. GrapesJS documents customizable block, style, layer, asset, and rich-text managers; choose the controls that fit your users rather than exposing framework internals wholesale.

4. Save editable project data separately from export

Persist the structured project representation so a user can reopen and continue editing. Generated HTML and CSS are delivery artifacts; they usually do not preserve all editor-specific structure and settings needed to restore the editing session.

A minimal application-level persistence flow could look like this:

// Client-side example: send project state to an application endpoint you implement.
async function saveProject(editor, projectId) {
  const response = await fetch(`/api/projects/${encodeURIComponent(projectId)}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      schemaVersion: 1,
      project: editor.getProjectData()
    })
  });
  if (!response.ok) throw new Error(`Save failed: ${response.status}`);
}

async function loadProject(editor, projectId) {
  const response = await fetch(`/api/projects/${encodeURIComponent(projectId)}`);
  if (!response.ok) throw new Error(`Load failed: ${response.status}`);
  const record = await response.json();
  if (record.schemaVersion !== 1) throw new Error('Unsupported project schema');
  editor.loadProjectData(record.project);
}

The /api/projects routes above are application endpoints to implement, not GrapesJS routes. Add authentication and authorization on the server, enforce request size limits, and validate the project data before accepting it. Keep a schema version with each saved document and define migrations when component definitions change. For concurrent editing, decide how revisions and conflicting saves are handled; the framework documentation does not prescribe your application’s protocol.

5. Export the actual deliverable

For a single-page project, retrieve HTML and CSS, then apply the output contract you defined. For multi-page projects, GrapesJS documents page selection and per-page HTML/CSS retrieval in its Pages guide, which applies to version 0.21.1 or newer.

function exportSinglePage(editor) {
  return {
    html: editor.getHtml(),
    css: editor.getCss()
  };
}

function downloadText(filename, text, type) {
  const blob = new Blob([text], { type });
  const link = document.createElement('a');
  link.href = URL.createObjectURL(blob);
  link.download = filename;
  link.click();
  URL.revokeObjectURL(link.href);
}

function downloadExport(editor) {
  const { html, css } = exportSinglePage(editor);
  downloadText('template.html', html, 'text/html;charset=utf-8');
  downloadText('template.css', css, 'text/css;charset=utf-8');
}

For multi-page export, select the intended page and retrieve its HTML and CSS using the documented Pages API. Confirm the exported artifact in the renderer where it will actually be used. Component scripts execute in the editor canvas, and canvas dependencies are not automatically included in exported HTML; define and validate an explicit dependency and script policy. Avoid exporting arbitrary executable scripts unless the product deliberately supports them and can handle the resulting security risks.

6. Treat the preview as a security boundary

Imported templates and user-authored HTML are untrusted input. Do not inject arbitrary imported markup into the trusted application DOM. Render previews in a sandboxed iframe and choose the minimum capabilities the preview needs. MDN explains that sandbox restrictions can block scripts, forms, and top-level navigation; combining allow-scripts and allow-same-origin for same-origin content can defeat the intended isolation. See MDN’s iframe sandbox guidance.

If you must display markup in a trusted DOM, use a reputable sanitizer and context-appropriate output encoding. A restrictive Content Security Policy adds defense in depth but does not replace safe handling. The browser HTML Sanitizer API has limited availability, so check support or use an established compatible sanitizer. OWASP also recommends sandboxing untrusted iframe content in its HTML5 Security Cheat Sheet.

Make the capability policy explicit: whether preview content can run scripts, submit forms, navigate the top-level page, load remote resources, or access application data. Test with hostile and malformed input, including links and embedded content, before accepting imports from users.

7. Make responsive editing and accessibility part of the design

Give users a few viewport presets and show where responsive styles apply. Ensure it is clear whether a style affects all viewports or only the current one. Test exported pages at the target widths, not only in the editor canvas.

Keyboard users should be able to insert blocks, select and reorder components, edit properties, and reach save and export actions. Provide labels, visible focus states, and understandable selection feedback. The framework does not guarantee your application’s accessibility; test the interface you build.

8. Choose a build approach

Approach Useful when Work you still own
Custom editor Your document schema and interaction model are unusual or tightly constrained. Canvas, selection, drag and drop, controls, serialization, export, and security.
GrapesJS framework You want an extensible builder framework and control over the surrounding application. Product UI, output rules, persistence, validation, upgrades, and preview isolation.
GrapesJS Studio SDK You are evaluating an embeddable visual builder. Check current commercial terms, integration fit, customization, and security requirements directly; this research does not establish pricing or terms.

Compare approaches by saved-document schema and export control, UI work, maintenance and plugin requirements, preview security, suitability for webpage versus newsletter output, and integration terms. GrapesJS is a framework, not a complete ready-to-publish application.

Or skip the browser setup

To capture the finished template as an image or PDF, you can use ScreenshotNeo, a website screenshot API and MCP server. Its API takes a URL in one GET request; see the API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', image));

Replace the example URL with a publicly reachable preview URL. Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card.

Troubleshooting

Problem Likely cause Fix
Editor does not appear The GrapesJS script or stylesheet did not load, or the container selector does not match. Check the browser console and network panel, confirm the assets load, and ensure the container exists before calling grapesjs.init.
Dragging a block does not create the expected component The block content is malformed or the content does not match the component model and allowed output. Inspect the block definition and resulting component tree; keep blocks aligned with registered component types and test each block.
Saved project reopens differently Only HTML was saved, project data was omitted, or the schema/component definitions changed. Persist project data and schema version; migrate older records and verify custom component types are registered before loading.
Exported page is missing styling CSS was not exported or linked, or styles depended on canvas-only resources. Export the page CSS and include it according to your delivery contract; preview the exported artifact in its real destination.
Canvas behavior is absent from export Canvas scripts or dependencies are not automatically packaged with the exported markup. Define which scripts and assets are allowed, include approved dependencies deliberately, and test the final artifact.
Preview can affect the application Untrusted markup is running in a privileged document or the iframe sandbox grants too many capabilities. Use a sandboxed iframe with minimum permissions; avoid same-origin access combined with script permission for untrusted same-origin content.
Images fail after export The asset URL is private, temporary, or relative to the editor host. Use an asset workflow that produces durable URLs or package assets as required by the target renderer; verify access from that renderer.

Performance, reliability, and cost

  • Keep the document manageable: a small component vocabulary and focused controls make selection and editing easier to reason about. Test with the largest templates your product intends to support rather than assuming performance from a small demo.
  • Save deliberately: avoid losing work on navigation or network failure. Show save state, handle rejected writes, and consider revisions or recovery appropriate to your product.
  • Load assets thoughtfully: large remote images and editor dependencies affect perceived load time. Define supported asset sizes and behavior when a resource is unavailable.
  • Plan for version changes: framework and plugin maintenance can affect stored projects and exports. Pin the version you deploy, review upgrades, and test representative saved documents after changes.
  • Budget application work: the framework does not remove the need to build persistence, authentication, validation, isolation, accessibility, and publishing. The research does not establish a performance benchmark or a framework price comparison.

FAQ

Should I store the generated HTML as the editable document?

Usually no. Store structured project data for continued editing and generate HTML/CSS for delivery. HTML alone may not preserve the editor’s component configuration.

Can I use the editor for newsletters?

GrapesJS has a newsletter preset, but constrain output to the requirements of your delivery workflow and validate it in the destination clients or renderer.

Does a canvas preview prove the exported template is safe?

No. Preview execution, export contents, and the privileges of the host application are separate concerns. Define and test policies for each.

Can users add arbitrary JavaScript?

Only if your product intentionally supports it and defines where it may execute, how dependencies are supplied, and how untrusted code is isolated. The safer default for constrained template products is to omit arbitrary scripts.