March 29, 2026 06:43 PM

Article platform with AI out of the box: editor, media, analytics, and Markdown for LLM agents

Overview: server-side model calls without leaking keys, structured suggestions for SEO and body text, image and voice generation, model usage and view tracking, and the same public article served as HTML for humans and as Markdown—with metadata and token estimates—for tools and AI agents.

AI generated preview
AI generated preview

Why this matters out of the box

The shipped stack covers a typical editorial and site workflow:

  • The editor gets model suggestions in the context of the already saved article and revision—without a separate “chat in the browser with a key.”

  • SEO and preview can be generated in a structured way and applied to forms in one action; article body is a separate Markdown stream with round-trip into TipTap.

  • Images and voice go through the same media pipeline (CDN URLs) as author uploads—no manual handoff between services.

  • Administrators see aggregated token spend by source and user, plus article and revision views separately.

  • External systems and LLM agents get the same canonical URL /article/{slug} as readers, but with the Accept: text/markdown header—the response is a document with YAML front matter, a Content-Signal header, and a token count in x-markdown-tokens, without parsing HTML or a separate “API for bots.”

Below: principles, UI surfaces, API routes, and placeholders for illustrations and sample responses.


How it works — principles

Principle

Meaning

Keys only on the server

LLM_API_KEY never reaches the client; only flags and model lists are exposed via GET /api/v1/llm/models.

Explicit AI enablement

NEXT_PUBLIC_LLM_ENABLED must be literally true; otherwise UI and server checks treat LLM as off.

Immutable usage log

Every significant model call is recorded in LlmUsageEvent with a source field (chat, audit, SEO, preview, content, TTS, images, etc.).

Media in one pipeline

Generated images and TTS mp3 upload through the existing media flow; the UI sees ordinary URLs.

Public article — two representations

HTML vs Markdown is negotiated via Accept; caching uses Vary: Accept so a CDN does not mix responses.

Environment variables for LLM and chat limits are described in config/env.ts (LLM_CONFIG) and .env.example: LLM_API_KEY, NEXT_PUBLIC_LLM_ENABLED, LLM_CHAT_MODELS, LLM_IMAGE_MODELS, LLM_CHAT_RATE_LIMIT_POINTS, LLM_CHAT_RATE_DURATION_SEC, and APP_INTERNAL_ORIGIN for correct server-side calls to your own APIs (e.g. behind a reverse proxy or in Docker) when setting Content-Signal on the article HTML page.



One URL — HTML for humans, Markdown for agents

For a published public article, the canonical URL stays **/article/{slug}**. The browser requests HTML as usual. A client that sends **Accept: text/markdown** (with higher priority than text/html) receives not HTML but Markdown text with YAML front matter at the top: title, description, slug, canonical, language, training flag (allow_ai_training), publication and update dates—then the article body in Markdown, aligned with the same content rendering used for HTML.

Practical benefits for LLMs and integrations:

  • No need to fetch HTML, strip tags, and lose structure;

  • One request returns token-efficient text plus machine-readable metadata;

  • The **x-markdown-tokens** header estimates document size in tokens (via gpt-tokenizer, o200k_base profile)—handy for budgeting calls to your own or an external model;

  • The **Content-Signal** header conveys policy in the spirit of Content Signals: e.g. whether the material may be used for training (ai-train=yes|no) while keeping search=yes, ai-input=yes for public delivery.

Technically the request is handled in **src/proxy.ts**: when Accept matches, a rewrite runs to the internal route GET /api/v1/public/article/markdown (slug via query and a helper header). For HTML responses, Content-Signal is merged from a separate request to GET /api/v1/public/article/content-signal?slug=... with base APP_INTERNAL_ORIGIN when the public request origin is unreliable.

Example calls (curl)

Request Markdown at the public article URL (as an agent or script would):

curl -sS -D - -o /tmp/article.md \
  -H "Accept: text/markdown;q=1.0, text/html;q=0.5" \
  "https://example.com/article/my-article-slug"

The response should include among other things:

  • Content-Type: text/markdown; charset=utf-8

  • Vary: Accept

  • Content-Signal: ai-train=yes, search=yes, ai-input=yes (values depend on article settings)

  • x-markdown-tokens:

