Menu

How to Use the CMS Module

Good to know

In the v3.2.3 - 3.2.4 updates, NEXTY.DEV's CMS was refactored using TipTap, gaining more features and a more perfect content editor experience.

Supported Features

The CMS functionality provided by the NEXTY.DEV boilerplate is among the most comprehensive of all SaaS boilerplates, with very rich features. Through TipTap's rich plugin ecosystem, you can easily extend even more functionality.

The following are the features already supported by the NEXTY.DEV boilerplate:

Core Features

  • Multi-content Module Support: Default provides two modules: blog and glossary
  • Unified Architecture Design: Common base components and shared data tables, allowing you to create new content modules by adding minimal files and code
  • Multi-content Source Support: Supports local MDX file rendering and server-side CMS data rendering, with each module independently configurable
  • Reading Statistics: Supports reading statistics with two modes:
    • all mode: Counts every page load
    • unique mode: Counts only once per hour for the same IP address
  • Table of Contents (TOC): Detail pages automatically generate article directories, supporting desktop sidebar and mobile collapsible display
  • Related Articles Recommendation: Detail pages support displaying related articles (based on the same tag)
  • Cover Image Support: Supports local upload, external links, and CloudFlare R2 selection
  • AI Translation: Supports built-in translation button in the editor, enabling quick translation of selected content
  • Flexible Configuration: Most toolbar plugins support enabling or disabling through configuration

Content Management Features

Creating and editing content supports:

  • Basic Information:
    • Title, URL alias (slug), description
    • Tag management, supports up to 5 tags
    • Cover image upload (supports local upload, external links, CloudFlare R2 selection)
  • Multi-language Support:
    • Select content language (supports all configured languages)
    • Supports different languages with the same slug design
  • Advanced Features:
    • Status Settings: Draft, published, archived
    • Visibility Control: Public, logged_in, subscribers
    • Pinning Feature: Supports pinning important content for display
  • Content Protection: For content requiring login or subscription, the system automatically displays access restriction prompts

Dashboard Management Features

  • Data Table Display:
    • Paginated browsing of all content
    • Supports search filtering (title, slug, description)
    • Supports filtering by status and language
  • Tag Management:
    • Create, edit, delete tags
    • Tags are associated with content modules (each module has an independent tag system)
  • Batch Operations:
    • Duplicate content functionality
    • Delete content
  • Cover Image Management:
    • Supports configuring whether to display cover images on list pages (showCoverInList configuration item)
    • Automatically generates dynamic OG images when no cover image is uploaded

Directory Structure

components/cms/               // CMS common components
  ├─ PostDataTable.tsx        // Dashboard list & pagination
  ├─ PostEditorClient.tsx     // Create/edit entry (client component)
  ├─ PostForm.tsx             // Form component
  ├─ PostList.tsx             // Frontend infinite scroll list
  ├─ PostCard.tsx             // Frontend content card
  ├─ PostListActions.tsx      // Dashboard action buttons (edit, duplicate, delete)
  ├─ RelatedPosts.tsx         // Related articles component
  ├─ ContentRestrictionMessage.tsx  // Content access restriction prompt
  ├─ ImageUpload.tsx          // Image upload component (supports R2)
  ├─ TagInput.tsx             // Tag input component
  ├─ TagSelector.tsx          // Tag selector (frontend filtering)
  ├─ TagSelectDialog.tsx      // Tag selection dialog
  ├─ TagCreateForm.tsx        // Tag creation form
  ├─ TagManagementDialog.tsx  // Tag management dialog
  └─ post-config.ts           // PostType configuration center

components/tiptap/            // TipTap rich text editor
  ├─ TiptapEditor.tsx         // Rich text editor main component
  ├─ TiptapRenderer.tsx       // Frontend read-only renderer
  ├─ ImageGridExtension.ts    // Image grid custom Node
  ├─ ImageGridNodeView.tsx    // Edit mode image grid view
  ├─ ImageGridReadOnlyView.tsx // Read mode image grid view
  ├─ R2ResourceSelector.tsx   // R2 media selector
  ├─ TableMenu.tsx            // Table floating toolbar
  ├─ TableOfContents.tsx      // Table of contents generation component
  └─ TranslationButton.tsx    // AI translation button

