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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/skills/vitnode-docs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ Keep important user-isolation and invalidation requirements beside the relevant
- Keep important explanations and constraints in selectable text. Screenshots supplement instructions; they must not be the only source of UI labels or outcomes.
- Support technical claims with implementation evidence, reproducible examples, or a relevant primary-source link. Add benchmark numbers, expert quotes, author attribution, or dates only when real and useful; do not add them as citation bait.
- Keep HTML and Markdown exports equivalent in meaning, including all labeled tab alternatives. Do not add hidden bot-only text, prompt instructions for agents, fabricated FAQs, unsupported superlatives, or repetitive summaries to attract citations.
- For a how-to or tutorial that ends in working code in a reader's plugin or app, consider the optional `implementation` frontmatter behind **Copy implementation prompt**. Follow "Implementation prompts" in `apps/web/content/docs/dev/contribution.mdx`; it is the only place for coding-agent instructions.
- Use a short question-and-answer section only for distinct recurring questions that are not already answered. Do not append a generic FAQ to every page.
- For a requested SEO/AI-readiness audit or visibility review, read `references/seo-review.md`. Do not run a broad crawl, install audit tools, or require competitor research for every MDX edit.
- Read `references/docs-site.md` for canonical metadata, crawlability, sitemaps, structured data, and AI discovery when site-level work is in scope. Clear content and accessible delivery improve usability; do not promise rankings, indexing, or AI citations.
Expand Down
19 changes: 19 additions & 0 deletions apps/web/content/docs/content-engine/container-policies.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@
title: Publishing policies
description: Set per-container publishing rules in the VitNode Content Engine. Decide who may post in a forum, whether new posts and edits wait for approval, and how long authors may edit, with defineContentContainer policies and the AdminCP Publishing sheet.
icon: ShieldCheck
implementation:
objective: Set who may post in a set of containers and whether new posts and edits wait for approval.
decisions:
- Which containers get publishing rules? Offer container definitions in the project whose content types members can write to. If none exist, say so and ask how to proceed.
- 'Who may start posts: members or only moderators?'
- Do new posts go live at once or wait for approval? And edits to published posts?
- 'How long may authors edit their posts: always, never, or for a set number of minutes (60 by default)?'
- Does a content type in these containers, such as replies, need different rules? Offer the content types filed into the chosen containers.
constraints:
- 'Write the rules in `policies: { defaults, contentTypes }` on `defineContentContainer`. Per-container overrides are set in the AdminCP, not in code, and nothing needs migrating.'
- Label the container-scoped `can_edit` and `can_approve` moderator permissions in the plugin's locale.
- When approval is on, public forms must handle the draft status and the `approval` object in write responses.
related:
- title: Approvals
href: /docs/content-engine/approvals
- title: Review approvals
href: /docs/dev/moderation/review-approvals
- title: Scoped permissions
href: /docs/dev/moderation/scoped-permissions
---

import { Tab, Tabs } from "fumadocs-ui/components/tabs";
Expand Down
19 changes: 19 additions & 0 deletions apps/web/content/docs/content-engine/containers.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@
title: Containers
description: Organize Content Engine records into a tree of containers, such as forum boards, blog sections or knowledge-base categories, with defineContentContainer and field.container.
icon: FolderTree
implementation:
objective: File the records of an existing content type into a tree of containers, such as forums or categories.
decisions:
- Which plugin owns the containers? Offer plugins in the project that define Content Engine content types.
- Which items are filed into them? Offer content types from the chosen plugin that have no container field yet; a type can have only one. If its table already has rows, say that a default container is needed.
- What are the containers called, and what extra details does each one carry? Settle it in user terms, such as "Boards" with a description. Extra details can be text, long text, numbers, yes/no switches or choices.
- How deep do they nest, and how are siblings sorted? A flat list or 2 to 10 levels (5 by default), sorted by hand or alphabetically.
- Do members pick the container when they post? Only for a content type that members can write to.
constraints:
- Use `defineContentContainer` and `field.container({ definition })`. Do not build a separate categories table.
- Register the definition with the admin and public content modules and in the plugin's `adminNav` and `adminContent` containers, as the guide shows.
- A required container on a table that already has rows needs the generated migration edited to create and assign a default before `SET NOT NULL`.
related:
- title: Containers on the frontend
href: /docs/content-engine/container-frontend
- title: Publishing policies
href: /docs/content-engine/container-policies
- title: Container hierarchy
href: /docs/content-engine/container-hierarchy
---

