# Murit CMS Content API v1 — Full AI Integration Specification ## 1. Environment Configuration CMS operators must deploy the registered 20260909_122610_legacy_redirects migration before using legacy URL fields. It covers posts, saved versions, redirect arrays, and nullable blogBasePath. Payload runs registered prodMigrations on production initialization; frontend clients do not migrate the CMS database. See docs/DEPLOYMENT.md for upgrade verification. ```env CMS_BASE_URL="https://murit.space" # Deployed Murit CMS origin CMS_SITE_KEY="site_your_registered_site_key" ``` ## 2. Model Context Protocol (MCP) Stdio Integration Developers using Cursor, Claude Desktop, Windsurf, or Antigravity can interact with Murit CMS directly via MCP. ### Cursor / Claude Desktop configuration: ```json { "mcpServers": { "murit-cms": { "command": "npx", "args": ["tsx", "scripts/mcp-server.ts"], "env": { "CMS_BASE_URL": "https://murit.space", "CMS_SITE_KEY": "site_your_registered_site_key" } } } } ``` Available MCP Tools: - `verify_connectivity`: Check connectivity to the Murit CMS server and verify the configured site key. - `get_site_info`: Retrieve public metadata and configuration for a registered Murit CMS website. - `list_posts`: List published post summaries with pagination and optional category/tag filters. - `get_post_by_slug`: Retrieve a single published post by slug, including full pre-rendered sanitized HTML. - `get_sitemap`: Fetch all published post URLs and last-modified dates for SEO sitemap generation. - `get_legacy_posts`: Fetch all published posts formatted as an Astro/flat legacy feed with preserved URLs, redirects, and rich HTML. ## 3. TypeScript Interfaces ```typescript export interface MuritMedia { url: string | null; alt: string; caption: string | null; width: number | null; height: number | null; } export interface MuritTaxonomy { name: string; slug: string; } export interface MuritPostMeta { title: string; description: string | null; socialImage: MuritMedia | null; canonicalURL: string; keywords: string | null; } export interface MuritPostSummary { id: string; slug: string; title: string; excerpt: string | null; publishedAt: string; modifiedAt: string; featuredMedia: MuritMedia | null; categories: MuritTaxonomy[]; tags: MuritTaxonomy[]; seo: MuritPostMeta; redirectUrls?: string[]; legacyUrl?: string; } export interface MuritPostDetail extends MuritPostSummary { contentHtml: string; // Sanitized HTML from the Lexical editor } export interface MuritPaginationMeta { limit: number; nextCursor: string | null; } export interface MuritListResponse { data: T[]; meta: MuritPaginationMeta; } export interface MuritSingleResponse { data: T; } export interface MuritSitemapItem { url: string; lastModified: string; } // Shape returned by ?format=astro, /legacy-posts, and /astro-posts. export interface AstroPost { id: string; // slug, or the numeric id when no slug exists title: string; date: string; // publishedAt excerpt: string | null; image: string | null; imageAlt: string; imageWidth: number | null; imageHeight: number | null; seoImage: string | null; seoImageWidth: number | null; seoImageHeight: number | null; categories: string[]; // category names tags: string[]; // tag names seoKeywords: string | null; seoTitle: string; seoDescription: string | null; contentHtml: string; contentMarkdown: string; updatedAt: string; lastUpdatedAt: string | null; redirectUrls?: string[]; legacyUrl?: string; } ``` ## 4. REST Endpoints & Contracts ### GET /api/content/v1/sites/{siteKey}/posts - Query Parameters: - limit: integer (1 to 50, default 20) - cursor: string (opaque nextCursor from previous response) - category: string (category slug) - tag: string (tag slug) - format: "default" | "astro" - Response: MuritListResponse, or { posts: AstroPost[] } when format=astro. ### GET /api/content/v1/sites/{siteKey}/posts/{slug} - Query Parameters: - format: "default" | "astro" - Response: MuritSingleResponse, or { post: AstroPost } when format=astro. - Note: Returns HTTP 404 for unknown, suspended, draft, or cross-site posts. ### GET /api/content/v1/sites/{siteKey}/legacy-posts - Dedicated JSON feed optimized for Astro SSG and legacy site migration. - Response: { posts: AstroPost[] } - Note: Preserves exact casing, hyphens, canonical URLs with root-level '/' base path, legacyUrl, and redirectUrls. Also available as /astro-posts and as /posts?format=legacy. ### GET /rjls-api/posts?siteKey={siteKey} - Backward-compatibility alias for the pre-migration RJLS frontend. Requires an explicit, registered siteKey. It returns { posts: AstroPost[] } for that website only and never falls back to another tenant. ### GET /api/content/v1/sites/{siteKey}/sitemap - Query Parameters: - limit: integer (1 to 500) - cursor: string - Response: MuritListResponse ### GET /api/content/v1/sites/{siteKey}/preview?token={signedToken} - Private preview endpoint for unpublished saved drafts. - Returns MuritSingleResponse with Cache-Control: private, no-store. ### POST /api/workspace/migrate - Authenticated endpoint for bulk legacy blog migration (WordPress, Payload, JSON feeds). - Actions: 'preview' (dry-run inspection) or 'import' (convert HTML to Lexical, sync taxonomies, attach media, save articles). - Remote endpointURL imports only fetch public HTTP(S) destinations; redirects are rejected. ## 5. Next.js 16 (App Router) Copy-Paste Recipe ```tsx // app/blog/page.tsx export default async function BlogPage() { const res = await fetch( `${process.env.CMS_BASE_URL}/api/content/v1/sites/${process.env.CMS_SITE_KEY}/posts?limit=12`, { next: { revalidate: 60 } } ); if (!res.ok) throw new Error('Failed to load posts'); const { data: posts } = await res.json(); return (

