CiteRelay
FeaturesHow It WorksGuidesPricing
Sign in
← CiteRelay/Guides

How to Deploy CiteRelay Markdown to Next.js (App Router)

How to Deploy CiteRelay Markdown to Next.js

You forged dozens of AEO pages and downloaded a ZIP of Markdown files. You do not need a traditional CMS for most indie stacks: Next.js (App Router), Astro, Nuxt, Remix, and Hugo all read Markdown from disk, parse YAML frontmatter (the block between --- lines at the top of each file), and render routes. That frontmatter is how you populate <title>, meta description, Open Graph tags, and canonical URLs.

This guide focuses on Next.js App Router, which is the same pattern CiteRelay uses for its own /guides section—so what you read here is production-grade, not a toy example.

🪄 Zero-Code AI Prompt (Fastest Path)

Using Cursor, Claude Code, Copilot, or another coding assistant? Copy this prompt and paste it into your AI chat inside your codebase. It is framework-agnostic by design, so the assistant can adapt to your stack automatically.

Prompt

I am adding programmatic SEO pages to this project.

I will place a batch of Markdown files in /content/aeo/ (or /content/guides/ based on the existing structure). Each file includes YAML frontmatter with fields like title, slug, metaDescription, targetKeywords, and vibeScore.

Please implement this natively for the current framework in this repo:

  1. Create dynamic routing so each markdown file can be served by URL slug.
  2. Read files from disk safely (prevent path traversal).
  3. Parse frontmatter and use it to populate SEO metadata/head tags.
  4. Render markdown body with a clean typography style suitable for docs/blog content.
  5. Add an index/list page for all generated guides with pagination.
  6. Add sitemap entries for these generated pages.
  7. If needed, include install commands for markdown/frontmatter dependencies used by this stack.

Important:

  • Follow existing code style and architecture in this repository.
  • Reuse existing UI components/design tokens where possible.
  • Keep implementation production-safe (validation, null checks, graceful fallbacks).
  • Show exact files created/updated and any commands I should run.

1. Install dependencies

From your Next.js project root:

npm install react-markdown remark-gfm
npm install -D @tailwindcss/typography

Enable Typography in your global CSS (Tailwind v4 example):

@import "tailwindcss";
@plugin "@tailwindcss/typography";

(If you use the classic tailwind.config setup, add require("@tailwindcss/typography") to plugins instead.)

react-markdown renders the body after you strip frontmatter. remark-gfm adds GitHub-flavored Markdown (tables, task lists, strikethrough).

2. Put Markdown on disk

  1. Create a folder such as content/guides/ at the repo root (name is up to you).
  2. Unzip your CiteRelay export and copy every .md file into that folder.

Each file should keep its YAML frontmatter (title, slug, metaDescription, targetKeywords, vibeScore, etc.). Your app (or a build script) reads those fields for SEO and routing. Do not delete the frontmatter—that is how frameworks inject metadata without a database.

Filenames from CiteRelay match URL-safe slugs (see the slug: field inside the file). A typical dynamic route uses the filename without .md as the URL segment, or the slug value from frontmatter if you prefer—just stay consistent.

3. Dynamic route (App Router)

Add app/guides/[slug]/page.tsx (adjust content/guides if your folder differs).

Security: never pass untrusted input straight into path.join. Restrict slug to safe characters (e.g. letters, numbers, hyphens) and ensure the resolved path stays under your content directory so ../../../etc/passwd style paths cannot escape.

Pattern:

  • generateStaticParams: list .md basenames from the folder.
  • generateMetadata: read the file, parse title and metaDescription from frontmatter for <head>.
  • Default export: strip the first frontmatter block, render the remainder with react-markdown inside a prose container (@tailwindcss/typography).

Minimal page (you can split a small "use client" wrapper if you prefer react-markdown only on the client):

import fs from "fs";
import path from "path";
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";

const CONTENT_DIR = path.join(process.cwd(), "content", "guides");
const SAFE_SLUG = /^[a-zA-Z0-9][a-zA-Z0-9-]*$/;

