Next.js
Render Inkies articles in a Next.js App Router blog with metadata, JSON-LD, and a sitemap.
The @inkies/sdk and @inkies/next packages are not published to npm yet. The commands below are
the intended future installation commands. Until publishing is complete, use the workspace
packages from the repository.
Install
pnpm add @inkies/sdk @inkies/nextCreate the client
Keep the API key in server-only environment variables. This mirrors the Inkies dogfood app.
import { createInkies } from "@inkies/sdk";
export const inkies = process.env.INKIES_API_KEY
? createInkies({
apiKey: process.env.INKIES_API_KEY,
baseUrl: process.env.INKIES_API_URL!,
requestInit: { next: { revalidate: 3600 } },
})
: null;baseUrl is the Convex site URL, such as https://<deployment>.convex.site. The SDK removes trailing slashes before making requests.
Article index
allArticles() follows API pagination and returns article summaries.
import Link from "next/link";
import { notFound } from "next/navigation";
import { inkies } from "@/lib/inkies";
export default async function BlogPage() {
if (!inkies) notFound();
const articles = await inkies.allArticles();
return (
<main>
{articles.map((article) => (
<Link key={article.slug} href={`/blog/${article.slug}`}>
{article.coverImage ? (
<img
src={article.coverImage.url}
alt={article.coverImage.alt}
width={article.coverImage.width}
height={article.coverImage.height}
/>
) : null}
<h2>{article.title}</h2>
<p>{article.description}</p>
</Link>
))}
</main>
);
}Article page
Use getArticle() for the complete article. It returns null for a 404 response.
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { InkiesArticle, articleJsonLd, articleMetadata } from "@inkies/next";
import { inkies } from "@/lib/inkies";
const siteUrl = "https://example.com";
type Props = { params: Promise<{ slug: string }> };
export async function generateMetadata({ params }: Props): Promise<Metadata> {
if (!inkies) notFound();
const article = await inkies.getArticle((await params).slug);
if (!article) notFound();
return articleMetadata(article, {
siteUrl,
path: `/blog/${article.slug}`,
});
}
export default async function BlogArticlePage({ params }: Props) {
if (!inkies) notFound();
const article = await inkies.getArticle((await params).slug);
if (!article) notFound();
return (
<main>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
articleJsonLd(article, {
siteUrl,
path: `/blog/${article.slug}`,
publisherName: "Example",
}),
),
}}
/>
<h1>{article.title}</h1>
<InkiesArticle article={article} cover />
</main>
);
}InkiesArticle renders the article HTML returned by the API. With cover, it renders the stored cover URL in a plain <img> with the returned dimensions and alt text. It does not use next/image.
If you render covers with next/image instead, stored cover URLs are on *.convex.cloud, so add the actual Convex host used by your stored URLs to images.remotePatterns:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{ protocol: "https", hostname: "*.convex.cloud" },
{ protocol: "https", hostname: "*.convex.site" },
],
},
};
export default nextConfig;Sitemap
articleSitemapEntries() turns article summaries into Next metadata sitemap entries.
import type { MetadataRoute } from "next";
import { articleSitemapEntries } from "@inkies/next";
import { inkies } from "@/lib/inkies";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
if (!inkies) return [];
const articles = await inkies.allArticles();
return articleSitemapEntries(articles, {
siteUrl: "https://example.com",
basePath: "/blog",
});
}