skills/rytass/utils/quadrats-module

quadrats-module

SKILL.md

Quadrats CMS NestJS Module (Quadrats 富文本 CMS 模組)

Overview

@rytass/quadrats-nestjs 提供 Quadrats CMS 的 NestJS 整合模組,支援富文本內容管理、文章版本控制、多語言內容和圖片上傳。

Quick Start

安裝

npm install @rytass/quadrats-nestjs @quadrats/core

基本設定

import { QuadratsModule } from '@rytass/quadrats-nestjs';

@Module({
  imports: [
    QuadratsModule.forRoot({
      accessKey: 'your-access-key',
      secret: 'your-secret',
      // host: 'https://api.quadrats.io',  // 預設值
    }),
  ],
})
export class AppModule {}

非同步設定

import { QuadratsModule } from '@rytass/quadrats-nestjs';
import { ConfigService } from '@nestjs/config';

@Module({
  imports: [
    QuadratsModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        accessKey: config.get('QUADRATS_ACCESS_KEY'),
        secret: config.get('QUADRATS_SECRET'),
      }),
    }),
  ],
})
export class AppModule {}

Services

QuadratsArticleService

文章 CRUD 與版本管理:

import { QuadratsArticleService, Language } from '@rytass/quadrats-nestjs';
import { Paragraph } from '@quadrats/core';

@Injectable()
export class ContentService {
  constructor(private readonly articleService: QuadratsArticleService) {}

  // 建立文章
  async createArticle() {
    return this.articleService.create({
      title: '文章標題',
      categoryIds: ['category-id'],
      tags: ['tag1', 'tag2'],
      contents: [
        Paragraph.create({ children: [{ text: '文章內容' }] }),
      ],
      language: Language.ZH_TW,
      releasedAt: new Date(),
    });
  }

  // 多語言文章
  async createMultiLangArticle() {
    return this.articleService.create({
      title: '多語言文章',
      categoryIds: ['category-id'],
      tags: ['multilang'],
      languageContents: [
        {
          language: Language.ZH_TW,
          elements: [Paragraph.create({ children: [{ text: '繁體中文內容' }] })],
        },
        {
          language: Language.EN,
          elements: [Paragraph.create({ children: [{ text: 'English content' }] })],
        },
      ],
    });
  }

  // 取得文章
  async getArticle(id: string, versionId?: string) {
    return this.articleService.get(id, versionId);
  }

  // 取得文章 ID 列表
  async getArticleIds() {
    return this.articleService.getIds({
      limit: 10,
      offset: 0,
      categoryIds: ['category-id'],
      tags: ['featured'],
    });
  }

  // 新增版本
  async addVersion(id: string) {
    return this.articleService.addVersion({
      id,
      title: '更新標題',
      categoryIds: ['category-id'],
      tags: ['updated'],
      contents: [
        Paragraph.create({ children: [{ text: '新版本內容' }] }),
      ],
    });
  }

  // 刪除文章
  async removeArticle(id: string) {
    return this.articleService.remove(id);
  }
}

QuadratsArticleCategoryService

分類管理:

import { QuadratsArticleCategoryService } from '@rytass/quadrats-nestjs';

@Injectable()
export class CategoryManagementService {
  constructor(private readonly categoryService: QuadratsArticleCategoryService) {}

  // 取得所有分類
  async getAllCategories() {
    return this.categoryService.getAll();
  }

  // 取得單一分類
  async getCategory(id: string) {
    return this.categoryService.get(id);
  }

  // 建立分類
  async createCategory(name: string, parentId?: string) {
    return this.categoryService.create(name, parentId);
  }

  // 重新命名分類
  async renameCategory(id: string, newName: string) {
    return this.categoryService.rename(id, newName);
  }
}

QuadratsArticleTagService

標籤查詢:

import { QuadratsArticleTagService } from '@rytass/quadrats-nestjs';

@Injectable()
export class TagService {
  constructor(private readonly tagService: QuadratsArticleTagService) {}

  // 取得所有標籤
  async getAllTags() {
    return this.tagService.getAll({
      limit: 50,
      offset: 0,
      searchTerm: 'tech',  // 可選搜尋詞
    });
  }
}

