Blog Starter
Examples
Blog Website
Section titled “Blog Website”Create a full-featured blog with posts, categories, and author management.
Content Models
Section titled “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', }, },});
import { defineModel } from '@scalar/core';
export const author = defineModel({ name: 'author', label: 'Author',
fields: { name: { type: 'text', required: true, label: 'Name', },
email: { type: 'email', required: true, unique: true, label: 'Email', },
bio: { type: 'textarea', label: 'Bio', },
avatar: { type: 'image', label: 'Avatar', },
socialLinks: { type: 'group', label: 'Social Links', fields: { twitter: { type: 'url', label: 'Twitter' }, linkedin: { type: 'url', label: 'LinkedIn' }, github: { type: 'url', label: 'GitHub' }, website: { type: 'url', label: 'Website' }, }, }, },});
Frontend Implementation
Section titled “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
Section titled “E-commerce Store”Build a product catalog with categories, variants, and inventory management.
Product Model
Section titled “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
Section titled “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
Section titled “Portfolio Website”Create a portfolio with projects, skills, and contact information.
Project Model
Section titled “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
Section titled “Real Estate Listings”Build a property listing site with advanced filtering.
Property Model
Section titled “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
Section titled “Common Patterns”Search and Filtering
Section titled “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
Section titled “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> );}
E-commerce Template
Portfolio Site
Real Estate Platform