API Documentation

Integrate Vibeblogger
into your app

Fetch your blog content via API and render it with your own components. Works with Next.js, Remix, Astro, or any frontend.

Quick Start

1. Get your API key

Go to your API Keys dashboard and generate a new key.

2. Store it in your environment

# .env.local
VIBEBLOGGER_API_KEY=vb_live_your_key_here

3. Fetch your posts

const response = await fetch('https://vibeblogger.io/api/v1/posts', {
  headers: {
    'Authorization': `Bearer ${process.env.VIBEBLOGGER_API_KEY}`
  }
});

const { posts } = await response.json();

API Reference

Authentication

All API requests require authentication via API key. Include it in the request headers:

// Option 1: Authorization header (recommended)
Authorization: Bearer vb_live_your_key_here

// Option 2: X-API-Key header
X-API-Key: vb_live_your_key_here
GET/api/v1/posts

List all published blog posts.

Query Parameters

ParameterTypeDescription
pagenumberPage number (default: 1)
limitnumberPosts per page (default: 10, max: 100)
categorystringFilter by category
tagstringFilter by tag

Response

{
  "posts": [
    {
      "_id": "...",
      "title": "My Blog Post",
      "slug": "my-blog-post",
      "description": "A short description...",
      "featuredImage": "https://...",
      "status": "published",
      "publishedAt": "2024-01-15T...",
      "readTime": 5,
      "tags": ["tech", "ai"],
      "author": {
        "name": "John Doe",
        "imageUrl": "https://..."
      },
      "components": [
        { "type": "rich_text", "content": "..." },
        { "type": "image", "url": "...", "alt": "..." }
      ]
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 25,
    "pages": 3,
    "hasMore": true
  }
}
GET/api/v1/posts/:slug

Get a single blog post by its slug.

Response

{
  "post": {
    "_id": "...",
    "title": "My Blog Post",
    "slug": "my-blog-post",
    "description": "A short description...",
    "featuredImage": "https://...",
    "components": [...],
    // ... full post data
  }
}

Rate Limits

API requests are rate limited to 1,000 requests per hour per API key.

Rate limit headers are included in every response:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1705123456789

Component Types

Blog posts are made up of components. Each component has a type and type-specific fields.

rich_text

Rich text content — requires markdown parsing

Fields: content (Markdown string)

image

Image with optional caption

Fields: url, alt, caption, width, height

callout

Highlighted box (info/success/warning/error)

Fields: variant, title, content

quote

Blockquote with attribution

Fields: content, author, citation

cta

Call-to-action button

Fields: text, link, style

video

Embedded video

Fields: videoUrl, thumbnail, videoTitle

table

Data table

Fields: headers, rows, tableCaption

bar_chart

Bar chart visualization

Fields: data.labels, data.datasets

line_chart

Line chart visualization

Fields: data.labels, data.datasets

pie_chart

Pie chart visualization

Fields: data.labels, data.values

comparison_table

Feature comparison table

Fields: data.items, data.features

pros_cons

Pros and cons list

Fields: data.pros, data.cons

timeline

Timeline of events

Fields: data.events

flowchart

Process flowchart

Fields: data.nodes, data.edges

step_by_step

Numbered steps guide

Fields: data.steps

code_block

Syntax-highlighted code

Fields: content, data.language

React Component Library

Copy this component renderer into your project to render all blog component types:

Important: The rich_text component contains Markdown, not HTML. You need to parse it using a library like react-markdown.

Install: npm install react-markdown

// components/BlogRenderer.tsx
import React from 'react';
import ReactMarkdown from 'react-markdown'; // npm install react-markdown

interface BlogComponent {
  _id: string;
  type: string;
  order: number;
  content?: string;
  url?: string;
  src?: string;
  alt?: string;
  caption?: string;
  variant?: 'info' | 'success' | 'warning' | 'error';
  title?: string;
  author?: string;
  citation?: string;
  text?: string;
  link?: string;
  style?: 'primary' | 'secondary' | 'outline';
  videoUrl?: string;
  videoTitle?: string;
  headers?: string[];
  rows?: string[][];
  tableCaption?: string;
  data?: any;
}

// Main renderer - renders a single component
export function BlogComponentRenderer({ component }: { component: BlogComponent }) {
  switch (component.type) {
    case 'rich_text':
      return (
        <div className="prose prose-lg max-w-none">
          <ReactMarkdown>{component.content || ''}</ReactMarkdown>
        </div>
      );

    case 'image':
      return (
        <figure className="my-8">
          <img
            src={component.url || component.src}
            alt={component.alt || ''}
            className="w-full rounded-lg"
          />
          {component.caption && (
            <figcaption className="text-center text-sm text-gray-500 mt-2">
              {component.caption}
            </figcaption>
          )}
        </figure>
      );

    case 'callout':
      const variantStyles = {
        info: 'bg-blue-50 border-blue-200 text-blue-800',
        success: 'bg-green-50 border-green-200 text-green-800',
        warning: 'bg-yellow-50 border-yellow-200 text-yellow-800',
        error: 'bg-red-50 border-red-200 text-red-800',
      };
      return (
        <div className={`p-4 rounded-lg border ${variantStyles[component.variant || 'info']} my-6`}>
          {component.title && <strong className="block mb-1">{component.title}</strong>}
          <ReactMarkdown>{component.content || ''}</ReactMarkdown>
        </div>
      );

    case 'quote':
      return (
        <blockquote className="border-l-4 border-gray-300 pl-4 my-6 italic">
          <ReactMarkdown>{component.content || ''}</ReactMarkdown>
          {component.author && (
            <cite className="block mt-2 text-sm text-gray-600 not-italic">
              — {component.author}
              {component.citation && <span>, {component.citation}</span>}
            </cite>
          )}
        </blockquote>
      );

    case 'cta':
      const buttonStyles = {
        primary: 'bg-blue-600 text-white hover:bg-blue-700',
        secondary: 'bg-gray-600 text-white hover:bg-gray-700',
        outline: 'border-2 border-blue-600 text-blue-600 hover:bg-blue-50',
      };
      return (
        <div className="my-8 text-center">
          <a
            href={component.link}
            className={`inline-block px-6 py-3 rounded-lg font-semibold transition-colors ${buttonStyles[component.style || 'primary']}`}
          >
            {component.text}
          </a>
        </div>
      );

    case 'video':
      const getEmbedUrl = (url: string) => {
        if (url.includes('youtube.com') || url.includes('youtu.be')) {
          const videoId = url.match(/(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=))([^&?]+)/)?.[1];
          return `https://www.youtube.com/embed/${videoId}`;
        }
        if (url.includes('vimeo.com')) {
          const videoId = url.match(/vimeo\.com\/(?:video\/)?(\d+)/)?.[1];
          return `https://player.vimeo.com/video/${videoId}`;
        }
        return url;
      };
      return (
        <div className="my-8 aspect-video">
          <iframe
            src={getEmbedUrl(component.videoUrl || '')}
            title={component.videoTitle || 'Video'}
            className="w-full h-full rounded-lg"
            allowFullScreen
          />
        </div>
      );

    case 'table':
      return (
        <div className="my-8 overflow-x-auto">
          <table className="w-full border-collapse">
            {component.headers && (
              <thead>
                <tr className="bg-gray-100">
                  {component.headers.map((header, i) => (
                    <th key={i} className="border border-gray-300 px-4 py-2 text-left font-semibold">
                      {header}
                    </th>
                  ))}
                </tr>
              </thead>
            )}
            <tbody>
              {component.rows?.map((row, i) => (
                <tr key={i} className={i % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
                  {row.map((cell, j) => (
                    <td key={j} className="border border-gray-300 px-4 py-2">
                      {cell}
                    </td>
                  ))}
                </tr>
              ))}
            </tbody>
          </table>
          {component.tableCaption && (
            <p className="text-center text-sm text-gray-500 mt-2">{component.tableCaption}</p>
          )}
        </div>
      );

    case 'code_block':
      return (
        <pre className="my-6 p-4 bg-gray-900 text-gray-100 rounded-lg overflow-x-auto">
          <code>{component.content}</code>
        </pre>
      );

    case 'pros_cons':
      return (
        <div className="my-8 grid md:grid-cols-2 gap-4">
          <div className="p-4 bg-green-50 rounded-lg">
            <h4 className="font-semibold text-green-800 mb-2">Pros</h4>
            <ul className="space-y-1">
              {component.data?.pros?.map((pro: string, i: number) => (
                <li key={i} className="text-green-700">{pro}</li>
              ))}
            </ul>
          </div>
          <div className="p-4 bg-red-50 rounded-lg">
            <h4 className="font-semibold text-red-800 mb-2">Cons</h4>
            <ul className="space-y-1">
              {component.data?.cons?.map((con: string, i: number) => (
                <li key={i} className="text-red-700">{con}</li>
              ))}
            </ul>
          </div>
        </div>
      );

    case 'step_by_step':
      return (
        <div className="my-8 space-y-4">
          {component.data?.steps?.map((step: { title: string; content: string }, i: number) => (
            <div key={i} className="flex gap-4">
              <div className="flex-shrink-0 w-8 h-8 rounded-full bg-blue-600 text-white flex items-center justify-center font-semibold">
                {i + 1}
              </div>
              <div>
                <h4 className="font-semibold">{step.title}</h4>
                <p className="text-gray-600">{step.content}</p>
              </div>
            </div>
          ))}
        </div>
      );

    case 'timeline':
      return (
        <div className="my-8 border-l-2 border-gray-300 pl-4 space-y-6">
          {component.data?.events?.map((event: { date: string; title: string; content: string }, i: number) => (
            <div key={i} className="relative">
              <div className="absolute -left-6 w-3 h-3 bg-blue-600 rounded-full" />
              <time className="text-sm text-gray-500">{event.date}</time>
              <h4 className="font-semibold">{event.title}</h4>
              <p className="text-gray-600">{event.content}</p>
            </div>
          ))}
        </div>
      );

    default:
      console.warn(`Unknown component type: ${component.type}`);
      return null;
  }
}

// Render all components for a post
export function BlogContent({ components }: { components: BlogComponent[] }) {
  return (
    <div className="blog-content">
      {components
        .sort((a, b) => a.order - b.order)
        .map((component) => (
          <BlogComponentRenderer key={component._id} component={component} />
        ))}
    </div>
  );
}

Full Integration Example

Here's a complete Next.js App Router integration:

lib/blog.ts

const API_URL = 'https://vibeblogger.io/api/v1';

export async function getBlogPosts(page = 1, limit = 10) {
  const res = await fetch(
    `${API_URL}/posts?page=${page}&limit=${limit}`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.VIBEBLOGGER_API_KEY}`
      },
      next: { revalidate: 60 } // Revalidate every 60 seconds
    }
  );

  if (!res.ok) {
    throw new Error('Failed to fetch posts');
  }

  return res.json();
}

export async function getBlogPost(slug: string) {
  const res = await fetch(
    `${API_URL}/posts/${slug}`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.VIBEBLOGGER_API_KEY}`
      },
      next: { revalidate: 60 }
    }
  );

  if (!res.ok) {
    if (res.status === 404) return null;
    throw new Error('Failed to fetch post');
  }

  return res.json();
}

