diff --git a/apps/docs/content/docs/dev/database/meta.json b/apps/docs/content/docs/dev/database/meta.json
new file mode 100644
index 000000000..868f87d18
--- /dev/null
+++ b/apps/docs/content/docs/dev/database/meta.json
@@ -0,0 +1,6 @@
+{
+ "title": "Database",
+ "description": "Learn how to work with databases in VitNode plugins using Drizzle ORM and PostgreSQL.",
+ "icon": "Database",
+ "pages": ["..."]
+}
diff --git a/apps/docs/content/docs/dev/i18n/meta.json b/apps/docs/content/docs/dev/i18n/meta.json
index ab92ff814..f01450363 100644
--- a/apps/docs/content/docs/dev/i18n/meta.json
+++ b/apps/docs/content/docs/dev/i18n/meta.json
@@ -1,4 +1,6 @@
{
"title": "Internationalization (I18n)",
+ "description": "Learn how to make your VitNode plugins multilingual with our I18n guide.",
+ "icon": "Globe",
"pages": ["expand-langs", "namespaces", "messages", "..."]
}
diff --git a/apps/docs/content/docs/dev/layouts-and-pages.mdx b/apps/docs/content/docs/dev/layouts-and-pages.mdx
deleted file mode 100644
index c87631a70..000000000
--- a/apps/docs/content/docs/dev/layouts-and-pages.mdx
+++ /dev/null
@@ -1,210 +0,0 @@
----
-title: Layouts and Pages
-description: Create beautiful layouts and pages in VitNode plugins using Next.js App Router patterns.
----
-
-Building layouts and pages in VitNode is just like Next.js - if you know Next.js, you're already a VitNode pro! Think of pages as rooms and layouts as the house structure that holds everything together.
-
-
- VitNode automatically copies files from your plugin's `app` directory to the
- main application. No extra setup needed!
-
-
-## Creating Pages
-
-Pages are React components that live in `page.tsx` files. Each page represents a unique URL route.
-
-### Basic Page
-
-```tsx title="plugins/blog/src/app/page.tsx"
-export default function BlogHomePage() {
- return (
-
-
Welcome to Our Blog! 🚀
-
Where awesome content lives
-
- );
-}
-```
-
-### Nested Routes
-
-Create folders to organize your routes:
-
-```tsx title="plugins/blog/src/app/dashboard/page.tsx"
-export default function DashboardPage() {
- return (
-
-
- );
-}
-```
-
-## Learn More
-
-
-
-
-
-
diff --git a/apps/docs/content/docs/dev/meta.json b/apps/docs/content/docs/dev/meta.json
index 1ca164515..1186208ec 100644
--- a/apps/docs/content/docs/dev/meta.json
+++ b/apps/docs/content/docs/dev/meta.json
@@ -11,9 +11,10 @@
"deployments",
"---Framework---",
"plugins",
- "api",
"database",
+ "fetcher",
"working-with-users",
+ "i18n",
"advanced",
"---Adapters---",
"captcha",
@@ -21,12 +22,7 @@
"sso",
"cron",
"websocket",
- "---Frontend---",
- "layouts-and-pages",
- "admin-page",
- "fetcher",
"---UI---",
- "i18n",
"not-found",
"..."
]
diff --git a/apps/docs/content/docs/dev/admin-page.mdx b/apps/docs/content/docs/dev/plugins/admin-page.mdx
similarity index 77%
rename from apps/docs/content/docs/dev/admin-page.mdx
rename to apps/docs/content/docs/dev/plugins/admin-page.mdx
index af14d1df8..57b346ad5 100644
--- a/apps/docs/content/docs/dev/admin-page.mdx
+++ b/apps/docs/content/docs/dev/plugins/admin-page.mdx
@@ -5,16 +5,20 @@ description: Create powerful admin interfaces for your plugins with protected pa
## Pages & Layouts
-AdminCP follows the same patterns like from [Layouts and Pages](/docs/plugins/layouts-and-pages) but uses the `src/app_admin` directory.
+AdminCP follows the same patterns like from [Layouts and Pages](/docs/dev/plugins/layouts-and-pages) but uses the `src/routes/admin` directory. Like everywhere in VitNode, render text with translations — see [Messages](/docs/dev/i18n/messages).
### Pages
-```tsx title="plugins/{plugin_name}/src/app_admin/blog/settings/page.tsx"
-export default function Page() {
+```tsx title="plugins/{plugin_name}/src/routes/admin/blog/settings/page.tsx"
+import { getTranslations } from 'next-intl/server';
+
+export default async function Page() {
+ const t = await getTranslations('@vitnode/blog.admin.settings'); // [!code highlight]
+
return (
-
Settings
-
Hello from Blog plugin in AdminCP
+
{t('title')}
+
{t('desc')}
);
}
@@ -22,16 +26,20 @@ export default function Page() {
### Layouts
-```tsx title="plugins/{plugin_name}/src/app_admin/blog/settings/layout.tsx"
-export default function AdminRootLayout({
+```tsx title="plugins/{plugin_name}/src/routes/admin/blog/settings/layout.tsx"
+import { getTranslations } from 'next-intl/server';
+
+export default async function AdminRootLayout({
children,
}: {
children: React.ReactNode;
}) {
+ const t = await getTranslations('@vitnode/blog.admin.settings');
+
return (
-
Blog Admin
+
{t('title')}
{children}
@@ -40,8 +48,8 @@ export default function AdminRootLayout({
```
- All pages & layouts in the `app_admin` directory are automatically protected
- and require admin authentication.
+ All pages & layouts in the `src/routes/admin` directory are automatically
+ protected and require admin authentication.
## Navigation items
diff --git a/apps/docs/content/docs/dev/api/meta.json b/apps/docs/content/docs/dev/plugins/api/meta.json
similarity index 71%
rename from apps/docs/content/docs/dev/api/meta.json
rename to apps/docs/content/docs/dev/plugins/api/meta.json
index cd6a87808..e7f8365a1 100644
--- a/apps/docs/content/docs/dev/api/meta.json
+++ b/apps/docs/content/docs/dev/plugins/api/meta.json
@@ -1,5 +1,4 @@
{
"title": "REST API",
- "defaultOpen": true,
"pages": ["modules", "..."]
}
diff --git a/apps/docs/content/docs/dev/api/modules.mdx b/apps/docs/content/docs/dev/plugins/api/modules.mdx
similarity index 85%
rename from apps/docs/content/docs/dev/api/modules.mdx
rename to apps/docs/content/docs/dev/plugins/api/modules.mdx
index c164d7dc8..7a97396ff 100644
--- a/apps/docs/content/docs/dev/api/modules.mdx
+++ b/apps/docs/content/docs/dev/plugins/api/modules.mdx
@@ -1,6 +1,6 @@
---
title: Modules
-description: xxx
+description: Learn how to organize your API routes into modules for better structure and maintainability.
---
## Usage
@@ -15,7 +15,7 @@ import { CONFIG_PLUGIN } from "@/config";
export const categoriesModule = buildModule({
pluginId: CONFIG_PLUGIN.id,
name: "categories",
- routes: [] // We'll populate this soon!
+ routes: [], // We'll populate this soon!
});
```
@@ -34,7 +34,7 @@ export const categoriesModule = buildModule({
pluginId: CONFIG_PLUGIN.id,
name: "categories",
routes: [],
- modules: [postsModule] // [!code ++]
+ modules: [postsModule], // [!code ++]
});
```
@@ -52,7 +52,7 @@ import { categoriesModule } from "./api/modules/categories/categories.module"; /
export const blogApiPlugin = () => {
return buildApiPlugin({
pluginId: CONFIG_PLUGIN.pluginId,
- modules: [categoriesModule] // [!code ++]
+ modules: [categoriesModule], // [!code ++]
});
};
```
diff --git a/apps/docs/content/docs/dev/api/routes.mdx b/apps/docs/content/docs/dev/plugins/api/routes.mdx
similarity index 76%
rename from apps/docs/content/docs/dev/api/routes.mdx
rename to apps/docs/content/docs/dev/plugins/api/routes.mdx
index b54f65318..38ecffe45 100644
--- a/apps/docs/content/docs/dev/api/routes.mdx
+++ b/apps/docs/content/docs/dev/plugins/api/routes.mdx
@@ -1,6 +1,6 @@
---
title: Routes
-description: xxx
+description: Learn how to create API routes in your VitNode plugins, including handling path parameters, query parameters, and request bodies.
---
## Usage
@@ -27,25 +27,25 @@ export const getCategoriesRoute = buildRoute({
z.object({
id: z.string(),
name: z.string(),
- description: z.string().optional()
- })
- )
- })
- }
+ description: z.string().optional(),
+ }),
+ ),
+ }),
+ },
},
- description: "Successfully retrieved categories"
- }
- }
+ description: "Successfully retrieved categories",
+ },
+ },
},
- handler: (c) => {
+ handler: c => {
// Your business logic goes here
return c.json({
categories: [
{ id: "1", name: "Technology", description: "All things tech" },
- { id: "2", name: "Lifestyle" }
- ]
+ { id: "2", name: "Lifestyle" },
+ ],
});
- }
+ },
});
```
@@ -60,7 +60,7 @@ import { getCategoriesRoute } from "./routes/get.route"; // [!code ++]
export const categoriesModule = buildModule({
pluginId: CONFIG_PLUGIN.id,
name: "categories",
- routes: [getCategoriesRoute] // [!code ++]
+ routes: [getCategoriesRoute], // [!code ++]
});
```
@@ -88,9 +88,9 @@ export const getCategoryByIdRoute = buildRoute({
params: z.object({
id: z.string().openapi({
description: "Unique identifier for the category",
- example: "tech-category-123"
- })
- })
+ example: "tech-category-123",
+ }),
+ }),
},
responses: {
200: {
@@ -99,18 +99,18 @@ export const getCategoryByIdRoute = buildRoute({
schema: z.object({
id: z.string(),
name: z.string(),
- description: z.string().optional()
- })
- }
+ description: z.string().optional(),
+ }),
+ },
},
- description: "Category details retrieved successfully"
+ description: "Category details retrieved successfully",
},
404: {
- description: "Category not found"
- }
- }
+ description: "Category not found",
+ },
+ },
},
- handler: (c) => {
+ handler: c => {
// [!code highlight]
const { id } = c.req.valid("param"); // Extract the path parameter
@@ -122,9 +122,9 @@ export const getCategoryByIdRoute = buildRoute({
return c.json({
id,
name: `Category ${id}`,
- description: "A fantastic category for amazing content"
+ description: "A fantastic category for amazing content",
});
- }
+ },
});
```
@@ -148,9 +148,9 @@ export const searchCategoriesRoute = buildRoute({
query: z.object({
search: z.string().optional().openapi({
description: "Search term to filter categories",
- example: "technology"
- })
- })
+ example: "technology",
+ }),
+ }),
},
responses: {
200: {
@@ -161,37 +161,39 @@ export const searchCategoriesRoute = buildRoute({
z.object({
id: z.string(),
name: z.string(),
- description: z.string().optional()
- })
+ description: z.string().optional(),
+ }),
),
- total: z.number()
- })
- }
+ total: z.number(),
+ }),
+ },
},
- description: "Search results with pagination info"
- }
- }
+ description: "Search results with pagination info",
+ },
+ },
},
- handler: (c) => {
+ handler: c => {
const { search } = c.req.valid("query"); // [!code highlight]
// Your search logic here
const mockResults = [
{ id: "1", name: "Technology", description: "Tech-related posts" },
- { id: "2", name: "Lifestyle" }
+ { id: "2", name: "Lifestyle" },
];
const filteredResults = search
- ? mockResults.filter((cat) => cat.name.toLowerCase().includes(search.toLowerCase()))
+ ? mockResults.filter(cat =>
+ cat.name.toLowerCase().includes(search.toLowerCase()),
+ )
: mockResults;
const paginatedResults = filteredResults.slice(offset, offset + limit);
return c.json({
categories: paginatedResults,
- total: filteredResults.length
+ total: filteredResults.length,
});
- }
+ },
});
```
@@ -208,11 +210,11 @@ import { CONFIG_PLUGIN } from "@/config";
const createCategorySchema = z.object({
name: z.string().min(1).max(100).openapi({
description: "Name of the category",
- example: "Web Development"
+ example: "Web Development",
}),
description: z.string().optional().openapi({
description: "Optional description for the category",
- example: "Everything about building websites and web applications"
+ example: "Everything about building websites and web applications",
}),
color: z
.string()
@@ -220,8 +222,8 @@ const createCategorySchema = z.object({
.optional()
.openapi({
description: "Hex color code for the category",
- example: "#3B82F6"
- })
+ example: "#3B82F6",
+ }),
});
export const createCategoryRoute = buildRoute({
@@ -234,12 +236,12 @@ export const createCategoryRoute = buildRoute({
body: {
content: {
"application/json": {
- schema: createCategorySchema
- }
+ schema: createCategorySchema,
+ },
},
description: "Category data to create",
- required: true
- }
+ required: true,
+ },
},
responses: {
201: {
@@ -250,28 +252,28 @@ export const createCategoryRoute = buildRoute({
name: z.string(),
description: z.string().optional(),
color: z.string().optional(),
- createdAt: z.string()
- })
- }
+ createdAt: z.string(),
+ }),
+ },
},
- description: "Category created successfully"
+ description: "Category created successfully",
},
400: {
- description: "Invalid input data"
- }
- }
+ description: "Invalid input data",
+ },
+ },
},
- handler: async (c) => {
+ handler: async c => {
const data = c.req.valid("json"); // [!code highlight]
// Simulate category creation
const newCategory = {
id: `cat_${Date.now()}`,
...data,
- createdAt: new Date().toISOString()
+ createdAt: new Date().toISOString(),
};
return c.json(newCategory, 201);
- }
+ },
});
```
diff --git a/apps/docs/content/docs/dev/plugins/breadcrumbs.mdx b/apps/docs/content/docs/dev/plugins/breadcrumbs.mdx
new file mode 100644
index 000000000..99fe87ac2
--- /dev/null
+++ b/apps/docs/content/docs/dev/plugins/breadcrumbs.mdx
@@ -0,0 +1,171 @@
+---
+title: Breadcrumbs
+description: Learn how to add breadcrumbs to your plugin's admin pages for better navigation and user experience.
+---
+
+VitNode renders dynamic, localized breadcrumbs in two places:
+
+- **AdminCP** — in the header, next to the sidebar trigger.
+- **Main site** — below the header, on the public pages.
+
+They are built on [Next.js Parallel Routes](https://nextjs.org/docs/app/api-reference/file-conventions/parallel-routes) (the `@breadcrumb` slot) and work automatically — in most cases you don't have to do anything.
+
+## AdminCP
+
+AdminCP breadcrumbs are generated from your **sidebar navigation**. As soon as you add [navigation items](/docs/dev/plugins/admin-page#navigation-items) and their translations, the breadcrumb reuses the same translated titles — there is nothing extra to configure.
+
+Using the blog navigation from [AdminCP Pages](/docs/dev/plugins/admin-page#navigation-items), visiting `/admin/blog/settings` renders:
+
+```
+Blog / Settings
+```
+
+
+ Labels come from the same translation keys as the sidebar nav. URL segments
+ that aren't part of the nav (for example a dynamic id) fall back to a
+ humanized version of the segment (`reset-password` → `Reset Password`).
+
+
+## Main site
+
+On the public site the breadcrumb is derived from the URL. Each segment is humanized and links to its path, and the home page (`/`) shows no breadcrumb.
+
+```
+/login -> Login
+/account/settings -> Account / Settings
+```
+
+## Custom breadcrumbs
+
+Sometimes the automatic label isn't enough — you want a **translated** label for a public page, or a **real name resolved on the server** for a dynamic route (e.g. `/blog/posts/[id]` showing the post title instead of the id).
+
+For that, a plugin can ship breadcrumb **slots** in `src/routes/breadcrumb`. The folder mirrors the URL and maps to the matching `@breadcrumb` slot:
+
+- `routes/breadcrumb/admin` → AdminCP routes (path after `/admin`)
+- `routes/breadcrumb/main` → public site (path after `/`)
+
+VitNode copies these into the app automatically and keeps them in sync while you develop — exactly like the `src/routes/main` / `src/routes/admin` directories. Your slots are namespaced under your plugin, so they never collide with core or other plugins. Core already provides the generic fallback (and the home page), so you only add a slot for the specific route you want to customize.
+
+
+
+
+### Translated label
+
+Render a translated label for one of your public pages. Pass a `labels` map of `path → label`; everything else still falls back to the humanized default.
+
+```tsx title="plugins/{plugin_name}/src/routes/breadcrumb/main/blog/page.tsx"
+import { BreadcrumbMain } from '@vitnode/core/views/breadcrumb/breadcrumb-main';
+import { getTranslations } from 'next-intl/server';
+
+export default async function BreadcrumbSlot() {
+ const t = await getTranslations('@vitnode/blog');
+
+ return ;
+}
+```
+
+
+
+
+### Server-resolved name
+
+For a dynamic route whose id is the **last** segment, resolve the real name on the server and pass it as `overrideLastLabel` — it replaces the last crumb. The folder mirrors the dynamic segment.
+
+```tsx title="plugins/{plugin_name}/src/routes/breadcrumb/main/blog/posts/[id]/page.tsx"
+import { BreadcrumbMain } from '@vitnode/core/views/breadcrumb/breadcrumb-main';
+
+export default async function BreadcrumbSlot({
+ params,
+}: {
+ params: Promise<{ id: string }>;
+}) {
+ const { id } = await params;
+ const post = await getPost(id); // your data fetching
+
+ return (
+
+ );
+}
+```
+
+
+
+
+### AdminCP dynamic page
+
+In AdminCP, use `BreadcrumbAdmin` instead. `segments` are the path after `/admin`, and the labels for known routes come from your sidebar nav automatically — so you usually only need `overrideLastLabel` for the dynamic part.
+
+```tsx title="plugins/{plugin_name}/src/routes/breadcrumb/admin/blog/posts/[id]/page.tsx"
+import { BreadcrumbAdmin } from '@vitnode/core/views/admin/layouts/breadcrumb/breadcrumb-admin';
+
+export default async function BreadcrumbSlot({
+ params,
+}: {
+ params: Promise<{ id: string }>;
+}) {
+ const { id } = await params;
+ const post = await getPost(id);
+
+ return (
+
+ );
+}
+```
+
+
+
+
+### Name in the middle of the path
+
+`overrideLastLabel` only replaces the **last** crumb. When the resolved name sits in the **middle** of the path — for example the post in `/blog/posts/[id]/comments` — use the `labels` prop instead, keyed by that crumb's full path. It works at any position (and the labelled crumb becomes a link). `labels` is available on both `BreadcrumbMain` and `BreadcrumbAdmin`.
+
+```tsx title="plugins/{plugin_name}/src/routes/breadcrumb/main/blog/posts/[id]/comments/page.tsx"
+import { BreadcrumbMain } from '@vitnode/core/views/breadcrumb/breadcrumb-main';
+
+export default async function BreadcrumbSlot({
+ params,
+}: {
+ params: Promise<{ id: string }>;
+}) {
+ const { id } = await params;
+ const post = await getPost(id);
+
+ // Blog / Posts / / Comments
+ return (
+
+ );
+}
+```
+
+
+
+
+
+ The folder under `routes/breadcrumb` must mirror the real route path. A slot
+ only overrides the matching URL — every other route keeps the automatic
+ breadcrumb.
+
+
+## Learn More
+
+
+
+
+
diff --git a/apps/docs/content/docs/dev/plugins.mdx b/apps/docs/content/docs/dev/plugins/create.mdx
similarity index 100%
rename from apps/docs/content/docs/dev/plugins.mdx
rename to apps/docs/content/docs/dev/plugins/create.mdx
diff --git a/apps/docs/content/docs/dev/plugins/layouts-and-pages.mdx b/apps/docs/content/docs/dev/plugins/layouts-and-pages.mdx
new file mode 100644
index 000000000..5a84e1988
--- /dev/null
+++ b/apps/docs/content/docs/dev/plugins/layouts-and-pages.mdx
@@ -0,0 +1,270 @@
+---
+title: Layouts and Pages
+description: Create beautiful layouts and pages in VitNode plugins using Next.js App Router patterns.
+---
+
+Building layouts and pages in VitNode is just like Next.js - if you know Next.js, you're already a VitNode pro! Think of pages as rooms and layouts as the house structure that holds everything together.
+
+
+ VitNode automatically copies files from your plugin's `src/routes/main`
+ directory to the main application. No extra setup needed!
+
+
+
+ VitNode is **i18n-first** — render all user-facing text with translations
+ instead of hardcoded strings: `getTranslations` in Server Components and
+ `useTranslations` in Client Components. See [Messages](/docs/dev/i18n/messages)
+ and [Namespaces](/docs/dev/i18n/namespaces).
+
+
+## Creating Pages
+
+Pages are React components that live in `page.tsx` files. Each page represents a unique URL route.
+
+### Basic Page
+
+Resolve text from your plugin's namespace with `getTranslations`:
+
+```tsx title="plugins/blog/src/routes/main/page.tsx"
+import { getTranslations } from 'next-intl/server';
+
+export default async function BlogHomePage() {
+ const t = await getTranslations('@vitnode/blog'); // [!code highlight]
+
+ return (
+
+
{t('home.title')}
+
{t('home.desc')}
+
+ );
+}
+```
+
+Add the messages under your plugin's namespace:
+
+```json title="plugins/blog/src/locales/en.json"
+{
+ "@vitnode/blog": {
+ "home": {
+ "title": "Welcome to Our Blog! 🚀",
+ "desc": "Where awesome content lives"
+ }
+ }
+}
+```
+
+### Nested Routes
+
+Create folders to organize your routes:
+
+```tsx title="plugins/blog/src/routes/main/dashboard/page.tsx"
+import { getTranslations } from 'next-intl/server';
+
+export default async function DashboardPage() {
+ const t = await getTranslations('@vitnode/blog');
+
+ return (
+
+ );
+}
+```
+
+## Creating Layouts
+
+Layouts wrap your pages and provide shared UI elements like headers, navigation, and footers.
+
+### Basic Layout
+
+```tsx title="plugins/blog/src/routes/main/layout.tsx"
+import { getTranslations } from 'next-intl/server';
+
+export default async function BlogLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const t = await getTranslations('@vitnode/blog');
+
+ return (
+
+
+
+
+ {t('layout.title')}
+
+
+
+
+ {children}
+
+
+
+ );
+}
+```
+
+### Nested Layout for Specific Sections
+
+```tsx title="plugins/blog/src/routes/main/dashboard/layout.tsx"
+import { getTranslations } from 'next-intl/server';
+
+export default async function DashboardLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const t = await getTranslations('@vitnode/blog');
+
+ return (
+
+ {/* Sidebar */}
+
+
+ {/* Main content */}
+
{children}
+
+ );
+}
+```
+
+
+ In a Client Component (`'use client'`) use `useTranslations` instead of
+ `getTranslations`, and wrap it with `I18nProvider` to expose non-`core.global`
+ namespaces. See [Namespaces](/docs/dev/i18n/namespaces).
+
+
+## Pages without a layout
+
+Files in `src/routes/main` are wrapped by the main site layout (header, footer). To render a page **without** that chrome — only the root providers — put it in `src/routes/blank` instead. Handy for full-screen views, embeds, or standalone screens.
+
+```tsx title="plugins/{plugin_name}/src/routes/blank/standalone/page.tsx"
+import { getTranslations } from 'next-intl/server';
+
+export default async function StandalonePage() {
+ const t = await getTranslations('@vitnode/blog');
+
+ return (
+
+
{t('standalone.title')}
+
+ );
+}
+```
+
+This creates a `/standalone` route that renders on its own. Admin pages live in `src/routes/admin` (see [AdminCP Pages](/docs/dev/plugins/admin-page)).
+
+## Adding Metadata
+
+Improve SEO and social sharing with metadata — resolve the title/description from messages too:
+
+```tsx title="plugins/blog/src/routes/main/posts/[slug]/page.tsx"
+import { Metadata } from 'next';
+import { getTranslations } from 'next-intl/server'; // [!code highlight]
+
+interface PostPageProps {
+ params: Promise<{ slug: string }>;
+}
+
+// [!code highlight:11]
+export async function generateMetadata({
+ params,
+}: PostPageProps): Promise {
+ const { slug } = await params;
+ const t = await getTranslations('@vitnode/blog');
+
+ return {
+ title: t('post.meta.title', { slug }),
+ description: t('post.meta.desc', { slug }),
+ };
+}
+
+export default async function PostPage({ params }: PostPageProps) {
+ const { slug } = await params;
+ const t = await getTranslations('@vitnode/blog');
+
+ return (
+
+
{t('post.title', { slug })}
+
{t('post.desc')}
+
+ );
+}
+```
+
+## Learn More
+
+
+
+
+
+
+
diff --git a/apps/docs/content/docs/dev/plugins/meta.json b/apps/docs/content/docs/dev/plugins/meta.json
new file mode 100644
index 000000000..1d72db9d6
--- /dev/null
+++ b/apps/docs/content/docs/dev/plugins/meta.json
@@ -0,0 +1,7 @@
+{
+ "title": "Plugins",
+ "description": "Make plugins and APIs",
+ "icon": "Plug",
+ "defaultOpen": true,
+ "pages": ["create", "layouts-and-pages", "admin-page", "breadcrumbs", "api", "..."]
+}
diff --git a/apps/docs/content/docs/dev/working-with-users/meta.json b/apps/docs/content/docs/dev/working-with-users/meta.json
index 4f1634a1d..3506e5448 100644
--- a/apps/docs/content/docs/dev/working-with-users/meta.json
+++ b/apps/docs/content/docs/dev/working-with-users/meta.json
@@ -1,4 +1,6 @@
{
"title": "Working with Users",
+ "description": "Learn how to manage users and roles in VitNode with our comprehensive guide.",
+ "icon": "Users",
"pages": ["users", "roles", "..."]
}
diff --git a/apps/docs/src/app/[locale]/(blank)/(plugins)/(vitnode-core)/test/page.tsx b/apps/docs/src/app/[locale]/(blank)/(plugins)/(vitnode-core)/test/page.tsx
new file mode 100644
index 000000000..ae24cdcd4
--- /dev/null
+++ b/apps/docs/src/app/[locale]/(blank)/(plugins)/(vitnode-core)/test/page.tsx
@@ -0,0 +1,11 @@
+export default function Page() {
+ return (
+
+
Blank test page
+
+ This page renders without the main layout — no header or footer, just
+ the root providers.
+
+
+ );
+}
diff --git a/apps/docs/src/app/[locale]/(main)/@breadcrumb/[...rest]/page.tsx b/apps/docs/src/app/[locale]/(main)/@breadcrumb/[...rest]/page.tsx
new file mode 100644
index 000000000..4d61cbd1a
--- /dev/null
+++ b/apps/docs/src/app/[locale]/(main)/@breadcrumb/[...rest]/page.tsx
@@ -0,0 +1,17 @@
+import { BreadcrumbMain } from "@vitnode/core/views/breadcrumb/breadcrumb-main";
+
+// Generic catch-all breadcrumb for public pages: humanizes URL segments.
+// Specific slot folders (login, register, …) override it with translated labels.
+//
+// NOTE: must live at the `@breadcrumb` slot ROOT (not under a `(plugins)` route
+// group) — a parallel-route slot only matches `children` sharing its route-group
+// structure, so a nested catch-all would miss pages outside that group.
+export default async function BreadcrumbSlot({
+ params,
+}: {
+ params: Promise<{ rest?: string[] }>;
+}) {
+ const { rest } = await params;
+
+ return ;
+}
diff --git a/apps/docs/src/app/[locale]/(main)/@breadcrumb/default.tsx b/apps/docs/src/app/[locale]/(main)/@breadcrumb/default.tsx
new file mode 100644
index 000000000..4c84c0634
--- /dev/null
+++ b/apps/docs/src/app/[locale]/(main)/@breadcrumb/default.tsx
@@ -0,0 +1,4 @@
+// Fallback for the @breadcrumb slot on unmatched soft-navigation/hard loads.
+export default function Default() {
+ return null;
+}
diff --git a/apps/docs/src/app/[locale]/(main)/@breadcrumb/login/page.tsx b/apps/docs/src/app/[locale]/(main)/@breadcrumb/login/page.tsx
new file mode 100644
index 000000000..4805d6f7b
--- /dev/null
+++ b/apps/docs/src/app/[locale]/(main)/@breadcrumb/login/page.tsx
@@ -0,0 +1,11 @@
+import { getTranslations } from "next-intl/server";
+
+import { BreadcrumbMain } from "@vitnode/core/views/breadcrumb/breadcrumb-main";
+
+export default async function BreadcrumbSlot() {
+ const t = await getTranslations("core.global");
+
+ return (
+
+ );
+}
diff --git a/apps/docs/src/app/[locale]/(main)/@breadcrumb/login/reset-password/page.tsx b/apps/docs/src/app/[locale]/(main)/@breadcrumb/login/reset-password/page.tsx
new file mode 100644
index 000000000..aa304276c
--- /dev/null
+++ b/apps/docs/src/app/[locale]/(main)/@breadcrumb/login/reset-password/page.tsx
@@ -0,0 +1,20 @@
+import { getTranslations } from "next-intl/server";
+
+import { BreadcrumbMain } from "@vitnode/core/views/breadcrumb/breadcrumb-main";
+
+export default async function BreadcrumbSlot() {
+ const [tGlobal, tAuth] = await Promise.all([
+ getTranslations("core.global"),
+ getTranslations("core.auth"),
+ ]);
+
+ return (
+
+ );
+}
diff --git a/apps/docs/src/app/[locale]/(main)/@breadcrumb/page.tsx b/apps/docs/src/app/[locale]/(main)/@breadcrumb/page.tsx
new file mode 100644
index 000000000..6ab2ce8a2
--- /dev/null
+++ b/apps/docs/src/app/[locale]/(main)/@breadcrumb/page.tsx
@@ -0,0 +1,6 @@
+// Home page (/) has no breadcrumb. An explicit slot page (not just default.tsx)
+// is required so client-side navigation back to "/" clears a previously-rendered
+// breadcrumb — otherwise Next.js keeps the slot's stale active state on soft nav.
+export default function BreadcrumbSlot() {
+ return null;
+}
diff --git a/apps/docs/src/app/[locale]/(main)/@breadcrumb/register/page.tsx b/apps/docs/src/app/[locale]/(main)/@breadcrumb/register/page.tsx
new file mode 100644
index 000000000..f128344a0
--- /dev/null
+++ b/apps/docs/src/app/[locale]/(main)/@breadcrumb/register/page.tsx
@@ -0,0 +1,14 @@
+import { getTranslations } from "next-intl/server";
+
+import { BreadcrumbMain } from "@vitnode/core/views/breadcrumb/breadcrumb-main";
+
+export default async function BreadcrumbSlot() {
+ const t = await getTranslations("core.global");
+
+ return (
+
+ );
+}
diff --git a/apps/docs/src/app/[locale]/(main)/layout.tsx b/apps/docs/src/app/[locale]/(main)/layout.tsx
index d16c65026..87d8bc5d1 100644
--- a/apps/docs/src/app/[locale]/(main)/layout.tsx
+++ b/apps/docs/src/app/[locale]/(main)/layout.tsx
@@ -13,9 +13,16 @@ import { ThemeLayout } from "@vitnode/core/views/layouts/theme/layout";
import { vitNodeConfig } from "../../../vitnode.config";
-export default function Layout({ children }: { children: React.ReactNode }) {
+export default function Layout({
+ children,
+ breadcrumb,
+}: {
+ breadcrumb: React.ReactNode;
+ children: React.ReactNode;
+}) {
return (
}
vitNodeConfig={vitNodeConfig}
>
diff --git a/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/[...all]/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/[...all]/page.tsx
new file mode 100644
index 000000000..fde75805c
--- /dev/null
+++ b/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/[...all]/page.tsx
@@ -0,0 +1,17 @@
+import { BreadcrumbAdmin } from "@vitnode/core/views/admin/layouts/breadcrumb/breadcrumb-admin";
+
+// Generic catch-all breadcrumb for every authenticated admin route (core +
+// plugins). More specific slot folders (e.g. core/users/[nameCode]) override it.
+//
+// NOTE: must live at the `@breadcrumb` slot ROOT (not under a `(plugins)` route
+// group) — a parallel-route slot only matches `children` sharing its route-group
+// structure, so a nested catch-all would miss pages from other plugin groups.
+export default async function BreadcrumbSlot({
+ params,
+}: {
+ params: Promise<{ all?: string[] }>;
+}) {
+ const { all } = await params;
+
+ return ;
+}
diff --git a/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/users/[nameCode]/page.tsx b/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/users/[nameCode]/page.tsx
new file mode 100644
index 000000000..7ff4a3a56
--- /dev/null
+++ b/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/core/users/[nameCode]/page.tsx
@@ -0,0 +1,12 @@
+import { BreadcrumbUserAdmin } from "@vitnode/core/views/admin/layouts/breadcrumb/breadcrumb-user-admin";
+
+// Resolves the real user name server-side for the user detail breadcrumb.
+export default async function BreadcrumbSlot({
+ params,
+}: {
+ params: Promise<{ nameCode: string }>;
+}) {
+ const { nameCode } = await params;
+
+ return ;
+}
diff --git a/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/default.tsx b/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/default.tsx
new file mode 100644
index 000000000..4c84c0634
--- /dev/null
+++ b/apps/docs/src/app/[locale]/admin/(auth)/@breadcrumb/default.tsx
@@ -0,0 +1,4 @@
+// Fallback for the @breadcrumb slot on unmatched soft-navigation/hard loads.
+export default function Default() {
+ return null;
+}
diff --git a/apps/docs/src/components/animated-beam/animated-beam-home.tsx b/apps/docs/src/components/animated-beam/animated-beam-home.tsx
index 1e030d865..a7b2e40d3 100644
--- a/apps/docs/src/components/animated-beam/animated-beam-home.tsx
+++ b/apps/docs/src/components/animated-beam/animated-beam-home.tsx
@@ -44,9 +44,9 @@ const Circle = ({
return (
-
-
-
+ }
+ />
{tooltip}
diff --git a/apps/docs/src/examples/tooltip.tsx b/apps/docs/src/examples/tooltip.tsx
index 3ed643b5d..3071d2670 100644
--- a/apps/docs/src/examples/tooltip.tsx
+++ b/apps/docs/src/examples/tooltip.tsx
@@ -12,9 +12,7 @@ export default function TooltipDemo() {
return (
-
-
-
+ Hover} />