@nexifi/mdx-blog
v2.1.0
Published
Complete headless blog toolkit — API client, React hooks, SEO components, sitemap, RSS, MDX widgets. Works with Next.js, Remix, Astro, Nuxt, SvelteKit, and any React app.
Maintainers
Readme
@nexifi/mdx-blog
Complete headless blog toolkit — API client, React hooks, SEO components, sitemap, RSS, MDX widgets. Works with Next.js, Remix, Astro, Nuxt, SvelteKit, and any React app.
Powering getmax.io — the AI-powered marketing OS by Nexifi. Site, SEO, campaigns, CRM — getMax creates, optimizes and converts 24/7.
Installation
# Core (required)
npm install @nexifi/mdx-blog react react-dom swr
# Framework-specific (install what you use):
npm install next # Next.js
npm install @remix-run/react # Remix
npm install @astrojs/react # Astro
# Optional — for MDX rendering:
npm install next-mdx-remote @mdx-js/react remark-gfm rehype-highlightArchitecture
@nexifi/mdx-blog
├── Main entry (.) → React components, hooks, provider, types
├── Server entry (./server) → ContentAPIAdapter, sitemap, RSS, security (Node.js only)
└── MDX entry (./mdx) → MDX-dependent components (ESM only)| Import | Usage |
|--------|-------|
| @nexifi/mdx-blog | Client-side: React components, hooks, provider, i18n, SEO, images |
| @nexifi/mdx-blog/server | Server-side: ContentAPIAdapter, sitemap, RSS, security utilities |
| @nexifi/mdx-blog/mdx | MDX rendering: BlogArticlePage, MDXProvider, SSG factories (ESM only) |
Features
| Feature | Description |
|---------|-------------|
| BlogProvider | React context with SWR caching, i18n labels |
| 7 React hooks | useArticles, useArticle, useCategories, usePagination, useRelatedArticles, useArticlesByTag, useSearch |
| Pre-built pages | BlogListPage (category filters, pagination), BlogArticlePage (MDX + SEO) |
| 7 MDX widgets | Newsletter, TableOfContents, AuthorBio, ProductCard, RelatedPosts, StatsBox, FeatureList |
| SEO | ArticleSchema / BlogListSchema (JSON-LD), ArticleHead / BlogListHead (meta tags, OG, Twitter Cards) |
| Sitemap | XML sitemap generation (runtime SSR + build-time), sitemap index, robots.txt |
| RSS/Atom | generateRSSFeed, generateAtomFeed with media extensions |
| llms.txt | Build-time llmstxt.org file generation |
| Images | BlogImage (responsive, lazy, blur placeholder, srcset), image utilities |
| Server adapter | ContentAPIAdapter (API key auth, server-only guard) |
| Security | XSS-safe JSON-LD, slug sanitization, fetch timeout, CDATA escaping, XML injection protection |
| i18n | All UI strings overridable via labels (French defaults) |
| TypeScript | Fully typed, strict mode |
| Tree-shakeable | ESM + CJS dual output, sideEffects: false |
CLI — AI-Powered Installation
The package includes an AI agent (powered by @openai/agents) that can autonomously integrate @nexifi/mdx-blog into your project:
# Install with AI agent (requires OPENAI_API_KEY)
npx @nexifi/mdx-blog install
# Dry-run — preview what the agent would do
npx @nexifi/mdx-blog install --dry-run
# Validate an existing integration
npx @nexifi/mdx-blog validateCommands
| Command | Description |
|---------|-------------|
| install / setup / init | Run the AI agent to integrate the blog into your project |
| validate / check | Static validation of an existing integration (no API key needed) |
| help | Show available commands |
| version | Show package version |
Install Options
| Flag | Description |
|------|-------------|
| --dry-run | Preview the integration prompt without running the agent |
| --verbose | Show detailed agent activity |
| --quiet | Minimal output |
| --model <model> | OpenAI model to use (default: gpt-4.1) |
| --keep-on-failure | Keep changes even if the agent fails (default: rollback) |
The agent analyzes your project structure (framework, router, styling, conventions), reads your AGENTS.md, and generates a complete integration — including provider setup, API routes, blog pages, Tailwind config, and environment variables.
Supported Frameworks
| Framework | Router | How to use |
|-----------|--------|------------|
| Next.js | App Router | React components + Route Handlers for API |
| Next.js | Pages Router | React components + API routes + SSG factories |
| Remix | v2 | React components + loaders/actions for data |
| Astro | v4+ | React components via @astrojs/react + API endpoints |
| Nuxt | v3 | React components via islands + server routes |
| SvelteKit | v2 | Server utilities (sitemap, RSS, ContentAPIAdapter) + custom UI |
| Plain React | Any | React components + your own API layer |
The React components (hooks, provider, pages, SEO) work in any React environment. The server utilities (ContentAPIAdapter, sitemap, RSS) work in any Node.js runtime.
Showcase: getmax.io
getmax.io is the reference production implementation of @nexifi/mdx-blog:
- Framework: Next.js 15 App Router
- i18n: Locale-prefixed routes (
/fr/blog,/en/blog) with[locale]segment - Styling: Tailwind CSS v4 with
@sourcedirective - Features used: Full blog with MDX rendering, JSON-LD SEO, sitemap, RSS/Atom,
BlogProviderwith French labels,ContentAPIAdapterfor server-side data fetching,BlogImageresponsive images - Architecture:
src/app/[locale]/blog/page.tsx(list) +src/app/[locale]/blog/[slug]/page.tsx(article) - API routes:
src/app/api/blog/route.ts+src/app/api/blog/[slug]/route.ts - Live blog: getmax.io/fr/blog
getMax is the AI-powered marketing operating system — site, SEO, campaigns, CRM and analytics in a single console. Built by getMax.
Quick Start
1. Wrap your app with BlogProvider
import { BlogProvider } from '@nexifi/mdx-blog';
<BlogProvider
config={{
endpoints: {
articles: '/api/blog',
article: '/api/blog/:slug',
categories: '/api/blog/categories',
},
}}
labels={{
home: 'Home',
blog: 'Blog',
readMore: 'Read more',
backToBlog: 'Back to blog',
}}
>
{children}
</BlogProvider>2. Create API routes (server-side only)
ContentAPIAdaptermust ONLY be used server-side. It throws if instantiated in the browser.
Next.js App Router
// app/api/blog/route.ts
import { NextResponse } from 'next/server';
import { ContentAPIAdapter } from '@nexifi/mdx-blog/server';
const adapter = new ContentAPIAdapter({
apiKey: process.env.CONTENT_API_KEY!,
baseUrl: process.env.CONTENT_API_URL,
});
export async function GET() {
const articles = await adapter.getAllArticles();
return NextResponse.json(articles, {
headers: { 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600' },
});
}Next.js Pages Router
// pages/api/blog/index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { ContentAPIAdapter } from '@nexifi/mdx-blog/server';
const adapter = new ContentAPIAdapter({
apiKey: process.env.CONTENT_API_KEY!,
baseUrl: process.env.CONTENT_API_URL,
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const articles = await adapter.getAllArticles();
res.setHeader('Cache-Control', 'public, s-maxage=300, stale-while-revalidate=600');
res.json(articles);
}Remix
// app/routes/api.blog.tsx
import { json } from '@remix-run/node';
import { ContentAPIAdapter } from '@nexifi/mdx-blog/server';
const adapter = new ContentAPIAdapter({
apiKey: process.env.CONTENT_API_KEY!,
baseUrl: process.env.CONTENT_API_URL,
});
export async function loader() {
const articles = await adapter.getAllArticles();
return json(articles, {
headers: { 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600' },
});
}Astro
// src/pages/api/blog.ts
import type { APIRoute } from 'astro';
import { ContentAPIAdapter } from '@nexifi/mdx-blog/server';
const adapter = new ContentAPIAdapter({
apiKey: import.meta.env.CONTENT_API_KEY,
baseUrl: import.meta.env.CONTENT_API_URL,
});
export const GET: APIRoute = async () => {
const articles = await adapter.getAllArticles();
return new Response(JSON.stringify(articles), {
headers: { 'Content-Type': 'application/json' },
});
};SvelteKit
// src/routes/api/blog/+server.ts
import { json } from '@sveltejs/kit';
import { ContentAPIAdapter } from '@nexifi/mdx-blog/server';
import { CONTENT_API_KEY, CONTENT_API_URL } from '$env/static/private';
const adapter = new ContentAPIAdapter({
apiKey: CONTENT_API_KEY,
baseUrl: CONTENT_API_URL,
});
export async function GET() {
const articles = await adapter.getAllArticles();
return json(articles);
}3. Blog list page
import { BlogListPage, BlogListHead, BlogListSchema } from '@nexifi/mdx-blog';
const seoConfig = { siteUrl: 'https://getmax.io', siteName: 'getMax', blogPath: '/blog' };
export default function Blog() {
return (
<>
<BlogListHead config={seoConfig} title="Blog" description="Our latest articles" />
<BlogListSchema config={seoConfig} articles={[]} />
<BlogListPage title="Blog" blogPath="/blog" perPage={9} />
</>
);
}4. Article page
Next.js App Router (Server Component — recommended)
Do NOT use
next-mdx-remote/rscin Next.js 15+ App Router — it causes uncatchable RSC streaming errors. UserenderMarkdowninstead:
// app/blog/[slug]/page.tsx
import { ContentAPIAdapter, renderMarkdown } from '@nexifi/mdx-blog/server';
import { ArticleHead, ArticleSchema, ArticleLayout } from '@nexifi/mdx-blog';
const adapter = new ContentAPIAdapter({
apiKey: process.env.CONTENT_API_KEY!,
baseUrl: process.env.CONTENT_API_URL,
});
export default async function ArticlePage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const article = await adapter.getArticleBySlug(slug);
if (!article) notFound();
const html = await renderMarkdown(article.content || '');
return (
<ArticleLayout article={article}>
<div className="prose prose-invert" dangerouslySetInnerHTML={{ __html: html }} />
</ArticleLayout>
);
}Next.js Pages Router (SSG)
// pages/blog/[slug].tsx
import { BlogArticlePage, createGetStaticPaths, createGetStaticProps } from '@nexifi/mdx-blog/mdx';
export const getStaticPaths = createGetStaticPaths({ articlesEndpoint: '/api/blog' });
export const getStaticProps = createGetStaticProps({
articlesEndpoint: '/api/blog',
articleEndpoint: '/api/blog/:slug',
revalidateSeconds: 3600,
relatedCount: 3,
});
export default BlogArticlePage;5. Tailwind CSS
Required: Install the typography plugin for prose styling of article content:
npm install @tailwindcss/typographyTailwind v4 — add to your main CSS:
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@source "../../node_modules/@nexifi/mdx-blog/dist";Tailwind v3 — add to tailwind.config.js:
plugins: [
require('@tailwindcss/typography'),
],
content: [
'./node_modules/@nexifi/mdx-blog/**/*.{js,ts,jsx,tsx}',
],6. Environment Variables
CONTENT_API_KEY=ak_xxxxxxxxxxxxx
CONTENT_API_URL=https://api-growthos.nexifi.com/api/contentmaster/projects/<PROJECT_ID>/articles
NEXT_PUBLIC_SITE_URL=https://getmax.ioi18n / Labels
All UI text is configurable via BlogProvider's labels prop. French defaults are built-in.
<BlogProvider
config={config}
labels={{
home: 'Home', blog: 'Blog', backToBlog: 'Back to blog',
readMore: 'Read more', readArticle: 'Read article',
relatedArticles: 'Related articles',
tags: 'Tags:', share: 'Share:', minRead: 'min read',
allArticles: 'All articles',
previous: 'Previous', next: 'Next',
pageXofY: (c, t) => `Page ${c} of ${t}`,
articleCount: (n) => `${n} article${n > 1 ? 's' : ''}`,
blogSubtitle: 'Discover our latest articles',
loading: 'Loading...', noArticlesFound: 'No articles found.',
newsletterTitle: 'Subscribe to our newsletter',
tableOfContents: 'Table of Contents',
// ... see Labels interface for all 30+ keys
}}
/>SEO
JSON-LD Structured Data
import { ArticleSchema, BlogListSchema } from '@nexifi/mdx-blog';
<ArticleSchema article={article} config={{
siteUrl: 'https://getmax.io', siteName: 'getMax', blogPath: '/blog',
}} />
<BlogListSchema config={schemaConfig} articles={articles} />Meta Tags (Open Graph, Twitter Cards)
import { ArticleHead, BlogListHead } from '@nexifi/mdx-blog';
<ArticleHead article={article} config={{
siteUrl: 'https://getmax.io', siteName: 'getMax',
twitterHandle: '@getmaxio', blogPath: '/blog',
}} />Build-Time Sitemap + robots.txt + llms.txt
import { generateBuildTimeSEO } from '@nexifi/mdx-blog/server';
await generateBuildTimeSEO({
siteUrl: 'https://getmax.io',
apiKey: process.env.CONTENT_API_KEY,
blogPath: '/blog',
outputDir: './public',
staticPages: [
{ path: '/', priority: 1.0, changefreq: 'daily' },
{ path: '/fr', priority: 1.0, changefreq: 'daily' },
{ path: '/fr/blog', priority: 0.9, changefreq: 'daily' },
{ path: '/fr/integrations', priority: 0.8, changefreq: 'weekly' },
{ path: '/fr/mcp', priority: 0.8, changefreq: 'weekly' },
],
llmsConfig: {
name: 'getMax', description: 'Votre croissance marketing en pilote automatique. Site, SEO, campagnes, CRM : getMax crée, optimise et convertit 24/7.',
services: ['Site web & Landing Pages', 'SEO & Référencement', 'CRM & Nurturing', 'Publicité & Acquisition', 'Analytics & Reporting', 'Marketing Automation'],
},
});SSR llms.txt (Pages Router)
// pages/llms.txt.tsx
import { LlmsPage, createLlmsServerSideProps } from '@nexifi/mdx-blog/server';
export default LlmsPage;
export const getServerSideProps = createLlmsServerSideProps({
siteUrl: 'https://getmax.io',
blogPath: '/blog',
llmsConfig: {
name: 'getMax',
description: 'AI-powered marketing operating system.',
contact: { email: '[email protected]' },
services: [
{ title: 'SEO', url: 'https://getmax.io/seo', description: 'AI-driven SEO' },
],
},
});
// pages/llms-full.txt.tsx — same but with full: trueApp Router / Remix / Astro — llms.txt
// app/llms.txt/route.ts (Next.js App Router)
import { generateLlmsTxt, ContentAPIAdapter } from '@nexifi/mdx-blog/server';
const adapter = new ContentAPIAdapter({ apiKey: process.env.CONTENT_API_KEY! });
export async function GET() {
const articles = await adapter.getAllArticles();
const { llmsTxt } = generateLlmsTxt(
{ name: 'getMax', description: 'AI-powered marketing OS.' },
articles, 'https://getmax.io', '/blog',
);
return new Response(llmsTxt, {
headers: { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'public, max-age=86400' },
});
}RSS / Atom Feeds
import { generateRSSFeed, generateAtomFeed } from '@nexifi/mdx-blog/server';
const rss = generateRSSFeed(articles, {
siteUrl: 'https://getmax.io', title: 'Blog getMax',
description: 'Votre croissance marketing en pilote automatique — articles SEO, acquisition et CRM', blogPath: '/blog', language: 'fr',
});API Reference
Provider & Context
| Export | Type | Description |
|--------|------|-------------|
| BlogProvider | Component | Root context provider — config + labels |
| useBlogClient() | Hook | Access the BlogApiClient instance |
| useLabels() | Hook | Access the current labels (i18n) |
Hooks
| Hook | Returns | Description |
|------|---------|-------------|
| useArticles() | { articles, isLoading, error, refresh } | All published articles (SWR cached) |
| useArticle(slug) | { article, isLoading, error, refresh } | Single article by slug |
| useCategories() | { categories, isLoading, error, refresh } | Available categories |
| useRelatedArticles(slug, limit?) | { relatedArticles, isLoading, error } | Related articles (same category/tags) |
| usePagination(options?) | { items, pagination, setPage, setCategory } | Client-side pagination + category filter |
| useArticlesByTag(tag) | { articles, isLoading, error } | Articles by tag |
| useSearch(query, options?) | { results, isSearching, totalResults } | Client-side full-text search |
Page Components
| Component | Key Props | Description |
|-----------|-----------|-------------|
| BlogListPage | title, blogPath, perPage, showCategories, ImageComponent | Full blog listing with category filters + pagination |
| BlogArticlePage | article, mdxSource, relatedArticles, seoConfig | Full article page with MDX rendering (via ./mdx) |
| ArticleLayout | article, children, showBreadcrumb, showShareButtons, showTags | Article layout wrapper |
| ArticlePlaceholder | category, icon | Gradient placeholder when article has no image |
SEO Components
| Component | Key Props | Description |
|-----------|-----------|-------------|
| ArticleHead | article, config: SEOConfig | <head> meta tags (OG, Twitter, canonical) |
| BlogListHead | config: SEOConfig, title, description | Blog index meta tags |
| ArticleSchema | article, config: SchemaConfig | JSON-LD: BlogPosting + BreadcrumbList + WebPage |
| BlogListSchema | config: SchemaConfig, articles | JSON-LD: CollectionPage + ItemList |
MDX Components (@nexifi/mdx-blog/mdx)
| Component | Description |
|-----------|-------------|
| BlogArticlePage | Full article page with MDX rendering |
| MDXProvider | Wraps MDX content with styled HTML elements + all widgets |
| ArticleCTA | Call-to-action block (3 variants: primary, secondary, success) |
| Alert | Callout box (info, warning, success, danger, tip) |
| createGetStaticPaths(config) | SSG factory for Next.js getStaticPaths |
| createGetStaticProps(config) | SSG factory for Next.js getStaticProps with MDX serialization |
Widgets (usable in MDX content)
| Widget | Key Props | Description |
|--------|-----------|-------------|
| Newsletter | onSubmit, apiEndpoint, variant | Email signup (inline, card, minimal) |
| TableOfContents | maxDepth, collapsible | Auto-generated TOC with active heading tracking |
| AuthorBio | name, bio, links, variant | Author card (compact or full) |
| ProductCard | name, price, rating, link | E-commerce product card |
| RelatedPosts | posts: Article[], maxPosts | Related articles block |
| StatsBox | stats: StatItem[], variant | Statistics display (horizontal, vertical, grid) |
| FeatureList | features, variant | Feature/advantage list (list, grid, compact) |
Image
| Export | Type | Description |
|--------|------|-------------|
| BlogImage | Component | Responsive image with lazy load, blur, srcset, error fallback |
| resolveImageUrl | Function | Resolve relative images to absolute URLs |
| generateSrcSet | Function | Generate srcset string |
| generateSizes | Function | Generate sizes attribute |
| computeHeight | Function | Compute height from width + aspect ratio |
| buildImageObject | Function | Build JSON-LD ImageObject |
| isExternalImage | Function | Check if URL is external |
Server Adapter (@nexifi/mdx-blog/server)
| Export | Description |
|--------|-------------|
| ContentAPIAdapter | Server-only HTTP client with API key auth (throws in browser) |
| .getAllArticles() | Fetch all published articles |
| .getArticleBySlug(slug) | Fetch single article |
| .updateArticleStatus(id, status) | Update article status |
| .transformArticle(data) | Transform raw API data to Article |
Sitemap & SEO Utilities (@nexifi/mdx-blog/server)
| Export | Description |
|--------|-------------|
| generateSitemap(articles, config) | Generate XML sitemap from articles |
| buildSitemapXML(entries) | Build XML from custom entries |
| generateSitemapIndex(sitemaps, siteUrl) | Sitemap index for large sites |
| getArticleSitemapEntries(articles, blogPath?) | Entries for next-sitemap integration |
| generateRobotsTxt(siteUrl, options?) | Generate robots.txt |
| generateStaticSitemap(config) | Build-time sitemap with article fetch |
| generateSitemapOnBuild(options) | Write sitemap.xml to disk |
| generateBuildTimeSEO(config) | Full pipeline: sitemap + robots + llms.txt |
| generateLlmsTxt(config, articles, baseUrl, blogPath) | Generate llms.txt + llms-full.txt content |
| LlmsPage | SSR page component for llms.txt (Pages Router) |
| createLlmsServerSideProps(config) | Factory for llms.txt getServerSideProps |
| generateRSSFeed(articles, config) | RSS 2.0 XML |
| generateAtomFeed(articles, config) | Atom 1.0 XML |
Security
| Export | Description |
|--------|-------------|
| safeJsonLd(data) | XSS-safe JSON serialization for <script type="application/ld+json"> |
| sanitizeSlug(slug) | Validate slug (alphanumeric + hyphens, no path traversal) |
| fetchWithTimeout(url, options, timeout?) | Fetch with configurable timeout (default: 10s) |
| escapeXml(str) | Escape XML special characters |
Security
| Protection | Implementation |
|------------|----------------|
| XSS in JSON-LD | safeJsonLd() escapes </script>, <!--, -->, \u2028, \u2029 |
| Slug injection | sanitizeSlug() validates alphanumeric + hyphens, blocks path traversal |
| Fetch timeout | fetchWithTimeout() prevents hanging requests (default: 10s) |
| API key exposure | ContentAPIAdapter throws if instantiated in browser |
| Auth header warning | BlogApiClient warns if Authorization header used client-side |
| XML injection | escapeXml() applied to all sitemap interpolation |
| CDATA breakout | RSS/Atom feeds escape ]]> sequences in CDATA sections |
License
MIT © Powering getMax