Direct call to the internal API (handy for docs and debugging; production base URL and rate limits may differ):

curl -sS -D - -o /tmp/article.md \
  "https://example.com/api/v1/public/article/markdown?slug=my-article-slug"

Sample JSON from content-signal (for documentation)

The GET /api/v1/public/article/content-signal?slug=... endpoint returns JSON with a ready-made string for the Content-Signal header:

{
  "contentSignal": "ai-train=yes, search=yes, ai-input=yes"
}

If the article has allowAiTraining: false, the response will include ai-train=no.

Sample Markdown body fragment (YAML + start of text)

Below is an illustrative format fragment (front matter fields match buildPublicArticleMarkdownDocument):

---
title: "Article title"
description: "Short description for preview and agents"
slug: "my-article-slug"
canonical: "https://example.com/article/my-article-slug"
language: "en"
allow_ai_training: yes
published_at: "2026-03-01T12:00:00.000Z"
updated_at: "2026-03-15T09:30:00.000Z"

Comparison: HTML vs Markdown for one slug

Criterion

HTML (browser default Accept)

Markdown (Accept: text/markdown preferred over HTML)

Content type

text/html

text/markdown; charset=utf-8

Ease of parsing for agents

low (markup noise)

high (structure, fewer tokens per unit of meaning)

Metadata

mostly in and markup

YAML at the top of the file

Volume estimate for LLMs

non-standardized

x-markdown-tokens header


Editor: streaming chat, audit, structured suggestions

Chat

POST /api/v1/llm/chat/stream — SSE stream; context is built from the article title, SEO fields, and plain text from TipTap (extractPlainTextFromRevisionContent). Only ADMIN / EDITOR roles; per-user rate limit applies. Usage events use source: chat_stream. History is loaded via GET /api/v1/llm/chat/history (LlmChatSession, LlmChatMessage). AI entry points in the UI are available after the article is saved with a revision.

{
  "enabled": true,
  "chat": { "models": [{ "id": "gpt-4o-mini", "label": "GPT-4o mini" }] },
  "audit": { "models": [{ "id": "gpt-4o-mini", "label": "GPT-4o mini" }] },
  "image": {
    "models": [
      {
        "id": "gpt-image-1",
        "label": "GPT Image 1",
        "defaultAspectRatioId": "16:9",
        "aspectRatios": [{ "id": "16:9", "label": "16:9", "size": "1536x1024" }]
      }
    ]
  }
}

Article audit

POST /api/v1/llm/article-audit — JSON mode; results are stored in LlmArticleAudit, list via GET /api/v1/llm/article-audit. Response types include preview, content, seo sections with scores, strengths, issues, and recommendations.

{
  "audit": {
    "preview": {
      "score": 8,
      "summary": "Preview matches the topic.",
      "strengths": ["Clear lead"],
      "issues": ["Long second sentence"],
      "recommendations": ["Shorten the subtitle"]
    },
    "content": { "summary": "…", "strengths": [], "issues": [], "recommendations": [] },
    "seo": { "summary": "…", "strengths": [], "issues": [], "recommendations": [] },
    "overall": "Ready to publish with preview tweaks."
  },
  "usage": { "promptTokens": 1200, "completionTokens": 400, "totalTokens": 1600 },
  "model": "gpt-4o-mini",
  "savedId": "67d…"
}

SEO, preview, and body (structured API responses)

Route

Purpose

Key JSON fields

POST /api/v1/llm/seo/suggest

Meta and OG

metaTitle, metaDescription, ogTitle, ogDescription, keywords

POST /api/v1/llm/preview/suggest

Preview card

title, description, rationale

POST /api/v1/llm/content/suggest

Article body

markdown, rationale

Sample SEO response (SeoSuggestApiResponse fields):

{
  "suggest": {
    "metaTitle": "New article: introduction to the platform",
    "metaDescription": "A short overview of the editor and public delivery.",
    "ogTitle": "New article — platform",
    "ogDescription": "Same for social previews.",
    "keywords": "platform, articles, markdown, seo"
  },
  "usage": { "promptTokens": 800, "completionTokens": 200, "totalTokens": 1000 },
  "model": "gpt-4o-mini"
}

Sample content response (ContentSuggestApiResponse):

