Guides & Tutorials
Examples
Real-world examples and use cases for Scalar CMS
Blog Website
Create a full-featured blog with posts, categories, and author management.
Content Models
import { defineModel } from '@scalar/core';
export const blogPost = defineModel({
name: 'blogPost',
label: 'Blog Post',
fields: {
title: {
type: 'text',
required: true,
label: 'Title',
},
slug: {
type: 'slug',
source: 'title',
required: true,
unique: true,
},
content: {
type: 'richtext',
required: true,
label: 'Content',
},
excerpt: {
type: 'textarea',
label: 'Excerpt',
maxLength: 200,
},
featuredImage: {
type: 'image',
label: 'Featured Image',
},
author: {
type: 'relationship',
model: 'author',
required: true,
label: 'Author',
},
categories: {
type: 'relationship',
model: 'category',
multiple: true,
label: 'Categories',
},
tags: {
type: 'array',
of: 'text',
label: 'Tags',
},
publishedAt: {
type: 'datetime',
label: 'Published At',
},
status: {
type: 'select',
options: ['draft', 'published', 'archived'],
defaultValue: 'draft',
label: 'Status',
},
seo: {
type: 'group',
label: 'SEO',
fields: {
metaTitle: { type: 'text', label: 'Meta Title' },
metaDescription: { type: 'textarea', label: 'Meta Description' },
ogImage: { type: 'image', label: 'OG Image' },
},
},
},
});
import { defineModel } from '@scalar/core';
export const category = defineModel({
name: 'category',
label: 'Category',
fields: {
name: {
type: 'text',
required: true,
label: 'Name',
},
slug: {
type: 'slug',
source: 'name',
required: true,
unique: true,
},
description: {
type: 'textarea',
label: 'Description',
},
color: {
type: 'color',
label: 'Color',
defaultValue: '#3b82f6',
},
parent: {
type: 'relationship',
model: 'category',
label: 'Parent Category',
},
},
});
Frontend Implementation
import { scalar } from '@/lib/scalar';
import { BlogCard } from '@/components/blog-card';
export default async function BlogPage() {
const posts = await scalar.content.findMany({
model: 'blogPost',
filter: { status: 'published' },
populate: ['author', 'categories', 'featuredImage'],
sort: '-publishedAt',
limit: 12,
});
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-8">Blog</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{posts.map((post) => (
<BlogCard key={post.id} post={post} />
))}
</div>
</div>
);
}
import Link from 'next/link';
import Image from 'next/image';
import { formatDate } from '@/lib/utils';
interface BlogCardProps {
post: {
id: string;
title: string;
slug: string;
excerpt: string;
publishedAt: string;
featuredImage?: { url: string; alt: string };
author: { name: string; avatar?: { url: string } };
categories: { name: string; color: string }[];
};
}
export function BlogCard({ post }: BlogCardProps) {
return (
<article className="bg-white rounded-lg shadow-md overflow-hidden">
{post.featuredImage && (
<Link href={`/blog/${post.slug}`}>
<div className="aspect-video relative">
<Image
src={post.featuredImage.url}
alt={post.featuredImage.alt || post.title}
fill
className="object-cover"
/>
</div>
</Link>
)}
<div className="p-6">
<div className="flex flex-wrap gap-2 mb-3">
{post.categories.map((category) => (
<span
key={category.name}
className="px-2 py-1 text-xs rounded-full text-white"
style={{ backgroundColor: category.color }}
>
{category.name}
</span>
))}
</div>
<h2 className="text-xl font-semibold mb-2">
<Link href={`/blog/${post.slug}`} className="hover:text-blue-600">
{post.title}
</Link>
</h2>
<p className="text-gray-600 mb-4">{post.excerpt}</p>
<div className="flex items-center justify-between text-sm text-gray-500">
<div className="flex items-center gap-2">
{post.author.avatar && (
<Image
src={post.author.avatar.url}
alt={post.author.name}
width={24}
height={24}
className="rounded-full"
/>
)}
<span>{post.author.name}</span>
</div>
<time>{formatDate(post.publishedAt)}</time>
</div>
</div>
</article>
);
}
<template>
<div class="container mx-auto px-4 py-8">
<h1 class="text-4xl font-bold mb-8">Blog</h1>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<BlogCard
v-for="post in posts"
:key="post.id"
:post="post"
/>
</div>
</div>
</template>
<script setup>
const { $scalar } = useNuxtApp();
const { data: posts } = await $scalar.content.findMany({
model: 'blogPost',
filter: { status: 'published' },
populate: ['author', 'categories', 'featuredImage'],
sort: '-publishedAt',
limit: 12,
});
useSeoMeta({
title: 'Blog',
description: 'Read our latest blog posts and insights',
});
</script>
import { scalar } from '$lib/scalar';
export async function load() {
const posts = await scalar.content.findMany({
model: 'blogPost',
filter: { status: 'published' },
populate: ['author', 'categories', 'featuredImage'],
sort: '-publishedAt',
limit: 12,
});
return { posts };
}
<script>
import BlogCard from '$lib/components/BlogCard.svelte';
export let data;
</script>
<div class="container mx-auto px-4 py-8">
<h1 class="text-4xl font-bold mb-8">Blog</h1>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{#each data.posts as post}
<BlogCard {post} />
{/each}
</div>
</div>
E-commerce Store
Build a product catalog with categories, variants, and inventory management.
Product Model
import { defineModel } from '@scalar/core';
export const product = defineModel({
name: 'product',
label: 'Product',
fields: {
name: {
type: 'text',
required: true,
label: 'Product Name',
},
slug: {
type: 'slug',
source: 'name',
required: true,
unique: true,
},
description: {
type: 'richtext',
label: 'Description',
},
shortDescription: {
type: 'textarea',
label: 'Short Description',
maxLength: 160,
},
images: {
type: 'array',
of: 'image',
label: 'Product Images',
minItems: 1,
},
price: {
type: 'number',
required: true,
min: 0,
label: 'Price',
decimals: 2,
},
comparePrice: {
type: 'number',
min: 0,
label: 'Compare at Price',
decimals: 2,
},
sku: {
type: 'text',
unique: true,
label: 'SKU',
},
inventory: {
type: 'group',
label: 'Inventory',
fields: {
trackQuantity: {
type: 'boolean',
defaultValue: true,
label: 'Track Quantity',
},
quantity: {
type: 'number',
min: 0,
label: 'Quantity',
},
lowStockThreshold: {
type: 'number',
min: 0,
defaultValue: 10,
label: 'Low Stock Threshold',
},
},
},
categories: {
type: 'relationship',
model: 'productCategory',
multiple: true,
label: 'Categories',
},
tags: {
type: 'array',
of: 'text',
label: 'Tags',
},
variants: {
type: 'array',
of: 'group',
label: 'Variants',
fields: {
name: { type: 'text', required: true },
sku: { type: 'text', unique: true },
price: { type: 'number', min: 0, decimals: 2 },
inventory: { type: 'number', min: 0 },
options: {
type: 'json',
label: 'Options',
// e.g., { "color": "red", "size": "large" }
},
},
},
status: {
type: 'select',
options: ['draft', 'active', 'archived'],
defaultValue: 'draft',
label: 'Status',
},
seo: {
type: 'group',
label: 'SEO',
fields: {
metaTitle: { type: 'text', label: 'Meta Title' },
metaDescription: { type: 'textarea', label: 'Meta Description' },
ogImage: { type: 'image', label: 'OG Image' },
},
},
},
hooks: {
beforeCreate: async (data) => {
if (!data.sku) {
data.sku = generateSKU(data.name);
}
},
afterUpdate: async (record, changes) => {
if (changes.inventory?.quantity !== undefined) {
await checkLowStock(record);
}
},
},
});
Shopping Cart Implementation
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { useCart } from '@/hooks/use-cart';
interface AddToCartProps {
product: {
id: string;
name: string;
price: number;
images: { url: string }[];
variants?: Array<{
id: string;
name: string;
price: number;
options: Record<string, string>;
}>;
};
}
export function AddToCart({ product }: AddToCartProps) {
const [selectedVariant, setSelectedVariant] = useState(
product.variants?.[0] || null,
);
const [quantity, setQuantity] = useState(1);
const [loading, setLoading] = useState(false);
const { addItem } = useCart();
const handleAddToCart = async () => {
setLoading(true);
try {
await addItem({
productId: product.id,
variantId: selectedVariant?.id,
quantity,
price: selectedVariant?.price || product.price,
name: selectedVariant
? `${product.name} - ${selectedVariant.name}`
: product.name,
image: product.images[0]?.url,
});
} catch (error) {
console.error('Failed to add to cart:', error);
} finally {
setLoading(false);
}
};
return (
<div className="space-y-4">
{product.variants && (
<div>
<label className="mb-2 block text-sm font-medium">
Select Variant
</label>
<select
value={selectedVariant?.id || ''}
onChange={(e) => {
const variant = product.variants?.find(
(v) => v.id === e.target.value,
);
setSelectedVariant(variant || null);
}}
className="w-full rounded border p-2"
>
{product.variants.map((variant) => (
<option key={variant.id} value={variant.id}>
{variant.name} - ${variant.price}
</option>
))}
</select>
</div>
)}
<div>
<label className="mb-2 block text-sm font-medium">Quantity</label>
<input
type="number"
min="1"
value={quantity}
onChange={(e) => setQuantity(parseInt(e.target.value))}
className="w-20 rounded border p-2"
/>
</div>
<Button onClick={handleAddToCart} disabled={loading} className="w-full">
{loading ? 'Adding...' : 'Add to Cart'}
</Button>
</div>
);
}
Portfolio Website
Create a portfolio with projects, skills, and contact information.
Project Model
export const project = defineModel({
name: 'project',
label: 'Project',
fields: {
title: {
type: 'text',
required: true,
label: 'Project Title',
},
slug: {
type: 'slug',
source: 'title',
required: true,
unique: true,
},
description: {
type: 'richtext',
required: true,
label: 'Description',
},
summary: {
type: 'textarea',
label: 'Summary',
maxLength: 200,
},
images: {
type: 'array',
of: 'image',
label: 'Project Images',
minItems: 1,
},
technologies: {
type: 'array',
of: 'text',
label: 'Technologies Used',
},
links: {
type: 'group',
label: 'Links',
fields: {
live: { type: 'url', label: 'Live Demo' },
github: { type: 'url', label: 'GitHub Repository' },
case_study: { type: 'url', label: 'Case Study' },
},
},
featured: {
type: 'boolean',
defaultValue: false,
label: 'Featured Project',
},
completedAt: {
type: 'date',
label: 'Completion Date',
},
client: {
type: 'text',
label: 'Client',
},
category: {
type: 'select',
options: ['web', 'mobile', 'desktop', 'api', 'design'],
label: 'Category',
},
},
});
Real Estate Listings
Build a property listing site with advanced filtering.
Property Model
export const property = defineModel({
name: 'property',
label: 'Property',
fields: {
title: {
type: 'text',
required: true,
label: 'Property Title',
},
address: {
type: 'group',
required: true,
label: 'Address',
fields: {
street: { type: 'text', required: true, label: 'Street Address' },
city: { type: 'text', required: true, label: 'City' },
state: { type: 'text', required: true, label: 'State' },
zipCode: { type: 'text', required: true, label: 'ZIP Code' },
country: { type: 'text', defaultValue: 'USA', label: 'Country' },
},
},
location: {
type: 'geo',
required: true,
label: 'Map Location',
},
price: {
type: 'number',
required: true,
min: 0,
label: 'Price',
decimals: 0,
},
propertyType: {
type: 'select',
required: true,
options: ['house', 'apartment', 'condo', 'townhouse', 'land'],
label: 'Property Type',
},
listingType: {
type: 'select',
required: true,
options: ['sale', 'rent'],
label: 'Listing Type',
},
bedrooms: {
type: 'number',
min: 0,
label: 'Bedrooms',
},
bathrooms: {
type: 'number',
min: 0,
decimals: 1,
label: 'Bathrooms',
},
squareFootage: {
type: 'number',
min: 0,
label: 'Square Footage',
},
lotSize: {
type: 'number',
min: 0,
label: 'Lot Size (sq ft)',
},
yearBuilt: {
type: 'number',
min: 1800,
max: new Date().getFullYear() + 1,
label: 'Year Built',
},
features: {
type: 'array',
of: 'text',
label: 'Features',
},
images: {
type: 'array',
of: 'image',
label: 'Property Images',
minItems: 1,
},
virtualTour: {
type: 'url',
label: 'Virtual Tour URL',
},
agent: {
type: 'relationship',
model: 'agent',
required: true,
label: 'Listing Agent',
},
status: {
type: 'select',
options: ['active', 'pending', 'sold', 'withdrawn'],
defaultValue: 'active',
label: 'Listing Status',
},
},
});
Common Patterns
Search and Filtering
'use client';
import { useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
export function PropertySearch() {
const router = useRouter();
const searchParams = useSearchParams();
const [filters, setFilters] = useState({
propertyType: searchParams.get('propertyType') || '',
minPrice: searchParams.get('minPrice') || '',
maxPrice: searchParams.get('maxPrice') || '',
bedrooms: searchParams.get('bedrooms') || '',
bathrooms: searchParams.get('bathrooms') || '',
city: searchParams.get('city') || '',
});
const handleSearch = () => {
const params = new URLSearchParams();
Object.entries(filters).forEach(([key, value]) => {
if (value) {
params.set(key, value);
}
});
router.push(`/properties?${params.toString()}`);
};
return (
<div className="rounded-lg bg-white p-6 shadow-md">
<div className="grid grid-cols-1 gap-4 md:grid-cols-3 lg:grid-cols-6">
<select
value={filters.propertyType}
onChange={(e) =>
setFilters((prev) => ({ ...prev, propertyType: e.target.value }))
}
className="rounded border p-2"
>
<option value="">All Types</option>
<option value="house">House</option>
<option value="apartment">Apartment</option>
<option value="condo">Condo</option>
</select>
<input
type="number"
placeholder="Min Price"
value={filters.minPrice}
onChange={(e) =>
setFilters((prev) => ({ ...prev, minPrice: e.target.value }))
}
className="rounded border p-2"
/>
<input
type="number"
placeholder="Max Price"
value={filters.maxPrice}
onChange={(e) =>
setFilters((prev) => ({ ...prev, maxPrice: e.target.value }))
}
className="rounded border p-2"
/>
<select
value={filters.bedrooms}
onChange={(e) =>
setFilters((prev) => ({ ...prev, bedrooms: e.target.value }))
}
className="rounded border p-2"
>
<option value="">Bedrooms</option>
<option value="1">1+</option>
<option value="2">2+</option>
<option value="3">3+</option>
<option value="4">4+</option>
</select>
<input
type="text"
placeholder="City"
value={filters.city}
onChange={(e) =>
setFilters((prev) => ({ ...prev, city: e.target.value }))
}
className="rounded border p-2"
/>
<button
onClick={handleSearch}
className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
Search
</button>
</div>
</div>
);
}
Pagination Component
'use client';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
interface PaginationProps {
currentPage: number;
totalPages: number;
baseUrl: string;
}
export function Pagination({
currentPage,
totalPages,
baseUrl,
}: PaginationProps) {
const searchParams = useSearchParams();
const createPageUrl = (page: number) => {
const params = new URLSearchParams(searchParams.toString());
params.set('page', page.toString());
return `${baseUrl}?${params.toString()}`;
};
return (
<nav className="flex justify-center space-x-2">
{currentPage > 1 && (
<Link
href={createPageUrl(currentPage - 1)}
className="rounded bg-gray-200 px-3 py-2 hover:bg-gray-300"
>
Previous
</Link>
)}
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<Link
key={page}
href={createPageUrl(page)}
className={`rounded px-3 py-2 ${
page === currentPage
? 'bg-blue-600 text-white'
: 'bg-gray-200 hover:bg-gray-300'
}`}
>
{page}
</Link>
))}
{currentPage < totalPages && (
<Link
href={createPageUrl(currentPage + 1)}
className="rounded bg-gray-200 px-3 py-2 hover:bg-gray-300"
>
Next
</Link>
)}
</nav>
);
}
Blog Starter
Complete blog implementation with posts, categories, and SEO
E-commerce Template
Product catalog with variants, cart, and checkout
Portfolio Site
Personal portfolio with projects and contact forms
Real Estate Platform
Property listings with search and filtering
All examples are available as starter templates. Use npx scalar create --template [template-name]
to get started quickly.