webhouse.appwebhouse.appdocs

Data-driven interactive content — charts, calculators, demos with CMS-managed data.

The separation principle

When building interactive content (charts, animations, calculators), all text and data must be stored in CMS collections — never hardcoded.

WhatWhereEditable by
Text labels, headingsCMS text fieldsEditor in admin
Data points, numbersCMS array/object fieldsEditor in admin
Visualization, animationInteractive componentDeveloper
Styling, colorsInteractive CSSDeveloper

Pattern: CMS → Page → Interactive

1. Define a data collection:

typescript
defineCollection({
  name: "chart-data",
  fields: [
    { name: "title", type: "text", required: true },
    { name: "chartType", type: "select", options: [
      { label: "Line", value: "line" },
      { label: "Bar", value: "bar" },
    ]},
    { name: "dataPoints", type: "array", fields: [
      { name: "label", type: "text" },
      { name: "value", type: "number" },
    ]},
  ],
})

2. Create the component (client):

typescript
"use client";
export function Chart({ title, data }: { title: string; data: { label: string; value: number }[] }) {
  // Use Chart.js, D3, or any visualization library
  return <div><h3>{title}</h3>{/* render chart */}</div>;
}

3. Use in a page (server reads CMS, passes props):

typescript
import { getDocument } from "@/lib/content";
import { Chart } from "@/components/chart";

export default function Page() {
  const data = getDocument("chart-data", "monthly-sales");
  if (!data) return null;
  return <Chart title={data.data.title} data={data.data.dataPoints} />;
}

Standalone HTML interactives

The CMS also supports standalone HTML interactives managed via the Interactives Manager. These are complete HTML files that render in iframes. Use for:

  • Self-contained interactives without CMS data
  • Quick prototyping with "Create with AI" in admin
  • One-off visualizations

Richtext embedding

Interactives can be embedded in richtext fields:

!!INTERACTIVE[chart-id|Chart Title|align:center]

Your renderer must convert these tokens to iframes:

typescript
html = html.replace(
  /!!INTERACTIVE\[([^\]]+)\]/g,
  (_match, inner) => {
    const [id, title = id] = inner.split("|");
    return `<iframe src="/uploads/interactives/${id}.html" title="${title}"
      style="width:100%; border:none; border-radius:0.5rem;"
      loading="lazy" sandbox="allow-scripts allow-same-origin"></iframe>`;
  },
);

Scaled rendering

Render full-size interactives as miniatures using CSS transform:

typescript
<div style={{ width: 500, height: 400, overflow: "hidden" }}>
  <iframe
    src="/interactives/chart.html"
    style={{ width: 1000, height: 800, transform: "scale(0.5)", transformOrigin: "top left" }}
  />
</div>
Previous
Troubleshooting