@content-streamline/nextjs
v0.0.12
Published
Next.js library for Content Streamline API with reusable components
Maintainers
Readme
@content-streamline/nextjs
Next.js library for Content Streamline API with reusable, responsive components.
Features
- 🚀 Easy Setup - Configure via environment variables
- 📦 Reusable Components - Pre-built React components for posts
- 💾 Next.js Native Caching - Uses
fetch()withrevalidatefor ISR support - 🔄 On-Demand Revalidation - Cache tags for targeted refresh via
revalidateTag() - 🎨 Customizable - Responsive components with configurable props
- 📱 Mobile-Friendly - Built-in responsive design
- 🔧 TypeScript - Full TypeScript support
- 🏷️ Platform Filtering - Filter posts by platform (blog, linkedin, x, etc.)
Installation
npm install @content-streamline/nextjs
# or
bun add @content-streamline/nextjsNote: React and Next.js are peer dependencies. Make sure you have them installed in your Next.js project.
Quick Start
1. Configure Environment Variables
Create a .env.local file in your Next.js project:
CONTENT_STREAMLINE_API_BASE_URL=https://your-api-domain.com
CONTENT_STREAMLINE_API_ACCESS_TOKEN=your_access_token_hereOr use shorter names:
API_BASE_URL=https://your-api-domain.com
API_ACCESS_TOKEN=your_access_token_here2. Import Styles
In your app/layout.tsx or pages/_app.tsx:
import '@content-streamline/nextjs/components/styles.css';3. Use in Server Components (App Router)
Blog Page with Featured Posts (app/blog/page.tsx):
import { getAllPosts } from '@content-streamline/nextjs/server';
import { LatestPosts, PostsList } from '@content-streamline/nextjs/components';
export const revalidate = 300; // Revalidate every 5 minutes (ISR)
export default async function BlogPage() {
const posts = await getAllPosts({ platform: 'blog' });
return (
<div>
<h1>Blog</h1>
{/* Featured section with latest 3 posts */}
<section>
<h2>Latest Posts</h2>
<LatestPosts posts={posts} count={3} baseUrl="/blog" />
</section>
{/* Remaining posts in vertical list */}
<section>
<h2>All Posts</h2>
<PostsList posts={posts} skipFirst={3} baseUrl="/blog" />
</section>
</div>
);
}Post Detail Page (app/blog/[id]/[slug]/page.tsx):
import { getPostBySlug } from '@content-streamline/nextjs/server';
import { PostDetail } from '@content-streamline/nextjs/components';
import { notFound } from 'next/navigation';
export async function generateStaticParams() {
const { getAllPosts } = await import('@content-streamline/nextjs/server');
const posts = await getAllPosts({ platform: 'blog' });
return posts.flatMap(post =>
post.post_translations
.filter(t => t.slug)
.map(translation => ({
id: post.id.toString(),
slug: translation.slug,
}))
);
}
export default async function PostPage({
params,
}: {
params: { id: string; slug: string };
}) {
const post = await getPostBySlug(params.slug, { platform: 'blog' });
if (!post) {
notFound();
}
return (
<PostDetail post={post} backUrl="/blog" />
);
}4. Use in Pages Router
Blog Page (pages/blog/index.tsx):
import { getAllPosts } from '@content-streamline/nextjs/server';
import { LatestPosts, PostsList } from '@content-streamline/nextjs/components';
import type { GetStaticProps } from 'next';
import type { Post } from '@content-streamline/nextjs';
export const getStaticProps: GetStaticProps = async () => {
const posts = await getAllPosts({ platform: 'blog' });
return {
props: { posts },
revalidate: 300, // Revalidate every 5 minutes
};
};
export default function BlogPage({ posts }: { posts: Post[] }) {
return (
<div>
<h1>Blog</h1>
<LatestPosts posts={posts} count={3} baseUrl="/blog" />
<PostsList posts={posts} skipFirst={3} baseUrl="/blog" />
</div>
);
}Post Detail Page (pages/blog/[id]/[slug].tsx):
import { getPostBySlug, getAllPosts } from '@content-streamline/nextjs/server';
import { PostDetail } from '@content-streamline/nextjs/components';
import type { GetStaticProps, GetStaticPaths } from 'next';
import type { PostDetailResponse } from '@content-streamline/nextjs';
export const getStaticPaths: GetStaticPaths = async () => {
const posts = await getAllPosts({ platform: 'blog' });
const paths = posts.flatMap(post =>
post.post_translations
.filter(t => t.slug)
.map(translation => ({
params: {
id: post.id.toString(),
slug: translation.slug,
},
}))
);
return { paths, fallback: 'blocking' };
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const post = await getPostBySlug(params!.slug as string, { platform: 'blog' });
if (!post) {
return { notFound: true };
}
return {
props: { post },
revalidate: 300,
};
};
export default function PostPage({ post }: { post: PostDetailResponse }) {
return <PostDetail post={post} backUrl="/blog" />;
}API Reference
Server Functions
getAllPosts(options?)
Get all posts with Next.js caching (ISR-compatible).
import { getAllPosts } from '@content-streamline/nextjs/server';
const posts = await getAllPosts({
platform: 'blog', // Filter by platform (default: undefined = all)
revalidate: 300, // Cache TTL in seconds (default: 300 = 5 min)
forceRefresh: false, // Bypass cache (default: false)
tags: ['custom-tag'], // Additional cache tags for revalidateTag()
});Platform values: 'blog', 'x', 'linkedin', 'facebook', 'instagram', 'other'
getPost(id, options?)
Get a single post by ID.
import { getPost } from '@content-streamline/nextjs/server';
const post = await getPost(123, {
language: 'en',
contentFormat: 'markdown', // or 'html'
revalidate: 300, // Cache TTL in seconds (default: 300)
forceRefresh: false,
tags: ['custom-tag'],
});getPostBySlug(slug, options?)
Get a post by slug.
import { getPostBySlug } from '@content-streamline/nextjs/server';
const post = await getPostBySlug('my-post-slug', {
language: 'en',
contentFormat: 'markdown',
platform: 'blog',
revalidate: 300,
forceRefresh: false,
});getCacheTags()
Get cache tag names for use with revalidateTag().
import { getCacheTags } from '@content-streamline/nextjs/server';
const tags = getCacheTags();
// tags.all → 'content-streamline' (all content)
// tags.postsList → 'posts-list' (post listings)
// tags.post(123) → 'post-123' (specific post)Components
<LatestPosts />
Display featured/hero posts with the first post highlighted. Perfect for homepage or blog landing sections.
import { LatestPosts } from '@content-streamline/nextjs/components';
<LatestPosts
posts={posts}
count={3} // Number of posts to show (default: 3)
baseUrl="/blog" // Base URL for post links
platform="blog" // Filter by platform (default: 'blog')
language="en" // Language for translations (default: 'en')
className="custom-class" // Custom container class
cardClassName="custom" // Custom card class
showDate={true} // Show post date (default: true)
showExcerpt={true} // Show post excerpt (default: true)
showImages={true} // Show post images (default: true)
renderTitle={(post, translation) => <CustomTitle />} // Custom title renderer
renderExcerpt={(post, translation) => <CustomExcerpt />} // Custom excerpt renderer
/>Layout behavior:
- First post is displayed as a featured card (larger, with image on left)
- Remaining posts are displayed in a responsive grid below
- Fully responsive - stacks vertically on mobile
<PostsList />
Display posts in a vertical list with thumbnails on the left. Ideal for archive pages or "more posts" sections.
import { PostsList } from '@content-streamline/nextjs/components';
<PostsList
posts={posts}
baseUrl="/blog" // Base URL for post links
platform="blog" // Filter by platform (default: 'blog')
skipFirst={3} // Skip first N posts (default: 0)
language="en" // Language for translations (default: 'en')
className="custom-class" // Custom container class
itemClassName="custom" // Custom item class
showDate={true} // Show post date (default: true)
showExcerpt={true} // Show post excerpt (default: true)
showImages={true} // Show post images (default: true)
renderTitle={(post, translation) => <CustomTitle />} // Custom title renderer
renderExcerpt={(post, translation) => <CustomExcerpt />} // Custom excerpt renderer
/>Layout behavior:
- Vertical list with image thumbnail on the left
- Title and excerpt on the right
- Compact design for browsing many posts
skipFirstprop allows combining withLatestPoststo avoid duplicates
<PostDetail />
Display a single post with full content.
import { PostDetail } from '@content-streamline/nextjs/components';
<PostDetail
post={post}
backUrl="/blog" // URL for back link
language="en" // Language for translation (default: 'en')
className="custom-class" // Custom container class
showBackLink={true} // Show back link (default: true)
showDate={true} // Show post date (default: true)
showType={true} // Show post type badge (default: true)
showImage={true} // Show featured image (default: true)
showGallery={true} // Show gallery images (default: true)
renderContent={(content) => <MarkdownRenderer content={content} />} // Custom content renderer
renderTitle={(post, translation) => <CustomTitle />} // Custom title renderer
/>Common Patterns
Featured Posts + Archive List
Combine LatestPosts and PostsList for a complete blog layout:
import { getAllPosts } from '@content-streamline/nextjs/server';
import { LatestPosts, PostsList } from '@content-streamline/nextjs/components';
export default async function BlogPage() {
const posts = await getAllPosts({ platform: 'blog' });
return (
<main>
{/* Hero section with 3 featured posts */}
<section className="hero">
<LatestPosts posts={posts} count={3} baseUrl="/blog" />
</section>
{/* Archive list (skip the 3 featured posts) */}
<section className="archive">
<h2>More Articles</h2>
<PostsList posts={posts} skipFirst={3} baseUrl="/blog" />
</section>
</main>
);
}Multi-Platform Content
Display content from different platforms:
// Blog posts
const blogPosts = await getAllPosts({ platform: 'blog' });
// LinkedIn posts
const linkedinPosts = await getAllPosts({ platform: 'linkedin' });
// All posts (no filter)
const allPosts = await getAllPosts();Homepage with Single Featured Post
<LatestPosts posts={posts} count={1} baseUrl="/blog" />Customization
Styling
Components use CSS classes with the content-streamline- prefix. You can override styles:
/* Override latest posts card */
.content-streamline-latest-card {
border-radius: 20px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
/* Override featured card */
.content-streamline-latest-card--featured {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
/* Override list items */
.content-streamline-list-item {
border-left: 4px solid #3b82f6;
}
/* Override post title */
.content-streamline-latest-title a,
.content-streamline-list-title a {
color: #your-brand-color;
}Custom Rendering
Use render props for complete control:
<LatestPosts
posts={posts}
renderTitle={(post, translation) => (
<h2 className="my-custom-title">
<span className="category">{post.content_type}</span>
{translation.title}
</h2>
)}
renderExcerpt={(post, translation) => (
<p className="my-custom-excerpt">
{translation.meta_description?.slice(0, 100)}...
</p>
)}
/>Markdown Rendering
For proper markdown rendering, use a library like react-markdown:
import ReactMarkdown from 'react-markdown';
import { PostDetail } from '@content-streamline/nextjs/components';
<PostDetail
post={post}
renderContent={(content) => (
<ReactMarkdown>{content}</ReactMarkdown>
)}
/>Caching
The library uses Next.js native fetch caching for optimal performance with ISR (Incremental Static Regeneration).
How It Works
- Default TTL: 5 minutes (300 seconds)
- Automatic revalidation: Next.js refreshes cache in background after TTL expires
- Cache tags: Each request is tagged for targeted invalidation
- Works with static generation: Pages can be statically generated at build time
Cache Tags
Every request is tagged for granular cache control:
| Tag | Description |
|-----|-------------|
| content-streamline | All content from this library |
| posts-list | Post listing requests |
| post-{id} | Individual post by ID (e.g., post-123) |
Automatic Revalidation (Time-Based)
Cache automatically refreshes after the TTL expires:
// 5 minute cache (default)
const posts = await getAllPosts();
// 1 minute cache
const posts = await getAllPosts({ revalidate: 60 });
// 1 hour cache
const posts = await getAllPosts({ revalidate: 3600 });
// No caching (always fresh)
const posts = await getAllPosts({ forceRefresh: true });On-Demand Revalidation
Invalidate cache instantly when content changes (e.g., from a webhook):
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { getCacheTags } from '@content-streamline/nextjs';
export async function POST(request: Request) {
const { postId } = await request.json();
// Revalidate specific post
if (postId) {
revalidateTag(getCacheTags().post(postId));
}
// Or revalidate all posts
revalidateTag(getCacheTags().postsList);
return Response.json({ revalidated: true });
}Static Generation with ISR
For fully static pages with background revalidation:
// app/blog/page.tsx
export const revalidate = 300; // Revalidate page every 5 minutes
export default async function BlogPage() {
const posts = await getAllPosts({ platform: 'blog' });
return <PostsList posts={posts} />;
}TypeScript
Full TypeScript support is included:
import type {
Post,
PostDetailResponse,
GetAllPostsOptions,
GetPostOptions,
CacheOptions,
PostsListProps,
LatestPostsProps,
PostDetailProps,
} from '@content-streamline/nextjs';
function MyComponent({ post }: { post: PostDetailResponse }) {
// Full type safety
}CSS Classes Reference
LatestPosts
.content-streamline-latest-posts- Container.content-streamline-latest-card- Individual card.content-streamline-latest-card--featured- First (featured) card.content-streamline-latest-image- Image container.content-streamline-latest-content- Content area.content-streamline-latest-date- Date element.content-streamline-latest-title- Title.content-streamline-latest-excerpt- Excerpt text.content-streamline-latest-link- "Read more" link
PostsList
.content-streamline-list- Container.content-streamline-list-item- Individual item.content-streamline-list-image- Image thumbnail.content-streamline-list-content- Content area.content-streamline-list-date- Date element.content-streamline-list-title- Title.content-streamline-list-excerpt- Excerpt text
PostDetail
.content-streamline-post-detail- Container.content-streamline-back-link- Back navigation link.content-streamline-post-header- Header section.content-streamline-post-title- Title.content-streamline-post-meta- Meta information.content-streamline-post-date- Date.content-streamline-post-type- Type badge.content-streamline-post-featured-image- Featured image.content-streamline-post-content- Content area.content-streamline-post-gallery- Image gallery
Common
.content-streamline-empty- Empty state message.content-streamline-post-not-found- Not found message
License
MIT
