Переглянути документацію

Документаціяv1.0.0 стабільна

Початок роботи

Посібник для невеликого проєкту

Додайте ForgeCMS до невеликого проєкту на Angular/Analog — користувачі, публікації, автентифікація через cookie та робоча адмін-панель — від початку до кінця.

У вас є невеликий проєкт на Angular/Analog. Вам потрібні користувачі, вхід, захищена адмін-панель і кілька типів контенту. Ця сторінка — саме такий шлях від початку до кінця, і тут немає нічого гіпотетичного: кожен фрагмент — це той самий код, який перевіряє apps/tiny-project у цьому репозиторії (spec 055), навмисно крихітний споживач у стилі зовнішнього проєкту, побудований лише на публічних експортах @forge-cms/*, наведених нижче.

1. Встановлення

pnpm add @forge-cms/core @forge-cms/runtime @forge-cms/auth @forge-cms/db @forge-cms/storage \
  @forge-cms/angular @forge-cms/admin

Додайте адаптери свого надійного профілю (див. §9):

pnpm add @forge-cms/cloudflare   # Cloudflare: D1 + R2
pnpm add @forge-cms/s3           # portable: libSQL (in @forge-cms/db) + S3

2. Визначення користувачів і публікацій

// server/collections.ts
import { defineCollection, defineField } from '@forge-cms/core';
import { defineUsersCollection } from '@forge-cms/auth';

export const users = defineUsersCollection();

export const posts = defineCollection({
  slug: 'posts',
  drafts: true,
  admin: { useAsTitle: 'title', defaultColumns: ['title', 'author'] },
  fields: {
    title: defineField.text({ required: true }),
    slug: defineField.slug({
      required: true,
      unique: true,
      autoGenerate: true,
      sourceField: 'title'
    }),
    body: defineField.richtext(),
    author: defineField.relation({ collection: 'users', required: true })
  },
  access: {
    read: () => true,
    create: ({ user }) => user?.role === 'admin' || user?.role === 'editor',
    update: ({ user }) => user?.role === 'admin' || user?.role === 'editor',
    delete: ({ user }) => user?.role === 'admin'
  }
});

export const collections = [users, posts];

defineUsersCollection() — рекомендована форма колекції users: email, ім’я, поле вибору role (admin | editor | viewer, за замовчуванням viewer) і вже підключене passwordHash. Використовувати її не обов’язково — withAuthFields() додає ті самі поля адаптера автентифікації до колекції, яку ви пишете вручну, — але це найшвидший шлях, і саме його цей посібник використовує всюди.

3. Вибір адаптерів і створення runtime

Це фабрика для розробки: адаптери in-memory, нічого не зберігається надійно. Саме вона потрібна під час навчання й у тестах. Продакшен-збірка має натомість використовувати фабрику надійного профілю (D1 + R2 або libSQL + S3), яка викидає помилку за неповної конфігурації, — див. §9 і Deployment (англійською).

// server/runtime.ts
import { InMemoryDatabaseAdapter } from '@forge-cms/db';
import { UsersCollectionAuthAdapter } from '@forge-cms/auth';
import { InMemoryStorageAdapter } from '@forge-cms/storage';
import { D1DatabaseAdapter, type D1Database } from '@forge-cms/cloudflare';
import { ForgeCmsRuntime } from '@forge-cms/runtime';
import { collections } from './collections';

export interface ServerEnv {
  DB?: D1Database;
  AUTH_SECRET?: string;
}

let runtimePromise: Promise<ForgeCmsRuntime<ServerEnv>> | undefined;

export function getServerRuntime(env?: ServerEnv): Promise<ForgeCmsRuntime<ServerEnv>> {
  runtimePromise ??= buildRuntime(env);
  return runtimePromise;
}

async function buildRuntime(env?: ServerEnv): Promise<ForgeCmsRuntime<ServerEnv>> {
  // DEVELOPMENT ONLY — never ship this selector: a missing binding silently becomes in-memory.
  const database = env?.DB ? new D1DatabaseAdapter() : new InMemoryDatabaseAdapter();
  // Explicit dev mode (never inferred from a missing secret): `true` only under the dev server.
  const auth = new UsersCollectionAuthAdapter({ devMode: import.meta.dev === true }).init({
    ...env,
    userDatabase: database
  });

  const runtime = new ForgeCmsRuntime<ServerEnv>({
    collections,
    adapters: { database, auth, storage: new InMemoryStorageAdapter() },
    ...(env !== undefined && { env })
  });

  runtime.init();
  await runtime.syncSchema();
  return runtime;
}

Створюйте його ліниво, під час першого запиту, а не під час завантаження модуля: Cloudflare Worker забороняє асинхронне введення-виведення на рівні модуля, а така структура так само працює й поза Cloudflare. У цій фабриці для розробки env?.DB обирає D1, коли binding існує, і in-memory в іншому разі — саме тому вона не придатна для вибору в продакшені: відсутній або неправильно названий binding непомітно призвів би до втрати даних. У продакшені використовується фабрика профілю з §9.

4. Підключення серверного API

Кожен маршрут — це тонка обгортка: справжня логіка живе в обробнику, а не у файлі маршруту. В Analog/h3:

// server/routes/api/auth/login.post.ts
import { defineEventHandler, toWebRequest } from 'h3';
import { handleLogin } from '@forge-cms/runtime';
import { getServerRuntime } from '../../../runtime';

export default defineEventHandler(async (event) => {
  const runtime = await getServerRuntime(event.context.cloudflare?.env);
  return handleLogin(
    { request: toWebRequest(event), env: event.context.cloudflare?.env },
    { runtime, cookie: { secure: !!event.context.cloudflare?.env } }
  );
});

Повторіть той самий чотирирядковий шаблон для signup.post.ts (handleSignup, захистіть його власним прапорцем enabled, за замовчуванням вимкненим), logout.post.ts (handleLogout) і me.get.ts (handleMe). Потім ідуть загальні CRUD-маршрути колекцій тієї самої форми, по одному файлу на метод: GET/POST /api/v1/[collection] (handleList/handleCreate) і GET/PUT/DELETE /api/v1/[collection]/[id] (handleRead/handleUpdate/handleDelete). Повний контракт, який вони реалізують, див. у REST API (англійською).

5. Підключення адмін-панелі

// app/admin.routes.ts
import type { Routes } from '@angular/router';
import {
  ForgeAdminLayoutComponent,
  ForgeUsersWorkspaceComponent,
  forgeAdminAuthRoutes,
  forgeAdminContentRoutes
} from '@forge-cms/admin';
import { forgeAuthGuard } from '@forge-cms/angular';

export const ADMIN_ROUTES: Routes = [
  ...forgeAdminAuthRoutes({ signup: true }),
  {
    path: '',
    component: ForgeAdminLayoutComponent,
    canActivate: [forgeAuthGuard()],
    children: [
      { path: '', pathMatch: 'full', redirectTo: 'collections' },
      ...forgeAdminContentRoutes(),
      {
        path: 'users',
        component: ForgeUsersWorkspaceComponent,
        canActivate: [forgeAuthGuard({ roles: ['admin'] })]
      }
    ]
  }
];

Це вся адмін-панель: вхід і реєстрація, захищена оболонка, багаторазовий перелік колекцій і редактор документів, а також розділ користувачів, доступний лише адміністраторам. Жодної CRUD- сторінки, написаної застосунком-хостом. В app.config.ts додайте provideForgeCms({ baseUrl: '/api/v1' }) — без authToken: сесія браузера — це HttpOnly-cookie forge_session, і CmsApiService надсилає її автоматично.

Хочете розмістити її за адресою /studio замість /admin або мати API деінде, ніж /api/v1 і /api/auth? Це конфігурація, а не код: basePath у forgeAdminAuthRoutes() і в конфігурації макета, signInPath/forbiddenPath у guard, baseUrl/authBaseUrl у provideForgeCms() — див. Admin UI (англійською).

На що варто зважати: @forge-cms/angular і @forge-cms/admin постачають частково скомпільований Ivy-код, якому потрібен лінкер Angular під час збірки вашого застосунку, інакше ви отримаєте збій JIT compiler unavailable, що з’являється лише в продакшені. Якщо ви на Vite (Analog.js), додайте плагін, який ForgeCMS постачає саме для цього (@forge-cms/admin/vite реекспортує той самий плагін):

// vite.config.ts
import { angularLinker } from '@forge-cms/angular/vite';

export default defineConfig({
  plugins: [angularLinker() /* your other plugins, e.g. analog() */]
});