lib/cms/
  └─ index.ts                 // CMS module factory function

actions/posts/                // Server Actions
  ├─ posts.ts                 // Content CRUD + frontend reading
  ├─ tags.ts                  // Tag CRUD
  └─ views.ts                 // View count statistics

app/[locale]/(protected)/dashboard/(admin)/blogs/
  ├─ page.tsx                 // Dashboard list page
  ├─ Columns.tsx              // Table column definitions
  ├─ new/page.tsx             // Create page
  └─ [postId]/edit/page.tsx   // Edit page

app/[locale]/(protected)/dashboard/(admin)/glossary/
  ├─ page.tsx                 // Dashboard list page
  ├─ Columns.tsx              // Table column definitions
  ├─ new/page.tsx             // Create page
  └─ [postId]/edit/page.tsx   // Edit page

app/[locale]/(basic-layout)/blog/
  ├─ page.tsx                 // Frontend list
  └─ [slug]/
      ├─ page.tsx             // Detail page
      └─ opengraph-image.tsx  // Dynamic OG image

app/[locale]/(basic-layout)/glossary/
  ├─ page.tsx                 // Frontend list
  └─ [slug]/
      ├─ page.tsx             // Detail page
      └─ opengraph-image.tsx  // Dynamic OG image

lib/db/schema.ts              // Drizzle data model
  // Related tables: posts, tags, postTags

types/cms.ts                  // TypeScript type definitions

config/common.ts              // Common configuration (image paths, etc.)

Configuration

PostConfig Configuration Items

In components/cms/post-config.ts, each content module has the following configuration items:

export interface PostConfig {
  postType: PostType;                    // Content type
  schema: z.ZodSchema;                   // Validation schema
  actionSchema: z.ZodSchema;             // Action validation schema
  imagePath: string;                     // Image storage path
  enableTags: boolean;                   // Whether to enable tags
  localDirectory?: string;               // Local MDX file directory (optional)
  viewCount: ViewCountConfig;            // View count statistics configuration
  showCoverInList: boolean;              // Whether to display cover image on list page
  routes: {                              // Route configuration
    list: string;
    create: string;
    edit: (id: string) => string;
  };
}

View Count Statistics Configuration

export interface ViewCountConfig {
  enabled: boolean;      // Whether to enable view count statistics
  mode: 'all' | 'unique'; // Statistics mode
  showInUI: boolean;     // Whether to display in UI
}

Feature Showcase

Since CMS features are all WYSIWYG, we won't go into detail here. You can understand the features through the images.

cms
cms
cms
cms
cms
cms

Image upload supports local upload, external links, and CloudFlare R2 selection

cms
cms

Image display supports multiple images side by side

cms

Supports inserting YouTube videos

cms

Editor content supports quick translation

cms

How to Modify Content Editor Features

All feature files for the content editor are under components/tiptap. You can familiarize yourself with the code and modify it, or let AI help you modify it. AI is very proficient with Tiptap.

Editor Configuration Options

The TiptapEditor component supports the following configuration:

interface TiptapEditorProps {
  content: string;
  onChange: (content: string) => void;
  placeholder?: string;
  disabled?: boolean;
  enableYoutube?: boolean;        // Enable YouTube video
  enableImage?: boolean;          // Enable image upload
  enableCodeBlock?: boolean;      // Enable code block
  enableTable?: boolean;          // Enable table
  enableBlockquote?: boolean;     // Enable blockquote
  enableHorizontalRule?: boolean; // Enable horizontal rule
  enableR2Selector?: boolean;     // Enable R2 resource selector
  r2PublicUrl?: string;           // R2 public access URL
  imageUploadConfig?: {
    maxSize?: number;             // File size limit (bytes)
    filenamePrefix?: string;      // Filename prefix
    path?: string;                // Upload path
  };
  enableTranslation?: boolean;    // Enable AI translation
  postType?: PostType;            // Content type
  outputFormat?: "markdown" | "text" | "html"; // Output format
}

