CMS Data & Page Assets Guide
Build CMS-driven pages with image uploads on the Visimade platform
This guide walks through building a complete CMS-powered page with image uploads, using the CmsData API for content and Page Assets API for images. Full API reference: developers/api
Authentication
All API requests require a bearer token in the Authorization header:
Authorization: Bearer vm_YOUR_TOKEN_HERE
Tokens are created in your Visimade account settings.
1. Pages API
Base URL: https://visimade.com/api/pages
Look up a page by slug
GET /api/pages/lookup?slug=my-publication
Returns the page ID and metadata. Use this when you know the slug but not the numeric ID.
Get a page
GET /api/pages/:id
Response structure:
json{
"page": {
"id": 500,
"name": "My Publication",
"slug": "my-publication",
"html_content": "<!DOCTYPE html>...",
"is_published": true,
"is_unlisted": false,
"description": "...",
"preview_image": "..."
},
"storageMode": "..."
}The page.html_content field contains the full HTML/CSS/JS of the page — this is the single source of truth for the page's frontend.
Update a page
PATCH /api/pages/:id
Content-Type: application/json
{
"html_content": "<!DOCTYPE html>..."
}The field name is html_content (not content or htmlContent).
Other updatable fields: name, slug, description, is_published, is_unlisted, preview_image.
Example with curl:
bashcurl -X PATCH "https://visimade.com/api/pages/500" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"html_content\": \"$(cat page.html | jq -sR .)\"}"Example with Python (recommended for large HTML):
pythonimport requests
with open('page.html', 'r') as f:
html = f.read()
resp = requests.patch(
'https://visimade.com/api/pages/PAGE_ID',
headers={
'Authorization': 'Bearer YOUR_TOKEN',
'Content-Type': 'application/json'
},
json={'html_content': html}
)
print(resp.status_code, resp.json())2. Page Assets (File Uploads)
Upload images and files to Visimade's CDN, scoped to a specific page.
Upload a file
POST /api/pages/:id/assets Content-Type: multipart/form-data
The form field name is file.
Example with curl:
bashcurl -X POST "https://visimade.com/api/pages/PAGE_ID/assets" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@./my_image.png"Example with Python:
pythonimport requests
resp = requests.post(
'https://visimade.com/api/pages/PAGE_ID/assets',
headers={'Authorization': 'Bearer YOUR_TOKEN'},
files={'file': ('image.png', open('image.png', 'rb'), 'image/png')}
)
asset = resp.json()
print(asset) # Contains the CDN URLResponse:
json{
"id": 1,
"filename": "image.png",
"content_type": "image/png",
"size": 245678,
"url": "https://cdn.visimade.com/pages/PAGE_ID/assets/image.png",
"created_at": "2024-01-15T10:30:00Z"
}The returned url is a permanent CDN link. Use these URLs in CMS records (e.g., article image fields) or directly in page HTML.
List page assets
GET /api/pages/:id/assets
Returns all uploaded files for that page.
3. CMS Data API
CMS Data provides schemaless, collection-based storage scoped to a page. You define collections implicitly — just start writing to any collection name.
Base URL: https://visimade.com/api/pages/:id/cms-data/:collection
List records
GET /api/pages/:id/cms-data/:collection?limit=50&offset=0
Response:
json{
"records": [
{
"id": "rec_abc123",
"data": {
"title": "My Article",
"category": "Markets"
},
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:00:00Z"
}
],
"total": 5,
"limit": 50,
"offset": 0
}Key points:
- Records are wrapped in
{ id, data, created_at, updated_at } - Your fields live inside the
dataobject - The
idis the CMS record ID (used for update/delete), not your application-level ID
Create a record
jsonPOST /api/pages/:id/cms-data/:collection
Content-Type: application/json
{
"data": {
"title": "New Article",
"category": "Markets",
"author": "Jane Smith",
"content": "<p>Article body HTML...</p>",
"image": "https://cdn.visimade.com/pages/PAGE_ID/assets/photo.png",
"sort_order": 1
}
}The REST API requires a { "data": { ... } } wrapper. No schema required — send any JSON fields you want inside data.
Note: The REST API requires the data wrapper, but the client-side SDK (CmsData.create()) does not — you pass the fields directly.
Update a record
jsonPATCH /api/pages/:id/cms-data/:collection/:recordId
Content-Type: application/json
{
"data": {
"title": "Updated Title"
}
}Merges the provided fields into the existing record data. The data wrapper is required for REST API calls.
Delete a record
DELETE /api/pages/:id/cms-data/:collection/:recordId
4. Client-Side JavaScript SDK
Inside page HTML, a global CmsData object is injected automatically by the Visimade runtime. No script import needed.
Important: Custom Domains
Always use the CmsData SDK instead of writing your own fetch() calls. If your page is served on a custom domain (e.g., mysite.com), direct API calls using relative URLs like /api/pages/... will fail — they'll go to mysite.com/api/... instead of visimade.com/api/.... The SDK handles this automatically by using absolute URLs to the Visimade API.
Initialization
javascript// Wait for CmsData to be ready before making any calls
await CmsData.ready;Check if the current viewer is the page creator (admin)
javascriptconst isAdmin = CmsData.isCreator();
// Returns true if the logged-in user owns this pageUse this to conditionally show admin UI (edit buttons, forms, etc.).
Find records (list/query)
javascriptconst result = await CmsData.find('articles', { limit: 50 });
// result.records is an array of { id, data, created_at, updated_at }
const articles = result.records.map(r => ({
...r.data,
_cmsId: r.id // preserve CMS record ID for updates/deletes
}));Create a record
javascriptawait CmsData.create('articles', {
title: 'New Article',
category: 'Markets',
author: 'Jane Smith',
content: '<p>Body HTML...</p>',
image: 'https://cdn.visimade.com/pages/PAGE_ID/assets/photo.png',
sort_order: 3
});Update a record
javascript// cmsId is the record's id from CmsData.find(), NOT your application ID
await CmsData.update('articles', cmsId, {
title: 'Updated Title',
is_lead: true
});Delete a record
javascriptawait CmsData.delete('articles', cmsId);5. Workflow: Building a CMS-Driven Page
Step 1: Create the page HTML locally
Build your HTML file with CSS, markup, and JavaScript. The JS should use the CmsData SDK to fetch and render content dynamically.
my_page.html <- single file with all CSS/JS inline
Step 2: Push the HTML to Visimade
pythonimport requests
with open('my_page.html', 'r') as f:
html = f.read()
requests.patch(
'https://visimade.com/api/pages/PAGE_ID',
headers={
'Authorization': 'Bearer YOUR_TOKEN',
'Content-Type': 'application/json'
},
json={'html_content': html}
)Step 3: Upload images via Page Assets
pythonresp = requests.post(
'https://visimade.com/api/pages/PAGE_ID/assets',
headers={'Authorization': 'Bearer YOUR_TOKEN'},
files={'file': ('photo.png', open('photo.png', 'rb'), 'image/png')}
)
cdn_url = resp.json()['url']
# Use this URL in CMS records or directly in HTMLStep 4: Seed CMS data via the API
pythonrequests.post(
'https://visimade.com/api/pages/PAGE_ID/cms-data/articles',
headers={
'Authorization': 'Bearer YOUR_TOKEN',
'Content-Type': 'application/json'
},
json={
'data': {
'title': 'My First Article',
'content': '<p>Hello world</p>',
'image': cdn_url,
'sort_order': 1
}
}
)Step 5: View the live page
https://visimade.com/p/YOUR_SLUG
The page HTML runs in the browser, calls CmsData.find() to load content, and renders it. Admin users (the page creator) see CRUD controls.
6. Tips & Gotchas
| Topic | Detail |
|---|---|
| Field name for page HTML | html_content — not content, not htmlContent |
| CMS record IDs | The id from CmsData.find() results is the CMS record ID. Your app-level IDs are separate fields inside data. |
| Schemaless collections | Collections are created implicitly. No migration step — just POST to a new collection name and it exists. |
| Image hosting | Always use /api/pages/:id/assets for image uploads. The returned CDN URLs are permanent. |
| Client SDK availability | CmsData is only available inside page HTML running on Visimade. For local testing, mock it or use the REST API directly. |
CmsData.ready | Always await CmsData.ready before any other SDK call. It resolves when the SDK is initialized. |
| Large HTML payloads | Use Python requests or similar for PATCH updates. Curl with large JSON payloads can hit shell escaping issues. |
| Admin detection | CmsData.isCreator() returns true for the page owner. Use it to gate admin UI. |
| Custom domains | Always use the CmsData SDK, not direct fetch() calls. On custom domains, relative URLs like /api/pages/... will fail because they resolve to the custom domain, not visimade.com. The SDK handles this automatically. |
| Pagination | CmsData.find() accepts { limit, offset }. Default limit is 10. Set higher if needed. |
7. Blueprint: Publication-Style Site
A complete architectural reference for building a newspaper/magazine-style CMS publication from scratch on Visimade. This covers the CMS schema, page structure, rendering logic, admin UI, routing, and responsive design — everything needed to recreate the pattern without referencing an existing implementation.
7.1 CMS Schema Design
You need two collections: categories and articles. Categories are loaded first so they can drive navigation, filtering, and the admin form.
categories collection:
| Field | Type | Purpose |
|---|---|---|
| name | string | Display name (e.g., "Markets", "Strategy") |
| slug | string | URL-friendly identifier |
| sort_order | number | Controls display order in nav |
| description | string | Subtitle shown when filtering by this category |
articles collection:
| Field | Type | Purpose |
|---|---|---|
| article_id | string | URL-friendly slug, used in hash routing |
| title | string | Headline |
| category | string | Must match a categories.name value |
| date | string | Human-readable date |
| author | string | Byline |
| summary | string | Short deck for cards and hero |
| content | string | Full article body as HTML |
| image | string | CDN URL from Page Assets |
| imageAlt | string | Alt text for accessibility |
| sort_order | number | Controls display order |
| is_lead | boolean | If true, gets the hero/featured position |
7.2 Page Structure (HTML Skeleton)
The page is a single HTML file with all CSS and JS inline. The body has these major sections:
html<body>
<!-- HEADER: masthead, category nav -->
<header>
<div class="header-accent-bar"></div>
<div class="header-top">
<div class="header-content">
<div>
<div class="masthead" onclick="goHome()">Publication Name</div>
<div class="tagline">Your Tagline Here</div>
</div>
<div class="header-right">
<div class="date-display" id="current-date"></div>
<button class="search-toggle" id="search-toggle">...</button>
</div>
</div>
</div>
<nav class="category-nav" id="category-nav">
<div class="category-nav-inner" id="category-nav-inner">
<a href="#" class="nav-link active" data-category="all">All</a>
<!-- populated dynamically from CMS categories -->
</div>
</nav>
</header>
<!-- HERO: lead article + sidebar -->
<section class="hero-section" id="hero-section">...</section>
<!-- ADMIN BAR: visible only to page owner -->
<div class="admin-bar" id="admin-bar">
<button onclick="openNewArticleForm()">+ New Article</button>
</div>
<!-- ARTICLE LIST: tiered card layout -->
<div id="articles-list" class="articles-section">...</div>
<!-- FULL ARTICLE VIEW: injected dynamically -->
<div id="article-full-container"></div>
<!-- FOOTER -->
<footer>...</footer>
<!-- MODAL: article create/edit form -->
<div class="modal-overlay" id="article-modal">...</div>
<script>/* All JavaScript here */</script>
</body>7.3 CSS Architecture
Use CSS custom properties for a consistent design language:
css:root {
--primary-color: #1a1a2e; /* navy-black for text and headings */
--accent-color: #c0392b; /* brand color (red, blue, etc.) */
--accent-light: #e74c3c; /* hover state for accent */
--accent-subtle: #fdf2f2; /* very light tint for hover backgrounds */
--text-color: #1a1a2e;
--text-secondary: #5a6072; /* muted text for bylines, meta */
--light-bg: #f7f7f8; /* card/section backgrounds */
--border-color: #e0e0e5; /* subtle dividers */
--serif: 'Georgia', 'Times New Roman', serif;
--sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}Typography rules:
- Headlines:
var(--serif), bold, tight letter-spacing - Body/UI:
var(--sans), 15px, line-height 1.55 - Category labels: uppercase, letter-spacing 1px, accent color
- Bylines/meta: 11-12px,
var(--text-secondary)
7.4 Three-Tier Article Layout
Articles are displayed in three visual tiers based on importance:
- Tier 1 (Hero): 70/30 grid with lead article + 3 sidebar articles
- Tier 2 (Mid-tier): 2-column grid with thumbnail + title/summary
- Tier 3 (Tertiary): 3-column grid, headline-only cards, no images
javascriptfunction renderArticleGrid(articleList, showAllAsMid) {
var list = articleList || articles;
var midTier, tertiary;
if (showAllAsMid) {
// Category filter mode: show all as mid-tier cards, no hero
midTier = list;
tertiary = [];
} else {
// Normal mode: hero takes lead + 3, mid-tier gets next 4, rest is tertiary
var lead = list.find(a => a.is_lead) || list[0];
var heroArticles = [lead, ...list.filter(a => a !== lead).slice(0, 3)];
var heroIds = new Set(heroArticles.map(a => a.article_id));
var remaining = list.filter(a => !heroIds.has(a.article_id));
midTier = remaining.slice(0, 4);
tertiary = remaining.slice(4);
}
// Render cards...
}7.5 Category Navigation & Filtering
Categories are fetched from CMS and rendered as a horizontal nav bar below the masthead. When filtering by category, the hero section hides and all matching articles display as mid-tier cards.
javascriptfunction renderCategoryNav() {
var nav = document.getElementById('category-nav-inner');
nav.innerHTML = '<a href="#" class="nav-link active" data-category="all">All</a>';
categories.forEach(function(cat) {
nav.innerHTML += '<a href="#" class="nav-link" data-category="' + cat.name + '">' + cat.name + '</a>';
});
nav.querySelectorAll('.nav-link').forEach(function(link) {
link.addEventListener('click', function(e) {
e.preventDefault();
nav.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
link.classList.add('active');
var category = link.dataset.category;
if (category === 'all') {
document.getElementById('hero-section').style.display = '';
renderHero();
renderArticleGrid(articles);
} else {
var filtered = articles.filter(a => a.category === category);
document.getElementById('hero-section').style.display = 'none';
renderArticleGrid(filtered, true);
}
});
});
}7.6 Article View & Routing
Uses hash-based routing (#article/article-id) with the History API for clean back/forward navigation:
javascriptfunction showArticle(articleId, pushState) {
var article = articles.find(a => a.article_id === articleId);
if (!article) return;
if (pushState) {
history.pushState({ articleId: articleId }, article.title, '#article/' + articleId);
}
document.body.classList.add('article-view');
document.getElementById('articles-list').style.display = 'none';
var container = document.getElementById('article-full-container');
container.innerHTML = /* article HTML */;
window.scrollTo(0, 0);
}
window.addEventListener('popstate', function(event) {
if (event.state && event.state.articleId) {
showArticle(event.state.articleId, false);
} else {
showArticleList(false);
}
});7.7 Prev/Next Article Navigation
At the bottom of each article, show links to the previous and next articles based on sort_order.
7.8 Reading Time Utility
javascriptfunction estimateReadTime(htmlContent) {
if (!htmlContent) return 1;
var text = htmlContent.replace(/<[^>]*>/g, '');
var wordCount = text.split(/\s+/).filter(w => w.length > 0).length;
return Math.max(1, Math.round(wordCount / 230));
}7.9 Admin UI (Owner-Only CRUD)
The admin system is gated by CmsData.isCreator() — no authentication code needed. The page owner sees:
- "+ New Article" button in the admin bar
- "Edit" and "Delete" buttons on each card
- A modal form for creating/editing articles
Admin form fields:
| Field | Input Type | Notes |
|---|---|---|
| Title | text | Auto-generates slug |
| Article ID / Slug | text | Auto-populated, manually overridable |
| Category | select | Populated from CMS categories |
| Date | text | Pre-filled with current date |
| Author | text | |
| Summary | textarea | |
| Content (HTML) | textarea | Monospace, tall |
| Image URL | text | Paste CDN URL from Page Assets |
| Sort Order | number | Pre-filled with articles.length + 1 |
| Lead / Hero | checkbox | Only one article should have this |
7.10 Search
A toggle button in the header reveals a search bar that filters articles client-side by title, summary, and category.
7.11 Footer with Dynamic Sections
The footer mirrors the category nav — section links are generated from CMS categories. Footer layout is a 3-column grid: brand + description, section links, about text.
7.12 Responsive Breakpoints
Two breakpoints handle the collapse from multi-column to single-column:
- max-width: 768px (mobile): All grids become single column
- 769px - 1024px (tablet): Tertiary grid becomes 2-column
7.13 Initialization Sequence
Use CmsData.find() to fetch data — never write your own fetch() calls to /api/pages/.... The SDK handles API routing correctly for both visimade.com and custom domains.
javascriptlet articles = [];
let categories = [];
let isAdmin = false;
async function loadArticles() {
await CmsData.ready;
await loadCategories(); // 1. Fetch categories
var result = await CmsData.find('articles', { limit: 50 });
articles = result.records
.map(r => ({ ...r.data, _cmsId: r.id }))
.sort((a, b) => (a.sort_order || 99) - (b.sort_order || 99));
isAdmin = CmsData.isCreator(); // 2. Check admin status
if (isAdmin) {
document.getElementById('admin-bar').classList.add('visible');
}
renderCategoryNav(); // 3. Build category nav
renderHero(); // 4. Render hero section
renderArticleList(); // 5. Render tiered grid
renderFooterSections(); // 6. Build footer links
checkInitialRoute(); // 7. Handle deep-link
}
loadArticles();Do NOT do this:
javascript// WRONG - will fail on custom domains!
fetch('/api/pages/' + pageId + '/cms-data/articles')
// CORRECT - use the SDK
CmsData.find('articles', { limit: 50 })7.14 Embedded Charts (Optional)
Articles can include interactive charts by placing container divs in the article HTML content with specific IDs, then initializing them with a charting library like ECharts after the article view renders.
7.15 Putting It All Together
To build a new publication site from scratch:
- Create a Visimade page (via the platform UI or API)
- Define your categories — seed the
categoriesCMS collection via API - Build the HTML file locally using the skeleton and patterns above
- Push the HTML to the page via
PATCH /api/pages/:id - Upload images via
POST /api/pages/:id/assets, save the CDN URLs - Seed articles via
POST /api/pages/:id/cms-data/articles - Visit the page — the JS loads CMS data and renders everything
- Ongoing editing — the page owner sees admin UI to CRUD articles in the browser
No build step, no framework, no database setup. The HTML is the app, CMS Data is the database, and Page Assets is the CDN.