{
  "suggest": {
    "markdown": "## Introduction\n\nGenerated article body in Markdown…",
    "rationale": "Structure: intro, three sections, conclusion."
  },
  "usage": { "promptTokens": 2000, "completionTokens": 1500, "totalTokens": 3500 },
  "model": "gpt-4o-mini"
}

Image generation

Routes: POST /api/v1/llm/image/generate, .../image/generate/stream, .../image/prompt/stream. Result: a media asset and proxyUrl, plus promptUsed and usage. In the UI: MediaUrlUploadField (article preview, OG, image dialog in the editor).

{
  "asset": { "id": "…", "mimeType": "image/png" },
  "proxyUrl": "https://…",
  "usage": { "promptTokens": 0, "completionTokens": 0, "totalTokens": 1000 },
  "model": "gpt-image-1",
  "aspectRatioId": "16:9",
  "promptUsed": "illustration for the platform article, flat style"
}

Listen audio (TTS)

POST /api/v1/article/listen-audio/generate — mp3 generation, updates listenAudioAssetId and related fields on Article. Usage: source: listen_tts (numeric fields carry character counts for TTS billing alignment).


Article views

POST /api/v1/article/view — increments counters on Article and published ArticleRevision; deduplication via Redis ~24h per visitor/user + article + revision when Redis is available. Admin: GET /api/v1/article/views/dashboard, GET /api/v1/article/views/by-article/[articleId], page /admin/article-views.


LLM usage tracking

LlmUsageEvent log with sources: chat_stream, article_audit, seo_suggest, preview_suggest, content_suggest, listen_tts, image_generate, image_prompt_stream, image_prompt_article.

  • GET /api/v1/llm/usage/dashboard?days= — admin summary (/admin/llm-usage).

  • GET /api/v1/llm/usage/article?articleId=&revisionId= — per article/revision summary for editors.

{
  "since": "2026-03-01T00:00:00.000Z",
  "until": "2026-03-27T23:59:59.999Z",
  "days": 14,
  "totals": {
    "eventCount": 420,
    "totalPromptTokens": 900000,
    "totalCompletionTokens": 300000,
    "totalTokens": 1200000
  },
  "bySource": [
    { "source": "chat_stream", "count": 200, "totalTokens": 400000 },
    { "source": "seo_suggest", "count": 50, "totalTokens": 80000 }
  ],
  "topUsers": [{ "userId": "…", "eventCount": 80, "totalTokens": 200000 }],
  "recent": []
}

HTTP API summary

Area

Method

Path

Chat

POST

/api/v1/llm/chat/stream

Chat

GET

/api/v1/llm/chat/history

Models

GET

/api/v1/llm/models

Audit

POST, GET

/api/v1/llm/article-audit

SEO / preview / content

POST

/api/v1/llm/seo/suggest, /api/v1/llm/preview/suggest, /api/v1/llm/content/suggest

Images

POST

/api/v1/llm/image/generate, /api/v1/llm/image/generate/stream, /api/v1/llm/image/prompt/stream

Usage

GET

/api/v1/llm/usage/dashboard, /api/v1/llm/usage/article

TTS

POST

/api/v1/article/listen-audio/generate

Views

POST

/api/v1/article/view

Views (admin)

GET

/api/v1/article/views/dashboard, /api/v1/article/views/by-article/[articleId]

Public: Markdown

GET

/api/v1/public/article/markdown

Public: signal

GET

/api/v1/public/article/content-signal


Summary

The platform provides a single workflow for editorial work: AI is tied to the saved document, structured responses map to forms and the editor, media and voice stay on the CDN path, model spend and views are visible in admin. Separately, public Markdown delivery shares content negotiation on the same URL as HTML: it lowers token cost for downstream LLMs, simplifies integrations, and works with **Content-Signal** and **x-markdown-tokens** — without a separate “human” vs “machine” site URL.


Additional materials in the repository

Detailed implementation checklists and plans: docs/AI_FEATURES_ROADMAP.md, docs/PRODUCT_ROADMAP.md.

UI screenshots, dashboards, etc.

Article edit / view — overview
Article edit / view — overview
AI assistant workflow form
AI assistant workflow form
Article revision audit flow
Article revision audit flow