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
});
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).