QuadratsArticleImageService

圖片上傳:

import { QuadratsArticleImageService, ImageDetailURL } from '@rytass/quadrats-nestjs';
import { Readable } from 'stream';

@Injectable()
export class ImageUploadService {
  constructor(private readonly imageService: QuadratsArticleImageService) {}

  // 上傳圖片 (urlMode = false 或省略,回傳詳細 URL 資訊)
  async uploadImageWithDetails(buffer: Buffer): Promise<ImageDetailURL> {
    return this.imageService.uploadImage(buffer, false);
    // 回傳: ImageDetailURL { id, preload, thumbnails, public, full }
  }

  // 上傳圖片 (不帶 urlMode 時預設為 false,回傳詳細 URL 資訊)
  async uploadImageDefault(buffer: Buffer): Promise<ImageDetailURL> {
    return this.imageService.uploadImage(buffer);
    // 回傳: ImageDetailURL { id, preload, thumbnails, public, full }
  }

  // 上傳圖片 (urlMode = true,回傳簡單 URL 字串)
  async uploadImageUrl(buffer: Buffer): Promise<string> {
    return this.imageService.uploadImage(buffer, true);
    // 回傳: string (URL)
  }

  // 支援 Stream 上傳
  async uploadFromStream(stream: Readable): Promise<ImageDetailURL> {
    return this.imageService.uploadImage(stream);
  }
}

### ImageDetailURL

