@spoolcms/nextjs
Version:
The beautiful headless CMS for Next.js developers
349 lines (247 loc) • 11.9 kB
Markdown
# /nextjs Integration Guide
This guide provides all the necessary steps and code examples to integrate Spool CMS into a Next.js application.
**Core Features:**
- **Headless CMS:** Manage your content in the Spool admin dashboard.
- **Real-time API:** Fetch content instantly in your Next.js app.
- **Simple Setup:** Get started with a single CLI command.
---
## 1. Installation & Setup
First, install the Spool Next.js package and run the setup command from the root of your Next.js project.
```bash
# 1. Install the package
npm install /nextjs
# 2. Run the setup command to create your API route
npx create-spool-route
```
This command automatically creates the file `app/api/spool/[...route]/route.ts` (or `pages/api/spool/[...route].ts` for Pages Router) for you.
---
## 2. Environment Variables
Next, add your Spool credentials to your local environment file (`.env.local`). You can find these keys in your Spool project settings.
```bash
# .env.local
SPOOL_API_KEY="your_spool_api_key"
SPOOL_SITE_ID="your_spool_site_id"
```
> **Note:** Be sure to copy the **entire** API key from your Spool dashboard, including the `spool_` prefix.
---
## 3. Core Concepts & API Helpers
The `/nextjs` package provides simple helpers to fetch your content.
### Shared Configuration (Recommended)
To avoid repeating your API key and Site ID, create a shared config file.
**`lib/spool.ts`**
```typescript
import { SpoolConfig } from '@spoolcms/nextjs/types';
export const spoolConfig: SpoolConfig = {
apiKey: process.env.SPOOL_API_KEY!,
siteId: process.env.SPOOL_SITE_ID!,
};
```
### Fetching a List of Content (`getSpoolContent`)
To get all items from a collection, call `getSpoolContent` with just the collection's slug.
```typescript
import { getSpoolContent } from '@spoolcms/nextjs';
import { spoolConfig } from '@/lib/spool';
// Returns an array of all items in the 'blog' collection
const posts = await getSpoolContent(spoolConfig, 'blog');
```
### Fetching a Single Content Item (`getSpoolContent`)
To get a single item by its slug, provide the slug as the third argument.
```typescript
import { getSpoolContent } from '@spoolcms/nextjs';
import { spoolConfig } from '@/lib/spool';
// Returns the single post with the matching slug
const post = await getSpoolContent(spoolConfig, 'blog', 'my-first-post');
```
### Fetching Collection Schemas (`getSpoolCollections`)
If you need the schema or metadata for your collections, use `getSpoolCollections`.
```typescript
import { getSpoolCollections } from '@spoolcms/nextjs';
import { spoolConfig } from '@/lib/spool';
// Returns an array of all collection objects (id, name, slug, schema)
const collections = await getSpoolCollections(spoolConfig);
```
---
## 4. Handling Images & Thumbnails
Spool automatically generates multiple sizes for every uploaded image:
| Label | Width | Format |
|-------|-------|--------|
| `thumb` | 160 px | webp |
| `small` | 480 px | webp |
| `medium` | 960 px | webp |
| `large` | 1600 px | webp |
| `original` | — | original mime |
Image fields now return **either** a plain URL string (legacy items) **or** an object:
```jsonc
{
"original": "https://media…/foo.jpg",
"thumb": "https://media…/foo_thumb.webp",
"small": "https://media…/foo_small.webp",
"medium": "https://media…/foo_medium.webp",
"large": "https://media…/foo_large.webp"
}
```
To make this seamless in Next.js you can import the helper exported by `@spoolcms/nextjs`:
```tsx
import { img } from '@spoolcms/nextjs';
// Use thumbnail for fast loading in lists
<Image src={img(item.headerImage, 'thumb')} width={160} height={90} />
// Use small for medium-sized displays
<Image src={img(item.headerImage, 'small')} width={480} height={270} />
// Use medium for large content blocks
<Image src={img(item.headerImage, 'medium')} width={960} height={540} />
// Use large for hero sections
<Image src={img(item.headerImage, 'large')} width={1600} height={900} />
// Use original for full-size displays
<Image src={img(item.headerImage, 'original')} width={1200} height={675} />
```
**Additional image behavior:**
- Never upscales: if the original image is smaller than a requested size, `img(value, 'large')` (or any size) returns the original URL.
- Backfill gracefully adds `medium`/`large` where possible; missing sizes are served via original.
**Additional utilities:**
```tsx
import { getImageSizes, hasMultipleSizes } from '@spoolcms/nextjs';
// Get all available sizes
const sizes = getImageSizes(item.headerImage);
// Returns: { original: "...", thumb: "...", small: "..." } or null
// Check if image has multiple sizes (vs legacy single URL)
const hasMultiple = hasMultipleSizes(item.headerImage);
// Returns: true for new images, false for legacy single URLs
```
• Pass the image field value (string _or_ object) and the desired size (`'thumb' | 'small' | 'original'`).
• Falls back gracefully so existing content keeps working.
• The admin interface automatically uses thumbnails for fast table rendering.
---
## 5. Default Fields in Every Collection
Every new collection you create in Spool automatically includes a set of foundational fields so you always have sensible SEO metadata and publication info without any extra configuration.
| **Field** | **Location** | **Type** | **Notes** |
|------------------|---------------|---------------------------|--------------------------------------------------------------------------|
| `description` | `item.data` | string | Optional short summary (used in lists & default meta description) |
| `seoTitle` | `item.data` | string | Optional. Overrides `title` for search engines |
| `seoDescription` | `item.data` | string | Optional. Overrides `description` for search engines |
| `ogTitle` | `item.data` | string | Optional. Title for social sharing (Open Graph) |
| `ogDescription` | `item.data` | string | Optional. Description for social sharing (Open Graph) |
| `ogImage` | `item.data` | image URL* | Optional. Social preview / hero image |
| `title` | top-level | string | **Required.** Main headline for the item |
| `slug` | top-level | string | **Required.** URL-friendly identifier set when creating the item |
| `status` | top-level | `draft` \| `published` | Defaults to `draft`. Controls visibility |
| `published_at` | top-level | datetime | Automatic. Set the first time `status` becomes `published` |
| `updated_at` | top-level | datetime | Automatic. Updated every time you modify the item |
* `ogImage` is returned as a full URL string when you request content.
You can add any custom fields you like on top of these defaults.
---
## 5. Handling Markdown Content
Spool makes working with markdown incredibly intuitive! When you have a markdown field in your collection, Spool creates smart field objects that default to HTML but provide access to raw markdown when needed.
### Smart Markdown Fields
When you request HTML processing with `{ renderHtml: true }`, markdown fields become smart objects:
```typescript
const post = await getSpoolContent(spoolConfig, 'blog', 'my-post', { renderHtml: true });
// ✅ Default behavior: HTML (perfect for rendering)
<div dangerouslySetInnerHTML={{ __html: post.body }} />
// ✅ Explicit HTML access
<div dangerouslySetInnerHTML={{ __html: post.body.html }} />
// ✅ Raw markdown access (when you need it)
const rawMarkdown = post.body.markdown;
```
### Before vs After
**Before (complex):**
```typescript
// Had to remember the _html suffix
<div dangerouslySetInnerHTML={{ __html: post.body_html }} />
const rawMarkdown = post.body; // Different field for raw markdown
```
**After (intuitive):**
```typescript
// Just use the field directly - defaults to HTML!
<div dangerouslySetInnerHTML={{ __html: post.body }} />
const rawMarkdown = post.body.markdown; // Easy access to raw markdown
```
---
## 6. Webhook Integration
Spool provides secure webhook utilities for real-time content updates in your Next.js application.
### Basic Webhook Handler
```typescript
// app/api/webhooks/spool/route.ts
import { createSpoolWebhookHandler } from '@spoolcms/nextjs';
import { revalidatePath } from 'next/cache';
const handleWebhook = createSpoolWebhookHandler({
secret: process.env.SPOOL_WEBHOOK_SECRET, // Optional but recommended
onWebhook: async (data, headers) => {
console.log(`Processing ${data.event} for ${data.collection}/${data.slug}`);
// Revalidate paths based on collection
if (data.collection === 'blog') {
revalidatePath('/blog');
if (data.slug) revalidatePath(`/blog/${data.slug}`);
}
revalidatePath('/'); // Always revalidate homepage
}
});
export const POST = handleWebhook;
```
### Manual Webhook Processing
For more control, use the individual utilities:
```typescript
// app/api/webhooks/spool/route.ts
import {
verifySpoolWebhook,
parseSpoolWebhook,
getSpoolWebhookHeaders
} from '/nextjs';
import { revalidatePath } from 'next/cache';
export async function POST(request: Request) {
const payload = await request.text();
const headers = getSpoolWebhookHeaders(request);
const secret = process.env.SPOOL_WEBHOOK_SECRET;
// Verify signature (recommended for security)
if (secret && headers.signature) {
const isValid = verifySpoolWebhook(payload, headers.signature, secret);
if (!isValid) {
return new Response('Unauthorized', { status: 401 });
}
}
// Parse and validate payload
const data = parseSpoolWebhook(payload);
if (!data) {
return new Response('Invalid payload', { status: 400 });
}
// Process webhook
console.log(`Processing ${data.event} for ${data.collection}`);
// Revalidate relevant paths
revalidatePath('/');
if (data.collection === 'blog') {
revalidatePath('/blog');
if (data.slug) revalidatePath(`/blog/${data.slug}`);
}
return new Response('OK');
}
```
### Webhook Types
```typescript
import type { SpoolWebhookPayload } from '/nextjs';
// Webhook payload structure
interface SpoolWebhookPayload {
event: 'content.created' | 'content.updated' | 'content.published' | 'content.deleted';
site_id: string;
collection: string;
slug?: string;
item_id: string;
timestamp: string;
}
```
### Environment Variables
Add your webhook secret to your environment file:
```bash
# .env.local
SPOOL_WEBHOOK_SECRET="your_webhook_secret_from_spool_admin"
```
---
## 7. Example: Building a Blog
Here is a complete example for creating a blog list and detail pages.
> **Important Setup Notes:**
> 1. **Dynamic Routes Required:** To enable individual post URLs like `/blog/your-post-slug` you **must** create a dynamic route folder `app/blog/[slug]` with its own `page.tsx`. Without this folder, Next.js will return a 404 even though the content exists in Spool.
> 2. **Add Content First:** Before testing your frontend, make sure to add some content in your Spool admin dashboard and fill in at least the `title` field. Empty titles will cause your blog listing to appear broken.
### Blog Listing Page
This page fetches all posts from the "blog" collection and displays them in a grid.
**`app/blog/page.tsx`**
```typescript
import { getSpoolContent } from '/nextjs';
```