import { Tab, Tabs } from "fumadocs-ui/components/tabs";
Expand Down
19 changes: 19 additions & 0 deletions apps/web/content/docs/content-engine/public-pages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@
title: Public pages
description: Give every published VitNode Content Engine record its own URL with delivery, a plugin page, slug redirects and localized URLs.
icon: Compass
implementation:
objective: Give each published record of an existing content type its own public page and URL.
decisions:
- Which items get their own page? Offer content types with a public API that have no `delivery` block yet.
- What URL do the pages use? Recommend the default `/{publicApi.path}/:slug`, or a custom path with exactly one `:slug` that does not start with `/admin` or `/api`.
- Which fields fill the page title, the description and "hide from search engines"? Offer the type's public text fields, its text or rich text fields, and a yes/no field.
- Should old URLs redirect after a slug changes? This needs editorial history; if the type has none, ask whether to add it.
- Should the pages be listed in the sitemap, and how often do they change? Always, hourly, daily, weekly, monthly, yearly or never.
constraints:
- Add the `delivery` block and claim the URL with a `page()` in the plugin's routes; the build fails while a content URL has no page.
- In the page loader, read the record with `fetcher` and build the page and its head with `contentDeliveryPage` and `contentDeliveryPageHead`, as the guide shows.
- Delivery adds no columns, so there is nothing to migrate.
related:
- title: SEO
href: /docs/content-engine/seo
- title: Localized URLs
href: /docs/dev/i18n/localized-urls
- title: Delivery reference
href: /docs/content-engine/reference#delivery
---

import { ImgDocs } from "@/components/fumadocs/img";
Expand Down
19 changes: 19 additions & 0 deletions apps/web/content/docs/content-engine/public-write.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@
title: Public write
description: Let signed-in members create and edit Content Engine records from the public site with publicWrite, with server-stamped ownership, moderator permissions, strict field allowlists and rate limiting.
icon: PenLine
implementation:
objective: Let signed-in members create and edit records of an existing content type from the public site.
decisions:
- Which plugin owns the content? Offer plugins in the project that define Content Engine content types.
- Which items do members write? Offer content types from the chosen plugin by their user-facing names. The type needs a public API and one user field that holds the owner; if either is missing, say so and ask whether to add it.
- What can members fill in when they create a record, and what can they change later? Offer the type's top-level fields. Files, users, groups, repeatables and widget fields cannot be written by members.
- Does a new record go live at once or start as a draft? Published or draft; draft is the default.
- Can authors edit their own records afterwards, and should creating a record ask for a captcha?
constraints:
- Use the content type `publicWrite` option with `ownership.field`. The server stamps the owner, so never accept it from the payload.
- Mount the generated routes through the plugin's `buildContentPublicModule`. Write no create or update route by hand, and add no public publish, hide or delete route.
- Label the generated `can_edit` moderator permission in the plugin's locale, then build and migrate with the project's scripts.
related:
- title: Public write frontend
href: /docs/content-engine/public-write-frontend
- title: Public write API
href: /docs/content-engine/public-write-api
- title: Public API
href: /docs/content-engine/public-api
---

import { Tab, Tabs } from "fumadocs-ui/components/tabs";
Expand Down
19 changes: 19 additions & 0 deletions apps/web/content/docs/content-engine/search.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@
title: Search
description: Add a Content Engine content type to the VitNode site search, keep the search index in sync with published records, and rebuild it from the AdminCP.
icon: Search
implementation:
objective: Make an existing content type findable in site search.
decisions:
- Which items appear in search? Offer content types with a public API that already have a public page. If the choice has no page yet, say so and ask how to proceed.
- What is the result heading? Offer the type's public text fields, or a required relation to another searchable type to borrow its title.
- Which text is searched, and which text leads the excerpt? Offer the type's public text, long text, rich text and slug fields.
- Where does a result link to? Offer the existing public routes for the chosen type that take one slug.
- Should results credit the authors or show an image? Offer the type's user field and public file field, if it has them.
constraints:
- Add a `search` block to the content type. Indexing is automatic, so do not call the search service for Content Engine records.
- Every indexed field except `authorField` must be in `publicApi.fields`.
- Records saved before the change need a Reindex in Debug Panel → Search. Own code that writes records must use the editorial service with `contentEditorialEffects` to keep the index current.
related:
- title: Public pages
href: /docs/content-engine/public-pages
- title: Search
href: /docs/dev/search
- title: Search reference
href: /docs/content-engine/reference#search
---

