VISIMADE

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 URL
Response:
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 data object
  • The id is 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 page

Use 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 HTML

Step 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

TopicDetail
Field name for page HTMLhtml_content — not content, not htmlContent
CMS record IDsThe id from CmsData.find() results is the CMS record ID. Your app-level IDs are separate fields inside data.
Schemaless collectionsCollections are created implicitly. No migration step — just POST to a new collection name and it exists.
Image hostingAlways use /api/pages/:id/assets for image uploads. The returned CDN URLs are permanent.
Client SDK availabilityCmsData is only available inside page HTML running on Visimade. For local testing, mock it or use the REST API directly.
CmsData.readyAlways await CmsData.ready before any other SDK call. It resolves when the SDK is initialized.
Large HTML payloadsUse Python requests or similar for PATCH updates. Curl with large JSON payloads can hit shell escaping issues.
Admin detectionCmsData.isCreator() returns true for the page owner. Use it to gate admin UI.
Custom domainsAlways 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.
PaginationCmsData.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:
FieldTypePurpose
namestringDisplay name (e.g., "Markets", "Strategy")
slugstringURL-friendly identifier
sort_ordernumberControls display order in nav
descriptionstringSubtitle shown when filtering by this category
articles collection:
FieldTypePurpose
article_idstringURL-friendly slug, used in hash routing
titlestringHeadline
categorystringMust match a categories.name value
datestringHuman-readable date
authorstringByline
summarystringShort deck for cards and hero
contentstringFull article body as HTML
imagestringCDN URL from Page Assets
imageAltstringAlt text for accessibility
sort_ordernumberControls display order
is_leadbooleanIf 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:
FieldInput TypeNotes
TitletextAuto-generates slug
Article ID / SlugtextAuto-populated, manually overridable
CategoryselectPopulated from CMS categories
DatetextPre-filled with current date
Authortext
Summarytextarea
Content (HTML)textareaMonospace, tall
Image URLtextPaste CDN URL from Page Assets
Sort OrdernumberPre-filled with articles.length + 1
Lead / HerocheckboxOnly 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:

  1. Create a Visimade page (via the platform UI or API)
  2. Define your categories — seed the categories CMS collection via API
  3. Build the HTML file locally using the skeleton and patterns above
  4. Push the HTML to the page via PATCH /api/pages/:id
  5. Upload images via POST /api/pages/:id/assets, save the CDN URLs
  6. Seed articles via POST /api/pages/:id/cms-data/articles
  7. Visit the page — the JS loads CMS data and renders everything
  8. 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.

← Back to API Reference