Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions apps/web/content/docs/content-engine/admincp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,14 @@ import type { ContentFrontendPluginSource } from "@vitnode/core/lib/plugin";

import { contentTypeAdmin } from "@vitnode/core/lib/plugin";

import { CONFIG_PLUGIN } from "@/const";
import { manifest } from "@/manifest.gen";
import { BlogCategoryColorCell } from "@/views/admin/category/color-cell";
import { BlogCategoryColorField } from "@/views/admin/category/color-field";

import { blogCategoryNav } from "./nav";

export const adminContent = {
pluginId: CONFIG_PLUGIN.pluginId,
pluginId: manifest.id,
contentTypes: [
contentTypeAdmin({
...blogCategoryNav,
Expand Down
12 changes: 6 additions & 6 deletions apps/web/content/docs/content-engine/container-frontend.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,11 @@ Create a helper that reads every board. The list comes back in tree order, flat,
```ts title="plugins/example/src/pages/boards-data.ts"
import { fetcher } from "@vitnode/core/tanstack/fetcher";

import { CONFIG_PLUGIN } from "@/const";
import { manifest } from "@/manifest.gen";

export const fetchBoards = async () => {
const res = await fetcher({
plugin: CONFIG_PLUGIN.pluginId,
plugin: manifest.id,
method: "get",
module: "content/boards",
path: "/",
Expand Down Expand Up @@ -255,7 +255,7 @@ import { contentPageItem } from "@vitnode/core/tanstack/content";
import { fetcher } from "@vitnode/core/tanstack/fetcher";
import { z } from "zod";

import { CONFIG_PLUGIN } from "@/const";
import { manifest } from "@/manifest.gen";

import type { BoardSearch } from "./board-search";

Expand All @@ -268,7 +268,7 @@ const zodDiscussionRow = z.object({

const loadBoard = async (idOrSlug: string, { cursor }: BoardSearch) => {
const detail = await fetcher({
plugin: CONFIG_PLUGIN.pluginId,
plugin: manifest.id,
method: "get",
module: "content/boards",
path: "/{idOrSlug}",
Expand All @@ -290,7 +290,7 @@ const loadBoard = async (idOrSlug: string, { cursor }: BoardSearch) => {
};

const list = await fetcher({
plugin: CONFIG_PLUGIN.pluginId,
plugin: manifest.id,
method: "get",
module: "content/discussions",
path: "/",
Expand Down Expand Up @@ -431,7 +431,7 @@ const onSubmit: AutoFormOnSubmit<DiscussionCreateFormSchema> = async (
form,
) => {
const res = await fetcher({
plugin: CONFIG_PLUGIN.pluginId,
plugin: manifest.id,
method: "post",
module: "content/discussions",
path: "/",
Expand Down
7 changes: 3 additions & 4 deletions apps/web/content/docs/content-engine/container-hierarchy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,15 @@ A collection holds at most 5,000 containers (`limitReached`, 409). The whole tre
`createContentContainerService` is the one way to read and change a collection. The AdminCP routes and the public routes both use it, so your code gets the same rules, cache expiry and events. Call it inside any Hono handler with the request context:

```ts title="plugins/example/src/api/modules/boards/archive.route.ts"
import { buildRoute } from "@vitnode/core/api/lib/route";
import { createContentContainerService } from "@vitnode/core/content/server";
import { HTTPException } from "hono/http-exception";
import { z } from "zod";

import { CONFIG_PLUGIN } from "@/const";
import { buildRoute } from "@/api/builders";
import { boardContainers } from "@/content/boards";
import { manifest } from "@/manifest.gen";

export const archiveBoardRoute = buildRoute({
pluginId: CONFIG_PLUGIN.pluginId,
adminStaffPermission: { module: "example_boards", permission: "can_edit" },
route: {
method: "post",
Expand All @@ -94,7 +93,7 @@ export const archiveBoardRoute = buildRoute({
const { id } = c.req.valid("param");
const boards = createContentContainerService(c, {
definition: boardContainers,
pluginId: CONFIG_PLUGIN.pluginId,
pluginId: manifest.id,
});

const archive = await boards.findBySlug("archive");
Expand Down
6 changes: 2 additions & 4 deletions apps/web/content/docs/content-engine/containers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,15 +130,13 @@ Pass the definition to both content modules. The admin module adds the staff rou
import { boardContainers } from "@/content/boards"; // [!code ++]

buildContentAdminModule({
pluginId: CONFIG_PLUGIN.pluginId,
containers: [boardContainers], // [!code ++]
contentTypes: [articleContent, discussionContent],
}),
```

```ts title="plugins/example/src/config.api.ts"
buildContentPublicModule({
pluginId: CONFIG_PLUGIN.pluginId,
containers: [boardContainers], // [!code ++]
contentTypes: [articleContent, discussionContent],
}),
Expand Down Expand Up @@ -166,15 +164,15 @@ export const exampleBoardsNav = {
};

export const adminNav = {
pluginId: CONFIG_PLUGIN.pluginId,
pluginId: manifest.id,
containers: [exampleBoardsNav], // [!code ++]
contentTypes: [exampleArticleNav, exampleDiscussionNav],
} satisfies AdminNavPluginSource;
```

```tsx title="plugins/example/src/admin/content.tsx"
export const adminContent = {
pluginId: CONFIG_PLUGIN.pluginId,
pluginId: manifest.id,
containers: [exampleBoardsNav], // [!code ++]
contentTypes: [exampleArticleNav, exampleDiscussionNav],
} satisfies ContentFrontendPluginSource;
Expand Down
28 changes: 15 additions & 13 deletions apps/web/content/docs/content-engine/create-a-content-type.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,20 @@ Every snippet is copied from `plugins/example`, with unrelated content types tri

## Before you begin

You need a plugin that your app already loads, with `src/config.api.ts`, `src/config.tsx` and a `src/const.ts` that exports the plugin id:
You need a plugin that your app already loads, with `src/config.api.ts`, `src/config.tsx` and `src/api/builders.ts`, which binds the API and Content Engine builders to your plugin, so none of them needs a plugin id:

```ts title="plugins/example/src/const.ts"
export const CONFIG_PLUGIN = { pluginId: "@vitnode/example" as const };
```ts title="plugins/example/src/api/builders.ts"
import { bindApiPlugin } from "@vitnode/core/api/lib/bind-api-plugin";
import { bindContentPlugin } from "@vitnode/core/content/server";

import { manifest } from "@/manifest.gen";

export const { buildModule, buildRoute } = bindApiPlugin(manifest);
export const { buildContentAdminModule, buildContentPublicModule } =
bindContentPlugin(manifest);
```

[Create a plugin](/docs/dev/plugins/create) generates all three. You also need a running PostgreSQL database and an AdminCP account.
`src/manifest.gen.ts` is written by `vitnode build` and `vitnode dev` from your `package.json`, and `manifest.id` is the plugin id. [Create a plugin](/docs/dev/plugins/create) generates all of these. You also need a running PostgreSQL database and an AdminCP account.

<Steps>

Expand Down Expand Up @@ -87,19 +94,14 @@ Export the table. Drizzle Kit finds tables by reading the exports of your plugin
Create an `admin` module and pass the model to `buildContentAdminModule`:

```ts title="plugins/example/src/api/modules/admin/admin.module.ts"
import { buildModule } from "@vitnode/core/api/lib/module";
import { buildContentAdminModule } from "@vitnode/core/content/server";

import { CONFIG_PLUGIN } from "@/const";
import { buildContentAdminModule, buildModule } from "@/api/builders";
import { categoryContent } from "@/database/categories";

export const adminModule = buildModule({
pluginId: CONFIG_PLUGIN.pluginId,
name: "admin",
routes: [],
modules: [
buildContentAdminModule({
pluginId: CONFIG_PLUGIN.pluginId,
contentTypes: [categoryContent],
}),
],
Expand All @@ -113,7 +115,7 @@ import { adminModule } from "@/api/modules/admin/admin.module"; // [!code ++]

export const exampleApiPlugin = () =>
buildApiPlugin({
pluginId: CONFIG_PLUGIN.pluginId,
manifest,
modules: [adminModule], // [!code highlight]
});
```
Expand All @@ -133,11 +135,11 @@ Create `src/admin/nav.tsx` with the sidebar link:
```tsx title="plugins/example/src/admin/nav.tsx"
import { FolderIcon } from "lucide-react";

import { CONFIG_PLUGIN } from "@/const";
import { categoryContentType } from "@/content/category";
import { manifest } from "@/manifest.gen";

export const adminNav = {
pluginId: CONFIG_PLUGIN.pluginId,
pluginId: manifest.id,
contentTypes: [{ definition: categoryContentType, icon: <FolderIcon /> }],
};
```
Expand Down
2 changes: 1 addition & 1 deletion apps/web/content/docs/content-engine/duplicate-content.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ import { contentDuplicateEffects, resolveContentActor } from "@vitnode/core/cont
const { editorialService } = postContent;
if (!editorialService) throw new Error("Articles have no editorial features.");

const pluginId = CONFIG_PLUGIN.pluginId;
const pluginId = manifest.id;
const actor = resolveContentActor(c);
const outcome = await editorialService(c, { pluginId }).duplicate(sourceId, {
actor,
Expand Down
2 changes: 0 additions & 2 deletions apps/web/content/docs/content-engine/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,10 @@ VitNode collects listeners only from the `events` array of a module passed direc

```ts title="plugins/example/src/api/modules/admin/admin.module.ts"
export const adminModule = buildModule({
pluginId: CONFIG_PLUGIN.pluginId,
name: "admin",
routes: [],
modules: [
buildContentAdminModule({
pluginId: CONFIG_PLUGIN.pluginId,
contentTypes: [articleContent],
}),
],
Expand Down
2 changes: 1 addition & 1 deletion apps/web/content/docs/content-engine/localization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ In a page loader, pass the route's locale through. This is the example plugin's

```ts title="plugins/example/src/pages/localized-article-page.tsx"
const detail = await fetcher({
plugin: CONFIG_PLUGIN.pluginId,
plugin: manifest.id,
method: "get",
module: "content/localized-articles",
path: "/{slug}",
Expand Down
13 changes: 7 additions & 6 deletions apps/web/content/docs/content-engine/plugin-files.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ VitNode reads a plugin's content types from several small files instead of one b
| `src/admin/content.tsx` | `adminContent` | The content screens, loaded on demand | Screen registrations, custom cells, fields and form layouts |
| `src/content.ts` | `contentTypes` | The web build and the API at startup | Every content type with `delivery` or `search` |
| `src/routes.ts` | `routes` | The web build | The pages that serve `delivery` and `search` URLs |
| `src/config.tsx` | The plugin | The app's `vitnode.config.ts` | Plugin id, `messages`, `localeFiles` and `routes` only |
| `src/config.tsx` | The plugin | The app's `vitnode.config.ts` | `manifest`, `messages`, `localeFiles` and `routes` only |
| `src/manifest.gen.ts` | `manifest` | Every file that needs the plugin id | The plugin id as `manifest.id`. Written by `vitnode build` / `dev` |
| `src/api/builders.ts` | `buildModule`, `buildRoute`, `buildContent*Module` | Every API module and route file | The builders bound to `manifest` by `bindApiPlugin` and `bindContentPlugin` |
| `src/locales/en.json` | JSON | The app, through `localeFiles` | Labels for content types, fields and permissions |

A content type with an AdminCP screen needs an entry in both `admin/nav` and `admin/content`. The first adds the sidebar link, the second the screen behind it. They are separate files so the sidebar stays small on every AdminCP page, while form and editor code loads only with the screen.
Expand All @@ -34,12 +36,12 @@ Once you customize a screen, build `adminContent` on its own and reuse the nav e
```tsx title="plugins/example/src/admin/content.tsx"
import type { ContentFrontendPluginSource } from "@vitnode/core/lib/plugin";

import { CONFIG_PLUGIN } from "@/const";
import { manifest } from "@/manifest.gen";

import { exampleArticleNav, exampleCategoryNav, examplePageNav } from "./nav";

export const adminContent = {
pluginId: CONFIG_PLUGIN.pluginId,
pluginId: manifest.id,
contentTypes: [exampleArticleNav, exampleCategoryNav, examplePageNav],
} satisfies ContentFrontendPluginSource;
```
Expand Down Expand Up @@ -83,14 +85,13 @@ Tables are the exception. Drizzle Kit reads `node_modules/<pluginId>/dist/src/da
```tsx title="plugins/example/src/config.tsx"
import { buildPlugin } from "@vitnode/core/lib/plugin";

import { CONFIG_PLUGIN } from "@/const";

import messages from "./locales";
import { manifest } from "./manifest.gen";
import { routes } from "./routes";

export const examplePlugin = () =>
buildPlugin({
...CONFIG_PLUGIN,
manifest,
localeFiles: {
en: "@vitnode/example/locales/en.json",
},
Expand Down
7 changes: 3 additions & 4 deletions apps/web/content/docs/content-engine/public-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,18 +67,17 @@ export const articleContentType = defineContentType({
Add `buildContentPublicModule` to the plugin's API config. The example plugin passes all five content models; one without `publicApi` is skipped:

```ts title="plugins/example/src/config.api.ts"
import { buildContentPublicModule } from "@vitnode/core/content/server"; // [!code ++]
import { buildContentPublicModule } from "@/api/builders"; // [!code ++]

import { articleContent } from "@/database/articles"; // [!code ++]

export const exampleApiPlugin = () =>
buildApiPlugin({
pluginId: CONFIG_PLUGIN.pluginId,
manifest,
modules: [
adminModule,
// [!code ++:4]
// [!code ++:3]
buildContentPublicModule({
pluginId: CONFIG_PLUGIN.pluginId,
contentTypes: [articleContent],
}),
],
Expand Down
6 changes: 3 additions & 3 deletions apps/web/content/docs/content-engine/public-pages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ import {
import { fetcher } from "@vitnode/core/tanstack/fetcher";
import { z } from "zod";

import { CONFIG_PLUGIN } from "@/const";
import { manifest } from "@/manifest.gen";

import { ContentArticle } from "./content-article";

Expand All @@ -118,14 +118,14 @@ const zodArticle = z.object({
const loadArticle = async (slug: string) => {
const [resolution, detail] = await Promise.all([
fetcher({
plugin: CONFIG_PLUGIN.pluginId,
plugin: manifest.id,
method: "get",
module: "content/articles",
path: "/delivery/resolve/{slug}",
args: { params: { slug: encodeURIComponent(slug) } },
}),
fetcher({
plugin: CONFIG_PLUGIN.pluginId,
plugin: manifest.id,
method: "get",
module: "content/articles",
path: "/{slug}",
Expand Down
7 changes: 3 additions & 4 deletions apps/web/content/docs/content-engine/public-write-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,6 @@ The methods throw instead of answering:
A route that starts a discussion from a single question. `withHttpErrors` turns a unique conflict into the structured `409`, and the `catch` maps a refusal to its status:

```ts title="plugins/example/src/api/modules/discussions/ask.route.ts"
import { buildRoute } from "@vitnode/core/api/lib/route";
import { ContentPublicWriteDenied } from "@vitnode/core/content";
import {
createContentPublicWriteService,
Expand All @@ -339,11 +338,11 @@ import {
import { HTTPException } from "hono/http-exception";
import { z } from "zod";

import { CONFIG_PLUGIN } from "@/const";
import { buildRoute } from "@/api/builders";
import { discussionContent } from "@/database/discussions";
import { manifest } from "@/manifest.gen";

export const askRoute = buildRoute({
pluginId: CONFIG_PLUGIN.pluginId,
route: {
method: "post",
path: "/ask",
Expand Down Expand Up @@ -377,7 +376,7 @@ export const askRoute = buildRoute({
const service = createContentPublicWriteService({
c,
model: discussionContent,
pluginId: CONFIG_PLUGIN.pluginId,
pluginId: manifest.id,
});

try {
Expand Down
Loading
Loading