Blog

{posts.map((post: any) => ( ))}
); } ``` ```tsx // app/blog/[slug]/page.tsx import { notFound } from 'next/navigation'; export default async function ArticlePage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const res = await fetch( `${process.env.CMS_BASE_URL}/api/content/v1/sites/${process.env.CMS_SITE_KEY}/posts/${slug}`, { next: { revalidate: 60 } } ); if (res.status === 404) notFound(); if (!res.ok) throw new Error('Failed to fetch post'); const { data: post } = await res.json(); return (

{post.title}

); } ``` ## 6. Astro 5 Dynamic Route Recipe (`src/pages/[...slug].astro`) ```astro --- export async function getStaticPaths() { const res = await fetch( `${import.meta.env.CMS_BASE_URL}/api/content/v1/sites/${import.meta.env.CMS_SITE_KEY}/legacy-posts` ); if (!res.ok) throw new Error('Failed to fetch posts'); const { posts } = await res.json(); return posts.map((post: any) => ({ params: { slug: post.id }, props: { post }, })); } const { post } = Astro.props; --- {post.seoTitle || post.title}

{post.title}

``` ## 7. Webhooks & Cache Invalidation Murit CMS POSTs a signed JSON event to a website's configured cacheInvalidationURL when published content changes: ```json { "eventId": "uuid", "schemaVersion": 1, "type": "post.published | post.updated | post.unpublished | post.deleted", "siteKey": "site_your_registered_site_key", "postId": "123", "slug": "current-slug", "previousSlug": "optional-old-slug", "timestamp": "2026-09-11T00:00:00.000Z" } ``` The event carries identifiers only — never the article body. Requests include: - `X-CMS-Event-ID`: the event id (idempotency key) - `X-CMS-Signature`: `t=,v1=` where hex is HMAC-SHA256 of `"."` using the website's cacheInvalidationSecret. In Next.js, verify the signature and revalidate on demand: ```ts // app/api/revalidate/route.ts import { createHmac } from 'crypto'; import { revalidatePath } from 'next/cache'; import { NextRequest, NextResponse } from 'next/server'; export async function POST(req: NextRequest) { const body = await req.text(); const match = /^t=(d+),v1=([0-9a-f]+)$/.exec(req.headers.get('x-cms-signature') || ''); const secret = process.env.CMS_INVALIDATION_SECRET || ''; const expected = match ? createHmac('sha256', secret).update(`${match[1]}.${body}`).digest('hex') : ''; if (!match || expected !== match[2]) { return NextResponse.json({ message: 'Invalid signature' }, { status: 401 }); } const event = JSON.parse(body); if (event.slug) revalidatePath(`/blog/${event.slug}`); revalidatePath('/blog'); return NextResponse.json({ revalidated: true }); } ``` ## 8. OpenAPI 3.1 Machine Specification Fetch complete OpenAPI 3.1 JSON at: GET https://murit.space/openapi.json ## 9. AI Agent Integration & Rules Murit CMS provides pre-configured directives for AI coding agents: - OpenCode: opencode.json (pre-wires scripts/mcp-server.ts) & OPENCODE.md - Claude Code: CLAUDE.md (imports @AGENTS.md) - Antigravity & OpenAI Codex: AGENTS.md - Cursor: .cursorrules - Windsurf: .windsurfrules - GitHub Copilot: .github/copilot-instructions.md - Stdio MCP Server: scripts/mcp-server.ts