@earlyseo/blog
v1.0.7
Published
Drop-in blog integration for React & Next.js — powered by EarlySEO. Articles are served from EarlySEO's CDN. Just add your site ID and render.
Maintainers
Readme
@earlyseo/blog
Drop-in blog integration for React & Next.js — powered by EarlySEO.
Articles are authored in the EarlySEO dashboard and rendered on your site. No database, no webhooks, no storage config — just add your site ID and go.
Features
- Zero infrastructure — Articles are managed by EarlySEO. Nothing to host or maintain.
- Paginated — Pre-built paginated data. No DB queries per visitor.
- Next.js App Router — Drop-in server components for
/blogand/blog/[slug] - React hooks —
useArticles()anduseArticle(slug)for custom UIs - Full SEO — Open Graph, Twitter cards, meta descriptions, canonical URLs
- Auto-styling — CSS custom properties adapt to your site's theme
- 100% customizable — Render props, custom components, or headless hooks
Quick Start (Next.js)
Option A: Auto-generate pages (recommended)
npm install @earlyseo/blog
npx @earlyseo/blog initThe CLI will detect your Next.js project, ask for your site ID, and generate:
app/blog/page.tsx— Paginated blog listingapp/blog/[slug]/page.tsx— Article detail with full SEO metadataapp/blog/sitemap.ts— Blog sitemap at/blog/sitemap.xml(auto-detects base URL from Host header).env.local—EARLYSEO_SITE_IDadded automatically
That's it — run npm run dev and visit /blog.
Option B: Manual setup
1. Install
npm install @earlyseo/blog2. Set your site ID
# .env.local
EARLYSEO_SITE_ID=your-site-id
NEXT_PUBLIC_BASE_URL=https://example.com3. Add the blog listing page
// app/blog/page.tsx
import { createBlogListPage, createBlogListMetadata } from "@earlyseo/blog/next";
export default createBlogListPage({
siteId: process.env.EARLYSEO_SITE_ID!,
title: "Our Blog",
});
export const generateMetadata = createBlogListMetadata({
title: "Our Blog",
description: "Read our latest articles about...",
siteName: "My Site",
});4. Add the article detail page
// app/blog/[slug]/page.tsx
import { createBlogPostPage, createBlogPostMetadata } from "@earlyseo/blog/next";
const siteId = process.env.EARLYSEO_SITE_ID!;
export default createBlogPostPage({ siteId });
export const generateMetadata = createBlogPostMetadata({ siteId, siteName: "My Site" });That's it! Articles published from EarlySEO automatically appear on your blog.
5. Add blog sitemap
The recommended approach — a dedicated blog sitemap at /blog/sitemap.xml:
// app/blog/sitemap.ts
import { createBlogSitemap } from "@earlyseo/blog/next";
const siteId = process.env.EARLYSEO_SITE_ID!;
// baseUrl is auto-detected from NEXT_PUBLIC_BASE_URL or the request Host header
export default createBlogSitemap({ siteId });Or merge blog entries into your existing app/sitemap.ts:
import { getBlogSitemapEntries } from "@earlyseo/blog/next";
import type { MetadataRoute } from "next";
const siteId = process.env.EARLYSEO_SITE_ID!;
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const blogEntries = await getBlogSitemapEntries({ siteId });
return [
{ url: "https://example.com", lastModified: new Date() },
...blogEntries,
];
}baseUrl is resolved automatically: explicit option → NEXT_PUBLIC_BASE_URL env var → request Host header.
How It Works
When you publish an article in EarlySEO, it becomes instantly available to your site via the SDK.
The SDK fetches article data at request time — no database, no API keys, no webhook handlers.
Only the affected data is updated on each publish:
- The individual article
- The paginated list pages
- The manifest
React Components
Using with any React framework (Vite, Remix, etc.)
Wrap your app in EarlySeoProvider and use the components:
import { EarlySeoProvider, BlogList, BlogPost, ArticleStyles } from "@earlyseo/blog/react";
function App() {
return (
<EarlySeoProvider siteId="your-site-id" basePath="/blog">
<ArticleStyles />
<Routes>
<Route path="/blog" element={<BlogList />} />
<Route path="/blog/:slug" element={<BlogPostRoute />} />
</Routes>
</EarlySeoProvider>
);
}
function BlogPostRoute() {
const { slug } = useParams();
return <BlogPost slug={slug!} />;
}Headless Hooks
For full control over the UI, use the hooks directly:
import { useArticles, useArticle } from "@earlyseo/blog/react";
function CustomBlogList() {
const { articles, loading, page, totalPages, goToPage } = useArticles();
if (loading) return <Skeleton />;
return (
<div>
{articles.map((a) => (
<Link key={a.slug} to={`/blog/${a.slug}`}>
<h2>{a.title}</h2>
<p>{a.metaDescription}</p>
</Link>
))}
<div>Page {page} of {totalPages}</div>
</div>
);
}
function CustomArticle({ slug }: { slug: string }) {
const { article, loading } = useArticle(slug);
if (loading) return <Skeleton />;
if (!article) return <NotFound />;
return (
<article>
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.contentRawHtml }} />
</article>
);
}Custom Card Rendering
Override the default card in the blog list:
<BlogList
renderCard={(article, basePath) => (
<a href={`${basePath}/${article.slug}`} className="my-custom-card">
<img src={article.imageUrl} alt={article.title} />
<h3>{article.title}</h3>
</a>
)}
/>Client
Use EarlySeoClient directly for full control:
import { EarlySeoClient } from "@earlyseo/blog";
const client = new EarlySeoClient(
{ siteId: "your-site-id" },
// Optional: Next.js ISR caching
{ next: { revalidate: 60 } }
);
const manifest = await client.getManifest(); // Manifest | null
const page1 = await client.getListPage(1); // ArticleListPage | null
const article = await client.getArticle("slug"); // Article | nullStyling & Theming
Articles come with a .earlyseo-article CSS wrapper that uses CSS custom properties for full theming control:
:root {
--earlyseo-font-family: "Inter", sans-serif;
--earlyseo-text-color: #1a1a1a;
--earlyseo-heading-color: #111;
--earlyseo-link-color: #2563eb;
--earlyseo-border-color: #e5e7eb;
--earlyseo-code-bg: #f3f4f6;
--earlyseo-pre-bg: #f9fafb;
--earlyseo-th-bg: #f3f4f6;
--earlyseo-muted-color: #6b7280;
--earlyseo-tag-bg: #f3f4f6;
--earlyseo-blog-max-width: 72rem;
--earlyseo-post-max-width: 48rem;
--earlyseo-blog-padding: 2rem 1rem;
}Or use htmlMode="raw" to strip all EarlySEO styles and use your own:
// Next.js
export default createBlogPostPage({ siteId, htmlMode: "raw" });
// React
<BlogPost slug={slug} htmlMode="raw" />API Reference
Types
interface Manifest {
schemaVersion: 1;
version: number;
totalArticles: number;
totalPages: number;
pageSize: number;
updatedAt: string;
featuredSlugs: string[];
}
interface ArticleListItem {
title: string;
slug: string;
imageUrl?: string;
imageAlt?: string;
metaDescription: string;
createdAt: string;
tags: string[];
}
interface ArticleListPage {
page: number;
totalPages: number;
totalArticles: number;
version: number;
articles: ArticleListItem[];
}
interface Article {
id: string;
title: string;
slug: string;
contentHtml: string; // Styled HTML (with .earlyseo-article wrapper)
contentRawHtml: string; // Raw HTML (inherits your site theme)
contentCss: string; // Standalone CSS for .earlyseo-article
metaDescription: string;
createdAt: string;
updatedAt: string;
imageUrl?: string;
imageAlt?: string;
tags: string[];
summary?: string;
canonicalUrl?: string;
version: number;
}
interface BlogSitemapOptions {
siteId: string;
baseUrl: string;
basePath?: string;
changeFrequency?: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
priority?: number;
fetchInit?: RequestInit;
}Imports
| Import Path | Contents |
|-------------|----------|
| @earlyseo/blog | EarlySeoClient, core types, CSS constants |
| @earlyseo/blog/react | Provider, hooks, components |
| @earlyseo/blog/next | Next.js page creators, metadata helpers, sitemap helpers |
| @earlyseo/blog/css | ARTICLE_CSS, BLOG_CSS |
License
Proprietary — see LICENSE for details.