app/blog/page.tsx

import Link from 'next/link';
import { getBlogPosts } from '@/lib/blog';

export default async function BlogPage() {
  const { posts, pagination } = await getBlogPosts();

  return (
    <main className="max-w-4xl mx-auto px-4 py-12">
      <h1 className="text-4xl font-bold mb-8">Blog</h1>

      <div className="grid gap-8">
        {posts.map((post) => (
          <article key={post._id} className="border-b pb-8">
            <Link href={`/blog/${post.slug}`}>
              {post.featuredImage && (
                <img
                  src={post.featuredImage}
                  alt={post.title}
                  className="w-full h-48 object-cover rounded-lg mb-4"
                />
              )}
              <h2 className="text-2xl font-semibold hover:text-blue-600">
                {post.title}
              </h2>
              <p className="text-gray-600 mt-2">{post.description}</p>
              <div className="flex gap-2 mt-3">
                {post.tags?.map((tag) => (
                  <span key={tag} className="px-2 py-1 bg-gray-100 text-sm rounded">
                    {tag}
                  </span>
                ))}
              </div>
            </Link>
          </article>
        ))}
      </div>

      {pagination.hasMore && (
        <Link
          href={`/blog?page=${pagination.page + 1}`}
          className="mt-8 inline-block text-blue-600 hover:underline"
        >
          Load more posts
        </Link>
      )}
    </main>
  );
}

app/blog/[slug]/page.tsx

import { notFound } from 'next/navigation';
import { getBlogPost } from '@/lib/blog';
import { BlogContent } from '@/components/BlogRenderer';

export default async function PostPage({
  params
}: {
  params: { slug: string }
}) {
  const data = await getBlogPost(params.slug);

  if (!data) {
    notFound();
  }

  const { post } = data;

  return (
    <article className="max-w-3xl mx-auto px-4 py-12">
      <header className="mb-8">
        <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
        <p className="text-xl text-gray-600">{post.description}</p>

        {post.author && (
          <div className="flex items-center gap-3 mt-6">
            {post.author.imageUrl && (
              <img
                src={post.author.imageUrl}
                alt={post.author.name}
                className="w-10 h-10 rounded-full"
              />
            )}
            <span className="font-medium">{post.author.name}</span>
          </div>
        )}
      </header>

      {post.featuredImage && (
        <img
          src={post.featuredImage}
          alt={post.title}
          className="w-full rounded-lg mb-8"
        />
      )}

      <BlogContent components={post.components} />
    </article>
  );
}

Need help?

If you have questions or run into issues, reach out at support@vibeblogger.io