Inkies Docs
Frameworks

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/next

Create the client

Keep the API key in server-only environment variables. This mirrors the Inkies dogfood app.

lib/inkies.ts
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.

app/blog/page.tsx
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.

app/blog/[slug]/page.tsx
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:

next.config.ts
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.

app/sitemap.ts
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",
  });
}

On this page