VELO/Docs
Dynamic Sanity Fetching

The Blog Engine

Velo uses a highly secure, multi-tenant Headless CMS approach. Every client (tenant) has their own completely isolated Sanity database.

CRITICAL SECURITY RULE

The frontend must NEVER hardcode a projectId in sanity.config.ts or similar files. The connection must be strictly dynamic.

Implementation Steps

Step 1: Identify the Tenant

The frontend must look up the tenant's configuration from the Velo database. Determine the slug from the hostname or URL.

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL, 
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
);

const { data: org } = await supabase
  .from('organizations')
  .select('sanity_project_id, sanity_dataset')
  .eq('slug', tenantSlug) // Dynamically determined
  .single();

Step 2: Dynamically Fetch Content

Use the sanity_project_id to initialize the client on the fly.

import { createClient } from '@sanity/client'

const sanityClient = createClient({
  projectId: org.sanity_project_id, 
  dataset: org.sanity_dataset || 'production',
  apiVersion: '2024-05-15',
  useCdn: true,
});

const query = `*[_type == "post" && status == "published"] | order(publishedAt desc) {
  _id,
  title,
  "slug": slug.current,
  mainImage { asset->{ url } },
  publishedAt,
  body, // TipTap JSON format
  seo
}`;

const posts = await sanityClient.fetch(query);

Rendering TipTap JSON

Velo stores body content as stringified TipTap JSON. You must parse and render it using TipTap extensions.

import { generateHTML } from '@tiptap/html'
import StarterKit from '@tiptap/starter-kit'

// 1. Parse the JSON
const jsonContent = JSON.parse(post.body);

// 2. Generate HTML (or use a TipTap component)
const html = generateHTML(jsonContent, [StarterKit]);

// 3. Render
<div dangerouslySetInnerHTML={{ __html: html }} className="prose" />

AI Agent Prompt

"Integrate the Velo Blog Engine into this [Next.js/React] project. First, look up the tenant configuration from Supabase using the 'organizations' table and the current hostname to get the 'sanity_project_id' and 'sanity_dataset'. Then, fetch all 'published' posts from Sanity. Finally, render the 'body' field by parsing it as TipTap JSON and using a compatible renderer like '@tiptap/html'. Ensure the UI is responsive and follows a [Modern/Minimalist] aesthetic."

Notes for AI Agents

No Portable Text

Velo does NOT use standard Sanity Portable Text. Ignore instructions about @portabletext/react. Use TipTap utilities instead.

Multi-Tenancy Guard

The projectId is different for every customer. Always fetch it dynamically from the organizations table in Supabase.

Security

Never expose the sanity_write_token on the frontend. Use it only in server-side dashboard actions.