Content Website
Build a blog or content site with the CMS module
This guide shows how to build a content-driven website using the Arky CMS module.
Overview
You’ll learn how to:
- Create and manage blog posts
- Build dynamic pages with blocks
- Implement content categories and tags
- Add search functionality
Content Structure
Arky CMS uses nodes as the core content unit. Each node has:
- Type (blog post, page, etc.)
- Title and slug
- Blocks (structured content)
- Metadata
Fetching Content
Get All Posts
async function getBlogPosts(options?: {
category?: string;
tag?: string;
cursor?: string;
}) {
const result = await sdk.cms.getNodes({
businessId: sdk.config.businessId,
type: 'BLOG_POST',
status: 'PUBLISHED',
tag: options?.tag,
sortBy: 'createdAt',
sortOrder: 'desc',
cursor: options?.cursor,
limit: 10
});
if (result.ok) {
return {
posts: result.val.items,
nextCursor: result.val.cursor
};
}
return { posts: [], nextCursor: null };
}
Get Single Post
async function getPost(slug: string) {
const result = await sdk.cms.getNode({
businessId: sdk.config.businessId,
slug
});
return result.ok ? result.val : null;
}
Get Page by Key
Use keys for unique pages like homepage:
async function getHomepage() {
const result = await sdk.cms.getNode({
businessId: sdk.config.businessId,
key: 'homepage'
});
return result.ok ? result.val : null;
}
Blog Listing Page
function BlogPage() {
const [posts, setPosts] = useState([]);
const [cursor, setCursor] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
getBlogPosts().then(({ posts, nextCursor }) => {
setPosts(posts);
setCursor(nextCursor);
setLoading(false);
});
}, []);
const loadMore = async () => {
const { posts: more, nextCursor } = await getBlogPosts({ cursor });
setPosts([...posts, ...more]);
setCursor(nextCursor);
};
if (loading) return <div>Loading...</div>;
return (
<div className="blog-page">
<h1>Blog</h1>
<div className="posts-grid">
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
{cursor && (
<button onClick={loadMore}>Load More</button>
)}
</div>
);
}
function PostCard({ post }) {
const heroBlock = post.blocks.find(b => b.type === 'HERO');
const heroImage = heroBlock?.content?.image;
return (
<a href={`/blog/${post.slug}`} className="post-card">
{heroImage && (
<img src={`${heroImage}?w=400&h=250&fit=cover`} alt={post.title} />
)}
<div className="content">
<h2>{post.title}</h2>
<p className="excerpt">
{post.metadata?.excerpt || getExcerpt(post.blocks)}
</p>
<time>{formatDate(post.createdAt)}</time>
</div>
</a>
);
}
function getExcerpt(blocks) {
const textBlock = blocks.find(b => b.type === 'TEXT');
if (textBlock?.content?.body) {
return textBlock.content.body.slice(0, 150) + '...';
}
return '';
}
Single Post Page
async function PostPage({ slug }) {
const post = await getPost(slug);
if (!post) {
return <div>Post not found</div>;
}
return (
<article className="post-page">
<header>
<h1>{post.title}</h1>
<div className="meta">
<time>{formatDate(post.createdAt)}</time>
{post.metadata?.author && (
<span className="author">by {post.metadata.author}</span>
)}
</div>
</header>
<div className="content">
{post.blocks.map((block, i) => (
<BlockRenderer key={i} block={block} />
))}
</div>
{post.metadata?.tags && (
<div className="tags">
{post.metadata.tags.map(tag => (
<a key={tag} href={`/blog?tag=${tag}`} className="tag">
{tag}
</a>
))}
</div>
)}
</article>
);
}
Block Renderer
Render different block types:
function BlockRenderer({ block }) {
switch (block.type) {
case 'HERO':
return <HeroBlock content={block.content} />;
case 'TEXT':
return <TextBlock content={block.content} />;
case 'IMAGE':
return <ImageBlock content={block.content} />;
case 'CODE':
return <CodeBlock content={block.content} />;
case 'QUOTE':
return <QuoteBlock content={block.content} />;
case 'CTA':
return <CTABlock content={block.content} />;
case 'GALLERY':
return <GalleryBlock content={block.content} />;
default:
return null;
}
}
function HeroBlock({ content }) {
return (
<div className="hero-block">
{content.image && (
<img src={`${content.image}?w=1200`} alt={content.title} />
)}
<h1>{content.title}</h1>
{content.subtitle && <p>{content.subtitle}</p>}
</div>
);
}
function TextBlock({ content }) {
return (
<div
className="text-block prose"
dangerouslySetInnerHTML={{ __html: parseMarkdown(content.body) }}
/>
);
}
function ImageBlock({ content }) {
return (
<figure className="image-block">
<img src={`${content.url}?w=800`} alt={content.alt || ''} />
{content.caption && <figcaption>{content.caption}</figcaption>}
</figure>
);
}
function CodeBlock({ content }) {
return (
<pre className="code-block">
<code className={`language-${content.language}`}>
{content.code}
</code>
</pre>
);
}
function QuoteBlock({ content }) {
return (
<blockquote className="quote-block">
<p>{content.text}</p>
{content.author && <cite>— {content.author}</cite>}
</blockquote>
);
}
function CTABlock({ content }) {
return (
<div className="cta-block">
<a
href={content.url}
className={`cta-button ${content.style || 'primary'}`}
>
{content.text}
</a>
</div>
);
}
function GalleryBlock({ content }) {
return (
<div className="gallery-block">
{content.images.map((img, i) => (
<img key={i} src={`${img}?w=400&h=400&fit=cover`} alt="" />
))}
</div>
);
}
Dynamic Pages
Create pages with unique keys:
// Create homepage
await sdk.cms.createNode({
businessId: sdk.config.businessId,
type: 'PAGE',
title: 'Home',
key: 'homepage', // Unique key for lookup
status: 'PUBLISHED',
blocks: [
{
type: 'HERO',
content: {
title: 'Welcome to Our Site',
subtitle: 'Discover amazing content',
image: 'media_hero123'
}
},
{
type: 'TEXT',
content: {
body: '## About Us\n\nWe create great content...'
}
}
]
});
Fetch by key:
async function DynamicPage({ pageKey }) {
const result = await sdk.cms.getNode({
businessId: sdk.config.businessId,
key: pageKey
});
if (!result.ok) {
return <div>Page not found</div>;
}
const page = result.val;
return (
<div className="page">
<h1>{page.title}</h1>
{page.blocks.map((block, i) => (
<BlockRenderer key={i} block={block} />
))}
</div>
);
}
Hierarchical Content
Create content hierarchies (e.g., documentation):
// Create parent category
const category = await sdk.cms.createNode({
businessId: sdk.config.businessId,
type: 'CATEGORY',
title: 'Getting Started',
slug: 'getting-started',
status: 'PUBLISHED'
});
// Create child pages
await sdk.cms.createNode({
businessId: sdk.config.businessId,
type: 'DOC',
title: 'Installation',
slug: 'installation',
parentId: category.val.id,
status: 'PUBLISHED',
blocks: [...]
});
// Fetch children
const children = await sdk.cms.getNodeChildren({
businessId: sdk.config.businessId,
id: category.val.id
});
Search
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const search = async () => {
const result = await sdk.cms.getNodes({
businessId: sdk.config.businessId,
type: 'BLOG_POST',
status: 'PUBLISHED',
search: query,
limit: 20
});
if (result.ok) {
setResults(result.val.items);
}
};
return (
<div className="search-page">
<div className="search-bar">
<input
value={query}
onChange={e => setQuery(e.target.value)}
onKeyDown={e => e.key === 'Enter' && search()}
placeholder="Search posts..."
/>
<button onClick={search}>Search</button>
</div>
<div className="results">
{results.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
</div>
);
}
SEO
Add SEO metadata:
function PostPage({ post }) {
return (
<>
<head>
<title>{post.title}</title>
<meta name="description" content={post.metadata?.excerpt} />
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.metadata?.excerpt} />
{post.metadata?.image && (
<meta property="og:image" content={post.metadata.image} />
)}
</head>
<article>...</article>
</>
);
}
Tip
Use key for singleton pages (homepage, about, contact) and slug for collections (blog posts, products).