# AutoBlogWriter Documentation — Full Content > AutoBlogWriter is an AI-powered blog content engine. The SDK works with any Node.js framework (Next.js, Remix, Astro, Express). It includes framework-agnostic helpers, Next.js App Router helpers with ISR cache tagging, React components, and webhook-based revalidation. --- # Introduction Welcome to the **AutoBlogWriter** documentation. AutoBlogWriter is an AI-powered content generation platform that automates blog post creation, scheduling, and publishing — integrated directly into your Next.js application through an official TypeScript SDK. ## What is AutoBlogWriter? AutoBlogWriter combines advanced AI models with a complete content management workflow. You generate topics, create drafts, polish content, generate images, and publish — all from the AutoBlogWriter dashboard. Then your Next.js app fetches and renders that content using the `@autoblogwriter/sdk`. ### Platform Components | Component | Description | |:----------|:------------| | **Dashboard** | Web UI for managing content, topics, scheduling, and analytics | | **API** | REST API powering the dashboard and SDK | | **SDK** | TypeScript package for fetching and rendering blog content in your app | ### Key Features - **AI-Powered Content** — Generate full blog posts from keywords or topics using state-of-the-art LLMs. - **Batch Generation** — Create dozens of posts at once with the auto-scheduler. - **Smart Scheduling** — Schedule posts to publish at optimal times automatically. - **Official SDK** — First-class TypeScript SDK with Next.js App Router support, React components, and SEO helpers. - **Webhook Revalidation** — Automatic cache invalidation when content publishes. - **SEO Built-In** — Sitemap generation, robots.txt, OpenGraph metadata, and structured data. - **Markdown Rendering** — Lightweight, dependency-free HTML renderer with XSS protection. - **Themeable Styles** — Default dark-theme CSS with full customization via CSS custom properties. ## How It Works 1. **Create a Workspace** — Sign up and create a workspace for your site. 2. **Generate Content** — Use the dashboard to generate topics, drafts, and images with AI. 3. **Publish** — Publish posts immediately or schedule them for later. 4. **Integrate** — Install the SDK in your Next.js app and fetch your posts with a few lines of code. 5. **Revalidate** — Set up a webhook so your site updates instantly when content changes. ## Quick Example Here's what it looks like to display your blog with the SDK: ```tsx // app/blog/page.tsx import Link from "next/link"; import { fetchBlogPosts } from "@autoblogwriter/sdk/next"; import { BlogPostList } from "@autoblogwriter/sdk/react"; export default async function BlogPage() { const { posts } = await fetchBlogPosts(); return ; } ``` That's it — server-rendered, SEO-friendly, and automatically revalidated. ## LLMS Files AutoBlogWriter publishes machine-readable documentation indexes for LLM tooling: - [`/llms.txt`](/llms.txt) — lightweight docs index and link map. - [`/llms-full.txt`](/llms-full.txt) — full plain-text docs content for ingestion. ## Next Steps - [Installation](/docs/getting-started/installation) — Install the SDK and configure your environment. - [Quickstart](/docs/getting-started/quickstart) — Build a full blog in 5 minutes. --- # Installation The `@autoblogwriter/sdk` is the official TypeScript SDK for fetching and rendering AutoBlogWriter content in your application. ## Prerequisites - **Node.js 18+** - **A Next.js 13.4+** project (for Next.js helpers — the core client works anywhere) - **A AutoBlogWriter account** with a workspace and API key ## Install the SDK ```bash npm install @autoblogwriter/sdk ``` ```bash yarn add @autoblogwriter/sdk ``` ```bash pnpm add @autoblogwriter/sdk ``` ### Peer Dependencies The SDK has **optional** peer dependencies: | Package | Required For | |:--------|:-------------| | `react` >= 18 | React components (`@autoblogwriter/sdk/react`) | | `next` >= 13.4 | Next.js helpers (`@autoblogwriter/sdk/next`) | If you only use the core client, neither is required. ## Package Entry Points The SDK ships multiple entry points for different use cases: ```typescript // Core client, types, and utilities import { createAutoBlogWriterClient } from "@autoblogwriter/sdk"; // Next.js App Router helpers (server-side) import { fetchBlogPosts, fetchBlogPost } from "@autoblogwriter/sdk/next"; // React components import { BlogPost, BlogPostList, Markdown } from "@autoblogwriter/sdk/react"; // Webhook signature verification (server-side) import { verifyWebhookSignature } from "@autoblogwriter/sdk/revalidate"; // Default styles import "@autoblogwriter/sdk/styles.css"; ``` ## Environment Variables Add these to your `.env.local` file: ```bash # Required AUTOBLOGWRITER_API_KEY=ba_pk_your_api_key_here AUTOBLOGWRITER_WORKSPACE_SLUG=your-workspace-slug # Optional AUTOBLOGWRITER_API_URL=https://api.autoblogwriter.app # Default API URL AUTOBLOGWRITER_WORKSPACE_ID=ws_abc123 # Alternative to slug for internal use SITE_URL=https://yoursite.com # Used for sitemap/robots generation AUTOBLOGWRITER_REVALIDATE_SECRET=your_webhook_secret # For webhook verification NEXT_PUBLIC_ANALYTICS_TOKEN=your_token # For analytics proxy ``` ### Where to Find Your Credentials 1. Log in to the [AutoBlogWriter Dashboard](https://app.autoblogwriter.app). 2. Navigate to your workspace **Settings**. 3. Under **API Keys**, create a new key or copy an existing one. 4. Your **workspace slug** is shown in your workspace URL (e.g., `app.autoblogwriter.app/workspace/your-slug`). ## Setup Options You have two ways to use the SDK: ### Option A: Environment-Based (Recommended for Next.js) The simplest approach — the SDK reads configuration from environment variables automatically: ```typescript // No setup file needed — just use the helpers directly import { fetchBlogPosts } from "@autoblogwriter/sdk/next"; // In any server component: const { posts } = await fetchBlogPosts(); ``` The `@autoblogwriter/sdk/next` helpers call `createAutoBlogWriterFromEnv()` internally, which reads `AUTOBLOGWRITER_API_KEY`, `AUTOBLOGWRITER_WORKSPACE_SLUG`, and other env vars automatically. ### Option B: Manual Client (Framework-Agnostic) For non-Next.js projects or when you need more control: ```typescript // lib/blog.ts import { createAutoBlogWriterClient } from "@autoblogwriter/sdk"; export const blog = createAutoBlogWriterClient({ apiUrl: "https://api.autoblogwriter.app", apiKey: process.env.AUTOBLOGWRITER_API_KEY!, workspaceSlug: process.env.AUTOBLOGWRITER_WORKSPACE_SLUG!, }); ``` See the [Client Reference](/docs/sdk/client) for all configuration options. ## Verify Installation Run a quick test to make sure everything is wired up: ```typescript import { createAutoBlogWriterClient } from "@autoblogwriter/sdk"; const client = createAutoBlogWriterClient({ apiUrl: "https://api.autoblogwriter.app", apiKey: process.env.AUTOBLOGWRITER_API_KEY!, workspaceSlug: process.env.AUTOBLOGWRITER_WORKSPACE_SLUG!, }); const { posts } = await client.getPosts({ limit: 1 }); console.log("Connected! Found", posts.length, "posts"); ``` ## Next Steps - [Quickstart](/docs/getting-started/quickstart) — Build a full blog page in minutes. - [Client Reference](/docs/sdk/client) — Explore all client configuration options and methods. --- # Quickstart This guide walks you through adding a fully functional blog to your Next.js App Router project using the AutoBlogWriter SDK — from installation to a live blog with SEO, revalidation, and styled components. ## 1. Install the SDK ```bash npm install @autoblogwriter/sdk ``` ## 2. Set Environment Variables Create or update `.env.local`: ```bash AUTOBLOGWRITER_API_KEY=ba_pk_your_api_key AUTOBLOGWRITER_WORKSPACE_SLUG=your-workspace SITE_URL=https://yoursite.com AUTOBLOGWRITER_REVALIDATE_SECRET=your_webhook_secret ``` ## 3. Import the Styles In your root layout (`app/layout.tsx`), import the default AutoBlogWriter styles: ```tsx // app/layout.tsx import "@autoblogwriter/sdk/styles.css"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ## 4. Create the Blog Listing Page ```tsx // app/blog/page.tsx import Link from "next/link"; import { fetchBlogPosts } from "@autoblogwriter/sdk/next"; import { BlogPostList } from "@autoblogwriter/sdk/react"; export default async function BlogPage() { const { posts } = await fetchBlogPosts(); return (
); } ``` ## 5. Create the Blog Post Page ```tsx // app/blog/[slug]/page.tsx import { fetchBlogPost, generatePostMetadata } from "@autoblogwriter/sdk/next"; import { BlogPost } from "@autoblogwriter/sdk/react"; import type { Metadata } from "next"; type Props = { params: Promise<{ slug: string }> }; export async function generateMetadata({ params }: Props): Promise { const { slug } = await params; return generatePostMetadata(slug); } export default async function PostPage({ params }: Props) { const { slug } = await params; const post = await fetchBlogPost(slug); return (
); } ``` The `generatePostMetadata` function automatically generates ``, `<meta description>`, OpenGraph, and Twitter Card metadata from your post's SEO fields. ## 6. Add the Revalidation Webhook This API route receives webhooks from AutoBlogWriter when content changes, automatically clearing your Next.js cache: ```typescript // app/api/autoblogwriter/revalidate/route.ts import { createEnvRevalidateHandler } from "@autoblogwriter/sdk/next"; export const runtime = "nodejs"; export const POST = createEnvRevalidateHandler(); ``` Then in your AutoBlogWriter dashboard, go to **Settings > Webhooks** and add: - **URL:** `https://yoursite.com/api/autoblogwriter/revalidate` - **Secret:** The same value as your `AUTOBLOGWRITER_REVALIDATE_SECRET` env var. ## 7. Add Sitemap and Robots ```typescript // app/sitemap.ts import { generateBlogSitemap } from "@autoblogwriter/sdk/next"; export const revalidate = 300; // Refresh every 5 minutes export default function sitemap() { return generateBlogSitemap(); } ``` ```typescript // app/robots.ts import { generateBlogRobots } from "@autoblogwriter/sdk/next"; export default function robots() { return generateBlogRobots(); } ``` ## 8. Run Your App ```bash npm run dev ``` Visit `http://localhost:3000/blog` to see your published posts. ## What You Get With these ~50 lines of code, your blog now has: - Server-rendered blog listing and post pages - Automatic SEO metadata (title, description, OpenGraph, Twitter Cards) - Automatic cache revalidation via webhooks - Sitemap and robots.txt generation - Styled components with a dark theme - Cursor-based pagination support - Reading time display ## Next Steps - [React Components](/docs/sdk/react-components) — Customize the blog UI with props and render functions. - [Styling](/docs/sdk/styling) — Theme the components with CSS custom properties. - [Webhooks & Revalidation](/docs/sdk/webhooks) — Deep dive into cache invalidation. - [Next.js Helpers](/docs/sdk/nextjs-helpers) — All available server-side helpers. --- # Public API This page documents direct REST access to the AutoBlogWriter public content API. If you are using Next.js, the SDK is usually the fastest path to production. For direct API calls or custom platforms, use the endpoints below. ## Base URL ```text https://api.autoblogwriter.app ``` ## Authentication Use your workspace API key (`ba_pk_...`) with one of the supported auth headers: ```http Authorization: Bearer <YOUR_API_KEY> ``` or ```http x-api-key: <YOUR_API_KEY> ``` ## Workspace Slug Requirement Public content endpoints are workspace-scoped and require a `workspaceSlug` in the URL path: ```text /v1/public/:workspaceSlug/... ``` Find your workspace slug in the AutoBlogWriter dashboard workspace settings. ## Endpoint: List Published Posts `GET /v1/public/:workspaceSlug/blogs` Returns published blog posts with pagination metadata. ### Query Parameters | Param | Type | Required | Default | Description | |:------|:-----|:---------|:--------|:------------| | `limit` | `number` | No | `20` | Page size | | `page` | `number` | No | `1` | 1-based page number | | `category` | `string` | No | — | Filter by category | ### cURL Example ```bash curl --request GET \ --url "https://api.autoblogwriter.app/v1/public/my-blog/blogs?limit=10&page=1&category=SEO" \ --header "Authorization: Bearer ba_pk_your_api_key" ``` ### Example Success Response ```json { "success": true, "data": { "items": [ { "id": "post_1", "title": "How to Scale Content Ops", "slug": "scale-content-ops", "excerpt": "A practical system for higher output.", "content": "# ...", "status": "PUBLISHED", "categories": ["SEO", "Automation"], "updatedAt": "2026-01-01T00:00:00.000Z" } ], "pagination": { "page": 1, "limit": 10, "total": 42, "totalPages": 5, "hasMore": true } } } ``` ## Endpoint: Get Post By Slug `GET /v1/public/:workspaceSlug/blogs/:slug` Returns a single published post by URL slug. ### cURL Example ```bash curl --request GET \ --url "https://api.autoblogwriter.app/v1/public/my-blog/blogs/scale-content-ops" \ --header "x-api-key: ba_pk_your_api_key" ``` ### Example Success Response ```json { "success": true, "data": { "post": { "id": "post_1", "title": "How to Scale Content Ops", "slug": "scale-content-ops", "content": "# ...", "status": "PUBLISHED", "categories": ["SEO"], "relatedPosts": [ { "id": "post_2", "title": "Editorial Calendars That Ship", "slug": "editorial-calendars-ship", "updatedAt": "2026-01-02T00:00:00.000Z" } ], "updatedAt": "2026-01-01T00:00:00.000Z" } } } ``` ## Response Envelope and Errors Successful JSON responses are wrapped as: ```json { "success": true, "data": {} } ``` When the API returns a non-2xx status, use the HTTP status code plus error body fields for handling. Common practical cases: - `401 Unauthorized`: invalid API key or header format. - `404 Not Found`: slug does not exist for the workspace. - `429 Too Many Requests`: retry with backoff. - `5xx`: temporary server failure. ## Pagination Guidance (SDK Parity) The official SDK exposes `nextCursor` for pagination, but the API uses `page` numbers. - Start with `page=1`. - If `pagination.hasMore` is `true`, request `page + 1`. - Stop when `hasMore` is `false`. ## SDK vs Direct API Use the SDK if you want typed helpers, Next.js cache tags, metadata utilities, and built-in error classes. Use direct API calls when integrating from non-Next.js stacks or custom data pipelines. See [Client Reference](/docs/sdk/client) for the SDK path. --- # Workspaces A **workspace** represents a single website or project in AutoBlogWriter. All your blog posts, topics, API keys, and settings belong to a workspace. ## Creating a Workspace 1. Log in to the [AutoBlogWriter Dashboard](https://app.autoblogwriter.app). 2. Click **New Workspace**. 3. Enter your website name and URL. 4. You'll be assigned a **workspace slug** (e.g., `my-tech-blog`) that you'll use in the SDK. ## Workspace Settings Each workspace has its own configuration: - **AI Personality** — Define the tone, style, and voice for AI-generated content. - **Website Context** — Provide background about your site so the AI generates relevant content. - **API Keys** — Create and manage API keys scoped to this workspace. - **Webhooks** — Configure revalidation endpoints for your deployed sites. - **Team Members** — Invite collaborators to manage content together. ## Workspace Identifiers The SDK uses two identifiers to reference a workspace: | Identifier | Format | Usage | |:-----------|:-------|:------| | `workspaceSlug` | `my-tech-blog` | **Required** for public API endpoints (what the SDK uses to fetch posts) | | `workspaceId` | `ws_abc123` | Internal ID, used for authenticated dashboard endpoints | When configuring the SDK, you must provide at least one: ```typescript const client = createAutoBlogWriterClient({ apiUrl: "https://api.autoblogwriter.app", apiKey: "ba_pk_...", workspaceSlug: "my-tech-blog", // Required for fetching posts }); ``` ## API Keys API keys authenticate your SDK requests and are scoped to a single workspace. ### Key Format All API keys use the prefix `ba_pk_` followed by a random string. ### Managing Keys - **Create** — Dashboard > Settings > API Keys > Create New Key - **Revoke** — Immediately invalidates a key. Existing requests using it will fail. - **Delete** — Permanently removes the key record. ### Security Best Practices - Store keys in environment variables, never in client-side code. - Use different keys for development and production. - Rotate keys periodically. - Revoke compromised keys immediately. ## Next Steps - [Blog Generation](/docs/core-concepts/blog-generation) — How AI content generation works. - [Client Reference](/docs/sdk/client) — Configure the SDK client for your workspace. --- # Blog Generation AutoBlogWriter uses state-of-the-art language models to generate SEO-optimized blog content through a multi-stage pipeline. ## The Generation Pipeline ### 1. Topic Generation The AI analyzes your niche, website context, and target keywords to suggest high-traffic blog topics. Topics are stored and can be reviewed, edited, or discarded before moving to the next stage. ### 2. Draft Creation From an approved topic, the AI generates a full blog post draft including: - Structured headings (H1–H4) - Well-organized body paragraphs - SEO-optimized meta description - Target keywords naturally woven into the content - Suggested excerpt ### 3. Content Polishing Use the built-in editor to refine AI-generated content. The editor supports: - AI-assisted rewriting of individual sections - Grammar and tone adjustments - Markdown formatting - Image placement ### 4. Image Generation AutoBlogWriter integrates with AI image generation to create: - **Hero images** — Featured images for your posts - **OG images** — OpenGraph images for social sharing Images are automatically uploaded to S3 and associated with your post. ## Batch Generation For scaling content production, AutoBlogWriter supports **batch generation** — creating multiple posts in a single operation. ### Auto-Schedule Generate The most powerful batch feature combines topic generation, drafting, and scheduling: 1. Specify the number of posts, target keywords, and schedule preferences. 2. AutoBlogWriter generates topics, writes drafts, creates images, and schedules publication. 3. Monitor progress from the dashboard or via the API. Batch jobs run asynchronously and can be monitored, polled, or cancelled. ## Blog Post Model Every blog post contains: | Field | Description | |:------|:------------| | `title` | Post title | | `slug` | URL-friendly identifier (auto-generated or custom) | | `content` | Markdown body content | | `excerpt` | Short summary for listings and meta descriptions | | `status` | `DRAFT`, `PUBLISHED`, or `HIDDEN` | | `seo.title` | SEO-specific title (falls back to `title`) | | `seo.description` | Meta description | | `seo.keywords` | Target keyword array | | `metadata.canonicalUrl` | Canonical URL for deduplication | | `metadata.ogImageUrl` | OpenGraph image URL | | `metadata.readingTimeMinutes` | Estimated reading time | | `metadata.wordCount` | Word count | | `publishedAt` | Publication timestamp | | `updatedAt` | Last modification timestamp | ## Content Workflow ``` Topic → Draft → Polish → Schedule/Publish → Live on your site ``` Posts transition through these statuses: - **DRAFT** — Work in progress, not visible to the public API. - **PUBLISHED** — Live and returned by SDK queries. - **HIDDEN** — Unpublished but retained. Not returned by the public API. When a post is published, AutoBlogWriter sends a webhook to your site so the cache is automatically invalidated. ## SEO Features AutoBlogWriter handles SEO automatically: - **Keyword targeting** — AI naturally incorporates your target keywords. - **Meta descriptions** — Generated and editable per post. - **Header structure** — Proper H1–H4 hierarchy for crawlability. - **Reading time** — Calculated and stored in metadata. - **Sitemap integration** — Published posts are included in your sitemap via the SDK. ## Next Steps - [Scheduling System](/docs/core-concepts/scheduling-system) — Automate your content calendar. - [React Components](/docs/sdk/react-components) — Render posts with built-in components. --- # Scheduling System Consistent publishing is key to SEO success. AutoBlogWriter's scheduling system lets you plan your content calendar and automatically publish posts at the right time. ## How Scheduling Works When you schedule a post, you set a future date and time. A background job checks for scheduled posts and automatically transitions them to `PUBLISHED` status when the time arrives. ### Scheduling a Post From the dashboard editor: 1. Write or generate your post. 2. Click **Schedule** instead of **Publish**. 3. Pick a date, time, and timezone. 4. The post remains in `DRAFT` status until the scheduled time. Scheduled posts include: - `scheduledAt` — The target publication timestamp. - `timezone` — The timezone for the schedule (e.g., `America/New_York`). - `scheduledByUserId` — Who scheduled it. ## Auto-Scheduler The **Auto-Scheduler** is AutoBlogWriter's most powerful scheduling feature. It combines AI content generation with intelligent scheduling: 1. Go to the **Auto-Scheduler** in your dashboard. 2. Configure: - Number of posts to generate - Target keywords or topics - Publishing frequency (e.g., 3 times per week) - Preferred posting windows - Timezone 3. AutoBlogWriter creates a batch job that: - Generates topics from your keywords - Writes full drafts for each topic - Generates hero and OG images - Schedules each post at optimal intervals ### Batch Job States | Status | Description | |:-------|:------------| | `pending` | Job created, waiting to start | | `processing` | Actively generating content | | `completed` | All posts generated and scheduled | | `failed` | An error occurred during generation | You can monitor batch jobs from the dashboard or poll the API for status updates. ## Manual Scheduling For individual posts, use the editor's scheduling feature: - **Schedule** — Set a specific date and time. - **Publish Now** — Immediately transition to `PUBLISHED`. - **Reschedule** — Change the scheduled time for a pending post. - **Cancel Schedule** — Return the post to a regular draft. ## Calendar View The dashboard includes a calendar view showing: - Published posts (by publication date) - Scheduled posts (by scheduled date) - Draft posts in progress This gives you a visual overview of your content pipeline and helps identify gaps in your publishing schedule. ## Publication Flow ``` Draft → Scheduled → [Background job runs at scheduled time] → Published → Webhook sent → Site revalidated ``` When a scheduled post is published: 1. The post status changes to `PUBLISHED`. 2. `publishedAt` is set to the current timestamp. 3. A webhook is sent to your configured endpoints. 4. Your Next.js site's cache is automatically cleared via the revalidation handler. ## Next Steps - [Webhooks & Revalidation](/docs/sdk/webhooks) — How cache invalidation works when posts publish. - [Next.js Helpers](/docs/sdk/nextjs-helpers) — Fetch scheduled and published content. --- # Analytics AutoBlogWriter integrates with **Google Analytics 4 (GA4)** to give you traffic and engagement data for your blog — all viewable directly in the AutoBlogWriter dashboard. ## How It Works 1. **Install GA4 on your site** — Add the Google Analytics 4 snippet to your site as you normally would (via `<script>` tag, Google Tag Manager, or a Next.js analytics package). 2. **Connect Google Analytics in the AutoBlogWriter dashboard** — In your workspace settings, click **Connect Google Analytics** and sign in with Google OAuth. This grants AutoBlogWriter read-only access to your analytics data. 3. **Add your GA4 property ID** — Enter the GA4 property ID that corresponds to your site URL. AutoBlogWriter validates that the property matches your connected site. Once connected, the AutoBlogWriter dashboard fetches analytics data from GA4 via the Google Analytics Data API and displays it alongside your blog content. ## Installing GA4 If you don't have GA4 set up yet, create a property at [analytics.google.com](https://analytics.google.com) and add the tracking snippet to your site. For Next.js, use the `<Script>` component from `next/script` in your root layout: ```tsx // app/layout.tsx import Script from "next/script"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> {children} <Script src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX" strategy="afterInteractive" /> <Script id="gtag-init" strategy="afterInteractive"> {` window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-XXXXXXXXXX'); `} </Script> </body> </html> ); } ``` Replace `G-XXXXXXXXXX` with your GA4 measurement ID. ## Connecting in the Dashboard 1. Go to **Workspace Settings → Integrations** in the AutoBlogWriter dashboard. 2. Click **Connect Google Analytics**. 3. Sign in with the Google account that has access to your GA4 property. 4. Select the GA4 property that matches your site URL. AutoBlogWriter uses read-only access to pull page views, sessions, and engagement metrics for your blog posts. ## What You'll See The dashboard displays analytics data per post and across your blog: - **Page views** and **unique visitors** - **Average engagement time** - **Top-performing posts** - **Traffic trends** over time All data comes directly from your GA4 property — AutoBlogWriter doesn't collect or proxy analytics separately. --- # Content Assistant The Content Assistant is a platform feature that helps AutoBlogWriter understand your brand, audience, and competitive landscape. The richer the context you provide, the more accurately the AI generates content that fits your voice and targets the right topics. Access it from the **Content Assistant** tab inside any workspace. --- ## What It Does AutoBlogWriter uses a structured **workspace context** — a set of facts about your product, positioning, target audience, and competitors — to inform every post it generates. The Content Assistant gives you two ways to build and refine that context: | Tab | Purpose | |:----|:--------| | **Source** | Extract context from an existing URL or uploaded document | | **Research** | Run deep-dive competitor and keyword analysis on a topic | --- ## Source Tab Use the Source tab to pull context from content you already own or reference — your homepage, a product page, a competitor's site, or any PDF or document. ### Workflow 1. **Add a source** — paste a URL or upload a document (PDF, Markdown, plain text). 2. **Extract** — the AI reads the source and proposes a context patch: additions or updates to your workspace context such as product descriptions, audience segments, or positioning notes. 3. **Review proposed updates** — inspect each proposed change before applying it. 4. **Apply** — confirm the changes you want; rejected sections are discarded. ### Tips - Run your own homepage first to establish a baseline context. - Add competitor URLs to help the AI differentiate your content. - Re-run sources periodically if your product or positioning changes. --- ## Research Tab The Research tab performs a deep, multi-signal analysis of a topic: competitor content, keyword opportunities, content angles, and positioning. Use it before kicking off a new content series or entering a new topic area. ### Workflow 1. **Enter a topic** — describe the subject you want to research (e.g., "TypeScript monorepo tooling"). 2. **Optionally seed with competitors or keywords** — pre-populate the research with known competitors or seed keywords to focus the analysis. 3. **Run research** — the AI performs multi-source analysis (this may take a moment). 4. **Review results** — the report is organized into sections: - **Competitors** — sites ranking for this topic with content gap analysis - **Keyword opportunities** — clusters of related keywords with estimated intent - **Content angles** — suggested post angles and formats - **Positioning** — how to differentiate your content given your workspace context 5. **Apply sections selectively** — choose which sections to merge into your workspace context. You can apply all, some, or none. ### Plan Requirements The Research tab requires a **Starter plan or above**. Free-plan workspaces can use the Source tab but cannot run deep research. --- ## Branding and Plan Tiers When the API returns `brandingRequired: true` in the [`BrandingInfo`](/docs/sdk/types#brandinginfo) object, the "Powered by AutoBlogWriter" attribution footer is shown in your rendered blog components and cannot be disabled. This applies to **free-plan workspaces**. On **Starter, Pro, and Enterprise** plans (`brandingRequired: false`), you can hide the attribution footer: ```tsx <BlogPost post={post} showBranding={false} /> <BlogPostList posts={posts} showBranding={false} /> ``` See [React Components](/docs/sdk/react-components#blogpost) for full branding prop documentation. --- ## Next Steps - [Blog Generation](/docs/core-concepts/blog-generation) — How AutoBlogWriter uses workspace context to generate posts. - [Workspaces](/docs/core-concepts/workspaces) — Managing workspace settings and API keys. - [React Components](/docs/sdk/react-components) — Render generated posts with the SDK. --- # Client The `createAutoBlogWriterClient` function creates a client instance for fetching published blog posts from the AutoBlogWriter API. ## Creating a Client ```typescript import { createAutoBlogWriterClient } from "@autoblogwriter/sdk"; const client = createAutoBlogWriterClient({ apiUrl: "https://api.autoblogwriter.app", apiKey: process.env.AUTOBLOGWRITER_API_KEY!, workspaceSlug: process.env.AUTOBLOGWRITER_WORKSPACE_SLUG!, }); ``` ## Configuration The `AutoBlogWriterClientConfig` interface accepts these options: | Option | Type | Required | Default | Description | |:-------|:-----|:---------|:--------|:------------| | `apiUrl` | `string` | Yes | — | AutoBlogWriter API base URL | | `apiKey` | `string` | Yes | — | Your workspace API key | | `workspaceSlug` | `string` | * | — | Workspace slug for public endpoints | | `workspaceId` | `string` | * | — | Workspace ID (alternative to slug) | | `authMode` | `"bearer" \| "x-api-key"` | No | `"bearer"` | Authentication header format | | `fetch` | `typeof fetch` | No | `globalThis.fetch` | Custom fetch implementation | | `headers` | `Record<string, string>` | No | `{}` | Additional headers for every request | | `timeoutMs` | `number` | No | `10000` | Request timeout in milliseconds | \* You must provide either `workspaceSlug` or `workspaceId`. The `workspaceSlug` is required for fetching posts via the public API. ### Configuration Validation The client validates configuration at creation time and throws a `ConfigError` if: - `apiKey` is missing or empty - Neither `workspaceId` nor `workspaceSlug` is provided - `apiUrl` is missing ```typescript import { createAutoBlogWriterClient, ConfigError } from "@autoblogwriter/sdk"; try { const client = createAutoBlogWriterClient({ apiUrl: "https://api.autoblogwriter.app", apiKey: "", // Will throw workspaceSlug: "my-blog", }); } catch (error) { if (error instanceof ConfigError) { console.error("Invalid config:", error.message); } } ``` ## Methods ### `getPosts(params?)` Fetches a paginated list of published blog posts. ```typescript const { posts, nextCursor } = await client.getPosts({ limit: 10, cursor: undefined, }); ``` **Parameters:** | Param | Type | Default | Description | |:------|:-----|:--------|:------------| | `limit` | `number` | `20` | Number of posts per page | | `cursor` | `string` | — | Page cursor from a previous `nextCursor` | | `cache` | `RequestCache` | — | Fetch API cache mode | | `next` | `{ revalidate?, tags? }` | — | Next.js cache options | **Returns:** `PostsResponse` ```typescript interface PostsResponse { posts: BlogPost[]; nextCursor?: string; // Pass to next call for pagination } ``` **Example: Paginated Fetching** ```typescript // First page const page1 = await client.getPosts({ limit: 10 }); console.log(page1.posts); // First 10 posts // Next page (if available) if (page1.nextCursor) { const page2 = await client.getPosts({ limit: 10, cursor: page1.nextCursor }); console.log(page2.posts); // Next 10 posts } ``` **Example: With Next.js Cache Tags** ```typescript const { posts } = await client.getPosts({ limit: 20, next: { tags: ["blog-posts"] }, }); ``` ### `getPostBySlug(slug, options?)` Fetches a single published post by its URL slug. Returns `null` if not found. ```typescript const post = await client.getPostBySlug("my-first-post"); if (post) { console.log(post.title); console.log(post.content); // Markdown string } ``` **Parameters:** | Param | Type | Description | |:------|:-----|:------------| | `slug` | `string` | The post's URL slug (required) | | `options.cache` | `RequestCache` | Fetch API cache mode | | `options.next` | `{ revalidate?, tags? }` | Next.js cache options | **Returns:** `BlogPost | null` - Returns the full `BlogPost` object if found. - Returns `null` if the post doesn't exist or is not published. - Throws `ApiError` for other HTTP errors (5xx, network failures). **Example: With Error Handling** ```typescript import { ApiError } from "@autoblogwriter/sdk"; try { const post = await client.getPostBySlug("my-post"); if (!post) { // Post not found — show 404 page } } catch (error) { if (error instanceof ApiError) { console.error(`API error ${error.status}: ${error.message}`); } } ``` ### `getSitemapEntries()` Fetches all published posts for sitemap generation. Automatically paginates through all pages. ```typescript const entries = await client.getSitemapEntries(); // [{ slug: "first-post", updatedAt: "2025-01-15T..." }, ...] ``` **Returns:** `Array<{ slug: string; updatedAt: string }>` This method fetches all posts in batches of 100 and returns a flat array of slugs and update timestamps — ideal for passing to `buildSitemap()`. ## BlogPost Type ```typescript interface BlogPost { id: string; title: string; slug: string; excerpt?: string; content: string; // Markdown status: "DRAFT" | "PUBLISHED" | "HIDDEN"; categories?: string[]; images?: { hero?: BlogPostImageAsset; }; relatedPosts?: RelatedPostSummary[]; faq?: BlogFaq; seo?: { title?: string; description?: string; keywords?: string[]; }; metadata?: { canonicalUrl?: string; ogImageUrl?: string; heroImageUrl?: string; readingTimeMinutes?: number; wordCount?: number; }; publishedAt?: string; // ISO 8601 updatedAt: string; // ISO 8601 } ``` ## Debug Mode Enable debug logging by setting the `AUTOBLOGWRITER_DEBUG` environment variable: ```bash AUTOBLOGWRITER_DEBUG=true ``` This logs request/response details to the console: ``` [autoblogwriter] response { method: 'GET', url: '...', status: 200, durationMs: 142 } ``` ## Custom Fetch You can provide a custom `fetch` implementation for testing or environments without native fetch: ```typescript import { createAutoBlogWriterClient } from "@autoblogwriter/sdk"; const client = createAutoBlogWriterClient({ apiUrl: "https://api.autoblogwriter.app", apiKey: "test", workspaceSlug: "demo", fetch: myCustomFetch, }); ``` ## Next Steps - [Environment Config](/docs/sdk/environment-config) — Auto-configure from environment variables. - [Next.js Helpers](/docs/sdk/nextjs-helpers) — Higher-level helpers that wrap the client. - [Error Handling](/docs/sdk/errors) — Understanding SDK error classes. --- # Environment Config The `createAutoBlogWriterFromEnv()` function creates a fully configured SDK instance by reading environment variables. This is the foundation of the `@autoblogwriter/sdk/next` helpers. ## Usage ```typescript import { createAutoBlogWriterFromEnv } from "@autoblogwriter/sdk"; const env = createAutoBlogWriterFromEnv(); // Access the client const { posts } = await env.client.getPosts(); // Access configuration console.log(env.workspaceSlug); // "my-blog" console.log(env.siteUrl); // "https://mysite.com" ``` ## Environment Variables | Variable | Required | Default | Description | |:---------|:---------|:--------|:------------| | `AUTOBLOGWRITER_API_KEY` | Yes | — | Workspace API key | | `AUTOBLOGWRITER_WORKSPACE_SLUG` | Yes | — | Workspace slug | | `AUTOBLOGWRITER_API_URL` | No | `https://api.autoblogwriter.app` | API base URL | | `AUTOBLOGWRITER_WORKSPACE_ID` | No | — | Workspace ID (optional) | | `SITE_URL` | No | `http://localhost:3000` | Your site URL (for sitemap/robots) | | `NEXT_PUBLIC_SITE_URL` | No | — | Falls back to `SITE_URL` | | `AUTOBLOGWRITER_REVALIDATE_SECRET` | No | — | Webhook HMAC secret | ## Return Value `createAutoBlogWriterFromEnv()` returns a `AutoBlogWriterEnvConfig` object: ```typescript interface AutoBlogWriterEnvConfig { client: AutoBlogWriterClient; // Pre-configured client instance workspaceSlug: string; workspaceId?: string; siteUrl: string; revalidateSecret?: string; apiUrl: string; apiKey: string; tags: { posts: string; // e.g. "autoblogwriter:my-blog:posts" post: (slug: string) => string; // e.g. "autoblogwriter:my-blog:post:hello-world" sitemap: string; // e.g. "autoblogwriter:my-blog:sitemap" }; } ``` ## Cache Tags The `tags` object provides pre-formatted Next.js cache tags: ```typescript const env = createAutoBlogWriterFromEnv(); // Use tags for granular cache invalidation const { posts } = await env.client.getPosts({ next: { tags: [env.tags.posts] }, }); const post = await env.client.getPostBySlug("hello-world", { next: { tags: [env.tags.post("hello-world")] }, }); ``` Tag format: - **All posts:** `autoblogwriter:{workspaceSlug}:posts` - **Single post:** `autoblogwriter:{workspaceSlug}:post:{postSlug}` - **Sitemap:** `autoblogwriter:{workspaceSlug}:sitemap` These tags are automatically used by the [Next.js helpers](/docs/sdk/nextjs-helpers) and invalidated by the [revalidation handler](/docs/sdk/webhooks). ## Singleton Pattern `createAutoBlogWriterFromEnv()` uses a singleton — the first call creates the instance, subsequent calls return the same one. This avoids re-reading env vars and re-creating clients on every request. ```typescript // These return the same instance: const a = createAutoBlogWriterFromEnv(); const b = createAutoBlogWriterFromEnv(); console.log(a === b); // true ``` ## When to Use | Scenario | Use | |:---------|:----| | Next.js App Router with standard env vars | `createAutoBlogWriterFromEnv()` (or the `@autoblogwriter/sdk/next` helpers that call it for you) | | Custom configuration or non-Next.js | `createAutoBlogWriterClient()` directly | | Testing | `createAutoBlogWriterClient()` with a mock `fetch` | ## Next Steps - [Next.js Helpers](/docs/sdk/nextjs-helpers) — Higher-level functions that use `createAutoBlogWriterFromEnv()` internally. - [Client Reference](/docs/sdk/client) — Manual client configuration. --- # Next.js Helpers The `@autoblogwriter/sdk/next` entry point provides high-level helper functions designed for Next.js App Router. They read configuration from environment variables automatically and integrate with Next.js caching. :::note[Resilient by default] All helpers in `@autoblogwriter/sdk/next` are now resilient — they log a `console.warn` and return safe defaults when env vars are missing or the API is unavailable, rather than throwing and crashing your page. `fetchBlogPost` calls `notFound()` on all error types (including config errors and API failures) so the page always shows a 404 instead of a 500. If you need the same graceful behaviour in a non-Next.js framework (Remix, Astro, Express), use [`@autoblogwriter/sdk/helpers`](/docs/sdk/helpers) instead. ::: ```typescript import { fetchBlogPosts, fetchBlogPost, generatePostMetadata, generateBlogSitemap, generateBlogRobots, createEnvRevalidateHandler, } from "@autoblogwriter/sdk/next"; ``` ## Data Fetching ### `fetchBlogPosts(options?)` Fetches a paginated list of published posts with automatic Next.js cache tagging. ```typescript import { fetchBlogPosts } from "@autoblogwriter/sdk/next"; // In a server component: const { posts, nextCursor } = await fetchBlogPosts(); ``` **Options:** | Param | Type | Default | Description | |:------|:-----|:--------|:------------| | `limit` | `number` | `20` | Posts per page | | `cursor` | `string` | — | Pagination cursor | **Returns:** `PostsResponse` — `{ posts: BlogPost[], nextCursor?: string }` Cache tag: `autoblogwriter:{workspaceSlug}:posts` **Example: Blog Listing Page** ```tsx // app/blog/page.tsx import Link from "next/link"; import { fetchBlogPosts } from "@autoblogwriter/sdk/next"; import { BlogPostList } from "@autoblogwriter/sdk/react"; export default async function BlogPage() { const { posts } = await fetchBlogPosts(); return <BlogPostList posts={posts} linkComponent={Link} />; } ``` **Example: Pagination** ```tsx // app/blog/page.tsx import Link from "next/link"; import { fetchBlogPosts } from "@autoblogwriter/sdk/next"; import { BlogPostList } from "@autoblogwriter/sdk/react"; type Props = { searchParams: Promise<{ page?: string }> }; export default async function BlogPage({ searchParams }: Props) { const { page } = await searchParams; const { posts, nextCursor } = await fetchBlogPosts({ limit: 10, cursor: page, }); return ( <div> <BlogPostList posts={posts} linkComponent={Link} /> {nextCursor && ( <Link href={`/blog?page=${nextCursor}`}>Next Page</Link> )} </div> ); } ``` ### `fetchBlogPost(slug)` Fetches a single post by slug. Automatically calls `notFound()` if the post doesn't exist **or** if there's a configuration or API error (so pages always show a 404 rather than crashing). ```typescript import { fetchBlogPost } from "@autoblogwriter/sdk/next"; const post = await fetchBlogPost("my-first-post"); // If not found, Next.js renders the nearest not-found.tsx ``` **Parameters:** | Param | Type | Description | |:------|:-----|:------------| | `slug` | `string` | The post's URL slug | **Returns:** `BlogPost` — never returns `null` (calls `notFound()` on missing post, config errors, and API errors) Cache tag: `autoblogwriter:{workspaceSlug}:post:{slug}` **Example: Post Page** ```tsx // app/blog/[slug]/page.tsx import { fetchBlogPost, generatePostMetadata } from "@autoblogwriter/sdk/next"; import { BlogPost } from "@autoblogwriter/sdk/react"; type Props = { params: Promise<{ slug: string }> }; export async function generateMetadata({ params }: Props) { const { slug } = await params; return generatePostMetadata(slug); } export default async function PostPage({ params }: Props) { const { slug } = await params; const post = await fetchBlogPost(slug); return <BlogPost post={post} />; } ``` ## SEO Metadata ### `generatePostMetadata(slug)` Generates Next.js `Metadata` object from a post's SEO fields. Returns `{}` if the post doesn't exist. ```typescript import { generatePostMetadata } from "@autoblogwriter/sdk/next"; import type { Metadata } from "next"; export async function generateMetadata({ params }: Props): Promise<Metadata> { const { slug } = await params; return generatePostMetadata(slug); } ``` The returned metadata includes: | Field | Source | |:------|:-------| | `title` | `post.seo.title` or `post.title` | | `description` | `post.seo.description` or `post.excerpt` | | `keywords` | `post.seo.keywords` | | `alternates.canonical` | `post.metadata.canonicalUrl` | | `openGraph.type` | `"article"` | | `openGraph.title` | Same as `title` | | `openGraph.description` | Same as `description` | | `openGraph.images` | `post.metadata.ogImageUrl` | | `openGraph.publishedTime` | `post.publishedAt` | | `openGraph.modifiedTime` | `post.updatedAt` | | `twitter.card` | `"summary_large_image"` (if OG image) or `"summary"` | | `twitter.title` | Same as `title` | | `twitter.description` | Same as `description` | | `twitter.images` | `post.metadata.ogImageUrl` | ### `generateBlogSitemap()` Generates a Next.js-compatible sitemap array from all published posts. ```typescript // app/sitemap.ts import { generateBlogSitemap } from "@autoblogwriter/sdk/next"; export const revalidate = 300; export default function sitemap() { return generateBlogSitemap(); } ``` Returns an array of `{ url, lastModified }` entries using the `SITE_URL` env var and `/blog` as the route prefix. Returns `[]` on config or API errors (prevents a 500 sitemap page). ### `generateBlogRobots()` Generates a Next.js-compatible robots.txt configuration. Returns a minimal `allow: "/"` fallback on config errors (prevents a 500 robots page). ```typescript // app/robots.ts import { generateBlogRobots } from "@autoblogwriter/sdk/next"; export default function robots() { return generateBlogRobots(); } ``` Returns: ```json { "rules": [{ "userAgent": "*", "allow": "/" }], "sitemap": "https://yoursite.com/sitemap.xml" } ``` ## Route Handlers ### `createEnvRevalidateHandler()` Creates a Next.js API route handler for receiving AutoBlogWriter webhooks. See [Webhooks & Revalidation](/docs/sdk/webhooks) for full details. ```typescript // app/api/autoblogwriter/revalidate/route.ts import { createEnvRevalidateHandler } from "@autoblogwriter/sdk/next"; export const runtime = "nodejs"; export const POST = createEnvRevalidateHandler(); ``` ## Next Steps - [React Components](/docs/sdk/react-components) — Render posts with built-in components. - [Webhooks & Revalidation](/docs/sdk/webhooks) — Deep dive into cache invalidation. - [Client Reference](/docs/sdk/client) — Manual client usage for non-Next.js apps. --- # Framework-Agnostic Helpers The `@autoblogwriter/sdk/helpers` entry point provides the same high-level helpers as `@autoblogwriter/sdk/next`, but with **no Next.js dependencies**. Use it with Remix, Astro, Express, Vite SSR, or any other Node.js server environment. ```typescript import { fetchPosts, fetchPost, buildBlogSitemap, buildBlogRobots, buildJsonLdScript, } from "@autoblogwriter/sdk/helpers"; ``` :::note[Always graceful] Every function in this module catches all errors internally. On a missing env var or API failure it logs a `console.warn` and returns a safe empty default — it **never throws**. `fetchPost` returns `null` instead of calling `notFound()`, so your framework's 404 handling stays in your hands. ::: ## Environment Variables | Env Variable | Required | Description | |:-------------|:---------|:------------| | `AUTOBLOGWRITER_API_KEY` | yes | Workspace API key | | `AUTOBLOGWRITER_WORKSPACE_SLUG` | yes | Workspace slug for content APIs | | `AUTOBLOGWRITER_API_URL` | no | Defaults to `https://api.autoblogwriter.app` | | `AUTOBLOGWRITER_WORKSPACE_ID` | no | Optional workspace ID | | `SITE_URL` or `NEXT_PUBLIC_SITE_URL` | no | Defaults to `http://localhost:3000` | ## Data Fetching ### `fetchPosts(options?)` Fetches a paginated list of published posts. ```typescript import { fetchPosts } from "@autoblogwriter/sdk/helpers"; const { posts, nextCursor } = await fetchPosts({ limit: 20 }); ``` **Options:** | Param | Type | Default | Description | |:------|:-----|:--------|:------------| | `limit` | `number` | `20` | Posts per page | | `cursor` | `string` | — | Pagination cursor | | `category` | `string` | — | Filter by category slug | **Returns:** `PostsResponse` — `{ posts: BlogPost[], nextCursor?: string, pagination? }` **On error:** returns `{ posts: [], nextCursor: undefined, pagination: { page: 1, limit: 20, total: 0, totalPages: 0, hasMore: false } }` and logs a warning. **Example: Remix loader** ```typescript // app/routes/blog._index.tsx import { fetchPosts } from "@autoblogwriter/sdk/helpers"; import type { LoaderFunctionArgs } from "@remix-run/node"; export async function loader({ request }: LoaderFunctionArgs) { const url = new URL(request.url); const cursor = url.searchParams.get("cursor") ?? undefined; const { posts, nextCursor } = await fetchPosts({ limit: 10, cursor }); return { posts, nextCursor }; } ``` **Example: Astro page** ```astro --- // src/pages/blog/index.astro import { fetchPosts } from "@autoblogwriter/sdk/helpers"; const { posts } = await fetchPosts({ limit: 20 }); --- <ul> {posts.map(post => ( <li><a href={`/blog/${post.slug}`}>{post.title}</a></li> ))} </ul> ``` ### `fetchPost(slug)` Fetches a single post by slug. Returns `null` if the post is not found or if there's any error. ```typescript import { fetchPost } from "@autoblogwriter/sdk/helpers"; const post = await fetchPost("my-first-post"); if (!post) { // handle not-found in your framework } ``` **Parameters:** | Param | Type | Description | |:------|:-----|:------------| | `slug` | `string` | The post's URL slug | **Returns:** `BlogPost | null` — `null` if not found or on any error. **On error:** logs a warning and returns `null`. Your code is responsible for showing a 404. **Example: Remix loader with 404** ```typescript // app/routes/blog.$slug.tsx import { fetchPost } from "@autoblogwriter/sdk/helpers"; import { json, redirect } from "@remix-run/node"; export async function loader({ params }: { params: { slug: string } }) { const post = await fetchPost(params.slug); if (!post) throw new Response("Not Found", { status: 404 }); return json({ post }); } ``` **Example: Astro with 404** ```astro --- // src/pages/blog/[slug].astro import { fetchPost } from "@autoblogwriter/sdk/helpers"; const { slug } = Astro.params; const post = await fetchPost(slug!); if (!post) return Astro.redirect("/404"); --- <h1>{post.title}</h1> ``` ## SEO ### `buildBlogSitemap()` Builds a sitemap array from all published posts. Returns plain objects — no Next.js types required. ```typescript import { buildBlogSitemap } from "@autoblogwriter/sdk/helpers"; const entries = await buildBlogSitemap(); // [{ url: "https://yoursite.com/blog/my-post", lastModified: "2025-01-01T00:00:00.000Z" }, ...] ``` **Returns:** `Array<{ url: string; lastModified?: string }>` **On error:** returns `[]` and logs a warning. **Example: Remix sitemap route** ```typescript // app/routes/sitemap[.xml].tsx import { buildBlogSitemap } from "@autoblogwriter/sdk/helpers"; export async function loader() { const entries = await buildBlogSitemap(); const xml = [ '<?xml version="1.0" encoding="UTF-8"?>', '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">', ...entries.map(e => `<url><loc>${e.url}</loc>${e.lastModified ? `<lastmod>${e.lastModified}</lastmod>` : ""}</url>` ), "</urlset>", ].join("\n"); return new Response(xml, { headers: { "Content-Type": "application/xml" } }); } ``` **Example: Astro sitemap** ```typescript // src/pages/sitemap.xml.ts import { buildBlogSitemap } from "@autoblogwriter/sdk/helpers"; import type { APIRoute } from "astro"; export const GET: APIRoute = async () => { const entries = await buildBlogSitemap(); const xml = [ '<?xml version="1.0" encoding="UTF-8"?>', '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">', ...entries.map(e => `<url><loc>${e.url}</loc>${e.lastModified ? `<lastmod>${e.lastModified}</lastmod>` : ""}</url>` ), "</urlset>", ].join("\n"); return new Response(xml, { headers: { "Content-Type": "application/xml" } }); }; ``` ### `buildBlogRobots()` Builds a robots configuration object. ```typescript import { buildBlogRobots } from "@autoblogwriter/sdk/helpers"; const robots = buildBlogRobots(); // { rules: [{ userAgent: "*", allow: "/" }], sitemap: "https://yoursite.com/sitemap.xml" } ``` **Returns:** `{ rules: MetadataRouteRobotsRule | MetadataRouteRobotsRule[]; sitemap?: string | string[] }` **On error:** returns `{ rules: [{ userAgent: "*", allow: "/" }] }` and logs a warning. **Example: Remix robots route** ```typescript // app/routes/robots[.txt].tsx import { buildBlogRobots } from "@autoblogwriter/sdk/helpers"; export async function loader() { const { rules, sitemap } = buildBlogRobots(); const lines = [ "User-agent: *", "Allow: /", sitemap ? `Sitemap: ${sitemap}` : "", ].filter(Boolean); return new Response(lines.join("\n"), { headers: { "Content-Type": "text/plain" } }); } ``` ### `buildJsonLdScript(post)` Re-exported from `@autoblogwriter/sdk`. Generates a `<script type="application/ld+json">` string for a blog post. Returns `null` if the post has no JSON-LD metadata. ```typescript import { buildJsonLdScript } from "@autoblogwriter/sdk/helpers"; const jsonLd = buildJsonLdScript(post); ``` ## When to use `./helpers` vs `./next` | | `@autoblogwriter/sdk/helpers` | `@autoblogwriter/sdk/next` | |:---|:---|:---| | **Framework** | Any Node.js (Remix, Astro, Express, …) | Next.js App Router only | | **ISR cache tags** | No | Yes (`next: { tags: [...] }`) | | **On config/API error** | Returns safe default, logs warning | Returns safe default, logs warning | | **`fetchPost` not-found** | Returns `null` — you handle the 404 | Calls `notFound()` — Next.js renders `not-found.tsx` | | **Next.js peer dep** | Not required | Required | :::tip If you only need one entry point and your project uses Next.js, prefer `@autoblogwriter/sdk/next` — it handles `notFound()` automatically and adds ISR cache tags so pages revalidate via webhooks. Use `@autoblogwriter/sdk/helpers` for every other framework or when you want explicit control over 404 handling. ::: ## Next Steps - [Next.js Helpers](/docs/sdk/nextjs-helpers) — Next.js-specific helpers with ISR cache tagging. - [Webhooks & Revalidation](/docs/sdk/webhooks) — Cache invalidation when posts are published. - [Client Reference](/docs/sdk/client) — Manual client usage for advanced scenarios. --- # React Components The `@autoblogwriter/sdk/react` entry point provides ready-to-use React components for rendering blog content. All components work with both server and client components. ```typescript import { BlogFaq, BlogPost, BlogPostList, Markdown, RelatedPosts, Branding, AutoBlogPost, AutoBlogPostList } from "@autoblogwriter/sdk/react"; ``` ## `<BlogPostList>` Renders a list of blog post cards with titles, dates, and excerpts. ### Basic Usage ```tsx import Link from "next/link"; import { fetchBlogPosts } from "@autoblogwriter/sdk/next"; import { BlogPostList } from "@autoblogwriter/sdk/react"; export default async function BlogPage() { const { posts } = await fetchBlogPosts(); return <BlogPostList posts={posts} linkComponent={Link} />; } ``` ### Props | Prop | Type | Default | Description | |:-----|:-----|:--------|:------------| | `posts` | `BlogPost[]` | — | Array of posts to display | | `title` | `string` | `"Latest posts"` | Section heading. Pass empty string to hide. | | `routePrefix` | `string` | `"/blog"` | URL prefix for post links (e.g., `/blog/my-post`) | | `linkComponent` | `React.ComponentType` | `<a>` | Link component (use Next.js `Link` for client navigation) | | `className` | `string` | `"ba-listing"` | CSS class on the wrapper `<section>` | | `theme` | `"light" \| "dark"` | — | Applies a theme via `data-ba-theme` on the wrapper. | | `layout` | `"card" \| "list"` | `"card"` | Layout style for the list. | | `postsPerPage` | `number` | — | Page size for controlled pagination. | | `page` | `number` | `1` | Current page (1-based) for controlled pagination. | | `onPageChange` | `(page: number) => void` | — | Handler invoked by pagination buttons. | | `paginationHref` | `(page: number) => string` | — | Render pagination links instead of buttons (server-safe). | | `showPagination` | `boolean` | `true` (when `postsPerPage` is set) | Whether to render pagination UI. | | `imageComponent` | `React.ComponentType` | — | Custom image component for card images (e.g. Next.js `Image`). | | `imageProps` | `Record<string, unknown>` | — | Extra props forwarded to the card image component. | | `renderCard` | `(post, href) => ReactNode` | — | Custom card renderer (overrides default card) | | `branding` | `BrandingInfo` | inferred from posts | Explicit branding tier info. Inferred from `posts[0].branding` if not set. | | `showBranding` | `boolean` | `true` | Show/hide the "Powered by" footer. On free plans this is forced `true`. | | `brandingLinkComponent` | `React.ComponentType` | `linkComponent` | Link component used specifically for the branding link. | ### Custom Link Component Pass Next.js `Link` for client-side navigation: ```tsx import Link from "next/link"; <BlogPostList posts={posts} linkComponent={Link} /> ``` ### Custom Route Prefix ```tsx <BlogPostList posts={posts} routePrefix="/articles" /> // Links will be /articles/my-post instead of /blog/my-post ``` ### Theme ```tsx <BlogPostList posts={posts} theme="light" /> <BlogPostList posts={posts} theme="dark" /> ``` ### Card vs List Layout ```tsx <BlogPostList posts={posts} layout="card" /> <BlogPostList posts={posts} layout="list" /> ``` ### Image Component Pass a custom image component (e.g., Next.js `Image`) for optimized card images: ```tsx import Image from "next/image"; <BlogPostList posts={posts} imageComponent={Image} imageProps={{ width: 1200, height: 675 }} /> ``` ### Controlled Pagination (Client-Side) Use `onPageChange` for client-side pagination with React state: ```tsx "use client"; import { useState } from "react"; import { BlogPostList } from "@autoblogwriter/sdk/react"; export function BlogList({ posts }) { const [page, setPage] = useState(1); return ( <BlogPostList posts={posts} postsPerPage={6} page={page} onPageChange={setPage} /> ); } ``` ### Server-Friendly Pagination Use `paginationHref` for server-side rendering without client state: ```tsx import Link from "next/link"; import { fetchBlogPosts } from "@autoblogwriter/sdk/next"; import { BlogPostList } from "@autoblogwriter/sdk/react"; type Props = { searchParams?: { page?: string }; }; export default async function BlogPage({ searchParams }: Props) { const { posts } = await fetchBlogPosts(); const page = Math.max(1, Number(searchParams?.page ?? "1")); return ( <BlogPostList posts={posts} linkComponent={Link} layout="card" postsPerPage={6} page={page} paginationHref={(nextPage) => `/blog?page=${nextPage}`} /> ); } ``` ### Custom Card Rendering Override the default card layout with `renderCard`: ```tsx <BlogPostList posts={posts} linkComponent={Link} renderCard={(post, href) => ( <Link href={href} className="custom-card"> <h3>{post.title}</h3> <p>{post.excerpt}</p> {post.metadata?.readingTimeMinutes && ( <span>{post.metadata.readingTimeMinutes} min read</span> )} </Link> )} /> ``` ### Default HTML Structure ```html <section class="ba-listing"> <h1 class="ba-listing-title">Latest posts</h1> <div class="ba-posts"> <article class="ba-post-card"> <h2 class="ba-post-card-title"> <a href="/blog/my-post" class="ba-post-card-link">My Post</a> </h2> <p class="ba-post-card-meta"> <time datetime="2025-01-15T10:00:00.000Z">January 15, 2025</time> </p> <div class="ba-post-card-excerpt"> <!-- Rendered excerpt markdown --> </div> </article> <!-- More cards... --> </div> </section> ``` ## `<BlogPost>` Renders a full blog post article with title, date, reading time, and markdown content. ### Basic Usage ```tsx import { fetchBlogPost } from "@autoblogwriter/sdk/next"; import { BlogPost } from "@autoblogwriter/sdk/react"; export default async function PostPage({ params }: Props) { const { slug } = await params; const post = await fetchBlogPost(slug); return <BlogPost post={post} />; } ``` ### Props | Prop | Type | Default | Description | |:-----|:-----|:--------|:------------| | `post` | `BlogPost` | — | The post to render | | `showTitle` | `boolean` | `true` | Show the post title as an `<h1>` | | `showDate` | `boolean` | `true` | Show publication date | | `showFaq` | `boolean` | `true` | Render FAQ after content when `post.faq` exists. | | `showRelatedPosts` | `boolean` | `true` | Render related posts when `post.relatedPosts` exists. | | `className` | `string` | `"ba-post"` | CSS class on the wrapper `<article>` | | `theme` | `"light" \| "dark"` | — | Applies a theme via `data-ba-theme` on the wrapper. | | `showHeroImage` | `boolean` | `false` | Render `post.images?.hero?.url` under the title when available. | | `heroImageAlt` | `string` | `post.title` | Optional `alt` text for the hero image. | | `linkComponent` | `React.ComponentType` | — | Custom link component for related posts (e.g. Next.js `Link`). | | `imageComponent` | `React.ComponentType` | — | Custom image component for the hero image (e.g. Next.js `Image`). | | `imageProps` | `Record<string, unknown>` | — | Extra props forwarded to the hero image component. | | `renderContent` | `(content: string) => ReactNode` | — | Custom content renderer (overrides default Markdown) | | `renderFaq` | `(faq, post) => ReactNode` | — | Override FAQ rendering, or return `null` to hide it. | | `renderRelatedPosts` | `(posts, post) => ReactNode` | — | Override related posts rendering, or return `null` to hide it. | | `branding` | `BrandingInfo` | from API | Explicit branding tier info. Overrides `post.branding`. | | `showBranding` | `boolean` | `true` | Show/hide the "Powered by" footer. On free plans this is forced `true`. | | `brandingLinkComponent` | `React.ComponentType` | `linkComponent` | Link component used specifically for the branding link. | ### Hero Image Display a hero image from `post.images.hero`: ```tsx <BlogPost post={post} showHeroImage /> ``` With a custom image component (e.g., Next.js `Image`): ```tsx import Image from "next/image"; <BlogPost post={post} showHeroImage imageComponent={Image} imageProps={{ width: 1200, height: 630 }} /> ``` ### Theme ```tsx <BlogPost post={post} theme="light" /> <BlogPost post={post} theme="dark" /> ``` ### Hiding Title and Date ```tsx <BlogPost post={post} showTitle={false} showDate={false} /> ``` ### Custom FAQ Rendering Override the default FAQ section: ```tsx <BlogPost post={post} renderFaq={(faq, post) => ( <div className="my-faq"> <h2>FAQ about {post.title}</h2> {faq.items.map((item, i) => ( <details key={i}> <summary>{item.question}</summary> <p>{item.answer}</p> </details> ))} </div> )} /> ``` ### Custom Related Posts Rendering Override the default related posts section: ```tsx <BlogPost post={post} renderRelatedPosts={(posts, post) => ( <aside> <h3>More like "{post.title}"</h3> <ul> {posts.map((p) => ( <li key={p.id}><a href={`/blog/${p.slug}`}>{p.title}</a></li> ))} </ul> </aside> )} /> ``` ### Next.js Image and Link Integration ```tsx import Link from "next/link"; import Image from "next/image"; <BlogPost post={post} showHeroImage linkComponent={Link} imageComponent={Image} imageProps={{ width: 1200, height: 630 }} /> ``` ### Custom Content Rendering Replace the built-in markdown renderer with your own (e.g., for syntax highlighting with `react-markdown` or `next-mdx-remote`): ```tsx import ReactMarkdown from "react-markdown"; <BlogPost post={post} renderContent={(content) => ( <ReactMarkdown className="prose">{content}</ReactMarkdown> )} /> ``` ### Default HTML Structure ```html <article class="ba-post"> <h1 class="ba-post-title">My Post Title</h1> <p class="ba-post-meta"> <time datetime="2025-01-15T10:00:00.000Z">January 15, 2025</time> <span class="ba-post-meta-sep"> · </span> <span class="ba-post-reading-time">5 min read</span> </p> <div class="ba-markdown"> <!-- Rendered markdown HTML --> </div> </article> ``` ## `<BlogFaq>` Renders an FAQ section from post FAQ data. ### Basic Usage ```tsx import { BlogFaq } from "@autoblogwriter/sdk/react"; <BlogFaq faq={post.faq!} /> ``` ### Props | Prop | Type | Default | Description | |:-----|:-----|:--------|:------------| | `faq` | `BlogFaq \| BlogFaqItem[]` | — | FAQ data to render. | | `title` | `string` | `"Frequently Asked Questions"` | Section heading text. | | `className` | `string` | `"ba-faq"` | CSS class on the FAQ section. | ### Custom Title ```tsx <BlogFaq faq={post.faq!} title="Common Questions" /> ``` ### With BlogPost The `<BlogPost>` component automatically renders `<BlogFaq>` when `post.faq` exists. Control it with `showFaq`: ```tsx <BlogPost post={post} showFaq={false} /> ``` ## `<RelatedPosts>` Renders a related-posts list from `post.relatedPosts`. ### Basic Usage ```tsx import { RelatedPosts } from "@autoblogwriter/sdk/react"; <RelatedPosts posts={post.relatedPosts ?? []} /> ``` ### Props | Prop | Type | Default | Description | |:-----|:-----|:--------|:------------| | `posts` | `RelatedPostSummary[]` | — | Related post summaries to render. | | `title` | `string` | `"Related Posts"` | Section heading text. | | `routePrefix` | `string` | `"/blog"` | URL prefix for links (`{routePrefix}/{slug}`). | | `linkComponent` | `React.ComponentType` | `<a>` | Custom link component (e.g. Next.js `Link`). | | `className` | `string` | `"ba-related"` | CSS class on the section wrapper. | | `renderItem` | `(post, href) => ReactNode` | — | Override default item rendering. | ### Custom Title and Route Prefix ```tsx <RelatedPosts posts={post.relatedPosts ?? []} title="Keep reading" routePrefix="/articles" /> ``` ### With Next.js Link ```tsx import Link from "next/link"; <RelatedPosts posts={post.relatedPosts ?? []} linkComponent={Link} /> ``` ### Custom Item Rendering ```tsx <RelatedPosts posts={post.relatedPosts ?? []} renderItem={(post, href) => ( <a href={href} className="my-related-link"> <strong>{post.title}</strong> {post.excerpt && <p>{post.excerpt}</p>} </a> )} /> ``` ### With BlogPost The `<BlogPost>` component automatically renders `<RelatedPosts>` when `post.relatedPosts` exists. Control it with `showRelatedPosts`: ```tsx <BlogPost post={post} showRelatedPosts={false} /> ``` ## `<Markdown>` Renders a markdown string to HTML using the built-in renderer. ### Basic Usage ```tsx import { Markdown } from "@autoblogwriter/sdk/react"; <Markdown source="# Hello World\n\nThis is **bold** text." /> ``` ### Props | Prop | Type | Default | Description | |:-----|:-----|:--------|:------------| | `source` | `string \| null` | — | Markdown string to render. Returns `null` if empty. | | `className` | `string` | `"ba-markdown"` | CSS class on the wrapper `<div>` | ### Custom Class ```tsx <Markdown source={post.content} className="prose dark:prose-invert" /> ``` ### How It Works The `<Markdown>` component uses the built-in `renderMarkdownToHtml()` function which: - Converts headings (`# H1` through `###### H6`) - Renders **bold**, *italic*, and `inline code` - Converts links `[text](url)` with `target="_blank"` and `rel="noreferrer"` - Converts images `![alt](src)` with proper attributes - Wraps code blocks in `<pre><code>` - Escapes HTML to prevent XSS attacks - Has zero dependencies If you need more advanced markdown features (tables, footnotes, syntax highlighting), use `renderContent` on `<BlogPost>` with a library like `react-markdown`. ## Standalone Markdown Renderer The `renderMarkdownToHtml()` function is also available directly: ```typescript import { renderMarkdownToHtml } from "@autoblogwriter/sdk"; const html = renderMarkdownToHtml("# Hello\n\nWorld"); // "<h1>Hello</h1>\n<p>World</p>" ``` ## `<Branding>` A standalone "Powered by AutoBlogWriter" attribution component. This is rendered automatically inside `<BlogPost>` and `<BlogPostList>` when branding is required, but you can also use it directly. ### Basic Usage ```tsx import { Branding } from "@autoblogwriter/sdk/react"; <Branding /> ``` ### Props | Prop | Type | Default | Description | |:-----|:-----|:--------|:------------| | `className` | `string` | `"ba-branding"` | CSS class on the wrapper element. | | `linkComponent` | `React.ComponentType` | `<a>` | Custom link component for the attribution link. | | `href` | `string` | AutoBlogWriter site URL | Override the attribution link target. | | `label` | `string` | `"AutoBlogWriter"` | Override the brand name text. | | `showLabel` | `boolean` | `true` | Show the "Powered by" label alongside the brand name. | ### Example ```tsx import Link from "next/link"; import { Branding } from "@autoblogwriter/sdk/react"; <Branding linkComponent={Link} className="my-branding" /> ``` > **Free plan note:** On free-tier workspaces, branding is required and cannot be hidden. Attempting to set `showBranding={false}` on `<BlogPost>` or `<BlogPostList>` will have no effect if the API response indicates `brandingRequired: true`. Upgrade to Pro+ to remove attribution. --- ## `<AutoBlogPost>` An async React Server Component that fetches and renders a single blog post in one JSX call. Requires Next.js App Router (or any environment that supports async components). ### Basic Usage ```tsx import { AutoBlogPost } from "@autoblogwriter/sdk/react"; export default async function PostPage({ params }: Props) { const { slug } = await params; return <AutoBlogPost slug={slug} />; } ``` ### Props | Prop | Type | Default | Description | |:-----|:-----|:--------|:------------| | `slug` | `string` | — | **(Required)** The post slug to fetch. | | `post` | `BlogPost` | — | Optional pre-fetched post. If provided, skips the fetch. | | `fetchPost` | `(slug: string) => Promise<BlogPost \| null>` | — | Custom fetch function. Overrides the default client. | | `client` | `AutoBlogWriterClient` | env client | Custom client instance to use for fetching. | | ...BlogPostProps | — | — | All props from `<BlogPost>` are supported. | ### Usage Examples **Basic:** ```tsx import { AutoBlogPost } from "@autoblogwriter/sdk/react"; export default async function PostPage({ params }: Props) { const { slug } = await params; return <AutoBlogPost slug={slug} />; } ``` **With a custom client:** ```tsx import { createAutoBlogWriterClient } from "@autoblogwriter/sdk"; import { AutoBlogPost } from "@autoblogwriter/sdk/react"; const client = createAutoBlogWriterClient({ apiUrl: "...", apiKey: "..." }); export default async function PostPage({ params }: Props) { const { slug } = await params; return <AutoBlogPost slug={slug} client={client} showHeroImage />; } ``` **With a custom fetcher:** ```tsx import { AutoBlogPost } from "@autoblogwriter/sdk/react"; async function myFetchPost(slug: string) { // your own data-fetching logic } export default async function PostPage({ params }: Props) { const { slug } = await params; return <AutoBlogPost slug={slug} fetchPost={myFetchPost} />; } ``` **With a pre-fetched post (e.g., from parallel data loading):** ```tsx import { fetchBlogPost } from "@autoblogwriter/sdk/next"; import { AutoBlogPost } from "@autoblogwriter/sdk/react"; export default async function PostPage({ params }: Props) { const { slug } = await params; const post = await fetchBlogPost(slug); return <AutoBlogPost slug={slug} post={post} showHeroImage />; } ``` --- ## `<AutoBlogPostList>` An async React Server Component that fetches and renders a list of blog posts in one JSX call. ### Basic Usage ```tsx import { AutoBlogPostList } from "@autoblogwriter/sdk/react"; export default async function BlogPage() { return <AutoBlogPostList />; } ``` ### Props | Prop | Type | Default | Description | |:-----|:-----|:--------|:------------| | `posts` | `BlogPost[]` | — | Optional pre-fetched posts. If provided, skips the fetch. | | `fetchPosts` | `() => Promise<PostsResponse>` | — | Custom fetch function. Overrides the default client. | | `client` | `AutoBlogWriterClient` | env client | Custom client instance to use for fetching. | | `limit` | `number` | — | Maximum number of posts to fetch. | | `cursor` | `string` | — | Pagination cursor for fetching a specific page. | | `category` | `string` | — | Filter posts by category slug. | | ...BlogPostListProps | — | — | All props from `<BlogPostList>` are supported. | ### Usage Examples **Basic:** ```tsx import { AutoBlogPostList } from "@autoblogwriter/sdk/react"; export default async function BlogPage() { return <AutoBlogPostList />; } ``` **With limit and category filter:** ```tsx import { AutoBlogPostList } from "@autoblogwriter/sdk/react"; export default async function BlogPage() { return ( <AutoBlogPostList limit={6} category="engineering" layout="card" /> ); } ``` **With pre-fetched posts:** ```tsx import { fetchBlogPosts } from "@autoblogwriter/sdk/next"; import { AutoBlogPostList } from "@autoblogwriter/sdk/react"; export default async function BlogPage() { const { posts } = await fetchBlogPosts({ limit: 20 }); return <AutoBlogPostList posts={posts} layout="list" />; } ``` --- ## Next Steps - [Styling](/docs/sdk/styling) — Customize the look of these components with CSS. - [Next.js Helpers](/docs/sdk/nextjs-helpers) — Fetch data for the components. --- # Styling The SDK ships with default styles optimized for dark themes. All visual properties are controlled via CSS custom properties, making it easy to adapt to your site's design. ## Importing Styles Import the stylesheet in your root layout: ```tsx // app/layout.tsx import "@autoblogwriter/sdk/styles.css"; ``` Or import it in your global CSS: ```css @import "@autoblogwriter/sdk/styles.css"; ``` ## CSS Custom Properties Override these variables in your own CSS to customize the theme: ### Colors | Variable | Default | Description | |:---------|:--------|:------------| | `--ba-color-bg` | `#0c0d10` | Page background | | `--ba-color-text` | `#f2f4f8` | Primary text | | `--ba-color-text-muted` | `rgba(242,244,248,0.6)` | Muted text (dates, meta) | | `--ba-color-text-secondary` | `rgba(242,244,248,0.8)` | Secondary text (excerpts) | | `--ba-color-text-content` | `rgba(242,244,248,0.9)` | Main content text | | `--ba-color-link` | `#6cf` | Link color | | `--ba-color-link-hover` | `#8df` | Link hover color | | `--ba-color-border` | `rgba(255,255,255,0.08)` | Card borders | | `--ba-color-border-hover` | `rgba(255,255,255,0.12)` | Card border on hover | | `--ba-color-card-bg` | `rgba(255,255,255,0.02)` | Card background | | `--ba-color-card-bg-hover` | `rgba(255,255,255,0.04)` | Card background on hover | | `--ba-color-code-bg` | `rgba(255,255,255,0.1)` | Inline code background | | `--ba-color-pre-bg` | `rgba(255,255,255,0.05)` | Code block background | | `--ba-color-img-shadow` | `rgba(0,0,0,0.3)` | Image drop shadow | ### Layout & Typography | Variable | Default | Description | |:---------|:--------|:------------| | `--ba-radius` | `8px` | Card border radius | | `--ba-radius-sm` | `6px` | Code block/image border radius | | `--ba-radius-xs` | `3px` | Inline code border radius | | `--ba-max-width` | `720px` | Maximum content width | | `--ba-font-sans` | `system-ui, -apple-system, ...` | Body font stack | | `--ba-font-mono` | `ui-monospace, SFMono-Regular, ...` | Code font stack | ## Light Theme Example ```css :root { --ba-color-bg: #ffffff; --ba-color-text: #1a1a2e; --ba-color-text-muted: rgba(26, 26, 46, 0.5); --ba-color-text-secondary: rgba(26, 26, 46, 0.7); --ba-color-text-content: rgba(26, 26, 46, 0.85); --ba-color-link: #2563eb; --ba-color-link-hover: #1d4ed8; --ba-color-border: rgba(0, 0, 0, 0.08); --ba-color-border-hover: rgba(0, 0, 0, 0.15); --ba-color-card-bg: rgba(0, 0, 0, 0.02); --ba-color-card-bg-hover: rgba(0, 0, 0, 0.04); --ba-color-code-bg: rgba(0, 0, 0, 0.06); --ba-color-pre-bg: rgba(0, 0, 0, 0.03); --ba-color-img-shadow: rgba(0, 0, 0, 0.1); } ``` ## Dark/Light Toggle Use CSS `prefers-color-scheme` or a class-based toggle: ```css /* Automatic system preference */ @media (prefers-color-scheme: light) { :root { --ba-color-bg: #ffffff; --ba-color-text: #1a1a2e; /* ... other light values ... */ } } /* Or class-based toggle */ .light { --ba-color-bg: #ffffff; --ba-color-text: #1a1a2e; /* ... */ } ``` ## Theme Attribute (`data-ba-theme`) When you pass `theme="light"` or `theme="dark"` to `<BlogPostList>` or `<BlogPost>`, the component sets a `data-ba-theme` attribute on its wrapper element. You can use this attribute to scope theme overrides to specific components: ```css [data-ba-theme="light"] { --ba-color-bg: #ffffff; --ba-color-text: #0c0d10; --ba-color-text-muted: rgba(12, 13, 16, 0.6); --ba-color-text-secondary: rgba(12, 13, 16, 0.8); --ba-color-text-content: rgba(12, 13, 16, 0.9); --ba-color-link: #0a66c2; --ba-color-link-hover: #0c74db; --ba-color-border: rgba(12, 13, 16, 0.12); --ba-color-card-bg: rgba(12, 13, 16, 0.03); --ba-color-card-bg-hover: rgba(12, 13, 16, 0.06); --ba-color-code-bg: rgba(12, 13, 16, 0.08); --ba-color-pre-bg: rgba(12, 13, 16, 0.04); } ``` This is useful when your site uses a dark background but you want a specific blog section to render in light mode (or vice versa), without affecting global CSS variables. ## CSS Class Reference ### Blog Listing | Class | Element | |:------|:--------| | `.ba-listing` | Listing wrapper `<section>` | | `.ba-listing-title` | Section heading `<h1>` | | `.ba-posts` | Post cards container (flex column) | | `.ba-posts--card` | Card layout modifier | | `.ba-posts--list` | List layout modifier | | `.ba-post-card` | Individual card `<article>` | | `.ba-post-card-title` | Card title `<h2>` | | `.ba-post-card-link` | Title link `<a>` | | `.ba-post-card-meta` | Date/meta paragraph | | `.ba-post-card-excerpt` | Excerpt content | | `.ba-post-card-image` | Card image | | `.ba-pagination` | Pagination wrapper | | `.ba-pagination-btn` | Pagination button | | `.ba-pagination-text` | Pagination text (e.g., "Page 1 of 3") | ### Blog Post | Class | Element | |:------|:--------| | `.ba-post` | Post wrapper `<article>` | | `.ba-post-title` | Post title `<h1>` | | `.ba-post-hero` | Hero image | | `.ba-post-meta` | Date and reading time | | `.ba-post-meta-sep` | Separator between date and reading time | | `.ba-post-reading-time` | Reading time span | ### FAQ | Class | Element | |:------|:--------| | `.ba-faq` | FAQ section wrapper | ### Related Posts | Class | Element | |:------|:--------| | `.ba-related` | Related posts section wrapper | | `.ba-related-title` | Related posts heading | | `.ba-related-list` | Related posts list | | `.ba-related-item` | Related posts item | | `.ba-related-link` | Related posts link | ### Markdown Content | Class | Element | |:------|:--------| | `.ba-markdown` | Markdown wrapper `<div>` | | `.ba-markdown p` | Paragraphs | | `.ba-markdown h1`–`h6` | Headings | | `.ba-markdown a` | Links | | `.ba-markdown code` | Inline code | | `.ba-markdown pre` | Code blocks | | `.ba-markdown img` | Images (centered, responsive) | | `.ba-markdown ul`, `ol` | Lists | ## Using Without Default Styles If you prefer to write your own styles from scratch, don't import the CSS file. Instead, use the `className` props on each component: ```tsx <BlogPostList posts={posts} className="my-blog-list" linkComponent={Link} /> <BlogPost post={post} className="my-blog-post" /> <Markdown source={post.content} className="my-markdown prose dark:prose-invert" /> ``` ## Tailwind CSS Integration The components work with Tailwind's `@apply` or by passing Tailwind class names via the `className` prop: ```tsx // Use Tailwind's typography plugin <Markdown source={post.content} className="prose dark:prose-invert max-w-none" /> ``` Or extend the default styles: ```css .ba-post-card { @apply hover:shadow-lg transition-shadow; } ``` ## Next Steps - [React Components](/docs/sdk/react-components) — Component props and customization. - [Quickstart](/docs/getting-started/quickstart) — Full setup example. --- # SEO Helpers The SDK provides utilities for generating sitemaps, robots.txt, and Next.js metadata — the building blocks for strong search engine optimization. ## Metadata Builder ### `buildNextMetadata(post)` Converts a `BlogPost` into a Next.js-compatible `Metadata` object. ```typescript import { buildNextMetadata } from "@autoblogwriter/sdk"; const metadata = buildNextMetadata(post); ``` **Input:** A `BlogPost` object. **Output:** A `NextMetadata` object with: ```typescript { title: "SEO title or post title", description: "Meta description or excerpt", keywords: ["keyword1", "keyword2"], alternates: { canonical: "https://..." }, openGraph: { type: "article", title: "...", description: "...", images: [{ url: "https://..." }], publishedTime: "2025-01-15T...", modifiedTime: "2025-01-15T...", }, twitter: { card: "summary_large_image", title: "...", description: "...", images: ["https://..."], }, } ``` **Field Resolution:** | Metadata Field | Primary Source | Fallback | |:---------------|:--------------|:---------| | `title` | `post.seo.title` | `post.title` | | `description` | `post.seo.description` | `post.excerpt` | | `keywords` | `post.seo.keywords` | Omitted if empty | | `canonical` | `post.metadata.canonicalUrl` | Omitted if not set | | `og:image` | `post.metadata.ogImageUrl` | Omitted if not set | | `twitter:card` | — | `summary_large_image` if OG image exists, otherwise `summary` | **Usage in Next.js:** ```tsx // app/blog/[slug]/page.tsx import { buildNextMetadata } from "@autoblogwriter/sdk"; import type { Metadata } from "next"; export async function generateMetadata({ params }: Props): Promise<Metadata> { const { slug } = await params; const post = await client.getPostBySlug(slug); if (!post) return {}; return buildNextMetadata(post); } ``` Or use the higher-level helper: ```tsx import { generatePostMetadata } from "@autoblogwriter/sdk/next"; export async function generateMetadata({ params }: Props): Promise<Metadata> { const { slug } = await params; return generatePostMetadata(slug); } ``` ## JSON-LD Helper ### `buildJsonLdScript(post)` Generates a `<script type="application/ld+json">` element for a post's structured data. > **Note:** `<BlogPost>` automatically injects JSON-LD when `post.metadata.jsonLd` is present — no extra configuration needed. Use `buildJsonLdScript()` only when you need manual injection in the `<head>` via Next.js `generateMetadata`. ```typescript import { buildJsonLdScript } from "@autoblogwriter/sdk"; const script = buildJsonLdScript(post); // Returns null if post.metadata.jsonLd is not present ``` **Output:** ```typescript { type: "application/ld+json", id: "post-jsonld", dangerouslySetInnerHTML: { __html: "..." }, } // or null ``` **Usage in Next.js `generateMetadata`:** ```tsx // app/blog/[slug]/page.tsx import { buildNextMetadata, buildJsonLdScript } from "@autoblogwriter/sdk"; import type { Metadata } from "next"; export async function generateMetadata({ params }: Props): Promise<Metadata> { const { slug } = await params; const post = await client.getPostBySlug(slug); if (!post) return {}; const metadata = buildNextMetadata(post); const jsonLdScript = buildJsonLdScript(post); return { ...metadata, ...(jsonLdScript ? { other: { scripts: [jsonLdScript] } } : {}), }; } ``` ## Sitemap Builder ### `buildSitemap(options)` Generates a Next.js-compatible sitemap array from post entries. ```typescript import { buildSitemap } from "@autoblogwriter/sdk"; const sitemap = buildSitemap({ siteUrl: "https://mysite.com", routePrefix: "/blog", entries: [ { slug: "first-post", updatedAt: "2025-01-15T10:00:00.000Z" }, { slug: "second-post", updatedAt: "2025-01-20T10:00:00.000Z" }, ], }); ``` **Result:** ```json [ { "url": "https://mysite.com/blog/first-post", "lastModified": "2025-01-15T10:00:00.000Z" }, { "url": "https://mysite.com/blog/second-post", "lastModified": "2025-01-20T10:00:00.000Z" } ] ``` **Options:** | Option | Type | Default | Description | |:-------|:-----|:--------|:------------| | `siteUrl` | `string` | — | Your site's base URL | | `routePrefix` | `string` | `"/blog"` | URL path prefix for blog posts | | `entries` | `SitemapEntry[]` | — | Array of `{ slug, updatedAt? }` | **Usage in Next.js:** ```typescript // app/sitemap.ts import { buildSitemap, createAutoBlogWriterClient } from "@autoblogwriter/sdk"; export const revalidate = 300; export default async function sitemap() { const client = createAutoBlogWriterClient({ /* ... */ }); const entries = await client.getSitemapEntries(); return buildSitemap({ siteUrl: "https://mysite.com", routePrefix: "/blog", entries, }); } ``` Or use the higher-level helper: ```typescript // app/sitemap.ts import { generateBlogSitemap } from "@autoblogwriter/sdk/next"; export const revalidate = 300; export default function sitemap() { return generateBlogSitemap(); } ``` ## Robots Builder ### `buildRobots(options)` Generates a Next.js-compatible robots.txt configuration. ```typescript import { buildRobots } from "@autoblogwriter/sdk"; const robots = buildRobots({ siteUrl: "https://mysite.com", sitemapPath: "/sitemap.xml", }); ``` **Result:** ```json { "rules": [{ "userAgent": "*", "allow": "/" }], "sitemap": "https://mysite.com/sitemap.xml" } ``` **Options:** | Option | Type | Default | Description | |:-------|:-----|:--------|:------------| | `siteUrl` | `string` | — | Your site's base URL | | `sitemapPath` | `string` | `"/sitemap.xml"` | Path to your sitemap | **Usage in Next.js:** ```typescript // app/robots.ts import { buildRobots } from "@autoblogwriter/sdk"; export default function robots() { return buildRobots({ siteUrl: "https://mysite.com", }); } ``` Or use the higher-level helper: ```typescript // app/robots.ts import { generateBlogRobots } from "@autoblogwriter/sdk/next"; export default function robots() { return generateBlogRobots(); } ``` ## Complete SEO Setup Here's a full example combining all SEO features: ```typescript // app/sitemap.ts import { generateBlogSitemap } from "@autoblogwriter/sdk/next"; export const revalidate = 300; export default function sitemap() { return generateBlogSitemap(); } ``` ```typescript // app/robots.ts import { generateBlogRobots } from "@autoblogwriter/sdk/next"; export default function robots() { return generateBlogRobots(); } ``` ```tsx // app/blog/[slug]/page.tsx import { fetchBlogPost, generatePostMetadata } from "@autoblogwriter/sdk/next"; import { BlogPost } from "@autoblogwriter/sdk/react"; export async function generateMetadata({ params }: Props) { const { slug } = await params; return generatePostMetadata(slug); } export default async function PostPage({ params }: Props) { const { slug } = await params; const post = await fetchBlogPost(slug); return <BlogPost post={post} />; } ``` This gives you: - Dynamic sitemap with all published posts - Standard robots.txt allowing all crawlers - Per-page metadata with OpenGraph and Twitter Cards - Automatic cache revalidation via tags ## Next Steps - [Webhooks & Revalidation](/docs/sdk/webhooks) — Keep your SEO data fresh. - [React Components](/docs/sdk/react-components) — Render your posts. --- # Webhooks & Revalidation When you publish, update, or unpublish a post in AutoBlogWriter, a webhook is sent to your configured endpoints. The SDK provides handlers that automatically verify the signature and revalidate your Next.js cache. ## How It Works ``` 1. Content published in AutoBlogWriter 2. AutoBlogWriter signs payload with HMAC-SHA256 3. POST request sent to your webhook URL 4. SDK handler verifies signature 5. Next.js cache tags and paths are revalidated 6. Updated content appears on your site ``` ## Quick Setup ### 1. Set Environment Variable ```bash AUTOBLOGWRITER_REVALIDATE_SECRET=your_webhook_secret_here ``` ### 2. Create the Route Handler ```typescript // app/api/autoblogwriter/revalidate/route.ts import { createEnvRevalidateHandler } from "@autoblogwriter/sdk/next"; export const runtime = "nodejs"; export const POST = createEnvRevalidateHandler(); ``` ### 3. Configure in Dashboard Go to your workspace **Settings > Webhooks** and add: - **URL:** `https://yoursite.com/api/autoblogwriter/revalidate` - **Secret:** Same as your `AUTOBLOGWRITER_REVALIDATE_SECRET` env var That's it — your site will now automatically update when content changes. ## Webhook Payload AutoBlogWriter sends a JSON payload: ```json { "workspaceSlug": "my-blog", "postSlug": "my-first-post", "event": "post.published", "ts": "2025-01-15T10:00:00.000Z" } ``` | Field | Type | Description | |:------|:-----|:------------| | `workspaceSlug` | `string` | Your workspace identifier | | `postSlug` | `string \| null` | The affected post's slug (null for workspace-wide events) | | `event` | `string` | Event type (e.g., `post.published`, `post.updated`) | | `ts` | `string` | ISO 8601 timestamp | ## Signature Verification The payload is signed with HMAC-SHA256 using your revalidation secret. The signature is sent in the `X-AutoBlogWriter-Signature` header as a hex string. ### Automatic (Recommended) The `createEnvRevalidateHandler()` and `createRevalidateRouteHandler()` functions verify signatures automatically. ### Manual Verification ```typescript import { verifyWebhookSignature } from "@autoblogwriter/sdk"; const isValid = verifyWebhookSignature({ rawBody: requestBodyString, signature: request.headers.get("x-autoblogwriter-signature"), secret: process.env.AUTOBLOGWRITER_REVALIDATE_SECRET!, }); if (!isValid) { return new Response("Unauthorized", { status: 401 }); } ``` The `verifyWebhookSignature` function: - Accepts the signature with or without a `sha256=` prefix. - Uses `crypto.timingSafeEqual` to prevent timing attacks. - Returns `false` for missing or malformed signatures (never throws). ## What Gets Revalidated The default handler revalidates these paths and tags: ### Paths | Path | Type | When | |:-----|:-----|:-----| | `/blog` | page | Always | | `/blog/{postSlug}` | page | When `postSlug` is provided | | `/sitemap.xml` | route | Always | | `/robots.txt` | route | Always | ### Cache Tags | Tag | When | |:----|:-----| | `autoblogwriter:{workspaceSlug}:posts` | Always | | `autoblogwriter:{workspaceSlug}:post:{postSlug}` | When `postSlug` is provided | | `autoblogwriter:{workspaceSlug}:sitemap` | Always | ## Timestamp Validation The handler rejects payloads older than **5 minutes** (configurable) to prevent replay attacks. If your server's clock is significantly off, increase the skew tolerance. ## Advanced: Custom Route Handler For full control over revalidation behavior: ```typescript import { createRevalidateRouteHandler } from "@autoblogwriter/sdk"; import { revalidatePath, revalidateTag } from "next/cache"; export const runtime = "nodejs"; export const POST = createRevalidateRouteHandler({ secret: process.env.AUTOBLOGWRITER_REVALIDATE_SECRET!, // Allow payloads up to 10 minutes old (default: 300 seconds) allowedSkewSeconds: 600, // Next.js cache functions revalidatePath, revalidateTag, // Custom paths to revalidate revalidatePaths: (payload) => { const paths = ["/blog", "/sitemap.xml"]; if (payload.postSlug) { paths.push(`/blog/${payload.postSlug}`); paths.push(`/articles/${payload.postSlug}`); // Custom route } return paths; }, // Custom tags to revalidate revalidateTags: (payload) => { const tags = [`blog:${payload.workspaceSlug}:all`]; if (payload.postSlug) { tags.push(`blog:${payload.workspaceSlug}:${payload.postSlug}`); } return tags; }, }); ``` ### `CreateRevalidateRouteHandlerOptions` | Option | Type | Default | Description | |:-------|:-----|:--------|:------------| | `secret` | `string` | — | **Required.** HMAC-SHA256 secret | | `allowedSkewSeconds` | `number` | `300` | Max timestamp age in seconds | | `revalidatePath` | `(path, type?) => void` | — | Next.js `revalidatePath` function | | `revalidateTag` | `(tag) => void` | — | Next.js `revalidateTag` function | | `revalidatePaths` | `(payload) => string[]` | Default paths | Custom path builder | | `revalidateTags` | `(payload) => string[]` | Default tags | Custom tag builder | ## Response Format ### Success (200) ```json { "ok": true, "event": "post.published", "revalidated": { "paths": ["/blog", "/blog/my-post", "/sitemap.xml", "/robots.txt"], "tags": ["autoblogwriter:my-blog:posts", "autoblogwriter:my-blog:post:my-post", "autoblogwriter:my-blog:sitemap"] } } ``` ### Errors | Status | Error | Cause | |:-------|:------|:------| | `401` | Invalid webhook signature | Secret mismatch or tampered payload | | `400` | Invalid JSON / missing fields | Malformed webhook body | | `409` | Timestamp outside allowed skew | Stale or replayed payload | | `500` | Failed to revalidate cache | Error calling `revalidatePath`/`revalidateTag` | ## Static Export (`output: "export"`) If you're using Next.js static export (`output: "export"` in `next.config.js`), API route handlers are **not supported** — the revalidation webhook endpoint won't work. Instead, use your hosting provider's **auto-deploy webhook** so that AutoBlogWriter triggers a full rebuild when content changes. Since static exports cache aggressively, make sure your build command clears the Next.js cache before building. ### Example: Render 1. In Render, copy your service's **Deploy Hook URL** (Settings → Build & Deploy → Deploy Hook). 2. In the AutoBlogWriter dashboard, go to **Settings → Webhooks** and add the Render deploy hook URL. 3. Set your Render **Build Command** to clear the cache before building: ```bash rm -rf .next/cache && npm run build ``` This ensures every deploy starts fresh — no stale cached pages from a previous build. ### Other Hosting Providers Most hosting platforms that support static sites offer a similar deploy hook mechanism: | Provider | Deploy Hook | |:---------|:------------| | Render | Settings → Deploy Hook URL | | Netlify | Build hooks (Settings → Build & Deploy) | | Vercel | Deploy hooks (Project Settings → Git) | | Cloudflare Pages | Deploy hooks (Settings → Builds & Deployments) | The pattern is the same: point AutoBlogWriter's webhook at your provider's deploy hook, and ensure the build command clears any cache (`rm -rf .next/cache && npm run build`). ## Next Steps - [Next.js Helpers](/docs/sdk/nextjs-helpers) — Data fetching functions that set cache tags. - [Environment Config](/docs/sdk/environment-config) — How the SDK reads your revalidation secret. --- # Error Handling The SDK provides a hierarchy of typed error classes for different failure modes. All errors extend the base `AutoBlogWriterError` class. ## Error Hierarchy ``` AutoBlogWriterError ├── ConfigError — Invalid configuration ├── ApiError — API request failures │ └── NotFoundError — 404 responses ``` ## `AutoBlogWriterError` Base class for all SDK errors. ```typescript import { AutoBlogWriterError } from "@autoblogwriter/sdk"; try { await client.getPosts(); } catch (error) { if (error instanceof AutoBlogWriterError) { console.error("SDK error:", error.message); console.error("Cause:", error.causeError); // Original error, if any } } ``` | Property | Type | Description | |:---------|:-----|:------------| | `message` | `string` | Human-readable error message | | `causeError` | `unknown` | Original underlying error | ## `ConfigError` Thrown when the client is created with invalid configuration. ```typescript import { createAutoBlogWriterClient, ConfigError } from "@autoblogwriter/sdk"; try { const client = createAutoBlogWriterClient({ apiUrl: "https://api.autoblogwriter.app", apiKey: "", // Empty — throws ConfigError workspaceSlug: "demo", }); } catch (error) { if (error instanceof ConfigError) { console.error(error.message); // "apiKey is required to authenticate with AutoBlogWriter" } } ``` ### Common Config Errors | Message | Cause | |:--------|:------| | `apiKey is required to authenticate with AutoBlogWriter` | Empty or missing API key | | `Provide either workspaceId or workspaceSlug` | Neither workspace identifier provided | | `apiUrl is required` | Missing API URL | | `workspaceSlug is required to call the AutoBlogWriter public API` | Calling `getPosts()` without a workspace slug | ## `ApiError` Thrown when the API returns a non-OK response or a network error occurs. ```typescript import { ApiError } from "@autoblogwriter/sdk"; try { await client.getPosts(); } catch (error) { if (error instanceof ApiError) { console.error("Status:", error.status); // HTTP status code (0 for network errors) console.error("Code:", error.code); // API error code (e.g., "NOT_FOUND") console.error("Message:", error.message); console.error("Details:", error.details); // Additional error context } } ``` | Property | Type | Description | |:---------|:-----|:------------| | `status` | `number` | HTTP status code. `0` for network failures. `408` for timeouts. | | `code` | `string \| undefined` | API error code from the response body | | `details` | `unknown` | Additional error details from the API | | `message` | `string` | Human-readable error message | ### Common API Errors | Status | Scenario | Message | |:-------|:---------|:--------| | `0` | Network failure | `Network request to AutoBlogWriter failed: ...` | | `400` | Invalid slug | `slug is required` | | `401` | Invalid API key | `Unauthorized` | | `404` | Post not found | `Not found` | | `408` | Timeout | `Request timed out after 10000ms` | | `500` | Server error | Varies | ## `NotFoundError` A convenience subclass of `ApiError` with a default status of `404`. ```typescript import { NotFoundError } from "@autoblogwriter/sdk"; try { // ... } catch (error) { if (error instanceof NotFoundError) { // Render a 404 page } } ``` ## 404 Handling The `getPostBySlug` method treats 404 responses as a normal result rather than an error: ```typescript const post = await client.getPostBySlug("nonexistent-post"); // Returns null — does NOT throw ``` The `fetchBlogPost` helper from `@autoblogwriter/sdk/next` goes further and calls Next.js `notFound()`: ```typescript import { fetchBlogPost } from "@autoblogwriter/sdk/next"; const post = await fetchBlogPost("nonexistent-post"); // Automatically calls notFound() — renders nearest not-found.tsx ``` ## Timeout Handling The SDK enforces a configurable request timeout (default: 10 seconds): ```typescript const client = createAutoBlogWriterClient({ apiUrl: "https://api.autoblogwriter.app", apiKey: "...", workspaceSlug: "demo", timeoutMs: 5000, // 5 seconds }); try { await client.getPosts(); } catch (error) { if (error instanceof ApiError && error.status === 408) { console.error("Request timed out"); } } ``` ## Error Handling Patterns ### Server Component (Next.js) ```tsx // app/blog/[slug]/page.tsx import { fetchBlogPost } from "@autoblogwriter/sdk/next"; import { BlogPost } from "@autoblogwriter/sdk/react"; export default async function PostPage({ params }: Props) { const { slug } = await params; // fetchBlogPost calls notFound() automatically for 404s // Other errors will bubble up to error.tsx const post = await fetchBlogPost(slug); return <BlogPost post={post} />; } ``` ```tsx // app/blog/[slug]/error.tsx "use client"; export default function Error({ error, reset }: { error: Error; reset: () => void }) { return ( <div> <h2>Something went wrong</h2> <p>{error.message}</p> <button onClick={reset}>Try again</button> </div> ); } ``` ### Manual Client ```typescript import { createAutoBlogWriterClient, ApiError, ConfigError } from "@autoblogwriter/sdk"; const client = createAutoBlogWriterClient({ /* ... */ }); try { const { posts } = await client.getPosts(); } catch (error) { if (error instanceof ConfigError) { // Fix your configuration } else if (error instanceof ApiError) { if (error.status === 401) { // Invalid API key } else if (error.status === 408) { // Timeout — retry or increase timeoutMs } else { // Other API error } } } ``` ## Next Steps - [Client Reference](/docs/sdk/client) — Client configuration and methods. - [Environment Config](/docs/sdk/environment-config) — Environment variable validation. --- # TypeScript Types All types are exported from the main `@autoblogwriter/sdk` entry point. ```typescript import type { AutoBlogWriterClientConfig, BlogPost, BlogPostImageAsset, BlogFaq, BlogFaqItem, RelatedPostSummary, PostsResponse, PaginatedList, SitemapEntry, BuildSitemapOptions, BuildRobotsOptions, MetadataRouteSitemap, MetadataRouteRobots, AutoBlogWriterRevalidatePayload, RevalidatePathFn, RevalidateTagFn, FetchRequestOptions, FetchNextConfig, BrandingInfo, PlanId, } from "@autoblogwriter/sdk"; ``` ## Branding ### `PlanId` Identifies the workspace subscription plan tier. ```typescript type PlanId = "free" | "starter" | "pro" | "enterprise"; ``` ### `BrandingInfo` Carries the branding configuration returned by the API. Used to determine whether the "Powered by AutoBlogWriter" attribution footer is required. ```typescript interface BrandingInfo { planId: PlanId; brandingRequired: boolean; } ``` `brandingRequired` is `true` on the `free` plan. On `starter`, `pro`, and `enterprise` plans it is `false`, allowing you to hide the attribution footer via `showBranding={false}` on `<BlogPost>` or `<BlogPostList>`. --- ## Client Configuration ### `AutoBlogWriterClientConfig` ```typescript interface AutoBlogWriterClientConfig { apiUrl: string; apiKey: string; workspaceId?: string; workspaceSlug?: string; authMode?: "bearer" | "x-api-key"; fetch?: typeof fetch; headers?: Record<string, string>; timeoutMs?: number; } ``` ### `AuthMode` ```typescript type AuthMode = "bearer" | "x-api-key"; ``` ## Blog Post ### `BlogPost` The primary data model returned by the API. ```typescript interface BlogPost { id: string; title: string; slug: string; excerpt?: string; content: string; status: "DRAFT" | "PUBLISHED" | "HIDDEN"; categories?: string[]; images?: { hero?: BlogPostImageAsset; }; relatedPosts?: RelatedPostSummary[]; faq?: BlogFaq; seo?: { title?: string; description?: string; keywords?: string[]; }; metadata?: { canonicalUrl?: string; ogImageUrl?: string; heroImageUrl?: string; readingTimeMinutes?: number; wordCount?: number; jsonLd?: Record<string, unknown>; }; branding?: BrandingInfo; publishedAt?: string; updatedAt: string; } ``` `metadata.jsonLd` contains structured data (Schema.org JSON-LD) generated for the post. `<BlogPost>` automatically injects a `<script type="application/ld+json">` tag when this field is present — no manual configuration required. For manual injection in `<head>`, see [`buildJsonLdScript()`](/docs/sdk/seo#buildjsonldscriptpost). `branding` reflects the workspace's plan tier and whether the attribution footer is required. See [`BrandingInfo`](#brandinginfo). ### `RelatedPostSummary` A lightweight summary of a related post, used in `BlogPost.relatedPosts`. ```typescript interface RelatedPostSummary { id: string; title: string; slug: string; excerpt?: string; categories?: string[]; publishedAt?: string | null; } ``` ### `BlogPostImageAsset` Represents an image asset attached to a post. ```typescript interface BlogPostImageAsset { status: string; style: string; width: number; height: number; url: string; createdAt?: string; source: string; } ``` ### `BlogFaq` FAQ data attached to a post. ```typescript interface BlogFaq { items: BlogFaqItem[]; } ``` ### `BlogFaqItem` A single FAQ question/answer pair. ```typescript interface BlogFaqItem { question: string; answer: string; } ``` ### `PostsResponse` Returned by `client.getPosts()` and `fetchBlogPosts()`. ```typescript interface PostsResponse { posts: BlogPost[]; nextCursor?: string; branding?: BrandingInfo; } ``` `branding` is returned at the response level so you can determine attribution requirements without inspecting individual posts — useful when rendering `<BlogPostList>` which may display many posts at once. ### `PaginatedList<T>` The raw paginated response from the API (used internally by the client). ```typescript interface PaginatedList<T> { items: T[]; pagination: { page: number; limit: number; total: number; totalPages: number; hasMore: boolean; }; } ``` ## SEO Types ### `SitemapEntry` Input for the sitemap builder. ```typescript interface SitemapEntry { slug: string; updatedAt?: string; } ``` ### `BuildSitemapOptions` ```typescript interface BuildSitemapOptions { siteUrl: string; routePrefix?: string; entries: SitemapEntry[]; } ``` ### `MetadataRouteSitemap` Output of `buildSitemap()`. Compatible with Next.js `MetadataRoute.Sitemap`. ```typescript type MetadataRouteSitemap = Array<{ url: string; lastModified?: string; }>; ``` ### `BuildRobotsOptions` ```typescript interface BuildRobotsOptions { siteUrl: string; sitemapPath?: string; } ``` ### `MetadataRouteRobots` Output of `buildRobots()`. Compatible with Next.js `MetadataRoute.Robots`. ```typescript interface MetadataRouteRobots { rules: MetadataRouteRobotsRule | MetadataRouteRobotsRule[]; sitemap?: string | string[]; } interface MetadataRouteRobotsRule { userAgent: string; allow?: string | string[]; disallow?: string | string[]; } ``` ## Webhook Types ### `AutoBlogWriterRevalidatePayload` The JSON body of a AutoBlogWriter webhook request. ```typescript interface AutoBlogWriterRevalidatePayload { workspaceSlug: string; postSlug?: string | null; event: string; ts: string; [key: string]: unknown; } ``` ### `RevalidatePathFn` Signature for the Next.js `revalidatePath` function. ```typescript type RevalidatePathFn = ( path: string, type?: "page" | "layout" | "route", ) => void | Promise<void>; ``` ### `RevalidateTagFn` Signature for the Next.js `revalidateTag` function. ```typescript type RevalidateTagFn = (tag: string) => void | Promise<void>; ``` ## Fetch Options ### `FetchRequestOptions` Optional caching parameters for client methods. ```typescript interface FetchRequestOptions { cache?: RequestCache; next?: FetchNextConfig; } ``` ### `FetchNextConfig` Next.js-specific fetch extensions. ```typescript interface FetchNextConfig { revalidate?: number | false; tags?: string[]; } ``` ## Environment Config ### `AutoBlogWriterEnvConfig` Returned by `createAutoBlogWriterFromEnv()`. ```typescript interface AutoBlogWriterEnvConfig { client: AutoBlogWriterClient; workspaceSlug: string; workspaceId?: string; siteUrl: string; revalidateSecret?: string; apiUrl: string; apiKey: string; tags: { posts: string; post: (slug: string) => string; sitemap: string; }; } ``` ## Client Interface ### `AutoBlogWriterClient` The client instance returned by `createAutoBlogWriterClient()`. ```typescript interface AutoBlogWriterClient { getPosts(params?: { limit?: number; cursor?: string; } & FetchRequestOptions): Promise<PostsResponse>; getPostBySlug( slug: string, options?: FetchRequestOptions, ): Promise<BlogPost | null>; getSitemapEntries(): Promise<Array<{ slug: string; updatedAt: string }>>; } ``` ## Error Types ### `ApiErrorDetails` ```typescript interface ApiErrorDetails { status: number; code?: string; details?: unknown; } ``` ## Next.js Helper Types ### `FetchBlogPostsOptions` ```typescript interface FetchBlogPostsOptions { limit?: number; cursor?: string; } ``` ### `NextMetadata` The metadata object returned by `buildNextMetadata()`. ```typescript interface NextMetadata { title?: string; description?: string; keywords?: string[]; alternates?: { canonical?: string }; openGraph?: { type?: string; title?: string; description?: string; images?: Array<{ url: string }>; publishedTime?: string; modifiedTime?: string; }; twitter?: { card?: string; title?: string; description?: string; images?: string[]; }; } ``` ## Next Steps - [Client Reference](/docs/sdk/client) — How to use these types with the client. - [Error Handling](/docs/sdk/errors) — Error class details. --- # Next.js Integration This page covers the complete Next.js integration pattern — from project setup to production deployment. ## Overview The AutoBlogWriter SDK is designed for the Next.js App Router. It leverages: - **Server Components** — All data fetching happens server-side (API keys never reach the browser). - **Cache Tags** — Granular cache invalidation via `next/cache`. - **Metadata API** — Automatic SEO metadata generation. - **Route Handlers** — Webhook endpoints. ## Project Structure A typical AutoBlogWriter + Next.js project: ``` app/ ├── layout.tsx # Import styles here ├── blog/ │ ├── page.tsx # Blog listing │ └── [slug]/ │ └── page.tsx # Individual post ├── api/ │ └── autoblogwriter │ └── revalidate/ │ └── route.ts # Webhook handler ├── sitemap.ts # Dynamic sitemap ├── robots.ts # Robots.txt └── not-found.tsx # 404 page ``` ## Full Implementation ### Root Layout ```tsx // app/layout.tsx import "@autoblogwriter/sdk/styles.css"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body>{children}</body> </html> ); } ``` ### Blog Listing Page ```tsx // app/blog/page.tsx import Link from "next/link"; import { fetchBlogPosts } from "@autoblogwriter/sdk/next"; import { BlogPostList } from "@autoblogwriter/sdk/react"; export default async function BlogPage() { const { posts } = await fetchBlogPosts(); return ( <main style={{ maxWidth: 720, margin: "0 auto", padding: "2rem" }}> <BlogPostList posts={posts} linkComponent={Link} /> </main> ); } ``` ### Blog Post Page ```tsx // app/blog/[slug]/page.tsx import { fetchBlogPost, generatePostMetadata } from "@autoblogwriter/sdk/next"; import { BlogPost } from "@autoblogwriter/sdk/react"; import type { Metadata } from "next"; type Props = { params: Promise<{ slug: string }> }; export async function generateMetadata({ params }: Props): Promise<Metadata> { const { slug } = await params; return generatePostMetadata(slug); } export default async function PostPage({ params }: Props) { const { slug } = await params; const post = await fetchBlogPost(slug); return ( <main style={{ maxWidth: 720, margin: "0 auto", padding: "2rem" }}> <BlogPost post={post} /> </main> ); } ``` ### Revalidation Webhook ```typescript // app/api/autoblogwriter/revalidate/route.ts import { createEnvRevalidateHandler } from "@autoblogwriter/sdk/next"; export const runtime = "nodejs"; export const POST = createEnvRevalidateHandler(); ``` ### Sitemap ```typescript // app/sitemap.ts import { generateBlogSitemap } from "@autoblogwriter/sdk/next"; export const revalidate = 300; export default function sitemap() { return generateBlogSitemap(); } ``` ### Robots ```typescript // app/robots.ts import { generateBlogRobots } from "@autoblogwriter/sdk/next"; export default function robots() { return generateBlogRobots(); } ``` ## Environment Variables ```bash # .env.local AUTOBLOGWRITER_API_KEY=ba_pk_your_api_key AUTOBLOGWRITER_WORKSPACE_SLUG=your-workspace SITE_URL=https://yoursite.com AUTOBLOGWRITER_REVALIDATE_SECRET=your_webhook_secret ``` ## Static Generation (SSG) To pre-render all blog posts at build time, add `generateStaticParams`: ```tsx // app/blog/[slug]/page.tsx import { fetchBlogPosts, fetchBlogPost, generatePostMetadata } from "@autoblogwriter/sdk/next"; import { BlogPost } from "@autoblogwriter/sdk/react"; export async function generateStaticParams() { const { posts } = await fetchBlogPosts({ limit: 100 }); return posts.map((post) => ({ slug: post.slug })); } export async function generateMetadata({ params }: Props) { const { slug } = await params; return generatePostMetadata(slug); } export default async function PostPage({ params }: Props) { const { slug } = await params; const post = await fetchBlogPost(slug); return <BlogPost post={post} />; } ``` Combined with the revalidation webhook, this gives you the best of both worlds: fast static pages that update automatically. ## Caching Strategy The SDK sets Next.js cache tags on every request: | Request | Cache Tag | |:--------|:----------| | `fetchBlogPosts()` | `autoblogwriter:{slug}:posts` | | `fetchBlogPost("hello")` | `autoblogwriter:{slug}:post:hello` | | Sitemap generation | `autoblogwriter:{slug}:sitemap` | When a webhook arrives, the revalidation handler invalidates the relevant tags, and Next.js regenerates only the affected pages. ## Custom Fetch Options Override caching behavior per-request using the lower-level client: ```typescript import { createAutoBlogWriterFromEnv } from "@autoblogwriter/sdk"; const { client, tags } = createAutoBlogWriterFromEnv(); // Disable cache for this request const { posts } = await client.getPosts({ cache: "no-store" }); // Set a revalidation interval const post = await client.getPostBySlug("hello", { next: { revalidate: 60 }, // Revalidate every 60 seconds }); // Add custom tags const { posts: tagged } = await client.getPosts({ next: { tags: [tags.posts, "custom-tag"] }, }); ``` ## Deployment ### Vercel No special configuration needed. Environment variables are set in the Vercel dashboard. ### Self-Hosted Ensure your server supports: - Node.js 18+ - The `crypto` module (for webhook verification) - Environment variables are accessible at runtime ### Static Export If you're using `output: "export"` in `next.config.js`, API route handlers (like the revalidation webhook) are not available. Use your hosting provider's auto-deploy webhook instead, and clear the Next.js cache on each build: ```bash rm -rf .next/cache && npm run build ``` See [Webhooks & Revalidation — Static Export](/docs/sdk/webhooks#static-export-output-export) for full details and provider-specific setup. ## Next Steps - [React Components](/docs/sdk/react-components) — Customize component rendering. - [Webhooks & Revalidation](/docs/sdk/webhooks) — Deep dive into cache invalidation. - [Styling](/docs/sdk/styling) — Theme the components. --- # Integrations Connect AutoBlogWriter to the CMS you already use. ## Available now - **[WordPress](/docs/integrations/wordpress)** — Publish posts automatically to your WordPress site using Application Passwords. Supports scheduled posts, drafts, and Yoast SEO fields. - **[Shopify](/docs/integrations/shopify)** — Publish blog posts to Shopify using a custom app and client credentials. Supports drafts, scheduled posts, and featured images. ## Coming soon - **Webflow** — CMS sync for marketing pages and editorial workflows. - **Wix** — Simplified publishing with metadata and sitemap updates. - **Framer** — Fast publishing for modern, component-driven sites. --- ## WordPress Integration AutoBlogWriter can publish posts directly to your WordPress site using the WordPress REST API and Application Passwords — no plugins required. ## Prerequisites - A self-hosted WordPress site or WordPress.com Business plan (REST API must be accessible over HTTPS) - WordPress 5.6 or later (Application Passwords were added in 5.6) - An admin or editor account with permission to create and publish posts ## Step 1 — Generate an Application Password Application Passwords let AutoBlogWriter authenticate to your site without using your main login password. 1. Log in to your WordPress dashboard. 2. Go to **Users → Profile** (or **Users → All Users → Edit** for another user). 3. Scroll down to the **Application Passwords** section. 4. Enter a name for the password (e.g. `AutoBlogWriter`) and click **Add New Application Password**. 5. Copy the generated password immediately — it will not be shown again. > The password is displayed in groups separated by spaces (e.g. `AbCd EfGh IjKl MnOp QrSt UvWx`). Copy it exactly as shown, spaces included — AutoBlogWriter handles normalization. ## Step 2 — Connect in AutoBlogWriter 1. Open your workspace and go to **Settings → Integrations**. 2. Click **Connect WordPress**. 3. Fill in the three fields: | Field | What to enter | |---|---| | **Site URL** | Your site's root URL, e.g. `https://myblog.com` | | **Username** | The WordPress username that owns the Application Password | | **Application Password** | The password you generated in Step 1 | 4. Click **Connect**. AutoBlogWriter will verify the credentials and save the integration. ## Step 3 — Test the Connection After connecting, click **Test Connection** on the WordPress card. This sends a live request to your site's REST API and confirms that posts can be created. If the test fails, you will see the error message returned by WordPress. Common causes are listed in [Troubleshooting](#troubleshooting) below. ## Publishing behavior - **Publish now** — creates and immediately publishes a post on your WordPress site. - **Schedule** — creates the post in WordPress as a scheduled post at the chosen time. - **Draft** — creates the post in WordPress with `draft` status. SEO fields (title, meta description, canonical URL, Open Graph image) are written to native WordPress fields and Yoast SEO fields when the Yoast plugin is active. ## Enable / disable the integration Use the toggle on the WordPress card to pause or resume publishing without disconnecting. When disabled, scheduled posts will not be sent to WordPress until you re-enable it. ## Disconnect Click **Disconnect** on the WordPress card and confirm. Your WordPress credentials are deleted from AutoBlogWriter immediately. Existing posts that were already published to WordPress are not affected. ## Troubleshooting ### `rest_cannot_create` — Forbidden The WordPress user does not have permission to create posts. Make sure the user role is **Editor** or **Administrator**. ### `401 Unauthorized` The username or Application Password is incorrect. Double-check that you copied the Application Password exactly as shown, including spaces, and that the username matches the account that created it. ### Connection refused / SSL error - Confirm your Site URL uses `https://` and the SSL certificate is valid. - If your site is behind a firewall or WAF, whitelist AutoBlogWriter's outbound IP range or disable REST API blocking rules for the `/wp-json/` path. ### Application Passwords section missing Some security plugins (e.g. Wordfence, iThemes Security) disable Application Passwords. Check your security plugin settings and re-enable Application Passwords, or allowlist the AutoBlogWriter user agent. ### REST API disabled Some hosts or plugins disable the REST API entirely. You can test whether it is available by visiting `https://yoursite.com/wp-json/wp/v2/posts` in a browser. If you see a JSON response, the API is accessible. If you see a 403 or an empty page, check your host's REST API settings. --- ## Shopify Integration AutoBlogWriter can publish blog posts directly to your Shopify store using a custom app and the client credentials grant. No manual token refresh required. ## Prerequisites - A Shopify store you control - A Shopify app created in the **Shopify Dev Dashboard** - Admin API scopes: `read_content` and `write_content` - The blog ID you want to publish to (numeric) > Shopify now issues Client ID + Client Secret for new apps. AutoBlogWriter exchanges these for an access token automatically. ## Step 1 — Create and install the app 1. Go to the **Shopify Dev Dashboard** and create a new app. 2. Install the app on your store. 3. Add Admin API scopes: `read_content` and `write_content`. 4. Save changes and re-install if prompted. 5. Copy the **Client ID** and **Client Secret** from the app credentials page. ## Step 2 — Find your Blog ID 1. Open **Online Store → Blog posts** in Shopify Admin. 2. Click the blog you want to publish to. 3. Copy the numeric ID from the URL. Example: ``` https://your-store.myshopify.com/admin/blogs/122321404276 ``` Blog ID = `122321404276` ## Step 3 — Connect in AutoBlogWriter 1. Open your workspace and go to **Settings → Integrations**. 2. Click **Connect Shopify**. 3. Fill in the fields: | Field | What to enter | |---|---| | **Shop Domain** | Your store domain, e.g. `your-store.myshopify.com` | | **Client ID** | From the Shopify Dev Dashboard app credentials | | **Client Secret** | From the Shopify Dev Dashboard app credentials | | **Blog ID** | The numeric blog ID from Step 2 | 4. Click **Connect**. AutoBlogWriter will verify access and save the integration. ## Step 4 — Test the Connection After connecting, click **Test Connection** on the Shopify card. This validates access to the blog and confirms posts can be created. ## Publishing behavior - **Publish now** — creates and publishes a blog article immediately. - **Schedule** — posts at the scheduled time when AutoBlogWriter publishes. - **Draft** — creates the article as a draft. - **Images** — the hero image is attached when a public image URL is available. ## Enable / disable the integration Use the toggle on the Shopify card to pause or resume publishing without disconnecting. When disabled, scheduled posts will not be sent to Shopify until you re-enable it. ## Disconnect Click **Disconnect** on the Shopify card and confirm. Your Shopify credentials are deleted from AutoBlogWriter immediately. Existing posts that were already published to Shopify are not affected. ## Troubleshooting ### `Oauth error invalid_request: Missing or invalid client secret` The Client Secret is not the app secret from the Shopify Dev Dashboard, or it was copied incorrectly. Re-copy from the app credentials screen. ### `Invalid API key or access token` The app is not installed on the store or the credentials are for a different store. Reinstall the app and confirm the shop domain matches. ### `shop_not_permitted` Client credentials grant only works when the app and store are in the same organization. Use an app created under the same org as the store.