import { ImgDocs } from "@/components/fumadocs/img";
Expand Down
20 changes: 20 additions & 0 deletions apps/web/content/docs/content-engine/threads.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@
title: Threaded replies
description: Build nested replies with the VitNode Content Engine thread option - a to-one self relation for the parent, a root relation that also gives the reply its container, generated slugs, public author summaries and the GET /thread route.
icon: MessagesSquare
implementation:
objective: Add threaded replies, such as comments, under an existing content type.
decisions:
- Which plugin owns the replies? Offer plugins in the project that define Content Engine content types.
- Which items get replies? Offer content types from the chosen plugin by their user-facing names, such as "Topics" or "Articles". A root needs a public API; if the choice has none, say so and ask whether to add it.
- Who writes replies? Signed-in members from the public site (uses Public write), or only staff in the AdminCP.
- Where do replies appear? Offer the public pages that already show the chosen items, such as the detail page.
- How deep can replies nest? From 1 (a flat list) to 32 levels; recommend the guide's depth unless a flat list is wanted.
constraints:
- Use the content type `thread` option. Do not build a separate comments table, tree query or placement checks.
- 'The root relation is required, to-one and `container: true`; `parent` is a nullable to-one self relation. Both belong in `publicApi.fields` and neither in `publicWrite.update.fields`.'
- 'Read replies with the generated `GET /{publicApi.path}/thread` route through `fetcher`, and render tombstones (`removed: true`) so the tree keeps its shape.'
- Create the table with the project's build and migration scripts, not hand-written SQL.
related:
- title: Public write
href: /docs/content-engine/public-write
- title: Public write frontend
href: /docs/content-engine/public-write-frontend
- title: Thread reference
href: /docs/content-engine/reference#thread
---

import { Tab, Tabs } from "fumadocs-ui/components/tabs";
Expand Down
20 changes: 20 additions & 0 deletions apps/web/content/docs/dev/ai/actions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@
title: AI Actions
description: Define managed AI features with defineAiAction, register them in a plugin and run them with c.get("ai").run(). VitNode Core handles permissions, limits, budgets and history.
icon: Wand
implementation:
objective: Add an AI feature to a plugin through a registered AI action.
decisions:
- Which plugin offers the AI feature? Offer plugins in the project with an API part (`config.api.ts`).
- What should the AI do, and with what input? Settle it in user terms. The input comes from data the chosen trigger already has.
- Plain text or structured data? Structured data needs a model with structured output and cannot be streamed.
- 'Who runs it: people on request, background jobs, or both? People are the default, and their runs need an access check.'
- Where is it triggered, and is it on for every role by default? Offer routes, AdminCP screens, queue tasks or cron jobs found in the chosen plugin.
constraints:
- Define the action with `defineAiAction` and register it in `aiActions` of the plugin's `buildApiPlugin()`.
- Run it through `aiActionRef` with `c.get("ai").run()`, `runAsSystem()` or `stream()`. Never hard-code a model or call a provider SDK directly.
- '`authorize()` checks access to the content the action reads, for example with `checkStaffPermission`.'
- The action's `title` and `description` are message keys in the plugin's locale, not plain text.
related:
- title: AI in fields and the editor
href: /docs/dev/ai/fields-and-editor
- title: AI costs and limits
href: /docs/dev/ai/costs-and-limits
- title: AI in the AdminCP
href: /docs/dev/ai/admin
---