6. Створення першого адміністратора

Немає ні seed-скрипту, ні окремого bootstrap-ендпоінту: найперший користувач, створений у свіжій інсталяції, завжди отримує роль admin, незалежно від того, яку роль ви передали:

const admin = await auth.createUser({ email: '[email protected]', password: 'a-real-password' });
// admin.user.role === 'admin', even though no role was requested

Маршрут, який захищає це умовою «жодного користувача ще немає», — це кілька рядків вашого власного серверного коду, а не нова можливість Forge:

// server/routes/api/bootstrap-admin.post.ts
export default defineEventHandler(async (event) => {
  const runtime = await getServerRuntime(event.context.cloudflare?.env);
  const auth = runtime.adapters.auth as UsersCollectionAuthAdapter;

  if ((await auth.listUsers()).length > 0) {
    throw createError({ statusCode: 409, statusMessage: 'Already initialized' });
  }

  const body = await readBody(event);
  const created = await auth.createUser({ email: body.email, password: body.password });
  if (!created.ok) throw createError({ statusCode: 400, statusMessage: created.reason });
  return { data: { user: created.user } };
});

Додайте перед ним невелику форму (або просто викличте його один раз через curl) — і матимете справжній сценарій першого запуску: нуль рядків, нуль користувачів, один запит і робоча адмін-панель.

7. Вхід