How to Add New Content Modules

If you're developing a content website, the blog and glossary modules might not be enough. You want to add multiple content modules to enrich your website's content structure. This requirement can be easily solved using the NEXTY.DEV boilerplate.

Taking adding a new guide module as an example, you only need to complete the following steps:

1. Extend Database Schema

Extend the CMS content type in lib/db/schema.ts:

export const postTypeEnum = pgEnum('post_type', [
  'blog',
  'glossary',
  'guide', // New addition
])

Then execute the database migration commands:

pnpm db:generate
pnpm db:migrate

2. Extend CMS Configuration

Add the new module configuration in components/cms/post-config.ts:

export const POST_CONFIGS: Record<PostType, PostConfig> = {
  blog: {
    // ... existing configuration
  },
  glossary: {
    // ... existing configuration
  },
  // New guide configuration
  guide: {
    postType: "guide",
    schema: basePostSchema,
    actionSchema: postActionSchema,
    imagePath: GUIDE_IMAGE_PATH, // Needs to be defined in config/common.ts
    enableTags: true,
    // localDirectory: 'guides', // If you need to support local MDX files
    viewCount: {
      enabled: true,  // Enable view count statistics
      mode: 'unique', // Use unique IP statistics
      showInUI: true,
    },
    showCoverInList: true,
    routes: {
      list: "/dashboard/guides",
      create: "/dashboard/guides/new",
      edit: (id: string) => `/dashboard/guides/${id}`,
    },
  },
};

3. Add Image Path Configuration

Add in config/common.ts:

export const GUIDE_IMAGE_PATH = "guide-images";
 
export const R2_CATEGORIES: R2Category[] = [
  { name: "All", prefix: "" },
  { name: "Admin Uploads", prefix: `${ADMIN_UPLOAD_IMAGE_PATH}/` },
  { name: "Blogs Images", prefix: `${BLOGS_IMAGE_PATH}/` },
  { name: "Glossary Images", prefix: `${GLOSSARY_IMAGE_PATH}/` },
  { name: "Guide Images", prefix: `${GUIDE_IMAGE_PATH}/` }, // New addition
];

4. Create Dashboard Management Pages

Copy the app/[locale]/(protected)/dashboard/(admin)/glossary folder and rename it to guides.

Modify the following files:

guides/page.tsx:

// Change postType to "guide"
const result = await listPostsAction({
  pageIndex: 0,
  pageSize: PAGE_SIZE,
  postType: "guide", // Modify here
});
 
// Update configuration
<PostDataTable
  config={{
    postType: "guide", // Modify here
    columns,
    listAction: listPostsAction,
    createUrl: "/dashboard/guides/new", // Modify here
    enableTags: true,
    searchPlaceholder: "Search guides...", // Modify here
  }}
  // ...
/>

guides/Columns.tsx: Adjust table column definitions as needed.

guides/new/page.tsx and guides/[postId]/edit/page.tsx:

<PostEditorClient
  postType="guide" // Modify here
  mode="create" // or "edit"
  r2PublicUrl={r2PublicUrl}
  postId={postId}
/>

5. Create Frontend Display Pages

Copy the app/[locale]/(basic-layout)/glossary folder and rename it to guide.

guide/page.tsx:

// Create CMS module instance (in lib/cms/index.ts)
export const guideCms = createCmsModule('guide');
 
// Use in page
const { posts: localPosts } = await guideCms.getLocalList(locale);
 
const initialServerPostsResult = await listPublishedPostsAction({
  pageIndex: 0,
  pageSize: SERVER_POST_PAGE_SIZE,
  postType: "guide", // Modify here
  locale: locale,
});
 
