Building a Modern Visual CMS with Google AI Studio: Our Journey Toward an AI-Native Design System
When we started this journey, the vision was ambitious but clear: to create more than just a page builder. We wanted to build a modern…
Building a Modern Visual CMS with Google AI Studio: Our Journey Toward an AI-Native Design System

When we started this journey, the vision was ambitious but clear: to create more than just a page builder. We wanted to build a modern, developer-friendly visual Content Management System (CMS) that respected the principles of structured, component-based architecture while offering the effortless creativity of visual editing.
But our mission eventually grew beyond that. We didn’t just build a CMS — we built a system designed to teach an AI how to use our CMS like an expert, enabling users to generate entire page layouts from a single prompt.
This is the story of how we achieved that — blending the craftsmanship of good engineering, the elegance of thoughtful UX, and the intelligence of AI.
Phase 1: The Foundation — Structure and Style
We began with the basics: a solid architectural core that could scale.
1. Component-First Data Model
We defined a flexible CustomComponent structure capable of representing both standard HTML elements (div, p, img) and advanced, reusable React components like Card, Carousel, or HeroSection. This gave us a unified component tree that could model any layout with precision and consistency.
2. The Core Editor Panels
Two foundational tools shaped our UI:
- The Component Selector for exploring and navigating the page structure.
- The Style Editor for modifying visual properties of any selected component.
3. Intelligent Styling
We resisted the temptation to create a flat list of CSS properties. Instead, we built a context-aware Style Editor — one that knows when to show Flexbox controls or when to hide irrelevant options, making the UI intuitive and intelligent.
Phase 2: A World-Class User Experience
A functional tool isn’t enough. It has to feel good to use.
The Dynamic Panel System
We implemented a draggable, resizable, dockable panel system — turning our web application into a desktop-grade design studio. Users could tailor their workspace to their preferences, giving a sense of mastery and control.
Intuitive Interaction
From right-click context menus to copy-paste of styles and wizards for complex tasks (like the FlexGrid Wizard), we added UX elements that balanced simplicity with depth.
Guidance and Help
To keep power accessible, we added inline HelpTooltips and a full Help Center, ensuring users understood the capabilities without being overwhelmed.
Phase 3: Unleashing True Design Power
At this stage, we evolved from a visual editor into a true design system.
Style Sets — The Game Changer
Managing consistency across a large site is hard. So, we built Style Sets — reusable, inheritable design tokens that act like visually managed CSS classes. A “Heading” Style Set could define typography and margins, while a “Sub-heading” inherited it and simply changed the font size. This system empowered large-scale visual consistency with minimal redundancy.
Responsive Design as a First-Class Citizen
We built a Breakpoint Manager to make responsive design integral to the workflow, not an afterthought. The Breakpoint Review Tool offered a unified view of all styles across screen sizes — a massive productivity leap for real-world designs.
Phase 4: Expanding Beyond the Basics
Once the foundation was rock-solid, we expanded into high-value features.
Custom Components & Interactivity
From a configurable Carousel to a powerful Button that could trigger AJAX requests or toggle component visibility, we introduced interactivity without breaking simplicity.
Developer-Focused Additions
We built a Code component for raw HTML and even an MJML Exporter, allowing web designs to be converted into production-ready email templates — a huge technical milestone.
Polished Animations
The useScrollAnimation hook empowered designers to add scroll-triggered fade-in or slide-in effects directly from the UI.
Little details that make experiences feel alive.
Phase 5: Teaching the AI — Building the CMS for Humans and Machines
This is where things truly became groundbreaking. We weren’t content with just building a CMS for people — we wanted it to be AI-native, capable of collaboration with generative models via Google AI Studio.
The goal:
To teach an AI how to use our CMS like an expert designer, so that users could generate entire page layouts from a single natural-language prompt.
Here’s how we achieved that, through three key pillars:
1. The Component Specification (specGenerator.ts) — The Dictionary
Our first challenge was simple but profound:
How does an AI understand what a Card or Carousel is?
How does it know which properties a component supports or which style rules apply only when certain conditions (like display: flex) are met?
We built the Spec Generator, which automatically:
- Scans the
componentRegistryto list all available components. - Analyzes the
styleServiceto catalog all visual properties. - Produces a structured JSON “dictionary” — the ground truth of our UI world.
This spec file serves as an API reference for our front-end design system, giving the AI perfect contextual awareness of every building block.
2. The Generation Policy (generatePolicySpec()) — The Rulebook
Knowledge alone isn’t enough. The AI needed wisdom — rules, constraints, and best practices.
So, we built a Policy Generator that converts the structured spec into a human-readable Markdown guide for AI. It’s the style manual of our design system.
It teaches:
- Never omit the
nameproperty. - Distinguish between props (for custom components) and attributes (for HTML elements).
- Follow our golden principle: Composition Over Configuration.
If the spec is what exists, the policy defines how to use it correctly.
3. The Default Example (initialState.ts) — The Blueprint
We know that examples are the best teachers. So, our initial state wasn’t a trivial “Hello, World.” Instead, it was a comprehensive Component Showcase — a live demonstration of layout hierarchies, component usage, and even advanced styling like hover effects.
When we feed this to Google AI Studio, it becomes a few-shot learning prompt. The AI sees patterns, understands composition, and learns what “good” looks like — producing structured, idiomatic component trees in response to prompts like:
“Create a three-card feature section with a headline and CTA.”
The Synergy: Creating an AI Co-Pilot
Together, these three layers — Spec, Policy, and Example — transform a general-purpose AI into a domain-expert co-pilot for our CMS.
- The Spec gives it perfect factual knowledge.
- The Policy enforces the grammar of good design.
- The Example provides real-world context and aesthetics.
By auto-generating these artifacts on every editor load, we made it easy for developers or AI agents to “learn” our system instantly — paving the way for seamless AI-assisted content generation and visual design.
What We Built Together
Looking back, what we’ve built is more than a product. It’s a platform, a learning system, and a vision of how humans and AI can co-create the future of design.
Our CMS now stands as a:
- Flexible, component-driven design environment.
- Professional, panel-based editing workspace.
- Fully AI-aware ecosystem — bridging natural language with structured visual composition.
- Living blueprint for the next generation of intelligent, collaborative content creation tools.
This journey wasn’t just about software. It was about reimagining what a CMS could be — not just a tool to design with, but a system that designs with you.
specGenerator.ts
import type { CustomPropGroupDefinition, StyleCategoryDefinition, StylePropertyDefinition, StyleSubGroupDefinition, ExtendedCSSProperties } from '../types';
import { componentRegistry, TEXT_EDITABLE_TAGS } from './componentRegistry';
import { getApplicableAttributes } from './htmlAttributeService';
import { getApplicableProperties, styleCategories } from './styleService';
/**
* A version of StyleCategoryDefinition for the spec, where sub-groups are references.
*/
type SpecStyleCategoryDefinition = Omit<StyleCategoryDefinition, 'subGroups'> & {
subGroups: readonly { $ref: string }[];
};
/**
* Defines the specification for a single configurable component.
*/
export interface ComponentSpec {
type: string; // e.g., 'div', 'Card'
category: 'custom' | 'html';
description: string;
canHaveChildren: boolean;
canHaveContent: boolean;
// All configurable properties (custom props for components, attributes for HTML elements)
settings: (CustomPropGroupDefinition | { $ref: string })[];
// All applicable style properties, grouped into the same categories as the UI
// FIX: Updated applicableStyles to use SpecStyleCategoryDefinition to match the generated structure.
applicableStyles: readonly SpecStyleCategoryDefinition[];
}
/**
* Defines all reusable parts of the component system.
*/
interface SystemDefinitions {
styleProperties: Record<string, StylePropertyDefinition>;
styleSubGroups: Record<string, StyleSubGroupDefinition>;
propDefinitions: Record<string, CustomPropGroupDefinition>;
}
/**
* Defines the base schema for a CustomComponent instance, following JSON Schema conventions.
*/
interface ComponentBaseSchema {
description: string;
type: 'object';
properties: Record<string, { type: string; description: string }>;
required: readonly string[];
}
/**
* Defines the complete specification for all components in the system.
*/
export interface SystemSpec {
definitions: SystemDefinitions;
components: ComponentSpec[];
baseSchema: ComponentBaseSchema;
}
// Hardcoded descriptions for clarity. These could be expanded.
const componentDescriptions: Record<string, string> = {
// HTML Elements
div: "A generic container for grouping content. The primary block for layout.",
p: "A paragraph of text.",
h1: "A top-level heading for a page or section.",
button: "An interactive button that can be clicked by the user. Note: for actions, use the custom 'Button' component.",
span: "An inline container for a small piece of content, often used to style parts of a text.",
img: "An element to embed an image into the page. Note: for drag-and-drop, use the custom 'Image' component.",
a: "A hyperlink to another web page, file, or location.",
section: "A thematic grouping of content, typically with a heading.",
// Custom Components
Card: "A custom component that presents content and actions about a single subject in a container with a border and optional shadow.",
Image: "A custom component for displaying images with built-in drag-and-drop upload functionality.",
Button: "A custom, interactive button that can trigger actions on other components, such as showing, hiding, or fetching data.",
Carousel: "A custom component for displaying a slideshow of other components in an interactive carousel.",
FlexGrid: "A custom component that creates a flexible grid layout with configurable columns and gaps.",
Code: "A custom component that renders raw HTML content.",
};
const VOID_ELEMENTS = new Set(['img']);
const toCamelCase = (str: string) => str.replace(/([A-Z])/g, ' $1').replace(/[^a-zA-Z0-9]+(.)?/g, (m, c) => c ? c.toUpperCase() : '').replace(/^./, (s) => s.toLowerCase());
/**
* Generates a detailed, normalized specification for all available components in the system.
* The output uses a `definitions` object and `$ref` pointers to avoid repetition.
* @returns {SystemSpec} A comprehensive specification object.
*/
export const generateComponentSpecs = (): SystemSpec => {
const specs: ComponentSpec[] = [];
const HTML_TAGS_TO_SPEC = ['div', 'section', 'p', 'h1', 'button', 'span', 'img', 'a'];
const definitions: SystemDefinitions = {
styleProperties: {},
styleSubGroups: {},
propDefinitions: {},
};
// 1. Populate Definitions
// Populate all unique style properties
styleCategories.forEach(cat => {
cat.subGroups.forEach(sub => {
sub.properties.forEach(prop => {
definitions.styleProperties[prop.name as string] = prop;
});
});
});
// Populate all unique style sub-groups with references
styleCategories.forEach(cat => {
cat.subGroups.forEach(sub => {
const subGroupId = toCamelCase(sub.name);
definitions.styleSubGroups[subGroupId] = {
...sub,
properties: sub.properties.map(prop => ({ $ref: `#/definitions/styleProperties/${prop.name as string}` } as any)),
};
});
});
// Populate all unique prop groups from custom components and attributes
Object.values(componentRegistry).forEach(reg => {
reg.propGroups.forEach(group => {
const groupId = toCamelCase(group.name);
definitions.propDefinitions[groupId] = group;
});
});
const attrGroup: CustomPropGroupDefinition = {
name: 'HTML Attributes',
properties: [
...getApplicableAttributes('a') || [],
...getApplicableAttributes('img') || [],
]
};
definitions.propDefinitions['htmlAttributes'] = attrGroup;
// 2. Generate Component Specs with References
/**
* Helper function to create a referenced structure of applicable styles for a component type.
*/
const getReferencedApplicableStyles = (type: string): readonly SpecStyleCategoryDefinition[] => {
const applicablePropNames = getApplicableProperties(type, {});
const applicablePropSet = new Set(applicablePropNames);
return styleCategories.map(category => {
const subGroups = category.subGroups
.filter(subGroup => subGroup.properties.some(prop => applicablePropSet.has(prop.name)))
.map(subGroup => ({ $ref: `#/definitions/styleSubGroups/${toCamelCase(subGroup.name)}` }));
return { ...category, subGroups };
}).filter(category => category.subGroups.length > 0);
};
// Process Custom Components
for (const [name, registration] of Object.entries(componentRegistry)) {
specs.push({
type: name,
category: 'custom',
description: componentDescriptions[name] || `A custom ${name} component.`,
canHaveChildren: name !== 'Image' && name !== 'Button',
canHaveContent: name === 'Button',
settings: registration.propGroups.map(group => ({ $ref: `#/definitions/propDefinitions/${toCamelCase(group.name)}` })),
applicableStyles: getReferencedApplicableStyles(name),
});
}
// Process standard HTML Elements
for (const tag of HTML_TAGS_TO_SPEC) {
const settings: (CustomPropGroupDefinition | { $ref: string })[] = [];
const applicableAttributes = getApplicableAttributes(tag);
if (applicableAttributes && applicableAttributes.length > 0) {
// Find which properties of the main 'htmlAttributes' group apply to this tag.
const applicableAttrNames = new Set(applicableAttributes.map(a => a.name));
const filteredGroup: CustomPropGroupDefinition = {
name: 'HTML Attributes',
properties: definitions.propDefinitions.htmlAttributes.properties.filter(p => applicableAttrNames.has(p.name))
}
if (filteredGroup.properties.length > 0) {
// This is a bit of a workaround since we don't have per-tag attribute groups
// We inline a filtered version of the main attribute group.
settings.push(filteredGroup);
}
}
specs.push({
type: tag,
category: 'html',
description: componentDescriptions[tag] || `A standard HTML <${tag}> element.`,
canHaveChildren: !VOID_ELEMENTS.has(tag),
canHaveContent: TEXT_EDITABLE_TAGS.has(tag),
settings,
applicableStyles: getReferencedApplicableStyles(tag),
});
}
// 3. Define the base schema for a component instance.
const baseSchema: ComponentBaseSchema = {
description: "The fundamental structure for any component instance in the page tree.",
type: 'object',
properties: {
id: { type: 'string', description: "A unique identifier for the component (e.g., 'comp-12345')." },
name: { type: 'string', description: "A human-readable name for the editor's component tree." },
type: { type: 'string', description: "The component type, either a lowercase HTML tag or a PascalCase custom component name." },
styles: { type: 'object', description: "A map of breakpoint IDs to style objects. Must include a 'base' key for default styles." },
props: { type: 'object', description: "(Optional) A map of key-value pairs for custom properties specific to a registered component." },
attributes: { type: 'object', description: "(Optional) A map of standard HTML attributes for HTML elements." },
content: { type: 'string', description: "(Optional) The direct text content of an element, used for text-based tags like 'p', 'h1', etc." },
children: { type: 'array', description: "(Optional) An array of nested component objects, forming the tree structure." },
},
required: ['id', 'name', 'type', 'styles'],
};
return { definitions, components: specs, baseSchema };
};
/**
* Generates a human-readable (and LLM-friendly) policy document in Markdown
* that outlines the rules for creating valid component JSON.
* @param systemSpec The machine-readable system specification.
* @returns A string containing the generation policy in Markdown format.
*/
export const generatePolicySpec = (systemSpec: SystemSpec): string => {
let policy = `
# LLM Generation Policy for Component JSON
You are an expert at generating structured JSON for a web page builder. Adhere strictly to the following rules when creating the JSON output. Any deviation will result in invalid output.
---
### 1. Core Component Schema (MANDATORY FOR ALL COMPONENTS)
Every single component object, at every level of the tree, **MUST** have the following four properties:
- **\`id\` (string, required):** A unique identifier. Example: \`"comp-123456"\`.
- **\`name\` (string, required):** A human-readable name for the editor. Example: \`"Hero Section"\`. **DO NOT OMIT THIS.**
- **\`type\` (string, required):** The component's type. This can be a lowercase HTML element (e.g., \`"div"\`) or a \`PascalCase\` custom component (e.g., \`"Card"\`).
- **\`styles\` (object, required):** An object for styling rules. See the "Styling Rules" section below.
### 2. Property Usage Rules (\`props\` vs. \`attributes\`)
This is a strict rule. Do not confuse these two properties.
- **Use \`props\` for Custom Components:** For types like \`Card\`, \`Image\`, \`Button\`, \`Carousel\`, \`FlexGrid\`, \`Code\`. The **only** available props for each are listed at the end of this policy. Do not invent props.
- **Use \`attributes\` for standard HTML Elements:** For types like \`a\`, \`img\`, \`p\`. Use this for standard HTML attributes like \`href\` or \`src\`.
### 3. Styling Rules
The \`styles\` object has a specific structure that **MUST** be followed:
- It **MUST** contain a top-level key named **\`"base"\`**.
- The value of \`"base"\` is an object containing the CSS properties (in camelCase).
**INCORRECT:**
\`\`\`json
"styles": {
"backgroundColor": "#ffffff"
}
\`\`\`
**CORRECT:**
\`\`\`json
"styles": {
"base": {
"backgroundColor": "#ffffff",
"padding": "1rem"
}
}
\`\`\`
### 4. Composition Over Configuration (CRITICAL RULE)
Do not add properties to a component that are not explicitly defined in its specification. To build complex visuals, **nest components inside each other** using the \`children\` array.
- **INCORRECT:** Adding a \`price\` or \`image\` property to a \`Card\`.
\`\`\`json
// WRONG: "Card" does not have an "image" or "price" prop.
{
"type": "Card",
"props": {
"title": "My Card",
"image": "url...",
"price": "$99.99"
}
}
\`\`\`
- **CORRECT:** Nesting an \`Image\` component and a \`p\` component *inside* the \`Card\`'s \`children\` array.
\`\`\`json
// RIGHT: The Card contains other components that display the content.
{
"type": "Card",
"props": { "title": "My Card" },
"children": [
{ "type": "Image", "name": "Card Image", "props": { "src": "url..." }, ... },
{ "type": "p", "name": "Card Price", "content": "$99.99", ... }
]
}
\`\`\`
---
### 5. Valid Component Properties
Only use the properties listed below for each custom component type. **DO NOT INVENT PROPERTIES.**
`;
const customComponents = systemSpec.components.filter(c => c.category === 'custom' && c.settings.length > 0);
for (const comp of customComponents) {
policy += `\n- **\`${comp.type}\`**\n`;
const propGroups = comp.settings.map(s => {
const ref = (s as { $ref: string }).$ref;
if (ref) {
const groupId = ref.split('/').pop()!;
return systemSpec.definitions.propDefinitions[groupId];
}
return s as CustomPropGroupDefinition;
});
const propNames = propGroups.flatMap(g => g.properties.map(p => `\`${p.name}\``)).join(', ');
policy += ` - Valid \`props\`: ${propNames}\n`;
}
policy += `\n---\nFollow these rules precisely to ensure the generated JSON is valid.`;
return policy.trim();
}; 메타데이터
- post_id
- 3f1f9b27a67b
- slug
- building-a-modern-visual-cms-with-google-ai-studio-our-journey-toward-an-ai-native-design-system-3f1f9b27a67b
- url
- https://www.designsystemscollective.com/building-a-modern-visual-cms-with-google-ai-studio-our-journey-toward-an-ai-native-design-system-3f1f9b27a67b
- canonical_url
- https://www.designsystemscollective.com/building-a-modern-visual-cms-with-google-ai-studio-our-journey-toward-an-ai-native-design-system-3f1f9b27a67b
- author_url
- https://medium.com/@amanjn53
- status
- ok
- fetched_at
- 2026-07-16 20:24:43