Plugins

AdminCP Pages

Create powerful admin interfaces for your plugins with protected pages, custom layouts, and integrated navigation.

Pages & Layouts

AdminCP follows the same patterns like from Layouts and Pages but uses the src/routes/admin directory. Like everywhere in VitNode, render text with translations - see Messages.

Pages

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"); 

  return (
    <div>
      <h1>{t("title")}</h1>
      <p>{t("desc")}</p>
    </div>
  );
}

Layouts

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 (
    <div className="min-h-screen bg-gray-50">
      <header className="border-b bg-white p-4">
        <h1 className="text-xl font-semibold">{t("title")}</h1>
      </header>
      {children}
    </div>
  );
}

All pages & layouts in the src/routes/admin directory are automatically protected and require admin authentication.

To create own navigation items in the admin sidebar you can just create nav object inside admin in your plugin config.

Define items in config

As an example let's create a navigation item for blog categories:

plugins/{plugin_name}/src/plugin.tsx
import { buildPlugin } from "@vitnode/core/lib/plugin";
import { SettingsIcon } from "lucide-react";

import { configPlugin } from "./config";

export const blogPlugin = () => {
  return buildPlugin({
    ...configPlugin,
    admin: {
      nav: [
        {
          id: "settings",
          href: "/admin/blog/settings",
          icon: <SettingsIcon />,
        },
      ],
    },
  });
};

Nested Navigation

To create nested navigation items, you can use the items property:

plugins/{plugin_name}/src/plugin.tsx
import { buildPlugin } from "@vitnode/core/lib/plugin";
import { ListIcon, SettingsIcon } from "lucide-react";
import { configPlugin } from "./config";

export const blogPlugin = () => {
  return buildPlugin({
    ...configPlugin,
    admin: {
      nav: [
        {
          id: "settings",
          href: "/admin/blog/settings",
          icon: <SettingsIcon />,
          items: [
            {
              id: "general",
              href: "/admin/blog/settings/general",
            },
            {
              id: "seo",
              href: "/admin/blog/settings/seo",
            },
          ],
        },
      ],
    },
  });
};

Open in new tab

To open a navigation item in a new tab, you can use the isOpenInNewTab property:

plugins/{plugin_name}/src/plugin.tsx
import { buildPlugin } from "@vitnode/core/lib/plugin";
import { SettingsIcon } from "lucide-react";
import { configPlugin } from "./config";
export const blogPlugin = () => {
  return buildPlugin({
    ...configPlugin,
    admin: {
      nav: [
        {
          id: "settings",
          href: "/admin/blog/settings",
          icon: <SettingsIcon />,
          isOpenInNewTab: true, 
        },
      ],
    },
  });
};

Gate by permission

To hide a navigation item from admins who lack a staff permission, add a permission. It takes { module, permission } - the plugin is inferred from your plugin, so you only reference the module and permission id you declared in permissionStaff:

plugins/{plugin_name}/src/plugin.tsx
import { buildPlugin } from "@vitnode/core/lib/plugin";
import { ListIcon } from "lucide-react";
import { configPlugin } from "./config";

export const blogPlugin = () => {
  return buildPlugin({
    ...configPlugin,
    admin: {
      nav: [
        {
          id: "categories",
          href: "/admin/blog/categories",
          icon: <ListIcon />,
          permission: { module: "categories", permission: "can_view" },
        },
      ],
    },
  });
};

The same permission field works on nested items. A parent item disappears once all of its children are hidden, and items without a permission are always shown.

Hiding a nav item only hides the link - it does not protect the route. Guard the page itself and the underlying API too. See Staff Permissions for the full pattern.

Add translation

VitNode automatically uses the id as translation key for the navigation item.

plugins/{plugin_name}/src/locales/en.json
{
  "@vitnode/blog": {
    "title": "Blog",
    "admin": {
      "nav": {
        "settings": "Settings"
      }
    }
  }
}

Learn More

On this page