// Tags
const tagsResult = await listTagsAction({ postType: "guide" });
 
// PostList component
<PostList
  postType="guide"
  baseUrl="/guide"
  localPosts={localPosts}
  initialPosts={initialServerPosts}
  initialTotal={totalServerPosts}
  serverTags={serverTags}
  locale={locale}
  pageSize={SERVER_POST_PAGE_SIZE}
  showTagSelector={true}
  showCover={POST_CONFIGS.guide.showCoverInList}
  emptyMessage="No guides found for this tag."
/>

guide/[slug]/page.tsx:

// Use guideCms to get content
const { post, errorCode } = await guideCms.getBySlug(slug, locale);
 
// View count statistics
const viewCountConfig = POST_CONFIGS.guide.viewCount;
if (viewCountConfig.enabled) {
  if (viewCountConfig.mode === "unique") {
    await incrementUniqueViewCountAction({ slug, postType: "guide", locale });
  } else {
    await incrementViewCountAction({ slug, postType: "guide", locale });
  }
}
 
// Related articles
<RelatedPosts
  postId={post.id}
  postType="guide"
  limit={10}
  title="Related Guides"
  locale={locale}
  CardComponent={GuidePostCard}
/>

6. Export New Module in lib/cms/index.ts

// Add at the end of the file
export const guideCms = createCmsModule('guide');

7. Update sitemap.ts

Add Guide page routes in app/sitemap.ts:

// Add guide list page
{
  url: `${siteUrl}/${locale}/guide`,
  lastModified: new Date(),
  changeFrequency: 'daily',
  priority: 0.8,
}
 
// Add guide detail pages
const guideResult = await listPublishedPostsAction({
  locale: locale,
  pageSize: 1000,
  visibility: "public",
  postType: "guide",
});
 
if (guideResult.success && guideResult.data?.posts) {
  guideResult.data.posts.forEach((post) => {
    sitemap.push({
      url: `${siteUrl}/${locale}/guide/${post.slug}`,
      lastModified: post.updatedAt ? new Date(post.updatedAt) : new Date(),
      changeFrequency: 'weekly',
      priority: 0.7,
    });
  });
}

8. Add Internationalization Translations

Add relevant translations in the language files under the messages/ directory.

Notes

  1. Local MDX File Support: If the new CMS module doesn't need to support local MDX files, don't set the localDirectory configuration item, just use the database directly.

  2. Independent Tag System: Each content module's tag system is independent, distinguished by the postType field.

  3. Image Path Planning: Set independent image storage paths for each content module for easier management and organization.

  4. View Count Statistics: Redis needs to be configured to use the view count statistics feature. If Redis is not configured, the system will gracefully degrade and return 0 counts.

  5. Content Visibility:

  • public: Visible to everyone
  • logged_in: Requires login
  • subscribers: Requires subscription (customize subscription logic in the checkUserSubscription function in actions/posts/posts.ts)
  1. SEO Optimization:
  • When no cover image is uploaded, the system automatically generates dynamic OG images
  • Remember to update sitemap.ts to include routes for the new module
  • Use the constructMetadata function to ensure correct metadata

That's it! A brand new CMS module is complete!

If you find adding modules yourself too troublesome, send the documentation link to AI, and AI will help you complete all the steps.

Summary

NEXTY.DEV's CMS system has the following advantages:

  1. Clear Architecture: Unified configuration center, easy to extend and maintain
  2. Comprehensive Features: Everything from content creation and editing to frontend display
  3. Flexible Configuration: Each module can independently configure feature characteristics
  4. Excellent User Experience: TipTap editor is powerful and smooth to operate
  5. High Development Efficiency: Adding new modules requires minimal code, most components are reusable
  6. Type Safety: Complete TypeScript type definitions
  7. Performance Optimization: Supports infinite scroll, pagination, caching, and other optimization methods

Whether it's a personal blog, enterprise content site, or complex multi-module content platform, NEXTY.DEV's CMS system can handle it with ease!