Getting Started
Example: CMS Blog with Images
Build a blog using CmsData for posts and PageAssets for images. Complete workflow from image upload to rendering.
Full Guide: For a complete walkthrough with Python examples and best practices, see the CMS Data & Page Assets Guide.
AI Agent Tutorial: Want to build this with a coding agent? See Create a Blog with a Coding Agent for a step-by-step guide using Claude Code or any CLI agent.
Pattern: Generate images using the Page Assets API and store the returned publicUrl in your CmsData records. Do not use external image services (Unsplash, Pexels, etc.) — the built-in image generation produces images that are permanently hosted on our CDN.
Blog Post Schema
Each blog post in the posts collection has this structure:
{
"title": "Getting Started with Visimade",
"slug": "getting-started",
"excerpt": "Learn how to build interactive pages...",
"content": "<p>Full HTML content here...</p>",
"featuredImage": "https://visimade-r2.com/page-assets/123/hero-getting-started-a1b2c3d4.jpg",
"category": "tutorials",
"tags": ["beginner", "tutorial"],
"publishedAt": "2024-01-15T10:00:00Z"
}featuredImage stores the full public CDN URL returned by the image generation endpoint.
Step 1: Generate Images
Use the Page Assets generate endpoint to create AI-generated images for your blog posts. Each call generates an image from a text prompt and saves it as a page asset, returning a permanent CDN URL.
# Generate a hero image for an article
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"prompt": "A developer workspace with multiple monitors showing code and charts, modern flat illustration style",
"aspectRatio": "16:9",
"filename": "hero-getting-started"
}' \
https://visimade.com/api/pages/123/assets/generate
# Response:
{
"id": 5,
"filename": "hero-getting-started-a1b2c3d4.jpg",
"publicUrl": "https://visimade-r2.com/page-assets/123/hero-getting-started-a1b2c3d4.jpg",
"contentType": "image/jpeg",
"sizeBytes": 184320,
"createdAt": "2026-01-15T09:00:00Z"
}Tip: Generate a hero image for each article (use 16:9 aspect ratio for hero images) and optionally a logo/header image for the blog itself (use 1:1 for logos). Use the returned publicUrl directly — no need to resolve filenames at runtime.
Already have images? You can also upload existing image files via POST /api/pages/:id/assets with multipart/form-data. See Upload Asset.
Step 2: Create Blog Post
Create a CMS record using the publicUrl from the generated image as the featuredImage:
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"title": "Getting Started with Visimade",
"slug": "getting-started",
"excerpt": "Learn how to build interactive pages with data storage, AI features, and more.",
"content": "<p>Welcome to Visimade! In this tutorial...</p>",
"featuredImage": "https://visimade-r2.com/page-assets/123/hero-getting-started-a1b2c3d4.jpg",
"category": "tutorials",
"tags": ["beginner", "tutorial"],
"publishedAt": "2026-01-15T10:00:00Z"
}
}' \
https://visimade.com/api/pages/123/cms-data/postsStep 3: Display Posts on the Page
In your page's JavaScript, fetch posts and render them. Since featuredImage stores the full CDN URL, you can use it directly as the src — no URL resolution needed:
<script>
async function loadBlog() {
// Wait for APIs to be ready
await CmsData.ready;
// Fetch all posts, newest first
const { records: posts } = await CmsData.find('posts', {
orderBy: 'created_at',
order: 'desc'
});
// Render posts — featuredImage is already a full CDN URL
const container = document.getElementById('blog-posts');
container.innerHTML = posts.map(post => `
<article class="blog-post">
${post.data.featuredImage ? `
<img
src="${post.data.featuredImage}"
alt="${post.data.title}"
class="featured-image"
/>
` : ''}
<h2>${post.data.title}</h2>
<p class="excerpt">${post.data.excerpt}</p>
<div class="meta">
<span class="category">${post.data.category}</span>
<time>${new Date(post.data.publishedAt).toLocaleDateString()}</time>
</div>
<div class="content">${post.data.content}</div>
</article>
`).join('');
}
loadBlog();
</script>Step 4: Admin Interface (Owner Only)
Show editing controls only to the page owner:
<script>
async function initAdmin() {
await CmsData.ready;
// Only show admin UI to page owner
if (!CmsData.isCreator()) {
return;
}
document.getElementById('admin-panel').style.display = 'block';
// Handle new post form
document.getElementById('new-post-form').addEventListener('submit', async (e) => {
e.preventDefault();
const form = e.target;
// Upload image first if provided
let featuredImage = null;
const imageFile = form.image.files[0];
if (imageFile) {
const asset = await PageAssets.upload(imageFile);
featuredImage = asset.filename;
}
// Create the post
await CmsData.create('posts', {
title: form.title.value,
slug: form.slug.value,
excerpt: form.excerpt.value,
content: form.content.value,
featuredImage,
category: form.category.value,
tags: form.tags.value.split(',').map(t => t.trim()),
publishedAt: new Date().toISOString()
});
// Refresh the blog list
loadBlog();
form.reset();
});
}
initAdmin();
</script>Complete API Workflow
Here's the full workflow for an AI agent or automation to create a blog with AI-generated images:
# 1. Enable CMS on your page (one-time setup)
curl -X PATCH \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"page_api_cms_enabled": true}' \
https://visimade.com/api/pages/123
# 2. Generate images for a new post
# Use the assets/generate endpoint — no external image services needed
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"prompt": "Advanced software architecture diagram with microservices", "aspectRatio": "16:9", "filename": "hero-advanced"}' \
https://visimade.com/api/pages/123/assets/generate
# → Returns: { "publicUrl": "https://visimade-r2.com/page-assets/123/hero-advanced-a1b2c3d4.jpg", ... }
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"prompt": "Clean flowchart showing data pipeline architecture", "aspectRatio": "4:3", "filename": "diagram-pipeline"}' \
https://visimade.com/api/pages/123/assets/generate
# → Returns: { "publicUrl": "https://visimade-r2.com/page-assets/123/diagram-pipeline-e5f6g7h8.jpg", ... }
# 3. Create the blog post using the publicUrl values from step 2
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"data": {
"title": "Advanced Patterns",
"slug": "advanced-patterns",
"excerpt": "Deep dive into...",
"content": "<p>Text... <img src=\"https://visimade-r2.com/page-assets/123/diagram-pipeline-e5f6g7h8.jpg\" /> more text...</p>",
"featuredImage": "https://visimade-r2.com/page-assets/123/hero-advanced-a1b2c3d4.jpg",
"category": "advanced",
"publishedAt": "2026-01-20T12:00:00Z"
}
}' \
https://visimade.com/api/pages/123/cms-data/posts
# 4. Update a post
curl -X PATCH \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"data": {"title": "Advanced Patterns (Updated)"}}' \
https://visimade.com/api/pages/123/cms-data/posts/RECORD_ID
# 5. Delete a post
curl -X DELETE \
-H "Authorization: Bearer YOUR_TOKEN" \
https://visimade.com/api/pages/123/cms-data/posts/RECORD_IDImportant: Always use POST /api/pages/:id/assets/generate for blog images instead of external image services. The generated images are permanently hosted on our CDN and the publicUrl can be used directly in featuredImage fields and inline <img> tags in content HTML.
On this page
- Blog Post Schema
- Step 1: Generate Images
- Step 2: Create Blog Post
- Step 3: Display Posts
- Step 4: Admin Interface
- Complete API Workflow