```typescript
interface ImageDetailURL {
  id: string;         // 檔案 ID
  preload: string;    // 模糊預載 URL
  thumbnails: string; // 小尺寸 URL
  public: string;     // 標準尺寸 URL
  full: string;       // 大尺寸 URL
}

Language Enum

支援的語言(含別名):

import { Language } from '@rytass/quadrats-nestjs';

enum Language {
  DEFAULT = 'DEFAULT',
  ZH = 'TRADITIONAL_CHINESE',       // 繁體中文(別名)
  ZH_TW = 'TRADITIONAL_CHINESE',    // 繁體中文
  ZH_CN = 'SIMPLIFIED_CHINESE',     // 簡體中文
  EN_US = 'ENGLISH_UNITED_STATES',  // 美式英文(別名)
  EN = 'ENGLISH_UNITED_STATES',     // 美式英文
  EN_GB = 'ENGLISH_UNITED_KINGDOM', // 英式英文
  JA_JP = 'JAPANESE',               // 日文
  JP = 'JAPANESE',                  // 日文(別名)
  KO_KR = 'KOREAN',                 // 韓文
  KR = 'KOREAN',                    // 韓文(別名)
  ES = 'SPANISH_SPAIN',             // 西班牙文
  PT = 'PORTUGUESE_PORTUGAL',       // 葡萄牙文
  DE = 'GERMANY_GERMAN',            // 德文
  IT = 'ITALIAN_ITALY',             // 義大利文
  FR = 'FRENCH_FRANCE',             // 法文
}

提示: ZH/ZH_TWEN/EN_USJP/JA_JPKR/KO_KR 為相同值的別名,可依習慣選用。

Data Types

QuadratsArticle

interface QuadratsArticle {
  id: string;
  versionId: string;
  title: string;
  categories: QuadratsArticleCategory[];
  tags: string[];
  releasedAt: Date | null;
  contents: QuadratsArticleContentItem[];
}

QuadratsArticleCategory

interface QuadratsArticleCategory {
  id: string;
  name: string;
}

QuadratsArticleContentItem

import type { QuadratsElement } from '@quadrats/core';

interface QuadratsArticleContentItem {
  language: string;
  elements: QuadratsElement[];
}

Complete Example

import { Module, Injectable } from '@nestjs/common';
import {
  QuadratsModule,
  QuadratsArticleService,
  QuadratsArticleCategoryService,
  QuadratsArticleImageService,
  Language,
} from '@rytass/quadrats-nestjs';
import { Paragraph, createEditor } from '@quadrats/core';

// Module 設定
@Module({
  imports: [
    QuadratsModule.forRoot({
      accessKey: process.env.QUADRATS_ACCESS_KEY!,
      secret: process.env.QUADRATS_SECRET!,
    }),
  ],
  providers: [BlogService],
})
export class BlogModule {}

// 部落格服務
@Injectable()
export class BlogService {
  constructor(
    private readonly articleService: QuadratsArticleService,
    private readonly categoryService: QuadratsArticleCategoryService,
    private readonly imageService: QuadratsArticleImageService,
  ) {}

  // 發佈文章(含圖片)
  async publishPost(data: {
    title: string;
    content: string;
    categoryId: string;
    tags: string[];
    coverImage?: Buffer;
  }) {
    // 上傳封面圖(不帶 urlMode 或 urlMode=false 時回傳 ImageDetailURL)
    let coverUrl: string | undefined;
    if (data.coverImage) {
      const imageResult = await this.imageService.uploadImage(data.coverImage);
      coverUrl = imageResult.public;
    }

    // 建立文章內容
    const elements = [
      Paragraph.create({ children: [{ text: data.content }] }),
    ];

    // 建立文章
    return this.articleService.create({
      title: data.title,
      categoryIds: [data.categoryId],
      tags: data.tags,
      contents: elements,
      language: Language.ZH_TW,
      releasedAt: new Date(),
    });
  }

  // 取得文章列表
  async getPosts(categoryId?: string, page = 1, pageSize = 10) {
    const ids = await this.articleService.getIds({
      categoryIds: categoryId ? [categoryId] : undefined,
      limit: pageSize,
      offset: (page - 1) * pageSize,
    });

    return Promise.all(ids.map(id => this.articleService.get(id)));
  }

  // 更新文章(建立新版本)
  async updatePost(id: string, updates: { title: string; content: string }) {
    return this.articleService.addVersion({
      id,
      title: updates.title,
      categoryIds: [],
      tags: [],
      contents: [
        Paragraph.create({ children: [{ text: updates.content }] }),
      ],
    });
  }

  // 分類管理
  async setupCategories() {
    const tech = await this.categoryService.create('技術');
    const frontend = await this.categoryService.create('前端', tech.id);
    const backend = await this.categoryService.create('後端', tech.id);

    return { tech, frontend, backend };
  }
}

Configuration Options

interface QuadratsModuleOptions {
  accessKey: string;   // Quadrats API Access Key
  secret: string;      // Quadrats API Secret
  host?: string;       // API Host (預設: https://api.quadrats.io)
}

interface QuadratsModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
  name?: string;
  useFactory: (...args: any[]) => Promise<QuadratsModuleOptions> | QuadratsModuleOptions;
  inject?: (InjectionToken | OptionalFactoryDependency)[];
}

Dependencies

Required:

  • @quadrats/core ^1.1.5 (富文本元素定義)

Peer Dependencies:

  • @nestjs/common ^9.4.2

Troubleshooting

文章建立失敗

確保 contentslanguageContents 其中之一有設定:

// 正確: 使用 contents
await articleService.create({
  title: '標題',
  categoryIds: [],
  tags: [],
  contents: [Paragraph.create({ children: [{ text: '內容' }] })],
});

// 正確: 使用 languageContents
await articleService.create({
  title: '標題',
  categoryIds: [],
  tags: [],
  languageContents: [
    { language: Language.ZH_TW, elements: [...] },
  ],
});

// 錯誤: 兩者都沒設定
await articleService.create({
  title: '標題',
  categoryIds: [],
  tags: [],
  // 會拋出錯誤: `contents` or `languageContents` should be set
});

圖片上傳失敗

確認圖片格式和大小符合要求,並使用正確的 Buffer 或 Stream:

import * as fs from 'fs';

// 從檔案讀取
const buffer = fs.readFileSync('image.jpg');
await imageService.uploadImage(buffer);

// 從 Stream 讀取
const stream = fs.createReadStream('image.jpg');
await imageService.uploadImage(stream);
Weekly Installs
7
Repository
rytass/utils
GitHub Stars
6
First Seen
Feb 5, 2026
Installed on
codex7
amp6
github-copilot6
replit6
kimi-cli6
gemini-cli6