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
8 changes: 0 additions & 8 deletions .cursor/rules/backend/backend.mdc

This file was deleted.

13 changes: 0 additions & 13 deletions .cursor/rules/docs.mdc

This file was deleted.

10 changes: 0 additions & 10 deletions .cursor/rules/frontend/frontend.mdc

This file was deleted.

9 changes: 0 additions & 9 deletions .cursor/rules/frontend/i18n.mdc

This file was deleted.

15 changes: 0 additions & 15 deletions .cursor/rules/global.mdc

This file was deleted.

46 changes: 46 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# VitNode Development Guidelines

VitNode is a comprehensive framework designed to simplify and accelerate application development with Next.js and Hono.js. Built as a monorepo solution managed by Turborepo, VitNode provides a structured environment that makes development faster and less complex. The framework includes an integrated AdminCP and plugin system to extend its core functionality.

## Global Rules

- Write ESModule only
- Always use snake_case for file names
- Use pnpm as package manager
- Use Zod 3 for schema validation
- Use react-hook-form 7 for forms
- Use Shadcn UI & Tailwind CSS 4 for UI
- Respect Prettier configuration in `packages/eslint/prettierrc.mjs` and ESLint configuration in `packages/eslint/eslint.config.mjs`
- Use TypeScript 5, React 19 & Hono.js 4

## Frontend Development (Next.js & React)

- Use Next.js 15
- Use App Router and Server Components
- Use server actions for form handling and data mutations from Server Components
- Leverage Next.js Image component with proper sizing for core web vitals optimization
- Navigation API is in `vitnode/lib/navigation` file. Avoid using `next/navigation` directly

### Internationalization (i18n)

- Use `next-intl` for internationalization
- Use `t('key')` for translation keys
- Use `getTranslation` function for server components but `useTranslation` for client components
- Language keys should be added in `apps/web/src/plugins/core/langs/en.json` file

## Backend Development (Hono.js)

- Use @hono/zod-openapi or Zod 3 for schema validation
- Use PostgreSQL with Drizzle ORM
- Use `t.serial().primaryKey()` for all database IDs

## Documentation (\*.mdx files)

- Use Fumadocs framework
- Write docs in `.mdx` files
- `apps/docs/content/docs/dev/index.mdx` contains the main documentation
- Use easy, clear and funny language for documentation to make it accessible to a wide audience (exclude title and description)
- Use clear and concise examples to illustrate concepts
- Use `// [!code ++]` to highlight code snippets and `// [!code --]` to hide code snippets
- Don't add h1 tag in markdown
- Don't use emoji on headings
7 changes: 6 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
{
"cSpell.words": ["vitnode"]
"cSpell.words": ["vitnode"],
"github.copilot.chat.commitMessageGeneration.instructions": [
{
"text": "Follow the Conventional Commits format strictly for commit messages. Use the structure below:\n\n```\n<type>[optional scope]: <gitmoji> <description>\n```\n\nGuidelines:\n\n1. **Type and Scope**: Choose an appropriate type (e.g., `feat`, `fix`, `refactor`, `docs`) and optional scope to describe the affected module or feature.\n\n2. **Gitmoji**: Include a relevant `gitmoji` that best represents the nature of the change.\n\n3. **Description**: Write a concise, informative description in the header; use backticks if referencing code or specific terms.\n\nCommit messages should be clear, informative, and professional, aiding readability and project tracking."
}
]
}
202 changes: 61 additions & 141 deletions apps/docs/content/docs/plugins/layouts-and-pages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,166 +5,86 @@ description: Learn how to create custom layouts and pages for your VitNode plugi

Welcome to the magical world of VitNode layouts and pages! Think of these as the digital feng shui for your plugin - they determine how things look and where everything goes. Let's dive in!

## Create a Page

Pages in VitNode are like unexpected guests - they show up when someone visits a URL matching your plugin's route. Here's how to roll out the welcome mat:

```tsx title="plugins/blog/src/plugin.tsx"
import { buildPlugin } from 'vitnode/lib/plugin';

import { configPlugin } from './config';

export const blogPlugin = () => {
return buildPlugin({
...configPlugin,
// [!code ++]
pages: {
// [!code ++]
component: ({ params }) => {
// [!code ++]
if (params[0] === 'blog' && !params[1]) {
// [!code ++]
return (
// [!code ++]
<div className="container mx-auto p-4">Page Blog plugin</div>
// [!code ++]
);
// [!code ++]
}
// [!code ++]
},
// [!code ++]
},
});
};
```
## How Routing Works

<Callout type="warn" title="RSC Require">
The page component must be a React Server Component (RSC). This means it
should be a function that returns JSX and does not use hooks or state. If you
want to use `use client;` then you need to create a new file and import the
page component.
</Callout>

## Page Parameters

Your pages can access URL parameters like a nosy neighbor reading your mail. The `params` array contains each segment of the URL path:

```tsx
component: ({ params, locale, searchParams }) => {
// params[0] is the first segment after the base URL
// For example, in "/blog/post/123":
// params[0] would be "blog"
// params[1] would be "post"
// params[2] would be "123"

if (params[0] === 'blog' && params[1] === 'post' && params[2] && !params[3]) {
return <BlogPost id={params[2]} locale={locale} />;
}
};
```
VitNode follows Next.js app directory conventions for routing. Each plugin can define its own pages that get integrated into the main application. When developing a plugin:

