Guides & Tutorials
Tutorials
Step-by-step tutorials for common scenarios
Tutorial Categories
Content Management
Building blogs, portfolios, and content sites
E-commerce
Product catalogs and online stores
User Management
Authentication and user profiles
Advanced Features
Search, analytics, and integrations
Content Management
Building a Multi-Author Blog
Create a blog with multiple authors, categories, and advanced features.
Set up the content models
export const author = defineModel({
name: 'author',
label: 'Author',
fields: {
name: {
type: 'text',
required: true,
label: 'Full Name',
},
email: {
type: 'email',
required: true,
unique: true,
},
bio: {
type: 'textarea',
label: 'Biography',
maxLength: 500,
},
avatar: {
type: 'image',
label: 'Profile Picture',
aspectRatio: '1:1',
},
socialLinks: {
type: 'group',
label: 'Social Media',
fields: {
twitter: { type: 'url', label: 'Twitter' },
linkedin: { type: 'url', label: 'LinkedIn' },
website: { type: 'url', label: 'Website' },
},
},
isActive: {
type: 'boolean',
defaultValue: true,
label: 'Active Author',
},
},
admin: {
listView: {
fields: ['name', 'email', 'isActive'],
},
},
});
export const category = defineModel({
name: 'category',
label: 'Category',
fields: {
name: {
type: 'text',
required: true,
label: 'Category Name',
},
slug: {
type: 'slug',
source: 'name',
required: true,
unique: true,
},
description: {
type: 'textarea',
label: 'Description',
},
color: {
type: 'color',
label: 'Brand Color',
defaultValue: '#3b82f6',
},
parent: {
type: 'relationship',
model: 'category',
label: 'Parent Category',
},
image: {
type: 'image',
label: 'Category Image',
},
},
});
Create the blog post model
export const blogPost = defineModel({
name: 'blogPost',
label: 'Blog Post',
fields: {
title: {
type: 'text',
required: true,
label: 'Title',
validation: {
minLength: 10,
maxLength: 100,
},
},
slug: {
type: 'slug',
source: 'title',
required: true,
unique: true,
},
excerpt: {
type: 'textarea',
label: 'Excerpt',
maxLength: 200,
description: 'Brief summary for previews',
},
content: {
type: 'richtext',
required: true,
label: 'Content',
toolbar: [
'bold', 'italic', 'underline', 'strikethrough',
'heading', 'blockquote', 'codeBlock',
'bulletList', 'orderedList',
'link', 'image', 'horizontalRule'
],
},
featuredImage: {
type: 'image',
label: 'Featured Image',
aspectRatio: '16:9',
},
author: {
type: 'relationship',
model: 'author',
required: true,
label: 'Author',
filter: { isActive: true },
},
categories: {
type: 'relationship',
model: 'category',
multiple: true,
label: 'Categories',
validation: {
minItems: 1,
maxItems: 3,
},
},
tags: {
type: 'array',
of: 'text',
label: 'Tags',
description: 'Keywords for SEO and filtering',
},
status: {
type: 'select',
options: [
{ value: 'draft', label: 'Draft' },
{ value: 'review', label: 'Under Review' },
{ value: 'published', label: 'Published' },
{ value: 'archived', label: 'Archived' },
],
defaultValue: 'draft',
label: 'Status',
},
publishedAt: {
type: 'datetime',
label: 'Published At',
conditional: {
when: 'status',
is: 'published',
},
},
featured: {
type: 'boolean',
defaultValue: false,
label: 'Featured Post',
},
readingTime: {
type: 'number',
label: 'Reading Time (minutes)',
min: 1,
computed: true,
},
seo: {
type: 'group',
label: 'SEO Settings',
collapsible: true,
fields: {
metaTitle: {
type: 'text',
label: 'Meta Title',
maxLength: 60,
},
metaDescription: {
type: 'textarea',
label: 'Meta Description',
maxLength: 160,
},
ogImage: {
type: 'image',
label: 'Social Media Image',
},
noIndex: {
type: 'boolean',
label: 'Hide from Search Engines',
defaultValue: false,
},
},
},
},
hooks: {
beforeSave: async (data) => {
// Auto-calculate reading time
if (data.content) {
const wordsPerMinute = 200;
const wordCount = data.content.split(/\s+/).length;
data.readingTime = Math.ceil(wordCount / wordsPerMinute);
}
// Auto-generate excerpt if not provided
if (!data.excerpt && data.content) {
data.excerpt = stripHtml(data.content).substring(0, 160) + '...';
}
// Set published date when status changes to published
if (data.status === 'published' && !data.publishedAt) {
data.publishedAt = new Date();
}
},
afterSave: async (record) => {
// Clear cache when post is updated
await clearCache(`blog:${record.slug}`);
// Send notification for new published posts
if (record.status === 'published' && record.publishedAt) {
await notifySubscribers(record);
}
},
},
admin: {
defaultSort: '-createdAt',
searchFields: ['title', 'excerpt', 'content'],
filterFields: ['status', 'author', 'categories', 'featured'],
listView: {
fields: ['title', 'author', 'status', 'publishedAt', 'featured'],
},
},
});
Build the frontend pages
import { scalar } from '@/lib/scalar';
import { BlogCard } from '@/components/blog-card';
import { CategoryFilter } from '@/components/category-filter';
import { FeaturedPosts } from '@/components/featured-posts';
import { Pagination } from '@/components/pagination';
interface BlogPageProps {
searchParams: {
page?: string;
category?: string;
author?: string;
tag?: string;
};
}
export default async function BlogPage({ searchParams }: BlogPageProps) {
const currentPage = parseInt(searchParams.page || '1');
const pageSize = 12;
// Build filter conditions
const filter: any = { status: 'published' };
if (searchParams.category) {
filter['categories.slug'] = searchParams.category;
}
if (searchParams.author) {
filter['author.slug'] = searchParams.author;
}
if (searchParams.tag) {
filter.tags = { $contains: searchParams.tag };
}
// Fetch posts and metadata
const [posts, categories, authors, featuredPosts] = await Promise.all([
scalar.content.findMany({
model: 'blogPost',
filter,
populate: ['author', 'categories', 'featuredImage'],
sort: '-publishedAt',
limit: pageSize,
offset: (currentPage - 1) * pageSize,
}),
scalar.content.findMany({
model: 'category',
sort: 'name',
}),
scalar.content.findMany({
model: 'author',
filter: { isActive: true },
sort: 'name',
}),
scalar.content.findMany({
model: 'blogPost',
filter: { status: 'published', featured: true },
populate: ['author', 'categories', 'featuredImage'],
sort: '-publishedAt',
limit: 3,
}),
]);
return (
<div className="container mx-auto px-4 py-8">
{currentPage === 1 && featuredPosts.length > 0 && (
<FeaturedPosts posts={featuredPosts} />
)}
<div className="flex flex-col lg:flex-row gap-8">
<aside className="lg:w-64">
<CategoryFilter
categories={categories}
authors={authors}
selectedCategory={searchParams.category}
selectedAuthor={searchParams.author}
/>
</aside>
<main className="flex-1">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12">
{posts.data.map((post) => (
<BlogCard key={post.id} post={post} />
))}
</div>
{posts.data.length === 0 && (
<div className="text-center py-12">
<p className="text-gray-500 text-lg">No posts found</p>
</div>
)}
<Pagination
currentPage={currentPage}
totalPages={Math.ceil(posts.meta.total / pageSize)}
baseUrl="/blog"
/>
</main>
</div>
</div>
);
}
Add search functionality
'use client';
import { useState, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { SearchIcon } from 'lucide-react';
import { scalar } from '@/lib/scalar';
export function BlogSearch() {
const router = useRouter();
const searchParams = useSearchParams();
const [query, setQuery] = useState(searchParams.get('q') || '');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const handleSearch = async (searchQuery: string) => {
if (!searchQuery.trim()) {
setResults([]);
return;
}
setLoading(true);
try {
const searchResults = await scalar.content.findMany({
model: 'blogPost',
filter: {
status: 'published',
$or: [
{ title: { $contains: searchQuery } },
{ excerpt: { $contains: searchQuery } },
{ content: { $contains: searchQuery } },
{ tags: { $contains: searchQuery } },
],
},
populate: ['author', 'categories'],
limit: 10,
sort: '-publishedAt',
});
setResults(searchResults.data);
} catch (error) {
console.error('Search error:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
const debounced = setTimeout(() => {
handleSearch(query);
}, 300);
return () => clearTimeout(debounced);
}, [query]);
return (
<div className="relative">
<div className="relative">
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search posts..."
className="w-full pl-10 pr-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
/>
<SearchIcon className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" />
</div>
{(query && results.length > 0) && (
<div className="absolute top-full left-0 right-0 bg-white border rounded-lg shadow-lg mt-1 z-50">
{results.map((post) => (
<div
key={post.id}
className="p-3 hover:bg-gray-50 border-b last:border-b-0 cursor-pointer"
onClick={() => router.push(`/blog/${post.slug}`)}
>
<h4 className="font-medium text-sm">{post.title}</h4>
<p className="text-xs text-gray-500 mt-1">
by {post.author.name} • {post.categories[0]?.name}
</p>
</div>
))}
</div>
)}
{loading && (
<div className="absolute top-full left-0 right-0 bg-white border rounded-lg shadow-lg mt-1 p-3">
<p className="text-sm text-gray-500">Searching...</p>
</div>
)}
</div>
);
}
Building a Portfolio Site
Create a portfolio to showcase projects and skills.
Define the 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: 'Project Description',
},
summary: {
type: 'textarea',
label: 'Short Summary',
maxLength: 200,
},
images: {
type: 'array',
of: 'image',
label: 'Project Screenshots',
minItems: 1,
maxItems: 10,
},
technologies: {
type: 'relationship',
model: 'technology',
multiple: true,
label: 'Technologies Used',
},
links: {
type: 'group',
label: 'Project Links',
fields: {
live: { type: 'url', label: 'Live Demo' },
github: { type: 'url', label: 'GitHub Repository' },
figma: { type: 'url', label: 'Design Files' },
},
},
status: {
type: 'select',
options: [
{ value: 'completed', label: 'Completed' },
{ value: 'in-progress', label: 'In Progress' },
{ value: 'on-hold', label: 'On Hold' },
],
label: 'Project Status',
},
featured: {
type: 'boolean',
defaultValue: false,
label: 'Featured Project',
},
startDate: {
type: 'date',
label: 'Start Date',
},
endDate: {
type: 'date',
label: 'End Date',
},
client: {
type: 'text',
label: 'Client/Company',
},
category: {
type: 'select',
options: [
{ value: 'web', label: 'Web Development' },
{ value: 'mobile', label: 'Mobile App' },
{ value: 'design', label: 'UI/UX Design' },
{ value: 'api', label: 'API Development' },
],
label: 'Project Category',
},
},
});
Create technology and skill models
export const technology = defineModel({
name: 'technology',
label: 'Technology',
fields: {
name: {
type: 'text',
required: true,
unique: true,
label: 'Technology Name',
},
icon: {
type: 'image',
label: 'Technology Icon',
aspectRatio: '1:1',
},
color: {
type: 'color',
label: 'Brand Color',
},
category: {
type: 'select',
options: [
{ value: 'frontend', label: 'Frontend' },
{ value: 'backend', label: 'Backend' },
{ value: 'database', label: 'Database' },
{ value: 'tool', label: 'Tool' },
{ value: 'framework', label: 'Framework' },
],
label: 'Category',
},
proficiency: {
type: 'select',
options: [
{ value: 'beginner', label: 'Beginner' },
{ value: 'intermediate', label: 'Intermediate' },
{ value: 'advanced', label: 'Advanced' },
{ value: 'expert', label: 'Expert' },
],
label: 'Proficiency Level',
},
},
});
E-commerce
Building a Product Recommendation System
Implement smart product recommendations based on user behavior.
Track user interactions
import { scalar } from '@/lib/scalar';
export async function trackProductView(productId: string, userId?: string) {
await scalar.content.create({
model: 'productView',
data: {
productId,
userId,
timestamp: new Date(),
sessionId: getSessionId(),
},
});
}
export async function trackPurchase(productIds: string[], userId?: string) {
await scalar.content.create({
model: 'purchase',
data: {
productIds,
userId,
timestamp: new Date(),
sessionId: getSessionId(),
},
});
}
Generate recommendations
export async function getRecommendations(productId: string, limit = 4) {
// Find products frequently viewed together
const relatedViews = await scalar.content.findMany({
model: 'productView',
filter: {
sessionId: { $in: await getSessionsWithProduct(productId) },
productId: { $ne: productId },
},
aggregate: [
{
$group: {
_id: '$productId',
count: { $sum: 1 },
},
},
{ $sort: { count: -1 } },
{ $limit: limit },
],
});
const recommendedIds = relatedViews.map(view => view._id);
return scalar.content.findMany({
model: 'product',
filter: { id: { $in: recommendedIds } },
populate: ['images', 'categories'],
});
}
User Management
Building a User Profile System
Create comprehensive user profiles with preferences and activity tracking.
Design the user profile model
export const userProfile = defineModel({
name: 'userProfile',
label: 'User Profile',
fields: {
user: {
type: 'relationship',
model: 'user',
required: true,
unique: true,
label: 'User Account',
},
displayName: {
type: 'text',
label: 'Display Name',
},
avatar: {
type: 'image',
label: 'Profile Picture',
aspectRatio: '1:1',
},
bio: {
type: 'textarea',
label: 'Biography',
maxLength: 500,
},
location: {
type: 'group',
label: 'Location',
fields: {
city: { type: 'text', label: 'City' },
country: { type: 'text', label: 'Country' },
timezone: { type: 'text', label: 'Timezone' },
},
},
preferences: {
type: 'group',
label: 'Preferences',
fields: {
theme: {
type: 'select',
options: ['light', 'dark', 'auto'],
defaultValue: 'auto',
label: 'Theme',
},
language: {
type: 'select',
options: ['en', 'es', 'fr', 'de'],
defaultValue: 'en',
label: 'Language',
},
emailNotifications: {
type: 'boolean',
defaultValue: true,
label: 'Email Notifications',
},
},
},
socialLinks: {
type: 'array',
of: 'group',
label: 'Social Links',
fields: {
platform: {
type: 'select',
options: ['twitter', 'linkedin', 'github', 'website'],
label: 'Platform',
},
url: { type: 'url', label: 'URL' },
},
},
interests: {
type: 'relationship',
model: 'interest',
multiple: true,
label: 'Interests',
},
},
});
Advanced Features
Implementing Full-Text Search
Add powerful search capabilities to your application.
Set up search indexing
import { Client } from '@elastic/elasticsearch';
const client = new Client({
node: process.env.ELASTICSEARCH_URL,
});
export async function indexContent(model: string, data: any) {
await client.index({
index: 'content',
id: `${model}-${data.id}`,
body: {
model,
title: data.title,
content: data.content || data.description,
slug: data.slug,
status: data.status,
publishedAt: data.publishedAt,
tags: data.tags || [],
categories: data.categories?.map(c => c.name) || [],
},
});
}
export async function searchContent(query: string, filters = {}) {
const body = {
query: {
bool: {
must: [
{
multi_match: {
query,
fields: ['title^3', 'content', 'tags^2'],
fuzziness: 'AUTO',
},
},
],
filter: Object.entries(filters).map(([key, value]) => ({
term: { [key]: value },
})),
},
},
highlight: {
fields: {
title: {},
content: {
fragment_size: 150,
number_of_fragments: 3,
},
},
},
};
const response = await client.search({
index: 'content',
body,
});
return response.body.hits.hits.map(hit => ({
id: hit._id,
score: hit._score,
data: hit._source,
highlights: hit.highlight,
}));
}
These tutorials provide practical examples for common use cases. Adapt them to your specific requirements and extend them with additional features as needed.