Перейдіть на /admin/login. Після успішного входу сервер встановлює cookie forge_session, і сесія Angular (ForgeAuthSession, яку можна інжектувати всюди, де потрібні session.user()/session.authenticated()) одразу це відображає — без перезавантаження сторінки, а справжнє перезавантаження сесія переживає, бо це cookie, а не localStorage.

8. Керування контентом

/admin/collections показує всі зареєстровані колекції; /admin/collections/posts — це повноцінний робочий простір із пошуком, сортуванням, фільтром за статусом, пагінацією, створенням, редагуванням, публікацією й видаленням, згенерований із визначення posts у §2. Поле author відображається як вибір із пошуком серед users, а не як текстове поле для вставлення id. Тут немає жодного коду застосунку-хоста.

9. Cloudflare чи портативний профіль

Обидва надійні профілі виконують ту саму предметну модель із §2; змінюються лише адаптери бази даних і файлів. Продакшен обирає рівно один повний профіль і в іншому разі відмовляється запускатися (жодного запасного варіанта in-memory). profile.ts в apps/tiny-project — перевірений еталон; Deployment (англійською) містить робочі рецепти.

Cloudflare — D1 + R2 (найкраще підтримуваний шлях):

import { D1DatabaseAdapter, R2StorageAdapter } from '@forge-cms/cloudflare';
if (!env.DB || !env.BUCKET)
  throw new Error('Incomplete Cloudflare profile: DB and BUCKET are required');
const database = new D1DatabaseAdapter(); // .init(env) happens inside runtime.init()
const storage = new R2StorageAdapter();

Конфігурація: binding D1 (DB) і binding R2 (BUCKET) у wrangler.toml, а також AUTH_SECRET (щонайменше 32 байти) у кожній збірці; лише сервер розробки через явне devMode: true використовує публічний секрет розробки Forge. Див. Deployment і Browser auth (англійською).

Портативний — libSQL + S3 (без облікового запису Cloudflare і без будь-яких binding):

import { LibSqlDatabaseAdapter } from '@forge-cms/db';
import { S3StorageAdapter } from '@forge-cms/s3';
// DATABASE_URL=file:/data/forge.db (a persistent volume!) or a libsql:// URL; S3_BUCKET, S3_REGION, …
const database = new LibSqlDatabaseAdapter(process.env['DATABASE_URL']!);
const storage = new S3StorageAdapter({ bucket, region, endpoint, forcePathStyle, credentials });

Його додає pnpm add @forge-cms/s3. @forge-cms/db завантажує libSQL під час першої операції, тому init() не відкриває базу даних (неправильна URL-адреса призводить до помилки першого виклику). S3- бакет має існувати заздалегідь.

Той самий UsersCollectionAuthAdapter, ті самі collections, той самий runtime, та сама адмін- панель. Синхронізація схеми, створення першого адміністратора, вхід, користувачі, CRUD публікацій, чернетки, зв’язок і завантаження файлів працюють без змін.

Завантаження та файли

Додайте колекцію з upload: true (див. Uploads, англійською), підключіть POST /api/v1/[collection] і файловий маршрут GET /api/media/[...key] → handleFile. Обидва профілі зберігають файли надійно (R2 або S3) і віддають їх через handleFile, який застосовує правила доступу документа-власника. Тримайте бакети приватними.

Рендеринг на сервері та продакшен (S01–S03)

Публічні сторінки рендеряться на сервері й гідратуються: встановіть ssr: true в analog(), надайте provideForgeCmsServer({ origin }) у main.server.ts, читайте публічний контент клієнтом із credentials: 'omit' і collectionResource(params, { transfer: 'public' }) та збирайте з публічним angularLinker() з @forge-cms/angular/vite. Посібник з SSR (англійською) містить усі фрагменти та дві продакшен-конфігурації (Cloudflare Pages + D1 + R2; node-server + libSQL + S3 з nitro: { externals: { trace: false } } і node_modules, що постачаються поруч із dist/). Шлях цього посібника — встановлення → колекції → фабрика профілю → обробники автентифікації → адмін-панель → перший адміністратор → SSR → публічний клієнт → гідратація → завантаження → збірка → запуск → перезапуск → відновлення — перевіряється наскрізно, лише із запакованих публічних пакетів, командою pnpm test:s3 profiles (продакшен-збірка Node + libSQL на диску + Garage S3, а також вихідні файли Cloudflare Pages під локальним workerd + локальні D1 і R2 — це свідчення з локального workerd; ізольоване віддалене відпрацювання на Cloudflare описано в Deployment) і pnpm release:ssr (технічний SSR-споживач).

Далі

  • Швидкий старт — мінімальний Local API без автентифікації й адмін-панелі.
  • Browser auth (англійською) — контракт cookie-сесії, CSRF і реєстрації, на якому побудовано цей посібник.
  • Admin UI (англійською) — усі компоненти, які експортує @forge-cms/admin.
  • Deployment (англійською) — обидва надійні профілі повністю, а також резервне копіювання й відновлення.