Docs
Features

Content Management

Full CMS for articles and standalone pages with TipTap rich-text editor, draft/publish/schedule workflow, categories, tags, SEO fields, and in-article product embeds.

Articles vs Pages

ArticlesPages
type valuearticlepage
Listed in blog index
Categories & tagsoptional
Scheduled publish
SEO fields
Product embeds

Rich Text Editor

Content is edited with a TipTap-based rich-text editor that supports:

  • Headings (H1–H6), bold, italic, underline, strikethrough
  • Ordered and unordered lists, blockquotes, code blocks
  • Hyperlinks with target/rel configuration
  • Image upload (drag-and-drop or file picker) via the File Upload system
  • oEmbed / iframe embeds (YouTube, Twitter, etc.)
  • In-article product cards — embed a purchasable product inline

Product Card Embed

While editing an article, click Insert → Product and select a product. The embed stores the product ID and renders as a styled card on the front end:

// The TipTap extension stores a node like:
// { type: 'productCard', attrs: { productId: 'prod_xxx' } }

// The renderer fetches and displays the product at read time
import { ProductCard } from '@/components/content/ProductCard'

Status Workflow

Every post moves through a clear status pipeline:

Draft → Published
      → Scheduled  (auto-publishes at scheduledAt datetime)
      → Trash
StatusVisible to publicNotes
draftWork in progress
publishedLive immediately
scheduledPublished automatically at scheduledAt
trashSoft-deleted; can be restored

Scheduled publishing is handled by a cron-compatible API route. Point your scheduler (Vercel Cron, GitHub Actions, etc.) at POST /api/admin/posts/publish-scheduled.

Categories

Categories support a 3-level hierarchy (root → second level → third level). Each category has a name, slug, optional description, an optional parentId pointing to its parent, and a locale field that ties the category to a specific language. A post can belong to multiple categories.

LevelDescription
Level 1 (root)Top-level category, no parent
Level 2Child of a root category
Level 3Child of a level-2 category; cannot have further children

Locale-aware Categories

Every category carries a locale value (e.g. en, zh). In the Admin Panel the category list is grouped by language, so English and Chinese categories are managed separately. When creating a child category, only parents with the same locale are shown in the parent selector.

Manage via API

// Create a root category (English)
const root = await prisma.category.create({
  data: { name: 'AI', slug: 'ai', locale: 'en', description: 'Artificial intelligence topics' },
})

// Create a child category (level 2, same locale)
const child = await prisma.category.create({
  data: { name: 'LLMs', slug: 'llms', parentId: root.id, locale: 'en' },
})

// Fetch full category tree with post counts
const categories = await prisma.category.findMany({
  where: { parentId: null },          // fetch roots only
  include: {
    children: {
      include: {
        children: { include: { _count: { select: { posts: true } } } },
        _count: { select: { posts: true } },
      },
    },
    _count: { select: { posts: true } },
  },
  orderBy: { name: 'asc' },
})

Tags

Tags are free-form and many-to-many. A post can have unlimited tags; each tag can appear on many posts.

const post = await prisma.post.create({
  data: {
    title: 'My Article',
    // ...
    tags: {
      connectOrCreate: ['nextjs', 'typescript'].map(name => ({
        where: { name },
        create: { name, slug: name },
      })),
    },
  },
})

SEO Fields

Every post has dedicated SEO fields independent of the main title and content:

FieldPurpose
seoTitleOverrides <title> tag
seoDescription<meta name="description">
featuredImageOG image, Twitter Card image

If seoTitle / seoDescription are empty the system falls back to title / excerpt automatically.

Set the featured image in one of two ways:

  1. Upload — drag a file onto the image picker in the editor; stored via the File Upload system
  2. URL — paste any external image URL directly

Querying Content

List Published Articles

import { prisma } from '@/lib/db/prisma'

const articles = await prisma.post.findMany({
  where: {
    type: 'article',
    status: 'published',
  },
  select: {
    id: true, title: true, slug: true, excerpt: true,
    featuredImage: true, publishedAt: true,
    categories: { select: { name: true, slug: true } },
    tags: { select: { name: true } },
  },
  orderBy: { publishedAt: 'desc' },
})

Single Article by Slug

const article = await prisma.post.findFirst({
  where: { slug: params.slug, type: 'article', status: 'published' },
  include: {
    categories: true,
    tags: true,
  },
})

Admin Panel Integration

All content is manageable from the Admin Panel without touching code:

  • Admin → Posts — create, edit, preview, change status, delete
  • Admin → Pages — same workflow for standalone pages
  • Admin → Categories — manage categories
  • Admin → Tags — view and merge tags

See Admin Panel for details on the editorial UI.

Next Steps

  • Admin Panel — manage content from the dashboard
  • File Upload — configure image storage for the editor
  • SEO — how post SEO fields are rendered
  • Payments — in-article product embed setup
Content Management | Tikship