function readRaw(slug: string): string | null {
  if (!SAFE_SLUG.test(slug)) return null;
  const filePath = path.resolve(path.join(CONTENT_DIR, `${slug}.md`));
  if (!filePath.startsWith(path.resolve(CONTENT_DIR) + path.sep)) return null;
  try {
    return fs.readFileSync(filePath, "utf8");
  } catch {
    return null;
  }
}

function stripFrontmatter(raw: string): { title: string; description: string; body: string } {
  let body = raw;
  let title = "";
  let description = "";
  if (raw.startsWith("---\n")) {
    const end = raw.indexOf("\n---\n", 4);
    if (end !== -1) {
      const fm = raw.slice(4, end);
      body = raw.slice(end + 5);
      const t = fm.match(/^title:\s*(.+)$/m);
      const d = fm.match(/^metaDescription:\s*(.+)$/m);
      const strip = (s: string) => s.replace(/^["']|["']$/g, "").trim();
      if (t?.[1]) title = strip(t[1]);
      if (d?.[1]) description = strip(d[1]);
    }
  }
  return { title, description, body };
}

export async function generateStaticParams() {
  if (!fs.existsSync(CONTENT_DIR)) return [];
  return fs
    .readdirSync(CONTENT_DIR)
    .filter((f) => f.endsWith(".md"))
    .map((f) => f.replace(/\.md$/i, ""))
    .filter((slug) => SAFE_SLUG.test(slug))
    .map((slug) => ({ slug }));
}

export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}): Promise<Metadata> {
  const { slug } = await params;
  const raw = readRaw(slug);
  if (!raw) return { title: "Not found" };
  const { title, description } = stripFrontmatter(raw);
  return {
    title: title || slug,
    description: description || undefined,
  };
}

export default async function GuidePage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const raw = readRaw(slug);
  if (!raw) notFound();
  const { title, body } = stripFrontmatter(raw);

  return (
    <main className="mx-auto max-w-3xl px-4 py-12">
      <article className="rounded-xl border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-950">
        <h1 className="mb-6 font-semibold text-2xl">{title || slug}</h1>
        <div className="prose prose-neutral max-w-none dark:prose-invert">
          <ReactMarkdown remarkPlugins={[remarkGfm]}>{body}</ReactMarkdown>
        </div>
      </article>
    </main>
  );
}

Add app/sitemap.ts that reads the same folder and emits https://your-domain.com/guides/{slug} entries so Google discovers every URL.

4. Not using Next.js?

  • Astro / Nuxt / Hugo / Remix: same idea—content collections or file routes + a frontmatter parser. Point the framework at a folder of .md files; use each file’s slug or basename for the path.
  • WordPress: use a bulk import plugin (for example WP All Import or a Markdown-oriented importer) to create posts or pages from your files. You may need to map YAML fields to custom fields or SEO plugins (Yoast, Rank Math).
  • Webflow / Framer CMS: there is no universal “drop Markdown” flow. Typical approach: convert Markdown bodies to CSV rows (title, slug, HTML or rich text column) using a small script or an online converter, then use the native CSV import into your CMS collection.
  • Google Docs / Notion: not ideal publishing targets; export to HTML/Markdown first, then import via your stack’s tooling.

CiteRelay’s job is to give you clean, portable Markdown + YAML. Your stack’s job is ingestion—either filesystem (code frameworks) or import tools (hosted CMS).

5. What CiteRelay puts in your ZIP

Besides your generated pages, exports may include:

  • README-CiteRelay.txt — quick context about the export.
  • 00-START-HERE-how-to-render-and-publish.md — this guide (or an equivalent), prefixed so it appears at the top of the ZIP in alphabetical file listings.

6. Roadmap: zero-manual publishing

Today, ZIP + frontmatter is the portable, CMS-agnostic path. Later, direct integrations (Webflow, WordPress, static hosts) can push the same content for teams that outgrow “copy files into the repo.” The Markdown contract stays the same.


You are reading this page as Markdown served through the same style of pipeline—if it looks good here, you can ship the same experience on your own domain in one afternoon.

CiteRelay

Get your SaaS recommended by AI search engines through optimized AEO content.

Product

  • Features
  • How It Works
  • Pricing

Company

  • Support

Legal

  • Privacy Policy
  • Terms of Service
  • Cookie Policy

© 2026 CiteRelay. All rights reserved.