<Callout type="warn" title="Page Ending Condition">
Always end your page-matching conditionals with <code>!params[X]</code> to
ensure you only match the intended page and not sub-paths.
</Callout>
- Files named `page.tsx` define a route
- The route path is determined by the folder structure
- Page files are automatically detected and copied to the appropriate location in the main app

## Static Paths
## Pages

Sometimes you know exactly who's coming to dinner. For those cases, define your static paths:
Pages in VitNode work the same way as in the Next.js framework. You can create a page by creating a `page.tsx` file in the `src/app` directory of your plugin.

```tsx
pages: {
component: ({ params }) => {
if (params[0] === 'about' && !params[1]) {
return <AboutPage />;
}
},
staticPaths: ['/about', '/terms', '/privacy'],
For example, if you want to create a page at `/dashboard`, you would create:

```tsx title="plugins/{plugin_name}/src/app/dashboard/page.tsx"
export default function DashboardPage() {
return <div>Welcome to the dashboard!</div>;
}
```

These paths will be pre-rendered at build time - like preparing snacks before your guests arrive!
When your plugin is loaded, VitNode will automatically copy this page to the appropriate location in the main application and handle all the routing.

## Layouts

Layouts are like the interior designers of your plugin - they wrap your content in a consistent style. Unlike Next.js layouts that use file-system conventions, VitNode uses a more explicit approach:

```tsx title="plugins/blog/src/plugin.tsx"
import { BlogLayout } from './components/blog-layout';
import { BlogListing } from './components/blog-listing';
import { BlogPost } from './components/blog-post';
import { AboutPage } from './components/about-page';

export const blogPlugin = () => {
return buildPlugin({
...configPlugin,
pages: {
component: ({ params }) => {
return (
<BlogLayout>
{/* Blog listing page: /blog */}
{params[0] === 'blog' && !params[1] && <BlogListing />}
{/* Blog post page: /blog/post/[id] */}
{params[0] === 'blog' &&
params[1] === 'post' &&
params[2] &&
!params[3] && <BlogPost id={params[2]} />}
{/* About page: /about */}
{params[0] === 'about' && !params[1] && <AboutPage />}
{/* You can add more pages here */}
</BlogLayout>
);
},
staticPaths: ['/blog', '/about'],
},
});
};
Layouts allow you to create shared UI for multiple pages. They follow Next.js conventions and should be defined as `layout.tsx` files.

For example, to create a layout for all pages under `/dashboard`:

```tsx title="plugins/{plugin_name}/src/app/dashboard/layout.tsx"
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="container">
<nav>Dashboard Navigation</nav>
<main>{children}</main>
</div>
);
}
```

Think of `searchParams` as that friend who always asks too many questions - sometimes annoying, but often useful!
This layout will wrap any pages within the `dashboard` directory.

## Mixing Client and Server Components
## Admin Pages

Want the best of both worlds? Here's how to dance the client-server tango:
For admin pages, follow the same pattern but create your files in the `src/app_admin` directory of your plugin. This is the only difference - the structure and implementation remain the same.

```tsx title="plugins/blog/src/components/blog-post.client.tsx"
'use client';
For example, to create an admin page at `/admin/{plugin_name}/settings`:

import { useState } from 'react';
```
src/app_admin/settings/page.tsx
```

export function ClientBlogPost({ initialData }) {
const [likes, setLikes] = useState(initialData.likes);
And for a layout:

return (
<div>
<h1>{initialData.title}</h1>
<p>{initialData.content}</p>
<button onClick={() => setLikes(likes + 1)}>Like ({likes})</button>
</div>
);
}
```
src/app_admin/settings/layout.tsx
Comment on lines +55 to +66
Copy link

Copilot AI May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example uses src/app_admin, but earlier you mention src/app. This inconsistency can confuse readers—align the directory path with your plugin conventions.

Copilot uses AI. Check for mistakes.
```

## Route Groups and Special Folders

VitNode supports Next.js route grouping with parentheses:

- `(group-name)` - Creates a logical group without affecting the URL path
- `[dynamicParam]` - Creates a dynamic route segment

```tsx title="plugins/blog/src/plugin.tsx"
import { ClientBlogPost } from './components/blog-post.client';

export const blogPlugin = () => {
return buildPlugin({
...configPlugin,
pages: {
component: async ({ params }) => {
if (params[0] === 'blog' && params[1] && !params[2]) {
const postData = await fetchBlogPost(params[1]);

return (
<div className="container mx-auto p-4">
<ClientBlogPost initialData={postData} />
</div>
);
}
},
},
});
};
For example:

```
src/app/(user-section)/profile/page.tsx // Route: /profile
src/app/products/[id]/page.tsx // Route: /products/:id
```

## Import Considerations

When developing your plugin pages, you can use:

- Relative imports (e.g., `import { Button } from '../components/Button'`)
- Imports with `@/` prefix (e.g., `import { utils } from '@/lib/utils'`)

VitNode will automatically transform these imports when your plugin is integrated into the main application.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { TestLayout } from 'vitnode-blog/views/test/layout';

export default function Layout({ children }: { children: React.ReactNode }) {
return <TestLayout>{children}</TestLayout>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Test } from 'vitnode-blog/views/test';
import { TestClient } from 'vitnode-blog/views/test/client';
import { Link } from 'vitnode/lib/navigation';

export default function Page() {
return (
<>
<Test />
<TestClient />
<Link href="/blog/test">Go to blog - test 123</Link>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Link } from 'vitnode/lib/navigation';

export default function Page() {
return (
<>
<Link href="/blog">Go to blog</Link>
</>
);
}
Loading
Loading