import { TypeTable } from 'fumadocs-ui/components/type-table'
Expand Down
15 changes: 10 additions & 5 deletions apps/web/content/docs/dev/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -353,18 +353,23 @@ import { vitnode } from '@vitnode/core/framework/vite'
export default defineConfig({
plugins: [
vitnode({ appRoot: import.meta.dirname }),
nitro({ compressPublicAssets: { brotli: true, gzip: true } }),
nitro(),
tailwindcss(),
tanstackStart(),
viteReact(),
],
})
```

`compressPublicAssets` writes a `.br` and a `.gz` twin next to every built asset,
so the Node server from `pnpm start` serves scripts and styles compressed the
way a CDN would. The server-rendered HTML itself is not compressed by the Node
server; put a reverse proxy or a platform such as Vercel in front of it for that.
The Node server from `pnpm start` sends static files and HTML uncompressed. Put
a reverse proxy, a CDN or a platform such as Vercel in front of it and let that
layer compress responses.

Nitro's `compressPublicAssets` can write a `.br` and a `.gz` twin next to every
built asset instead, but it stays off unless you pass it to `nitro()` yourself.
It compresses every file in the output at once, and on a 2-core, 8 GB build
machine that is enough to run out of memory. The twins only help when Nitro
itself serves the files: behind a CDN they are never read.

`appRoot` has to be `import.meta.dirname`: a Vite config is loaded with the
working directory set to wherever the command ran, which in a monorepo is
Expand Down
40 changes: 40 additions & 0 deletions apps/web/content/docs/dev/contribution.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,46 @@ The documentation portal in `apps/web` speaks Markdown as well as HTML, so agent

Requesting a docs page with an `Accept: text/markdown` header redirects to its `.md` twin, so a curious agent gets Markdown without asking twice. Every page also has a **Copy Markdown** button and an **Open in** menu for ChatGPT, Claude and friends.

### Implementation prompts

A guide that adds a feature to someone's project can also show **Copy implementation prompt**. One click copies a prompt for a coding agent: it links the guide and its Markdown, tells the agent to inspect the reader's project, ask a few questions, and then build the feature where the reader chose. The eye button next to it opens a preview of the exact same text, with its own copy button.

Add it only to a how-to or tutorial that ends in working code inside a plugin or app. Overviews, references and UI component demos do not get one.

Declare the `implementation` field in the guide's frontmatter:

```yaml title="apps/web/content/docs/dev/notifications/publishing.mdx"
---
title: Publish notifications
description: Send a notification from a VitNode plugin.
implementation:
objective: Send an in-app notification when something happens in a plugin.
decisions:
- Which plugin sends it? Offer plugins in the project with an API part (`config.api.ts`).
- Which event triggers it? Offer write routes, actions, queue tasks or cron jobs found in the chosen plugin.
- Who receives it? Offer recipients the chosen event can resolve from existing data, such as the content author.
- Should members also get email by default? None (inbox only), immediately, or in a daily or weekly digest.
constraints:
- Publish with `c.get("notifications").publish()` inside the same transaction as the write.
related:
- title: Notification types
href: /docs/dev/notifications/notification-types
---
```

| Field | Required | What to write |
| :------------ | :------- | :--------------------------------------------------------------------------- |
| `objective` | Yes | One line saying what gets built, without the guide's example names |
| `decisions` | Yes | 1 to 6 questions the reader must answer, in dependency order |
| `constraints` | No | Up to 6 rules the agent must keep, such as the API to reuse |
| `related` | No | Up to 4 other docs pages, as `/docs/...` paths, optionally with a `#heading` |

Write each decision as a question in the reader's words, followed by where the agent finds the answers in the project: "Offer content types from the chosen plugin". The agent discovers the real choices by reading the reader's code, so never list the guide's example plugin, such as `@acme/forum`, as an answer. That plugin exists in the guide, not in the reader's project. When the feature supports a fixed set of options, list exactly those and nothing it cannot do.

The shared workflow (inspect first, skip answered questions, ask dependent questions after their answer, work without a structured question tool, report the result) lives once in `apps/web/src/docs/implementation/prompt.ts`. Do not repeat it in a guide, and do not copy the tutorial into the metadata: the prompt links to it.

To check your prompt, run `pnpm dev`, open the guide, and open the preview with the eye button next to **Copy implementation prompt**. Every link should open, and every decision should make sense for a project that has never heard of the guide's example plugin. A typo in the field, such as an unknown key or a link that is not a `/docs/` path, fails the docs build with the file name. YAML reads `key: value` inside a list item as an object, so wrap a line that contains `: ` in single quotes.

---

## Pull Request Guidelines
Expand Down
Loading
Loading