-
+
diff --git a/app/utils/cardColor.ts b/app/utils/cardColor.ts
deleted file mode 100644
index 6ac1aff62..000000000
--- a/app/utils/cardColor.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import uiColors from '#ui-colors';
-
-function getInitials(str: string) {
- return str
- .split(' ')
- .map(word => word.charAt(0))
- .join('');
-}
-
-function hashStr(str: string) {
- let hash = 0;
- for (let i = 0; i < str.length; i++) {
- hash = str.charCodeAt(i) + ((hash << 5) - hash);
- }
- return hash;
-}
-
-/**
- * Get a UI color for a given string
- *
- * This uses a deterministic hash to retrieve a semi-random color to ensure server and client
- * rendering result in the same color name when given the same string
- *
- * It'll take the first letter of every word, generate a hash out of that, and use that hash to grab
- * the color out of the array of UI colors. This ensures that phrases starting with the same word don't
- * necessarily get the same color
- */
-export default function (str: string): typeof uiColors[number] {
- const hash = hashStr(getInitials(str));
- const index = hash % uiColors.length;
- return uiColors[index]!;
-}
diff --git a/app/utils/deployments.ts b/app/utils/deployments.ts
new file mode 100644
index 000000000..1c9caa916
--- /dev/null
+++ b/app/utils/deployments.ts
@@ -0,0 +1,16 @@
+import type { Deployment } from './userPreferences';
+
+export interface DeploymentOption {
+ slug: Deployment;
+ label: string;
+ icon: string;
+ description: string;
+}
+
+export const deployments: DeploymentOption[] = [
+ { slug: 'cloud', label: 'Directus Cloud', icon: 'material-symbols:cloud-outline', description: 'Managed hosting by Directus.' },
+ { slug: 'self-hosted', label: 'Self-Hosted', icon: 'material-symbols:dns-outline', description: 'Run Directus on your own infrastructure.' },
+];
+
+export const getDeployment = (slug: string): DeploymentOption | undefined =>
+ deployments.find(d => d.slug === slug);
diff --git a/app/utils/experience.ts b/app/utils/experience.ts
new file mode 100644
index 000000000..16931f826
--- /dev/null
+++ b/app/utils/experience.ts
@@ -0,0 +1,17 @@
+import type { Experience } from './userPreferences';
+
+export interface ExperienceOption {
+ slug: Experience;
+ label: string;
+ icon: string;
+ description: string;
+}
+
+export const experiences: ExperienceOption[] = [
+ { slug: 'new', label: 'New to Directus', icon: 'material-symbols:auto-awesome-outline', description: 'First time or just evaluating.' },
+ { slug: 'familiar', label: 'Some Experience', icon: 'material-symbols:potted-plant-outline', description: 'Built something, still learning.' },
+ { slug: 'experienced', label: 'Power User', icon: 'material-symbols:forest-outline', description: 'Daily / production use.' },
+];
+
+export const getExperience = (slug: string): ExperienceOption | undefined =>
+ experiences.find(e => e.slug === slug);
diff --git a/app/utils/libraries.ts b/app/utils/libraries.ts
new file mode 100644
index 000000000..d5a9f14c1
--- /dev/null
+++ b/app/utils/libraries.ts
@@ -0,0 +1,28 @@
+export interface LibraryOption {
+ value: string;
+ label: string;
+ icon: string;
+ matchLabels: string[];
+}
+
+export const libraries: LibraryOption[] = [
+ { value: '0', label: 'SDK', icon: 'simple-icons:directus', matchLabels: ['Directus SDK', 'SDK'] },
+ { value: '1', label: 'REST', icon: 'material-symbols:language', matchLabels: ['REST'] },
+ { value: '2', label: 'GraphQL', icon: 'simple-icons:graphql', matchLabels: ['GraphQL'] },
+];
+
+export const sampleVariants: LibraryOption[] = [
+ { value: 'fetch', label: 'Fetch', icon: 'simple-icons:javascript', matchLabels: ['Fetch', 'JavaScript fetch', 'fetch'] },
+ { value: 'curl', label: 'cURL', icon: 'simple-icons:curl', matchLabels: ['cURL', 'curl'] },
+];
+
+export const allSampleOptions: LibraryOption[] = [...libraries, ...sampleVariants];
+
+export const getSampleOptionByLabel = (label: string): LibraryOption | undefined =>
+ allSampleOptions.find(l => l.matchLabels.includes(label));
+
+export const getLibraryByLabel = (label: string): LibraryOption | undefined =>
+ libraries.find(l => l.matchLabels.includes(label));
+
+export const getLibraryByValue = (value: string): LibraryOption | undefined =>
+ libraries.find(l => l.value === value);
diff --git a/app/utils/relativeTime.ts b/app/utils/relativeTime.ts
new file mode 100644
index 000000000..cd46ffda2
--- /dev/null
+++ b/app/utils/relativeTime.ts
@@ -0,0 +1,12 @@
+export function relativeTime(ts?: number): string {
+ if (!ts) return '';
+ const diff = Date.now() - ts;
+ const m = Math.floor(diff / 60000);
+ if (m < 1) return 'just now';
+ if (m < 60) return `${m}m`;
+ const h = Math.floor(m / 60);
+ if (h < 24) return `${h}h`;
+ const d = Math.floor(h / 24);
+ if (d < 7) return `${d}d`;
+ return `${Math.floor(d / 7)}w`;
+}
diff --git a/app/utils/roles.ts b/app/utils/roles.ts
new file mode 100644
index 000000000..05d09461a
--- /dev/null
+++ b/app/utils/roles.ts
@@ -0,0 +1,16 @@
+import type { Role } from './userPreferences';
+
+export interface RoleOption {
+ slug: Role;
+ label: string;
+ icon: string;
+ description: string;
+}
+
+export const roles: RoleOption[] = [
+ { slug: 'developer', label: 'Developer', icon: 'material-symbols:code', description: 'Show code-first content and technical detail.' },
+ { slug: 'non-developer', label: 'Non-Developer', icon: 'material-symbols:person-outline', description: 'Focus on UI workflows and concepts.' },
+];
+
+export const getRole = (slug: string): RoleOption | undefined =>
+ roles.find(r => r.slug === slug);
diff --git a/app/utils/safePolygon.ts b/app/utils/safePolygon.ts
new file mode 100644
index 000000000..53669a106
--- /dev/null
+++ b/app/utils/safePolygon.ts
@@ -0,0 +1,179 @@
+/**
+ * Safe-triangle hover intent guard.
+ *
+ * Inspired by @floating-ui/react's safePolygon (MIT) and Amazon's mega-menu
+ * pattern. Our case is simpler: trigger (results list) is always to the LEFT
+ * of the floating pane (preview), so the safe area is a triangle from the
+ * cursor's exit point to the floating pane's two left corners.
+ *
+ * Usage: pass `trigger` (results container) and `floating` (preview pane) refs.
+ * Call `onPointerMove(event)` from a pointermove listener that covers both
+ * elements and the gap between them. While the cursor is inside the triangle,
+ * `isGuarding()` returns true — use that to swallow hover-driven highlight
+ * events on intermediate rows.
+ */
+
+type Point = [number, number];
+
+export interface SafePolygonOptions {
+ /** Pixel buffer around the polygon edges. Default 0.5 (matches Floating UI). */
+ buffer?: number;
+ /** Require sustained cursor motion toward the floating element. Default true. */
+ requireIntent?: boolean;
+}
+
+export interface SafePolygon {
+ /** Call from pointermove on the trigger element. Returns true if cursor is inside the safe polygon. */
+ onPointerMove: (event: PointerEvent | MouseEvent) => boolean;
+ /** True while a recent pointer motion is still inside the polygon. */
+ isGuarding: () => boolean;
+ /** Reset state — call when the floating target changes (e.g. results list rebuilds). */
+ reset: () => void;
+}
+
+function isPointInPolygon(point: Point, polygon: Point[]) {
+ const [x, y] = point;
+ let isInside = false;
+ const length = polygon.length;
+ for (let i = 0, j = length - 1; i < length; j = i++) {
+ const [xi, yi] = polygon[i] || [0, 0];
+ const [xj, yj] = polygon[j] || [0, 0];
+ const intersect = yi >= y !== yj >= y && x <= ((xj - xi) * (y - yi)) / (yj - yi) + xi;
+ if (intersect) isInside = !isInside;
+ }
+ return isInside;
+}
+
+export function createSafePolygon(
+ getTrigger: () => HTMLElement | null,
+ getFloating: () => HTMLElement | null,
+ options: SafePolygonOptions = {},
+): SafePolygon {
+ const { buffer = 0.5, requireIntent = true } = options;
+
+ let anchorX: number | null = null;
+ let anchorY: number | null = null;
+ let lastX: number | null = null;
+ let lastY: number | null = null;
+ let prevX: number | null = null;
+ let lastTime = 0;
+ let guarding = false;
+
+ function getCursorSpeed(x: number, y: number): number | null {
+ const now = performance.now();
+ const elapsed = now - lastTime;
+ if (lastX === null || lastY === null || elapsed === 0) {
+ lastX = x;
+ lastY = y;
+ lastTime = now;
+ return null;
+ }
+ const dx = x - lastX;
+ const dy = y - lastY;
+ const speed = Math.sqrt(dx * dx + dy * dy) / elapsed;
+ lastX = x;
+ lastY = y;
+ lastTime = now;
+ return speed;
+ }
+
+ function reset() {
+ anchorX = null;
+ anchorY = null;
+ lastX = null;
+ lastY = null;
+ prevX = null;
+ lastTime = 0;
+ guarding = false;
+ }
+
+ function onPointerMove(event: PointerEvent | MouseEvent): boolean {
+ const trigger = getTrigger();
+ const floating = getFloating();
+ if (!trigger || !floating) {
+ guarding = false;
+ return false;
+ }
+
+ const { clientX, clientY } = event;
+ const triggerRect = trigger.getBoundingClientRect();
+ const floatingRect = floating.getBoundingClientRect();
+
+ const insideTrigger
+ = clientX >= triggerRect.left
+ && clientX <= triggerRect.right
+ && clientY >= triggerRect.top
+ && clientY <= triggerRect.bottom;
+
+ // Anchor = cursor position when the user begins moving toward the
+ // floating pane. Update anchor while cursor is NOT moving rightward
+ // (toward the pane). Once the cursor commits to a rightward move,
+ // freeze the anchor so the triangle has a real apex behind the cursor.
+ if (insideTrigger) {
+ const movingTowardFloating = prevX !== null && clientX > prevX;
+ if (!movingTowardFloating || anchorX === null) {
+ anchorX = clientX;
+ anchorY = clientY;
+ }
+ }
+ prevX = clientX;
+
+ if (anchorX === null || anchorY === null) {
+ guarding = false;
+ return false;
+ }
+
+ // Triangle from anchor → top-left and bottom-left corners of floating.
+ // Buffer pulls the floating-side edge inward by a hair so the triangle
+ // doesn't render flush against the pane border.
+ const polygon: Point[] = [
+ [anchorX, anchorY],
+ [floatingRect.left + buffer, floatingRect.top],
+ [floatingRect.left + buffer, floatingRect.bottom],
+ ];
+
+ // Trough between trigger right edge and floating left edge: any cursor
+ // inside this strip should be guarded regardless of triangle math.
+ const top = Math.min(triggerRect.top, floatingRect.top);
+ const bottom = Math.max(triggerRect.bottom, floatingRect.bottom);
+ const rectPoly: Point[] = [
+ [triggerRect.right - 1, bottom],
+ [triggerRect.right - 1, top],
+ [floatingRect.left + 1, top],
+ [floatingRect.left + 1, bottom],
+ ];
+
+ const insideTrough = isPointInPolygon([clientX, clientY], rectPoly);
+ const insidePolygon = isPointInPolygon([clientX, clientY], polygon);
+
+ if (insideTrough) {
+ guarding = true;
+ return true;
+ }
+
+ // Cursor crossed the floating pane's left edge → no longer guarding.
+ if (clientX >= floatingRect.left + 1) {
+ guarding = false;
+ return false;
+ }
+
+ // Intent check: if the cursor stalls (slow motion) outside the trigger,
+ // drop the guard so hover responds normally.
+ if (requireIntent && !insideTrigger) {
+ const speed = getCursorSpeed(clientX, clientY);
+ if (speed !== null && speed < 0.1) {
+ guarding = false;
+ return false;
+ }
+ }
+
+ guarding = insidePolygon;
+ return insidePolygon;
+ }
+
+ return {
+ onPointerMove,
+ isGuarding: () => guarding,
+ reset,
+ };
+}
diff --git a/app/utils/useCases.ts b/app/utils/useCases.ts
new file mode 100644
index 000000000..1387e285a
--- /dev/null
+++ b/app/utils/useCases.ts
@@ -0,0 +1,19 @@
+export interface UseCase {
+ slug: string;
+ label: string;
+ icon: string;
+ description: string;
+}
+
+export const useCases: UseCase[] = [
+ { slug: 'headless-cms', label: 'Headless CMS', icon: 'material-symbols:article-outline', description: 'Power websites and apps with structured content.' },
+ { slug: 'client-website', label: 'Client Website', icon: 'material-symbols:language', description: 'Marketing sites and client projects.' },
+ { slug: 'internal-tool', label: 'Internal App', icon: 'material-symbols:corporate-fare', description: 'Admin panels and internal tooling.' },
+ { slug: 'api-backend', label: 'API Backend', icon: 'material-symbols:api', description: 'Data platform and backend-as-a-service.' },
+ { slug: 'multi-tenant-app', label: 'Multi-Tenant App', icon: 'material-symbols:groups-outline', description: 'SaaS apps serving multiple customers.' },
+ { slug: 'ecommerce', label: 'Ecommerce', icon: 'material-symbols:shopping-cart-outline', description: 'Storefronts and product catalogs.' },
+ { slug: 'other', label: 'Other', icon: 'material-symbols:more-horiz', description: 'Something else.' },
+];
+
+export const getUseCase = (slug: string): UseCase | undefined =>
+ useCases.find(u => u.slug === slug);
diff --git a/app/utils/userPreferences.ts b/app/utils/userPreferences.ts
new file mode 100644
index 000000000..a7fcd5959
--- /dev/null
+++ b/app/utils/userPreferences.ts
@@ -0,0 +1,34 @@
+export type Deployment = 'cloud' | 'self-hosted';
+export type Role = 'developer' | 'non-developer';
+export type Experience = 'new' | 'familiar' | 'experienced';
+export type OnboardingState = 'idle' | 'active' | 'onboarded' | 'dismissed';
+
+export interface UserPreferences {
+ framework: string | null;
+ useCase: string | null;
+ deployment: Deployment | null;
+ role: Role | null;
+ experience: Experience | null;
+ onboarding: OnboardingState | null;
+}
+
+export const PREFS_COOKIE = 'directus-docs-prefs';
+export const LEGACY_FRAMEWORK_COOKIE = 'framework';
+export const API_CONSUMER_LS_KEY = 'code-group-api-consumer';
+
+export const defaultPrefs: UserPreferences = {
+ framework: null,
+ useCase: null,
+ deployment: null,
+ role: null,
+ experience: null,
+ onboarding: null,
+};
+
+export const RECENTS_LS_KEY = 'directus-docs-recents';
+export const FAVORITES_LS_KEY = 'directus-docs-favorites';
+export const RECENTS_LIMIT = 20;
+
+export const INSTANCE_URLS_LS_KEY = 'directus-docs-instance-urls';
+export const LEGACY_INSTANCE_URL_COOKIE = 'directus-instance-url';
+export const INSTANCE_URLS_LIMIT = 5;
diff --git a/content/_partials/deployment-public-instance.md b/content/_partials/deployment-public-instance.md
index 4f4729c9b..52761a287 100644
--- a/content/_partials/deployment-public-instance.md
+++ b/content/_partials/deployment-public-instance.md
@@ -1,4 +1,4 @@
-::callout{icon="material-symbols:warning" color="warning"}
+::callout{icon="material-symbols:warning-outline" color="warning"}
**Public instance required**
Real-time deployment status updates rely on webhooks sent from your provider to your Directus instance. Your instance must be publicly accessible for these webhooks to reach it. If you're developing locally, use a tunneling tool like [ngrok](https://ngrok.com/) or [untun](https://github.com/unjs/untun) to expose your local API.
::
diff --git a/content/_partials/engine-studio-box.md b/content/_partials/engine-studio-box.md
index 2a45c0860..7f8066b87 100644
--- a/content/_partials/engine-studio-box.md
+++ b/content/_partials/engine-studio-box.md
@@ -1,5 +1,5 @@
-::shiny-grid{class="lg:grid-cols-2"}
- :::shiny-card
+::u-page-grid{class="lg:grid-cols-2"}
+ :::u-page-card
---
title: APIs and Developer Tools
description: Build with REST, GraphQL, the SDK, realtime, auth, and Flows.
@@ -8,7 +8,7 @@
:product-link{product="connect"} :product-link{product="realtime"} :product-link{product="auth"} :product-link{product="automate"}
:::
- :::shiny-card
+ :::u-page-card
---
title: Data Studio
description: A web app for your whole team to manage content, files, users, and dashboards.
diff --git a/content/cloud/1.getting-started/2.teams.md b/content/cloud/1.getting-started/2.teams.md
index 3d3f9dbad..d581f0af4 100644
--- a/content/cloud/1.getting-started/2.teams.md
+++ b/content/cloud/1.getting-started/2.teams.md
@@ -25,7 +25,7 @@ The team name is a text name assigned to a team, used in the cloud dashboard. Th
-To update team settings, open the team menu in the dashboard header and select the desired team. Click "Settings" to enter the team settings page. Toggle :icon{name="material-symbols:edit" title="Edit Button"} to allow edits. Edit team name and team slug as desired, and save accordingly.
+To update team settings, open the team menu in the dashboard header and select the desired team. Click "Settings" to enter the team settings page. Toggle :icon{name="material-symbols:edit-outline" title="Edit Button"} to allow edits. Edit team name and team slug as desired, and save accordingly.
## View Team Activity
diff --git a/content/cloud/1.getting-started/3.accounts.md b/content/cloud/1.getting-started/3.accounts.md
index a0d0dae72..14e1c2185 100644
--- a/content/cloud/1.getting-started/3.accounts.md
+++ b/content/cloud/1.getting-started/3.accounts.md
@@ -19,7 +19,7 @@ don't have a GitHub account or prefer not to use this login method, email-and-pa

-To update your name or email, click :icon{name="material-symbols:account-circle-full"} in the dashboard header to enter your account page, then toggle :icon{name="material-symbols:edit"} to allow edits.
+To update your name or email, click :icon{name="material-symbols:account-circle-full"} in the dashboard header to enter your account page, then toggle :icon{name="material-symbols:edit-outline"} to allow edits.
Change your name and email as desired, then click the "Save" button.
@@ -41,7 +41,7 @@ To destroy your Directus Cloud account, click :icon{name="material-symbols:accou
Type in your password, then click the "Destroy Account" button.
-::callout{icon="material-symbols:dangerous" class="max-w-2xl" color="error"}
+::callout{icon="material-symbols:dangerous-outline" class="max-w-2xl" color="error"}
Destroying your account completely removes your account and data from Directus Cloud. This action is permanent and
irreversible. Proceed with caution!
diff --git a/content/cloud/4.billing/2.changing-tier.md b/content/cloud/4.billing/2.changing-tier.md
index 1d8222d46..f2c99e86d 100644
--- a/content/cloud/4.billing/2.changing-tier.md
+++ b/content/cloud/4.billing/2.changing-tier.md
@@ -8,6 +8,6 @@ description: Learn how to change the tier of your Directus Cloud project.
You can change between the different [cloud project tiers](/cloud/getting-started/introduction) from the Cloud Dashboard.
-To change your project tier, navigate to "Projects" and click on :icon{name="material-symbols:edit"} next to the project for which you wish to change its tier. Scroll down to find the list of tiers. Once selected, you can click on "Make Changes" to confirm.
+To change your project tier, navigate to "Projects" and click on :icon{name="material-symbols:edit-outline"} next to the project for which you wish to change its tier. Scroll down to find the list of tiers. Once selected, you can click on "Make Changes" to confirm.
In order to change to an enterprise project, please [contact us](https://directus.io/contact).
diff --git a/content/cloud/4.billing/3.cancel-subscription.md b/content/cloud/4.billing/3.cancel-subscription.md
index 6643bce13..0f06b5822 100644
--- a/content/cloud/4.billing/3.cancel-subscription.md
+++ b/content/cloud/4.billing/3.cancel-subscription.md
@@ -8,7 +8,7 @@ description: Learn how to cancel your Directus Cloud project subscription.
Each Directus Cloud project is its own separate subscription.
-To cancel a subscription, navigate to the projects list and click on :icon{name="material-symbols:edit" title="Edit Button"} for said project.
+To cancel a subscription, navigate to the projects list and click on :icon{name="material-symbols:edit-outline" title="Edit Button"} for said project.
Scroll down and click on " :icon{name="material-symbols:local-fire-department-rounded" title="Fire Button"} Cancel Subscription". Enter the name of your project to confirm, and confirm. Your project will now be deleted.
diff --git a/content/configuration/ai.md b/content/configuration/ai.md
index 5f7a2754b..29f22b0ef 100644
--- a/content/configuration/ai.md
+++ b/content/configuration/ai.md
@@ -12,7 +12,7 @@ description: Configuration for AI Assistant and Model Context Protocol (MCP) fea
| -------- | ----------- | ------------- |
| `AI_ENABLED` | Whether AI Assistant features are available. Set to `false` to completely disable AI Assistant across the entire instance, hiding the sidebar for all users and disabling the settings for administrators. | `true` |
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
When `AI_ENABLED` is set to `false`:
- The API routes for the assistant are not mounted
- AI Assistant sidebar is hidden from all users
@@ -27,7 +27,7 @@ This is useful for compliance requirements where AI features must be completely
| -------- | ----------- | ------------- |
| `MCP_ENABLED` | Whether the Model Context Protocol server is available for system administrators to enable in project settings. Set to `false` to completely disable MCP functionality across the entire instance. | `true` |
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
When `MCP_ENABLED` is set to `false`, the MCP server cannot be enabled through **Settings → AI → Model Context Protocol** in the admin interface, providing system administrators with complete control over AI integration features. See the [MCP Server](/guides/ai/mcp/installation) guide for more information.
::
@@ -41,7 +41,7 @@ Send AI Assistant traces to an external observability platform for monitoring us
| `AI_TELEMETRY_PROVIDER` | Telemetry provider to use. Supported values: `langfuse`, `braintrust`. | `langfuse` |
| `AI_TELEMETRY_RECORD_IO` | Include full prompt inputs and response outputs in traces. | `false` |
-::callout{icon="material-symbols:warning" color="warning"}
+::callout{icon="material-symbols:warning-outline" color="warning"}
Enabling `AI_TELEMETRY_RECORD_IO` will send the full content of user messages and AI responses to your telemetry provider. Only enable this if your telemetry provider meets your data privacy requirements.
::
diff --git a/content/frameworks/.navigation.yml b/content/frameworks/.navigation.yml
index 014084143..9fccf8966 100644
--- a/content/frameworks/.navigation.yml
+++ b/content/frameworks/.navigation.yml
@@ -1,2 +1,2 @@
title: Frameworks
-icon: i-ph-brackets-curly
+icon: material-symbols:data-object
diff --git a/content/getting-started/7.create-an-automation.md b/content/getting-started/7.create-an-automation.md
index 8e63f2e81..10a4e8799 100644
--- a/content/getting-started/7.create-an-automation.md
+++ b/content/getting-started/7.create-an-automation.md
@@ -22,13 +22,13 @@ Create a `posts` collection with at least a `title` and `content` field. [Follow

-Navigate to the Flows section in the Settings module. Click on :icon{name="material-symbols:add-circle"} in the page header and name the new flow "Post Created".
+Navigate to the Flows section in the Settings module. Click on :icon{name="material-symbols:add-circle-outline"} in the page header and name the new flow "Post Created".
## Configure a Trigger

-Click on :icon{name="material-symbols:play-arrow"} to open trigger setup. Select "Event Hook" as the trigger type and select "Action (Non-Blocking)". This will allow you to set up this flow to respond to when an event takes place by running an action that doesn't interrupt.
+Click on :icon{name="material-symbols:play-arrow-outline"} to open trigger setup. Select "Event Hook" as the trigger type and select "Action (Non-Blocking)". This will allow you to set up this flow to respond to when an event takes place by running an action that doesn't interrupt.
Select `items.create` as the scope, and then check the "Posts" collection. This combination means that the operation will be triggered when an post is created.
@@ -36,7 +36,7 @@ Select `items.create` as the scope, and then check the "Posts" collection. This

-Click on :icon{name="material-symbols:add-circle"} on the trigger panel.
+Click on :icon{name="material-symbols:add-circle-outline"} on the trigger panel.
Here, you can create an operation. Give it the name "Notify Post Created" and the key "notify_post_created" will be written alongside.
@@ -50,4 +50,4 @@ Now, when you create a post, the user you entered will be notified.
## Next Steps
-Read more about different [triggers](/guides/automate/triggers) available in flows and how data is passed through a flow with [the data chain](/guides/automate/data-chain).
+Read more about different [triggers](/guides/flows/triggers) available in flows and how data is passed through a flow with [the data chain](/guides/flows/data-chain).
diff --git a/content/getting-started/9.resources.md b/content/getting-started/9.resources.md
index 49e62f3b4..26e341b93 100644
--- a/content/getting-started/9.resources.md
+++ b/content/getting-started/9.resources.md
@@ -18,7 +18,7 @@ title: Resources & Links
**Tutorials**: Framework, project, and other implementation guides.
::
-::callout{icon="material-symbols:groups" color="error" to="/community/overview/welcome"}
+::callout{icon="material-symbols:groups-outline" color="error" to="/community/overview/welcome"}
**Community**: Get involved through feature requests, code contribution, education, and more.
::
diff --git a/content/guides/01.data-model/.navigation.yml b/content/guides/01.data-model/.navigation.yml
index 287d10c68..e69de29bb 100644
--- a/content/guides/01.data-model/.navigation.yml
+++ b/content/guides/01.data-model/.navigation.yml
@@ -1 +0,0 @@
-icon: directus-explore
diff --git a/content/guides/01.data-model/4.relationships.md b/content/guides/01.data-model/4.relationships.md
index c9004a3e5..a63385890 100644
--- a/content/guides/01.data-model/4.relationships.md
+++ b/content/guides/01.data-model/4.relationships.md
@@ -75,7 +75,7 @@ Read our tutorial on using a Builder (M2A) to create reusable page components.
When you create a Translations interface in Directus, a translations O2M `Alias` field is created, as well as a `languages` collection and a junction collection between your main collection and `languages`. All translated text is stored in the junction collection.
-::callout{icon="material-symbols:auto-fix-high" color="secondary" to="/guides/content/translations#quick-setup-with-generate-translations"}
+::callout{icon="material-symbols:wand-stars-outline" color="secondary" to="/guides/content/translations#quick-setup-with-generate-translations"}
Use the **Generate Translations** wizard in collection settings to automatically create the full translations infrastructure — languages collection, junction collection, relationships, and fields — in one step.
::
diff --git a/content/guides/02.content/.navigation.yml b/content/guides/02.content/.navigation.yml
index 41f604a62..e69de29bb 100644
--- a/content/guides/02.content/.navigation.yml
+++ b/content/guides/02.content/.navigation.yml
@@ -1 +0,0 @@
-icon: directus-editor
diff --git a/content/guides/02.content/3.layouts.md b/content/guides/02.content/3.layouts.md
index c78c904e8..4662a0f6f 100644
--- a/content/guides/02.content/3.layouts.md
+++ b/content/guides/02.content/3.layouts.md
@@ -57,9 +57,9 @@ layout used in the content module.
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| **Adjust Column Width** | Click and drag the column divider to resize as desired. |
| **Add Field** | Select :icon{name="material-symbols:add-circle-outline-rounded"} in the page subheader and select the desired Field(s). |
-| **Remove Field** | Select :icon{name="material-symbols:arrow-drop-down-circle"} in the column title and click **"Hide Field"**. |
-| **Sort Items by Column** | Select :icon{name="material-symbols:arrow-drop-down-circle"} in the column title and sort ascending or descending. |
-| **Set Text Alignment** | Select :icon{name="material-symbols:arrow-drop-down-circle"} in the column title and set left, right, or center. |
+| **Remove Field** | Select :icon{name="material-symbols:arrow-drop-down-circle-outline"} in the column title and click **"Hide Field"**. |
+| **Sort Items by Column** | Select :icon{name="material-symbols:arrow-drop-down-circle-outline"} in the column title and sort ascending or descending. |
+| **Set Text Alignment** | Select :icon{name="material-symbols:arrow-drop-down-circle-outline"} in the column title and set left, right, or center. |
| **Toggle & Reorder Columns** | Click the column header, then drag-and-drop as desired. |
| **Select All** | Click :icon{name="material-symbols:check-box-outline"} in the selection column header. |
@@ -101,7 +101,7 @@ file library. It includes the following controls.
| **Card Size** | Toggle the card size as it appears in the page area. |
| **Order Field** | Click to select the field you wish to order by from the dropdown menu. |
| **Order Direction** | Toggle ascending and descending order. |
-| **Select All** | Click ":icon{name="material-symbols:check-circle"} Select All" in the selection column header. |
+| **Select All** | Click ":icon{name="material-symbols:check-circle-outline"} Select All" in the selection column header. |
### Page Area
@@ -172,7 +172,7 @@ There is no Subheader on the Map Layout.
| Control | Description |
|---|---|
| **Zoom** | Click :icon{name="material-symbols:add"} and :icon{name="material-symbols:remove"} in the upper left hand corner of the page area to zoom in and out. |
-| **Find my Location** | Click :icon{name="material-symbols:my-location"} to zoom into your current location on the map. |
+| **Find my Location** | Click :icon{name="material-symbols:my-location-outline"} to zoom into your current location on the map. |
| **Reframe** | Click the square in the upper left-hand corner to resize and reframe the map area. |
| **Select Item** | Click a single item to enter its item page. |
| **Select Items** | Click and drag to select multiple items at once, opening the item page. |
@@ -224,8 +224,8 @@ There is no Subheader for the Kanban Layout.
|---|---|
| **Create Task and Assign Status** | Click :icon{name="material-symbols:add"} in a status column and the item page will open. |
| **Sort Panels** | Drag and drop items to reposition or change task status. |
-| **Add Status Panel** | Click :icon{name="material-symbols:add-box"} and add a group name (i.e. new status column). |
-| **Edit or Delete Status Column** | Click :icon{name="material-symbols:more-horiz"} and then click :icon{name="material-symbols:edit"} to edit or :icon{name="material-symbols:delete"} to delete. |
+| **Add Status Panel** | Click :icon{name="material-symbols:add-box-outline"} and add a group name (i.e. new status column). |
+| **Edit or Delete Status Column** | Click :icon{name="material-symbols:more-horiz"} and then click :icon{name="material-symbols:edit-outline"} to edit or :icon{name="material-symbols:delete-outline"} to delete. |
::callout{icon="material-symbols:info-outline"}
**Configuration Requirements**
diff --git a/content/guides/02.content/4.import-export.md b/content/guides/02.content/4.import-export.md
index 61d7f257e..743e2ab46 100644
--- a/content/guides/02.content/4.import-export.md
+++ b/content/guides/02.content/4.import-export.md
@@ -38,7 +38,7 @@ During import operations, errors are collected up to the maximum defined by [`MA
When exporting items, the export items menu provides granular control over exactly which items and
fields are exported, how they are exported, and where they are exported.
-To export items, follow the steps below, navigate to the desired collection and select "Import / Export" from the sidebar. Click on "Export Items" and the export items menu will appear. Select the desired format from CSV, JSON, XML, or YAML and click :icon{name="material-symbols:download-for-offline"} to download the file.
+To export items, follow the steps below, navigate to the desired collection and select "Import / Export" from the sidebar. Click on "Export Items" and the export items menu will appear. Select the desired format from CSV, JSON, XML, or YAML and click :icon{name="material-symbols:download-for-offline-outline"} to download the file.
## Export Items Menu
diff --git a/content/guides/02.content/5.live-preview.md b/content/guides/02.content/5.live-preview.md
index c60b61514..8b4a2f41f 100644
--- a/content/guides/02.content/5.live-preview.md
+++ b/content/guides/02.content/5.live-preview.md
@@ -47,7 +47,7 @@ and "click" save, you should see a live preview of the item on the right-hand si

-Clicking on :icon{name="material-symbols:devices"} also lets you preview your content on desktop and mobile screens, while :icon{name="material-symbols:open-in-new"} allows you to pop the live preview out into a separate window.
+Clicking on :icon{name="material-symbols:devices-outline"} also lets you preview your content on desktop and mobile screens, while :icon{name="material-symbols:open-in-new"} allows you to pop the live preview out into a separate window.
## Using Versions
@@ -81,7 +81,7 @@ Visual editing in live preview requires:
### Using Visual Editing
-Click the :icon{name="material-symbols:edit"} button in the preview toolbar to highlight all editable elements. Click any highlighted element to open its editor in a drawer, modal, or popover.
+Click the :icon{name="material-symbols:edit-outline"} button in the preview toolbar to highlight all editable elements. Click any highlighted element to open its editor in a drawer, modal, or popover.
The display options menu provides additional controls:
diff --git a/content/guides/02.content/6.content-versioning.md b/content/guides/02.content/6.content-versioning.md
index 50330e6db..c6d8ba936 100644
--- a/content/guides/02.content/6.content-versioning.md
+++ b/content/guides/02.content/6.content-versioning.md
@@ -42,7 +42,7 @@ The draft version:
- Transforms from a virtual placeholder to an actual version when you save changes
- Uses "**draft**" as a reserved version key that cannot be used for custom versions
-::callout{icon="material-symbols:warning"}
+::callout{icon="material-symbols:warning-outline"}
**Backward Compatibility**
The reserved global "draft" version was introduced in Directus 11.16.0. If you have an existing version with the key `draft` and a custom name other than "Draft", the display name will be standardized to "Draft" (i.e. transformed) to support the new global versioning feature. The version content and functionality remain unchanged.
::
diff --git a/content/guides/02.content/7.translations.md b/content/guides/02.content/7.translations.md
index 15e355c89..bd6a9d098 100644
--- a/content/guides/02.content/7.translations.md
+++ b/content/guides/02.content/7.translations.md
@@ -24,7 +24,7 @@ This article refers to translating your content in Directus. Many parts of the D
To create a translation string, navigate to Settings > Translation Strings and click on :icon{name="material-symbols:add-circle-rounded"} in the page header and a drawer will open.
-Add a key and click on "Create New" to open another drawer. Select the language and type in the corresponding translation. Click on :icon{name="material-symbols:check-circle"} to confirm and add the translation.
+Add a key and click on "Create New" to open another drawer. Select the language and type in the corresponding translation. Click on :icon{name="material-symbols:check-circle-outline"} to confirm and add the translation.
## Use a Translation String
diff --git a/content/guides/02.content/8.visual-editor/1.frontend-library.md b/content/guides/02.content/8.visual-editor/1.frontend-library.md
index 598fa1ebc..dd2e21ad6 100644
--- a/content/guides/02.content/8.visual-editor/1.frontend-library.md
+++ b/content/guides/02.content/8.visual-editor/1.frontend-library.md
@@ -118,7 +118,7 @@ It is recommended to call the global `remove()` method on client-side navigation
If you have CSP configured, be sure to make your site available for use inside an iFrame in Directus. If you’re unsure where your CSP is defined, check your web server configuration files, your site’s build configuration, or your hosting platform’s security settings.
::
-::callout{icon="material-symbols:info"}
+::callout{icon="material-symbols:info-outline"}
**Usage with Directus Cloud and local development**
Connecting your local development environment to a Directus Cloud Professional instance must be done by exposing your localhost to the web through an SSL secured connection. There are multiple ways to achieve this:
diff --git a/content/guides/02.content/8.visual-editor/2.studio-module.md b/content/guides/02.content/8.visual-editor/2.studio-module.md
index 3436b9d89..c252a19e9 100644
--- a/content/guides/02.content/8.visual-editor/2.studio-module.md
+++ b/content/guides/02.content/8.visual-editor/2.studio-module.md
@@ -8,7 +8,7 @@ The visual editor module enables content editors to render their website within

-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
Visual editing also works in the [**Live Preview**](/guides/content/live-preview#visual-editing-in-live-preview) pane on item detail pages. This gives the same editing experience without switching modules.
::
@@ -69,11 +69,11 @@ Hovering over an editable item will highlight it within the module.

-Click the :icon{name="material-symbols:edit"} icon in the toolbar will highlight all the editable items on the page.
+Click the :icon{name="material-symbols:edit-outline"} icon in the toolbar will highlight all the editable items on the page.

-Clicking the :icon{name="material-symbols:edit"} beside an editable element will open an editor in either a drawer, modal, or popover depending on which `mode` was specified in the elements `data-directus` attribute on the frontend.
+Clicking the :icon{name="material-symbols:edit-outline"} beside an editable element will open an editor in either a drawer, modal, or popover depending on which `mode` was specified in the elements `data-directus` attribute on the frontend.

diff --git a/content/guides/03.auth/.navigation.yml b/content/guides/03.auth/.navigation.yml
index 8b3a757a0..e69de29bb 100644
--- a/content/guides/03.auth/.navigation.yml
+++ b/content/guides/03.auth/.navigation.yml
@@ -1 +0,0 @@
-icon: directus-auth
diff --git a/content/guides/03.auth/6.accountability.md b/content/guides/03.auth/6.accountability.md
index c0aae9079..0aeb7d5a2 100644
--- a/content/guides/03.auth/6.accountability.md
+++ b/content/guides/03.auth/6.accountability.md
@@ -8,7 +8,7 @@ description: Learn to audit user activity and enforce accountability using the a
The activity feed provides a collective timeline of all data-changing actions taken within your project. It is accessed via the notifications tray of the sidebar, and has the same filtering and search features as the [Collection Page](/guides/data-model/collections).
-The activity feed can be accessed via the notifications drawer (:icon{name="heroicons-outline:bell" title="Bell"}) located in the bottom-left corner.
+The activity feed can be accessed via the notifications drawer (:icon{name="material-symbols:notifications-outline" title="Bell"}) located in the bottom-left corner.
::callout{icon="material-symbols:warning-rounded" color="warning"}
**External Changes**
diff --git a/content/guides/04.connect/.navigation.yml b/content/guides/04.connect/.navigation.yml
index f492d43ad..ddb8571ce 100644
--- a/content/guides/04.connect/.navigation.yml
+++ b/content/guides/04.connect/.navigation.yml
@@ -1,2 +1 @@
title: APIs
-icon: directus-connect
diff --git a/content/guides/04.connect/5.errors.md b/content/guides/04.connect/5.errors.md
index 197815f68..99ed925b1 100644
--- a/content/guides/04.connect/5.errors.md
+++ b/content/guides/04.connect/5.errors.md
@@ -1,26 +1,272 @@
---
stableId: bcae2f68-6534-45df-8085-5a2c0d6e20c5
-title: Error Codes
-description: Learn about Directus error codes - understand what each code means, from validation failures to rate limits exceeded. Troubleshoot issues with your API requests and resolve errors efficiently.
+title: Errors
+description: Learn how Directus returns errors over REST, GraphQL, and the SDK. Includes core error codes, HTTP status codes, response shape, and patterns for handling errors in your application.
---
-Below are the global error codes used within Directus, and what they mean.
-
-| Error Code | Status | Description |
-| ------------------------ | ------ | ---------------------------------------------------------------- |
-| `FAILED_VALIDATION` | 400 | Validation for this particular item failed. |
-| `FORBIDDEN` | 403 | You are not allowed to do the current action. |
-| `INVALID_TOKEN` | 403 | Provided token is invalid. |
-| `TOKEN_EXPIRED` | 401 | Provided token is valid but has expired. |
-| `INVALID_CREDENTIALS` | 401 | Username / password or access token is wrong. |
-| `INVALID_IP` | 401 | Your IP address isn't allow-listed to be used with this user. |
-| `INVALID_OTP` | 401 | Incorrect OTP was provided. |
-| `INVALID_PAYLOAD` | 400 | Provided payload is invalid. |
-| `INVALID_QUERY` | 400 | The requested query parameters can not be used. |
-| `UNSUPPORTED_MEDIA_TYPE` | 415 | Provided payload format or `Content-Type` header is unsupported. |
-| `REQUESTS_EXCEEDED` | 429 | You have exceeded the rate limit. |
-| `ROUTE_NOT_FOUND` | 404 | Endpoint does not exist. |
-| `SERVICE_UNAVAILABLE` | 503 | Could not use external service. |
-| `UNPROCESSABLE_CONTENT` | 422 | You tried doing something illegal. |
-
-To prevent revealing which items exist, all actions for non-existing items will return a `FORBIDDEN` error.
+Directus uses conventional HTTP response codes to indicate the success or failure of an API request:
+
+- Codes in the `2xx` range indicate success.
+- Codes in the `4xx` range indicate an error caused by the request (a missing parameter, a permission issue, a validation failure, etc.).
+- Codes in the `5xx` range indicate an error on the server.
+
+All errors are returned in a consistent JSON shape so you can handle them programmatically using the `code` value in `extensions`.
+
+## Error Response Shape
+
+Every error response from the REST API follows the same structure:
+
+::code-group
+```json [JSON]
+{
+ "errors": [
+ {
+ "message": "You don't have permission to access this.",
+ "extensions": {
+ "code": "FORBIDDEN"
+ }
+ }
+ ]
+}
+```
+
+```ts [TypeScript]
+interface DirectusErrorResponse {
+ errors: DirectusError[];
+}
+
+interface DirectusError {
+ message: string;
+ extensions: {
+ code: string;
+ [key: string]: unknown;
+ };
+}
+```
+::
+
+A single response can contain multiple errors. Some errors include additional fields in `extensions` with context about what went wrong (the offending `collection`, `field`, or `value`, for example). In development mode, a `stack` trace is included in `extensions` to help with debugging.
+
+GraphQL responses follow the [GraphQL spec](https://spec.graphql.org/October2021/#sec-Errors) and place the `code` under `extensions.code` on each error in the `errors` array.
+
+## HTTP Status Codes
+
+| Status | Name | Description |
+| ------------------- | --------------------- | -------------------------------------------------------------------------------------------- |
+| `200` | OK | The request succeeded. |
+| `204` | No Content | The request succeeded and there is no response body. |
+| `400` | Bad Request | The request was invalid - usually a malformed payload, query, or validation failure. |
+| `401` | Unauthorized | Authentication failed or no valid credentials were provided. |
+| `403` | Forbidden | The authenticated user doesn't have permission to perform this action. |
+| `404` | Not Found | The requested route or resource doesn't exist. |
+| `405` | Method Not Allowed | The HTTP method isn't allowed on this endpoint. The `Allow` header lists supported methods. |
+| `408` | Request Timeout | The operation took too long to complete. |
+| `413` | Content Too Large | The uploaded payload exceeds the size limit. |
+| `415` | Unsupported Media Type | The `Content-Type` of the request body isn't supported. |
+| `416` | Range Not Satisfiable | The requested byte range can't be served for this file. |
+| `422` | Unprocessable Content | The request was well-formed but couldn't be processed. |
+| `429` | Too Many Requests | The rate limit has been exceeded. Back off and retry later. |
+| `500` | Internal Server Error | An unexpected error occurred. Non-admin users see a generic message. |
+| `503` | Service Unavailable | A required dependency or external service is unavailable. |
+
+::callout{icon="material-symbols:shield-outline"}
+To prevent revealing which items exist, all actions for non-existing items return a `FORBIDDEN` error rather than `404`.
+::
+
+## Error Codes
+
+The `code` value in `extensions` lets you handle errors programmatically without parsing the human-readable `message`. Built-in Directus error codes include:
+
+| Error Code | Status | Description |
+| ----------------------------- | ------ | --------------------------------------------------------------------------------- |
+| `CONTAINS_NULL_VALUES` | 400 | A field can't be set to non-nullable because existing rows contain null values. |
+| `CONTENT_TOO_LARGE` | 413 | Uploaded content exceeds the configured size limit. |
+| `EMAIL_LIMIT_EXCEEDED` | 429 | The email sending limit has been hit. |
+| `FAILED_VALIDATION` | 400 | A field value failed validation. |
+| `FORBIDDEN` | 403 | The user doesn't have permission to perform this action. |
+| `GRAPHQL_EXECUTION` | 400 | A GraphQL operation failed during execution setup. |
+| `GRAPHQL_VALIDATION` | 400 | A GraphQL operation failed validation. |
+| `ILLEGAL_ASSET_TRANSFORMATION`| 400 | The requested asset transformation parameters are not allowed. |
+| `INTERNAL_SERVER_ERROR` | 500 | An unexpected error occurred on the server. |
+| `INVALID_CREDENTIALS` | 401 | The provided email, password, or access token is wrong. |
+| `INVALID_FOREIGN_KEY` | 400 | A foreign key value doesn't reference an existing record. |
+| `INVALID_INVITE` | 400 | The invite link is no longer valid. |
+| `INVALID_IP` | 401 | The IP address isn't allow-listed for this user. |
+| `INVALID_METADATA` | 400 | Upload metadata is malformed. |
+| `INVALID_OTP` | 401 | The provided one-time password is incorrect. |
+| `INVALID_PAYLOAD` | 400 | The request body is invalid. |
+| `INVALID_PATH_PARAMETER` | 400 | A path parameter (like an ID) is malformed. |
+| `INVALID_PROVIDER` | 403 | The authentication provider is invalid or not enabled. |
+| `INVALID_PROVIDER_CONFIG` | 503 | The authentication provider is misconfigured. |
+| `INVALID_QUERY` | 400 | The query parameters can't be used as provided. |
+| `INVALID_TOKEN` | 403 | The access token is malformed or invalid. |
+| `LIMIT_EXCEEDED` | 403 | A configured limit (relations, depth, etc.) was exceeded. |
+| `METHOD_NOT_ALLOWED` | 405 | The HTTP method isn't allowed on this endpoint. |
+| `NOT_NULL_VIOLATION` | 400 | A required field was submitted as null. |
+| `OUT_OF_DATE` | 503 | The Directus instance is out of date for this operation. |
+| `OUT_OF_TIME` | 408 | The operation timed out. |
+| `RANGE_NOT_SATISFIABLE` | 416 | The byte range requested for a file can't be served. |
+| `RECORD_NOT_UNIQUE` | 400 | A unique constraint was violated. |
+| `REQUESTS_EXCEEDED` | 429 | The rate limit has been exceeded. |
+| `ROUTE_NOT_FOUND` | 404 | The requested endpoint doesn't exist. |
+| `SERVICE_UNAVAILABLE` | 503 | An external service Directus depends on is unavailable. |
+| `TOKEN_EXPIRED` | 401 | The access token is valid but has expired - refresh it. |
+| `UNEXPECTED_RESPONSE` | 503 | An external service returned an unexpected response. |
+| `UNPROCESSABLE_CONTENT` | 422 | The request was understood but can't be processed. |
+| `UNSUPPORTED_MEDIA_TYPE` | 415 | The `Content-Type` header or payload format isn't supported. |
+| `USER_SUSPENDED` | 401 | The user account is suspended. |
+| `VALUE_OUT_OF_RANGE` | 400 | A numeric value is outside the column's allowed range. |
+| `VALUE_TOO_LONG` | 400 | A value exceeds the column's maximum length. |
+
+::callout{icon="material-symbols:info-outline"}
+Extensions, flows, imports, and upload handlers can return additional error codes. Handle unknown codes with a generic fallback.
+::
+
+## Handling Errors
+
+### REST API
+
+Check the response status, then read `errors[].extensions.code` to branch on specific failure modes:
+
+```js
+const response = await fetch('https://example.directus.app/items/articles', {
+ headers: { Authorization: `Bearer ${token}` },
+});
+
+const body = await response.json();
+
+if (!response.ok) {
+ const error = body.errors?.[0];
+ const code = error?.extensions?.code;
+
+ switch (code) {
+ case 'TOKEN_EXPIRED':
+ // Refresh the token and retry
+ break;
+ case 'FORBIDDEN':
+ // Show a permissions message to the user
+ break;
+ case 'REQUESTS_EXCEEDED':
+ // Back off and retry later
+ break;
+ default:
+ console.error(error?.message);
+ }
+}
+```
+
+### SDK
+
+The SDK throws the parsed error response when a request fails. Wrap calls in `try/catch` and inspect `errors[].extensions.code`:
+
+```ts
+import { createDirectus, rest, readItems } from '@directus/sdk';
+
+const directus = createDirectus('https://example.directus.app').with(rest());
+
+try {
+ const articles = await directus.request(readItems('articles'));
+} catch (err) {
+ const error = err.errors?.[0];
+ const code = error?.extensions?.code;
+
+ if (code === 'TOKEN_EXPIRED') {
+ // Refresh the token and retry
+ } else if (code === 'FORBIDDEN') {
+ // Handle permission denial
+ } else {
+ console.error(error?.message ?? err);
+ }
+}
+```
+
+The SDK error includes the raw `response`, so you can read `err.response.status` when you use the default fetch client. Prefer `code` for programmatic handling because it is stable across transports.
+
+### GraphQL
+
+GraphQL resolver errors can return `200 OK` with an `errors` array in the response body. Request-level failures, such as invalid GraphQL syntax or validation errors, can return a non-2xx HTTP status. Check both the response status and the `errors` array:
+
+```js
+const response = await fetch('https://example.directus.app/graphql', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify({ query: '{ articles { id title } }' }),
+});
+
+const body = await response.json();
+const { data, errors } = body;
+
+if (!response.ok || errors) {
+ for (const error of errors ?? []) {
+ const code = error.extensions?.code;
+
+ if (code === 'FORBIDDEN') {
+ // Handle permission denial
+ }
+ }
+}
+```
+
+## Common Patterns
+
+### Refreshing an Expired Token
+
+`TOKEN_EXPIRED` indicates the request was authenticated but the access token has expired. Use the refresh token to get a new pair and retry the request:
+
+```ts
+import { createDirectus, rest, authentication } from '@directus/sdk';
+
+const directus = createDirectus('https://example.directus.app')
+ .with(authentication())
+ .with(rest());
+
+try {
+ await directus.request(/* ... */);
+} catch (err) {
+ if (err.errors?.[0]?.extensions?.code === 'TOKEN_EXPIRED') {
+ await directus.refresh();
+ await directus.request(/* ... */);
+ }
+}
+```
+
+### Backing Off on Rate Limits
+
+When you receive `REQUESTS_EXCEEDED`, retry with exponential backoff rather than retrying immediately:
+
+```ts
+async function withRetry(fn, attempts = 3) {
+ for (let i = 0; i < attempts; i++) {
+ try {
+ return await fn();
+ } catch (err) {
+ const code = err.errors?.[0]?.extensions?.code;
+ if (code !== 'REQUESTS_EXCEEDED' || i === attempts - 1) throw err;
+ await new Promise((r) => setTimeout(r, 2 ** i * 1000));
+ }
+ }
+}
+```
+
+### Surfacing Validation Errors
+
+`FAILED_VALIDATION` errors include the offending `field`, `path`, and validation `type` in `extensions`. Database constraint errors like `RECORD_NOT_UNIQUE`, `NOT_NULL_VIOLATION`, `INVALID_FOREIGN_KEY`, `VALUE_OUT_OF_RANGE`, and `VALUE_TOO_LONG` can include `collection`, `field`, or `value`. `INVALID_PAYLOAD` includes a `reason`. Use these fields to display actionable errors in your UI:
+
+```ts
+catch (err) {
+ for (const error of err.errors ?? []) {
+ const { code, field, collection } = error.extensions ?? {};
+ if (field) {
+ showFieldError(field, error.message);
+ }
+ }
+}
+```
+
+## Next Steps
+
+- Review [authentication](/guides/auth/tokens-cookies) for token and session handling.
+- Read the [SDK guide](/guides/connect/sdk) for the full client API.
diff --git a/content/guides/05.files/.navigation.yml b/content/guides/05.files/.navigation.yml
index 79a86a3c3..e69de29bb 100644
--- a/content/guides/05.files/.navigation.yml
+++ b/content/guides/05.files/.navigation.yml
@@ -1 +0,0 @@
-icon: directus-files
diff --git a/content/guides/05.files/1.upload.md b/content/guides/05.files/1.upload.md
index 311d40e18..1cef5a778 100644
--- a/content/guides/05.files/1.upload.md
+++ b/content/guides/05.files/1.upload.md
@@ -60,6 +60,6 @@ const result = await directus.request(uploadFiles(formData));
The file contents has to be provided in a property called `file`. All other properties of
the file object can be provided as well, except `filename_disk` and `filename_download`.
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
If `storage` is not specified, it defaults to the first location listed in [`STORAGE_LOCATIONS`](/configuration/files#storage-locations).
::
diff --git a/content/guides/05.files/3.manage.md b/content/guides/05.files/3.manage.md
index c48c58e34..e90ebb623 100644
--- a/content/guides/05.files/3.manage.md
+++ b/content/guides/05.files/3.manage.md
@@ -15,7 +15,7 @@ item page.
Notice the following buttons in the header:
-- :icon{name="material-symbols:check-circle"} – Saves any edits made to the file.
+- :icon{name="material-symbols:check-circle-outline"} – Saves any edits made to the file.
- :icon{name="material-symbols:tune"} – Opens the image editor.
- :icon{name="material-symbols:download"} – Downloads the file to your current device.
- :icon{name="material-symbols:drive-file-move-outline"} – Moves selected file(s) to another folder.
diff --git a/content/guides/06.automate/.navigation.yml b/content/guides/06.automate/.navigation.yml
deleted file mode 100644
index f7a94f1fe..000000000
--- a/content/guides/06.automate/.navigation.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-title: Flows
-icon: directus-automate
diff --git a/content/guides/06.flows/.navigation.yml b/content/guides/06.flows/.navigation.yml
new file mode 100644
index 000000000..ba66707ea
--- /dev/null
+++ b/content/guides/06.flows/.navigation.yml
@@ -0,0 +1 @@
+title: Flows
diff --git a/content/guides/06.automate/2.data-chain.md b/content/guides/06.flows/2.data-chain.md
similarity index 93%
rename from content/guides/06.automate/2.data-chain.md
rename to content/guides/06.flows/2.data-chain.md
index 491fee304..65ad58be5 100644
--- a/content/guides/06.automate/2.data-chain.md
+++ b/content/guides/06.flows/2.data-chain.md
@@ -42,7 +42,7 @@ Real-life examples of data chains and their data structures will vary, based on
## Data Chain Variables
-While [configuring your operations](/guides/automate/operations), you can use keys from the data chain as variables to
+While [configuring your operations](/guides/flows/operations), you can use keys from the data chain as variables to
access data. The syntax to do so is as follows:
```json
@@ -73,4 +73,4 @@ You can mix your own hardcoded JSON alongside variables. You can also use dot no
```
Certain operations use dropdowns, toggles, checkboxes, and other input options. However, you can bypass this entirely to
-input raw values directly with [Toggle to Raw Editor](/guides/automate/operations).
+input raw values directly with [Toggle to Raw Editor](/guides/flows/operations).
diff --git a/content/guides/06.automate/3.triggers.md b/content/guides/06.flows/3.triggers.md
similarity index 96%
rename from content/guides/06.automate/3.triggers.md
rename to content/guides/06.flows/3.triggers.md
index 8157d5283..6b77addbc 100644
--- a/content/guides/06.automate/3.triggers.md
+++ b/content/guides/06.flows/3.triggers.md
@@ -4,7 +4,7 @@ title: Triggers
description: Triggers define the action or events that start flows.
---
-A trigger defines the event that starts a [flow](/guides/automate/flows). This could be from an internal or external activity, such as
+A trigger defines the event that starts a [flow](/guides/flows). This could be from an internal or external activity, such as
changes to data, logins, errors, incoming webhooks, schedules, operations from other flows, or even the click of a
button within the Data Studio.
@@ -55,7 +55,7 @@ cancel the transaction.
::callout{icon="material-symbols:info-outline"}
**Cancelling Transactions**
To completely cancel a transaction, you'll need to throw an error within a
-[script operation](/guides/automate/operations) or end the Flow on a [failure path](/guides/automate/flows).
+[script operation](/guides/flows/operations) or end the Flow on a [failure path](/guides/flows).
::
### Actions
@@ -167,4 +167,4 @@ Each input field can have its own data type, interface, and display options. Som
to immediately alter the user input (such as trimming whitespace and slugifying text).
Data provided by users when triggering a manual Flow with a confirmation dialog will be accessible in `$trigger.body` in
-the [data chain](/guides/automate/data-chain).
+the [data chain](/guides/flows/data-chain).
diff --git a/content/guides/06.automate/4.operations.md b/content/guides/06.flows/4.operations.md
similarity index 97%
rename from content/guides/06.automate/4.operations.md
rename to content/guides/06.flows/4.operations.md
index a4228d0e8..2cb4c9456 100644
--- a/content/guides/06.automate/4.operations.md
+++ b/content/guides/06.flows/4.operations.md
@@ -32,7 +32,7 @@ fields that may be absent, use the inverse operator (such as `_nnull`) and place
::
::callout{icon="material-symbols:warning-rounded" color="warning"}
-When using an [Event Hook](/guides/automate/triggers) configured to be "Filter (Blocking)", if your flow ends
+When using an [Event Hook](/guides/flows/triggers) configured to be "Filter (Blocking)", if your flow ends
with a condition that executes with a `reject` path, it will cancel your database transaction.
::
@@ -97,7 +97,7 @@ Make sure your `return` value is valid JSON.
::callout{icon="material-symbols:info-outline"}
**Throwing Errors**
If you throw an error in a Run Script operation, it will immediately break your flow chain and stop execution of
-subsequent flows. If you used a ["Blocking" Event hook](/guides/automate/triggers), throwing an error will cancel
+subsequent flows. If you used a ["Blocking" Event hook](/guides/flows/triggers), throwing an error will cancel
the original event transaction to the database.
::
@@ -344,7 +344,7 @@ This operation throws a custom error to halt flow execution with a specific erro
This operation does not generate data. It immediately throws an error that halts flow execution.
::callout{icon="material-symbols:warning-rounded" color="warning"}
-When using an [Event Hook](/guides/automate/triggers) configured to be "Filter (Blocking)", if your flow ends with a
+When using an [Event Hook](/guides/flows/triggers) configured to be "Filter (Blocking)", if your flow ends with a
Throw Error operation, it will cancel your database transaction.
::
@@ -369,7 +369,7 @@ onto its `operationKey`.

This operation starts another flow and (optionally) passes data into it. It should be used in combination with the
-[Another Flow](/guides/automate/triggers) trigger.
+[Another Flow](/guides/flows/triggers) trigger.
### Options
diff --git a/content/guides/06.automate/1.flows.md b/content/guides/06.flows/index.md
similarity index 100%
rename from content/guides/06.automate/1.flows.md
rename to content/guides/06.flows/index.md
diff --git a/content/guides/07.realtime/.navigation.yml b/content/guides/07.realtime/.navigation.yml
index 10cc17f45..e69de29bb 100644
--- a/content/guides/07.realtime/.navigation.yml
+++ b/content/guides/07.realtime/.navigation.yml
@@ -1 +0,0 @@
-icon: directus-realtime
diff --git a/content/guides/08.insights/.navigation.yml b/content/guides/08.insights/.navigation.yml
index edf4e2297..e69de29bb 100644
--- a/content/guides/08.insights/.navigation.yml
+++ b/content/guides/08.insights/.navigation.yml
@@ -1 +0,0 @@
-icon: directus-insights
diff --git a/content/guides/09.extensions/.navigation.yml b/content/guides/09.extensions/.navigation.yml
index ed789fd8b..e69de29bb 100644
--- a/content/guides/09.extensions/.navigation.yml
+++ b/content/guides/09.extensions/.navigation.yml
@@ -1 +0,0 @@
-icon: directus-marketplace
diff --git a/content/guides/09.extensions/2.api-extensions/0.index.md b/content/guides/09.extensions/2.api-extensions/0.index.md
index bb7e7d68a..012bacc3a 100644
--- a/content/guides/09.extensions/2.api-extensions/0.index.md
+++ b/content/guides/09.extensions/2.api-extensions/0.index.md
@@ -10,8 +10,8 @@ API Extensions extend the functionality of the API.
## Extension Types
-::shiny-grid
- :::shiny-card
+::u-page-grid
+ :::u-page-card
---
title: Hooks
to: '/guides/extensions/api-extensions/hooks'
@@ -20,7 +20,7 @@ API Extensions extend the functionality of the API.
---
:::
- :::shiny-card
+ :::u-page-card
---
title: Endpoints
to: '/guides/extensions/api-extensions/endpoints'
@@ -29,7 +29,7 @@ API Extensions extend the functionality of the API.
---
:::
- :::shiny-card
+ :::u-page-card
---
title: Operations
to: '/guides/extensions/api-extensions/operations'
@@ -41,8 +41,8 @@ API Extensions extend the functionality of the API.
## Resources
-::shiny-grid
- :::shiny-card
+::u-page-grid
+ :::u-page-card
---
title: Services
to: '/guides/extensions/api-extensions/services'
@@ -51,7 +51,7 @@ API Extensions extend the functionality of the API.
---
:::
- :::shiny-card
+ :::u-page-card
---
title: Sandbox
to: '/guides/extensions/api-extensions/sandbox'
diff --git a/content/guides/09.extensions/2.api-extensions/3.operations.md b/content/guides/09.extensions/2.api-extensions/3.operations.md
index 4bc8a0e4f..393d5cbf5 100644
--- a/content/guides/09.extensions/2.api-extensions/3.operations.md
+++ b/content/guides/09.extensions/2.api-extensions/3.operations.md
@@ -81,7 +81,7 @@ The `id` in both the app and the api entrypoint must be the same.
### Handler Function
-The handler function is called when the operation is executed. It must return a value to trigger the `resolve` anchor or throw with a value to trigger the `reject` anchor. The returned value will be added to the [data chain](/guides/automate/data-chain).
+The handler function is called when the operation is executed. It must return a value to trigger the `resolve` anchor or throw with a value to trigger the `reject` anchor. The returned value will be added to the [data chain](/guides/flows/data-chain).
The handler function receives `options` and `context`. `options` contains the operation's option values, while `context` has the following properties:
diff --git a/content/guides/09.extensions/3.app-extensions/0.index.md b/content/guides/09.extensions/3.app-extensions/0.index.md
index c9ecf32e3..1fd25cad4 100644
--- a/content/guides/09.extensions/3.app-extensions/0.index.md
+++ b/content/guides/09.extensions/3.app-extensions/0.index.md
@@ -10,8 +10,8 @@ App Extensions extend the functionality of the Data Studio.
## Extension Types
-::shiny-grid
- :::shiny-card
+::u-page-grid
+ :::u-page-card
---
title: Interfaces
to: '/guides/extensions/app-extensions/interfaces'
@@ -19,7 +19,7 @@ App Extensions extend the functionality of the Data Studio.
---
:::
- :::shiny-card
+ :::u-page-card
---
title: Displays
to: '/guides/extensions/app-extensions/displays'
@@ -27,7 +27,7 @@ App Extensions extend the functionality of the Data Studio.
---
:::
- :::shiny-card
+ :::u-page-card
---
title: Layouts
to: '/guides/extensions/app-extensions/layouts'
@@ -35,7 +35,7 @@ App Extensions extend the functionality of the Data Studio.
---
:::
- :::shiny-card
+ :::u-page-card
---
title: Panels
to: '/guides/extensions/app-extensions/panels'
@@ -43,7 +43,7 @@ App Extensions extend the functionality of the Data Studio.
---
:::
- :::shiny-card
+ :::u-page-card
---
title: Modules
to: '/guides/extensions/app-extensions/modules'
@@ -51,7 +51,7 @@ App Extensions extend the functionality of the Data Studio.
---
:::
- :::shiny-card
+ :::u-page-card
---
title: Themes
to: '/guides/extensions/app-extensions/themes'
@@ -63,8 +63,8 @@ App Extensions extend the functionality of the Data Studio.
## Resources
-::shiny-grid
- :::shiny-card
+::u-page-grid
+ :::u-page-card
---
title: UI Library
to: '/guides/extensions/app-extensions/ui-library'
@@ -72,7 +72,7 @@ App Extensions extend the functionality of the Data Studio.
---
:::
- :::shiny-card
+ :::u-page-card
---
title: Composables
to: '/guides/extensions/app-extensions/composables'
diff --git a/content/guides/10.deployments/.navigation.yml b/content/guides/10.deployments/.navigation.yml
index 31de8deab..e1d492502 100644
--- a/content/guides/10.deployments/.navigation.yml
+++ b/content/guides/10.deployments/.navigation.yml
@@ -1,2 +1 @@
title: Deployments
-icon: directus-deployments
diff --git a/content/guides/11.ai/.navigation.yml b/content/guides/11.ai/.navigation.yml
index 7663840fc..cf01788f5 100644
--- a/content/guides/11.ai/.navigation.yml
+++ b/content/guides/11.ai/.navigation.yml
@@ -1,2 +1 @@
title: AI
-icon: directus-ai
diff --git a/content/guides/11.ai/0.index.md b/content/guides/11.ai/0.index.md
index 8e70f3a81..319df3338 100644
--- a/content/guides/11.ai/0.index.md
+++ b/content/guides/11.ai/0.index.md
@@ -14,7 +14,7 @@ The built-in AI Assistant provides a conversational assistant directly within th
- No additional client setup required
- Best for content editors, quick tasks, and users who live in Directus
-::card{icon="material-symbols:chat" title="AI Assistant" to="/guides/ai/assistant"}
+::card{icon="material-symbols:chat-outline" title="AI Assistant" to="/guides/ai/assistant"}
Learn how to configure and use the built-in AI assistant.
::
@@ -26,6 +26,6 @@ The Model Context Protocol (MCP) server lets you connect external AI tools to Di
- Bring Directus capabilities to where you already work
- Best for developers, power users, and complex workflows
-::card{icon="material-symbols:hub" title="MCP Server" to="/guides/ai/mcp"}
+::card{icon="material-symbols:hub-outline" title="MCP Server" to="/guides/ai/mcp"}
Connect your preferred AI tools to Directus.
::
diff --git a/content/guides/11.ai/1.assistant/0.index.md b/content/guides/11.ai/1.assistant/0.index.md
index 9937dd992..a42af9df1 100644
--- a/content/guides/11.ai/1.assistant/0.index.md
+++ b/content/guides/11.ai/1.assistant/0.index.md
@@ -10,7 +10,7 @@ Directus AI Assistant is an embedded conversational assistant that helps you int
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
**AI Assistant requires an API key from a supported provider.** Administrators [configure API keys](/guides/ai/assistant/setup) for OpenAI, Anthropic, Google, or an OpenAI-compatible endpoint in Settings → AI.
::
@@ -63,7 +63,7 @@ Directus AI Assistant is an embedded conversational assistant that helps you int
| Gemini 3.1 Pro Preview | `gemini-3.1-pro-preview` | 1M tokens |
| Gemini 2.5 Flash Lite | `gemini-2.5-flash-lite` | 1M tokens |
-### :icon{name="material-symbols:cloud" class="align-middle mr-1 size-5"} OpenAI-Compatible
+### :icon{name="material-symbols:cloud-outline" class="align-middle mr-1 size-5"} OpenAI-Compatible
Use any OpenAI-compatible API endpoint including self-hosted models (Ollama, LM Studio), Azure OpenAI, DeepSeek, Mistral, and more. Configure custom models in [Settings → AI](/guides/ai/assistant/setup#openai-compatible-providers).
@@ -76,7 +76,7 @@ Have feedback or feature requests? [Submit on our roadmap](https://roadmap.direc
- **File Attachments**: Upload files or select from your library. Supports images, PDFs, text, audio, and video up to 50MB.
- **Streaming Responses**: Responses stream in real-time. Stop or retry at any time.
-::callout{icon="material-symbols:warning" color="warning"}
+::callout{icon="material-symbols:warning-outline" color="warning"}
**Conversations are stored in your browser only.** They are not saved to the server or shared between devices. Closing your browser or clearing localStorage will delete your conversation history.
::
@@ -86,11 +86,11 @@ Have feedback or feature requests? [Submit on our roadmap](https://roadmap.direc
::card-group
-:::card{title="Admin Setup" icon="material-symbols:settings" to="/guides/ai/assistant/setup"}
+:::card{title="Admin Setup" icon="material-symbols:settings-outline" to="/guides/ai/assistant/setup"}
Configure API keys and customize the AI assistant behavior.
:::
-:::card{title="User Guide" icon="material-symbols:chat" to="/guides/ai/assistant/usage"}
+:::card{title="User Guide" icon="material-symbols:chat-outline" to="/guides/ai/assistant/usage"}
Learn how to use AI Assistant effectively in your daily workflow.
:::
@@ -98,7 +98,7 @@ Learn how to use AI Assistant effectively in your daily workflow.
Reference of all tools the AI can use to interact with Directus.
:::
-:::card{title="Tips & Best Practices" icon="material-symbols:lightbulb" to="/guides/ai/assistant/tips"}
+:::card{title="Tips & Best Practices" icon="material-symbols:lightbulb-outline" to="/guides/ai/assistant/tips"}
Get the most out of AI Assistant with practical tips and example prompts.
:::
diff --git a/content/guides/11.ai/1.assistant/1.setup.md b/content/guides/11.ai/1.assistant/1.setup.md
index 69bba2376..a7f6f0be3 100644
--- a/content/guides/11.ai/1.assistant/1.setup.md
+++ b/content/guides/11.ai/1.assistant/1.setup.md
@@ -16,7 +16,7 @@ AI Assistant requires an API key from a supported AI provider. This page covers
Alternatively, you can use an [OpenAI-compatible provider](#openai-compatible-providers) like Ollama or LM Studio for self-hosted models.
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
Note that all users of AI Assistant will share a single API key from your configured provider. Usage limits and costs will be shared across all users. See your provider's dashboard for monitoring usage details and costs.
::
@@ -36,7 +36,7 @@ OpenAI provides GPT-5 models (Nano, Mini, Standard).
4. Give it a name like "Directus AI Assistant"
5. Copy the key immediately - you won't be able to see it again
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
OpenAI requires a payment method and has usage-based pricing. Set spending limits in **Settings → Limits** to control costs.
::
@@ -52,7 +52,7 @@ Anthropic provides Claude models (Haiku 4.5, Sonnet 4.5, Opus 4.5).
4. Give it a name like "Directus AI Assistant"
5. Copy the key immediately
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
Anthropic requires a payment method and has usage-based pricing. Monitor usage in the Console dashboard.
::
@@ -68,7 +68,7 @@ Google provides Gemini models (2.5 Flash, 2.5 Pro, 3 Flash Preview, 3 Pro Previe
4. Select or create a Google Cloud project
5. Copy the generated API key
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
Google AI Studio offers a free tier with rate limits. For production use, consider enabling billing in Google Cloud Console to increase quotas.
::
@@ -120,11 +120,11 @@ Click **Save** to apply your changes. AI Assistant is now available to all users
In addition to the built-in providers, Directus supports any OpenAI-compatible API endpoint. This allows you to use self-hosted models, alternative providers, or private deployments.
-::callout{icon="material-symbols:warning" color="warning"}
+::callout{icon="material-symbols:warning-outline" color="warning"}
**For best results, use built-in cloud providers.** Local models vary significantly in their tool-calling capabilities and may produce inconsistent results. If using OpenAI-compatible providers, we recommend cloud-hosted frontier models over locally-run models on personal hardware.
::
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
**File attachments are not supported with OpenAI-compatible providers.** [File uploads](/guides/ai/assistant/usage#files) require a built-in provider (OpenAI, Anthropic, or Google). The file attachment buttons are hidden when an OpenAI-compatible model is selected.
::
@@ -156,7 +156,7 @@ For each model, you can specify:
| **Supports Reasoning** | Whether the model has chain-of-thought capabilities |
| **Provider Options** | JSON object for model-specific parameters |
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
The **Provider Options** field allows you to pass provider-specific parameters to the AI SDK. This is useful for enabling features like extended thinking or custom sampling parameters. See the [Vercel AI SDK documentation](https://sdk.vercel.ai/providers/openai-compatible) for details.
::
@@ -177,7 +177,7 @@ The **Provider Options** field allows you to pass provider-specific parameters t
- **API Key**: `ollama` (required by the OpenAI SDK but ignored by Ollama)
- **Models**: Add your pulled models (e.g., `gpt-oss:20b`, `gpt-oss:120b`, `qwen3:8b`)
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
You can copy an existing model to an OpenAI-compatible name if needed: `ollama cp gpt-oss:20b gpt-4`
::
@@ -200,7 +200,7 @@ See [Ollama OpenAI compatibility docs](https://docs.ollama.com/api/openai-compat
- **API Key**: Your Azure OpenAI API key
- **Models**: Add your deployed model names
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
The v1 API (August 2025+) no longer requires an `api-version` header. If using an older API version, add `api-version` as a custom header (e.g., `2024-10-21`).
::
@@ -305,7 +305,7 @@ Enable reusable prompts in AI Assistant by configuring a prompts collection:
2. Find **AI Prompts Collection**
3. Either generate a new collection or select an existing one
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
This is the same collection used by the [MCP Server](/guides/ai/mcp/prompts). Prompts created here are available in both AI Assistant and external MCP clients. This also requires MCP to be enabled.
::
@@ -313,7 +313,7 @@ For details on creating prompts with variables, see [MCP Prompts](/guides/ai/mcp
## Managing Costs
-::callout{icon="material-symbols:warning" color="warning"}
+::callout{icon="material-symbols:warning-outline" color="warning"}
**AI Assistant uses your own AI provider API keys.** Every message and tool call costs money. Be mindful of usage, especially with larger models. You are responsible for the costs of your usage.
::
@@ -361,7 +361,7 @@ BRAINTRUST_PROJECT_NAME=my-project # optional
Optionally enable `AI_TELEMETRY_RECORD_IO=true` to include full prompt inputs and AI responses in traces.
-::callout{icon="material-symbols:warning" color="warning"}
+::callout{icon="material-symbols:warning-outline" color="warning"}
Enabling `AI_TELEMETRY_RECORD_IO` will send the full content of user messages and AI responses to your telemetry provider. Only enable this if your provider meets your data privacy requirements.
::
@@ -371,7 +371,7 @@ See the [AI Configuration](/configuration/ai#telemetry) reference for all availa
::card-group
-:::card{title="User Guide" icon="material-symbols:chat" to="/guides/ai/assistant/usage"}
+:::card{title="User Guide" icon="material-symbols:chat-outline" to="/guides/ai/assistant/usage"}
Learn how users interact with AI Assistant.
:::
@@ -379,7 +379,7 @@ Learn how users interact with AI Assistant.
See what actions the AI can perform.
:::
-:::card{title="Tips & Best Practices" icon="material-symbols:lightbulb" to="/guides/ai/assistant/tips"}
+:::card{title="Tips & Best Practices" icon="material-symbols:lightbulb-outline" to="/guides/ai/assistant/tips"}
Get the most out of AI Assistant.
:::
diff --git a/content/guides/11.ai/1.assistant/2.usage.md b/content/guides/11.ai/1.assistant/2.usage.md
index cfe2f65a4..b4db4361a 100644
--- a/content/guides/11.ai/1.assistant/2.usage.md
+++ b/content/guides/11.ai/1.assistant/2.usage.md
@@ -49,7 +49,7 @@ Reusable prompt templates stored in your Directus instance. Prompts can include
> "Write a blog post using our brand voice"
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
Prompts require MCP to be enabled and a prompts collection configured. See [Prompts Collection](/guides/ai/assistant/setup#prompts-collection) for setup.
::
@@ -65,7 +65,7 @@ When using the [Visual Editor](/guides/content/visual-editor/studio-module), you
> "Translate this heading to Spanish"
-::callout{icon="material-symbols:lightbulb" color="primary"}
+::callout{icon="material-symbols:lightbulb-outline" color="primary"}
Visual element context persists while navigating within the Visual Editor but clears when leaving the module.
::
@@ -97,11 +97,11 @@ You can also drag and drop files directly onto the conversation area.
Maximum file size is 50MB. Files are uploaded to your AI provider when the message is sent.
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
**File attachments require a built-in provider.** OpenAI-compatible providers do not support file uploads. See [Setup](/guides/ai/assistant/setup#openai-compatible-providers) for details.
::
-::callout{icon="material-symbols:warning" color="warning"}
+::callout{icon="material-symbols:warning-outline" color="warning"}
**Google file uploads expire after approximately 24 hours.** If you start a new conversation, you may need to re-upload files previously sent to Google.
::
@@ -172,7 +172,7 @@ If an error occurs, a retry button appears. Click to regenerate the last respons
## Context Limits
-::callout{icon="material-symbols:lightbulb" color="primary"}
+::callout{icon="material-symbols:lightbulb-outline" color="primary"}
Start a new conversation when switching topics. The AI performs better with focused, single-topic conversations. Long conversations may have older messages dropped automatically.
::
@@ -203,7 +203,7 @@ The AI operates with your existing [Directus permissions](/guides/auth/access-co
See what actions the AI can perform.
:::
-:::card{title="Tips & Best Practices" icon="material-symbols:lightbulb" to="/guides/ai/assistant/tips"}
+:::card{title="Tips & Best Practices" icon="material-symbols:lightbulb-outline" to="/guides/ai/assistant/tips"}
Get more out of AI Assistant with practical tips.
:::
diff --git a/content/guides/11.ai/1.assistant/3.tools.md b/content/guides/11.ai/1.assistant/3.tools.md
index 889ec811b..3cb83307c 100644
--- a/content/guides/11.ai/1.assistant/3.tools.md
+++ b/content/guides/11.ai/1.assistant/3.tools.md
@@ -25,7 +25,7 @@ These tools interact with your Directus instance via API to manage content, file
### Admin Only
-::callout{icon="material-symbols:warning" color="warning"}
+::callout{icon="material-symbols:warning-outline" color="warning"}
Be careful when using these tools as deleting or modifying schema can result in data loss.
::
@@ -41,7 +41,7 @@ Be careful when using these tools as deleting or modifying schema can result in
## Page Context Tools
-::callout{icon="material-symbols:lightbulb" color="primary"}
+::callout{icon="material-symbols:lightbulb-outline" color="primary"}
Page Context tools let the AI work directly with what's on your screen. They are only available when editing data and may not be available on all pages. Approval modes cannot be changed for Page Context tools.
::
| Tool | Description |
@@ -62,12 +62,12 @@ Each tool can be configured with one of three approval modes:
| Mode | Behavior |
|------|----------|
| :icon{name="material-symbols:check" class="text-success"} **Always Allowed** | Execute immediately without asking |
-| :icon{name="material-symbols:approval-delegation" class="text-warning"} **Needs Approval** | Show approval dialog before executing (default) |
-| :icon{name="material-symbols:block" class="text-error"} **Disabled** | Tool is hidden from AI and not loaded into context |
+| :icon{name="material-symbols:approval-delegation-outline" class="text-warning"} **Needs Approval** | Show approval dialog before executing (default) |
+| :icon{name="material-symbols:block-outline" class="text-error"} **Disabled** | Tool is hidden from AI and not loaded into context |
Tool approval settings are stored locally in your browser and are unique to you. They won't sync to your Directus instance or affect other users.
-::callout{icon="material-symbols:lightbulb" color="primary"}
+::callout{icon="material-symbols:lightbulb-outline" color="primary"}
**Disable unused tools to reduce costs.** Disabled tools are not sent to the AI provider, reducing token usage. If you only manage content, disable schema and flow tools.
::
@@ -88,7 +88,7 @@ All tools default to **Ask** mode. Nothing executes without your explicit approv
::card-group
-:::card{title="Tips & Best Practices" icon="material-symbols:lightbulb" to="/guides/ai/assistant/tips"}
+:::card{title="Tips & Best Practices" icon="material-symbols:lightbulb-outline" to="/guides/ai/assistant/tips"}
Get the most out of AI Assistant.
:::
diff --git a/content/guides/11.ai/1.assistant/4.tips.md b/content/guides/11.ai/1.assistant/4.tips.md
index 256ad451b..592eb0694 100644
--- a/content/guides/11.ai/1.assistant/4.tips.md
+++ b/content/guides/11.ai/1.assistant/4.tips.md
@@ -38,8 +38,8 @@ Long conversations can lose context. When switching to a different task, clear t
## Configure Tool Approvals
- Read-only tools like Schema can safely be set to :icon{name="material-symbols:check" class="text-success"} **Always Allowed**
-- Keep write operations on :icon{name="material-symbols:approval-delegation" class="text-warning"} **Needs Approval** until you're confident
-- :icon{name="material-symbols:block" class="text-error"} **Disable** tools you don't need to reduce token usage
+- Keep write operations on :icon{name="material-symbols:approval-delegation-outline" class="text-warning"} **Needs Approval** until you're confident
+- :icon{name="material-symbols:block-outline" class="text-error"} **Disable** tools you don't need to reduce token usage
See [Tool Behavior](/guides/ai/assistant/tools#tool-behavior) for more details.
@@ -126,7 +126,7 @@ Transcribe this audio recording
AI Assistant requires API keys from OpenAI or Anthropic — you cannot use a ChatGPT Plus or Claude Pro subscription. API access is billed per token, so costs scale with usage. Be mindful of this, especially with larger models.
-::callout{icon="material-symbols:paid" color="warning"}
+::callout{icon="material-symbols:paid-outline" color="warning"}
**Disable tools you don't use.** Disabled tools are not loaded into context, reducing token usage and API costs. If you only work with content, disable schema modification tools.
::
diff --git a/content/guides/11.ai/1.assistant/5.security.md b/content/guides/11.ai/1.assistant/5.security.md
index 6af8c13af..6d3436dcc 100644
--- a/content/guides/11.ai/1.assistant/5.security.md
+++ b/content/guides/11.ai/1.assistant/5.security.md
@@ -30,7 +30,7 @@ Your messages, schema information, item data, and tool responses are sent to the
- [Anthropic Privacy Policy](https://www.anthropic.com/privacy)
- [Google Privacy Policy](https://policies.google.com/privacy)
-::callout{icon="material-symbols:warning" color="warning"}
+::callout{icon="material-symbols:warning-outline" color="warning"}
**Be mindful of what you discuss.** Avoid sharing sensitive personal data, credentials, or confidential information in AI conversations. This includes files — do not upload documents containing sensitive data unless you trust your provider's data handling policies.
::
@@ -49,7 +49,7 @@ All tools require approval by default. Configure per-tool settings in the chat h
::card-group
-:::card{title="User Guide" icon="material-symbols:chat" to="/guides/ai/assistant/usage"}
+:::card{title="User Guide" icon="material-symbols:chat-outline" to="/guides/ai/assistant/usage"}
Learn how to use AI Assistant effectively.
:::
@@ -57,7 +57,7 @@ Learn how to use AI Assistant effectively.
See what actions the AI can perform.
:::
-:::card{title="Tips & Best Practices" icon="material-symbols:lightbulb" to="/guides/ai/assistant/tips"}
+:::card{title="Tips & Best Practices" icon="material-symbols:lightbulb-outline" to="/guides/ai/assistant/tips"}
Get the most out of AI Assistant.
:::
diff --git a/content/guides/11.ai/2.mcp/0.index.md b/content/guides/11.ai/2.mcp/0.index.md
index 935e916ca..717e03e65 100644
--- a/content/guides/11.ai/2.mcp/0.index.md
+++ b/content/guides/11.ai/2.mcp/0.index.md
@@ -11,7 +11,7 @@ description: Connect AI assistants directly to your Directus instance. Let Claud
AI assistants can now directly access your Directus content using the [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP). Instead of copying data back and forth, your AI tools connect directly to your Directus instance.
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
**MCP requires Directus v11.12+**. For older versions, use the [Local MCP alternative](/guides/ai/mcp/local-mcp).
::
@@ -26,11 +26,11 @@ AI assistants can now directly access your Directus content using the [Model Con
::card-group
-:::card{title="Quick Setup" icon="material-symbols:rocket-launch" to="/guides/ai/mcp/installation"}
+:::card{title="Quick Setup" icon="material-symbols:rocket-launch-outline" to="/guides/ai/mcp/installation"}
Enable MCP and connect your AI tools in under 5 minutes.
:::
-:::card{title="See What's Possible" icon="material-symbols:bolt" to="/guides/ai/mcp/use-cases"}
+:::card{title="See What's Possible" icon="material-symbols:bolt-outline" to="/guides/ai/mcp/use-cases"}
Real examples of AI-powered content workflows that save hours of manual work.
:::
@@ -38,7 +38,7 @@ Real examples of AI-powered content workflows that save hours of manual work.
Complete reference of MCP tools and their capabilities.
:::
-:::card{title="Custom Prompts" icon="material-symbols:chat" to="/guides/ai/mcp/prompts"}
+:::card{title="Custom Prompts" icon="material-symbols:chat-outline" to="/guides/ai/mcp/prompts"}
Create reusable prompt templates for consistent AI interactions.
:::
@@ -46,11 +46,11 @@ Create reusable prompt templates for consistent AI interactions.
Essential security practices for using MCP safely with your Directus data.
:::
-:::card{title="Troubleshooting" icon="material-symbols:help" to="/guides/ai/mcp/troubleshooting"}
+:::card{title="Troubleshooting" icon="material-symbols:help-outline" to="/guides/ai/mcp/troubleshooting"}
Common issues and solutions when setting up and using the Directus MCP server.
:::
-:::card{title="Local Alternative" icon="material-symbols:dns" to="/guides/ai/mcp/local-mcp"}
+:::card{title="Local Alternative" icon="material-symbols:dns-outline" to="/guides/ai/mcp/local-mcp"}
Node.js-based MCP server for advanced setups and older Directus versions.
:::
diff --git a/content/guides/11.ai/2.mcp/1.installation.md b/content/guides/11.ai/2.mcp/1.installation.md
index 9b4186991..a326b9b47 100644
--- a/content/guides/11.ai/2.mcp/1.installation.md
+++ b/content/guides/11.ai/2.mcp/1.installation.md
@@ -7,7 +7,7 @@ headline: MCP Server
Get AI assistants connected to your Directus instance in three simple steps. The MCP server is built into Directus with no additional setup required.
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
**MCP requires Directus v11.12+**. For older versions, use the [Local MCP alternative](/guides/ai/mcp/local-mcp).
::
@@ -35,7 +35,7 @@ Most users can keep the default settings. The MCP server is now ready at `https:
::tabs
-:::tabs-item{label="Create New User" icon="material-symbols:person-add"}
+:::tabs-item{label="Create New User" icon="material-symbols:person-add-outline"}
1. Navigate to **User Directory**
2. Click **Create User** with these settings:
@@ -50,9 +50,9 @@ Most users can keep the default settings. The MCP server is now ready at `https:
:::
-:::tabs-item{label="Use Existing User" icon="material-symbols:person"}
+:::tabs-item{label="Use Existing User" icon="material-symbols:person-outline"}
-::::callout{icon="material-symbols:warning" color="warning"}
+::::callout{icon="material-symbols:warning-outline" color="warning"}
**Not recommended**: It's best to use dedicated accounts for AI operations, instead of using your personal admin account.
::::
@@ -240,7 +240,7 @@ Configure your AI user's role based on what you want them to do:
or add the administrator role to your MCP user.
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
**Note**: The MCP server uses your existing permissions and access policy settings. AI tools can only access what you explicitly allow - just like any other Directus user. See [Access Control](/guides/auth/access-control) for more information.
::
@@ -268,7 +268,7 @@ Your MCP server is ready! Here's what to explore:
::card-group
-:::card{title="See What's Possible" icon="material-symbols:bolt" to="/guides/ai/mcp/use-cases"}
+:::card{title="See What's Possible" icon="material-symbols:bolt-outline" to="/guides/ai/mcp/use-cases"}
Real examples of AI-powered content workflows that save hours of manual work.
:::
@@ -276,7 +276,7 @@ Real examples of AI-powered content workflows that save hours of manual work.
Complete reference of MCP tools and their capabilities.
:::
-:::card{title="Custom Prompts" icon="material-symbols:chat" to="/guides/ai/mcp/prompts"}
+:::card{title="Custom Prompts" icon="material-symbols:chat-outline" to="/guides/ai/mcp/prompts"}
Create reusable prompt templates for consistent AI interactions.
:::
@@ -284,7 +284,7 @@ Create reusable prompt templates for consistent AI interactions.
Essential security practices for using MCP safely with your Directus data.
:::
-:::card{title="Troubleshooting" icon="material-symbols:help" to="/guides/ai/mcp/troubleshooting"}
+:::card{title="Troubleshooting" icon="material-symbols:help-outline" to="/guides/ai/mcp/troubleshooting"}
Common issues and solutions when setting up and using the Directus MCP server.
:::
diff --git a/content/guides/11.ai/2.mcp/3.tools.md b/content/guides/11.ai/2.mcp/3.tools.md
index e23f59904..6b56f1629 100644
--- a/content/guides/11.ai/2.mcp/3.tools.md
+++ b/content/guides/11.ai/2.mcp/3.tools.md
@@ -7,7 +7,7 @@ description: Learn about the tools available in the Directus remote MCP server a
The Directus remote MCP server provides a set of tools that allow AI assistants to interact with your Directus instance. These tools enable various operations while respecting your existing permissions and security settings.
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
**Note**: The remote MCP server uses unified tools compared to the local MCP server. For example, it has a single `items` tool that handles all CRUD operations, while the local MCP has separate `read-items`, `create-item`, `update-item`, and `delete-item` tools.
::
diff --git a/content/guides/11.ai/2.mcp/4.prompts.md b/content/guides/11.ai/2.mcp/4.prompts.md
index c7d33a556..b20a496ee 100644
--- a/content/guides/11.ai/2.mcp/4.prompts.md
+++ b/content/guides/11.ai/2.mcp/4.prompts.md
@@ -7,7 +7,7 @@ headline: MCP Server
The Directus MCP Server supports stored prompts, allowing you to create reusable interactions for AI assistants. This feature is particularly useful for standardizing responses, creating guided workflows, and ensuring consistent content creation.
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
**Client Support**: Not all AI clients support prompts. Check the [MCP clients compatibility matrix](https://modelcontextprotocol.io/clients) for your specific client.
::
@@ -30,7 +30,7 @@ Before you can create reusable prompts and use them in your AI conversations, yo
- Directus will validate the collection and prompt you to create any missing fields needed for prompts.
- If you approve, Directus will create the required fields for you
-::callout{icon="material-symbols:warning" color="warning"}
+::callout{icon="material-symbols:warning-outline" color="warning"}
**Don't forget to set permissions**: Make sure your MCP user can read (and optionally create/update) prompts in their role permissions. See [Access Control](/guides/auth/access-control) for more information.
::
diff --git a/content/guides/11.ai/2.mcp/6.security.md b/content/guides/11.ai/2.mcp/6.security.md
index 9295c1987..771b7daf3 100644
--- a/content/guides/11.ai/2.mcp/6.security.md
+++ b/content/guides/11.ai/2.mcp/6.security.md
@@ -7,11 +7,11 @@ headline: MCP Server
AI tools are powerful, but connecting them to your Directus data comes with real security risks. We've engineered the Directus MCP server to be as secure as possible. But that doesn't mean you should ignore security best practices. This guide covers the practical advice for using MCP safely.
-::callout{icon="material-symbols:warning" color="warning"}
+::callout{icon="material-symbols:warning-outline" color="warning"}
**Important: You control the LLM integration.** This tool connects to your own large language model - either self-hosted or via a public service like OpenAI, Anthropic, or others. You're responsible for configuring the LLM connection, managing the access tokens, and ensuring compliance with your chosen provider's terms of service.
::
-::callout{icon="material-symbols:shield" color="info"}
+::callout{icon="material-symbols:shield-outline" color="info"}
**Built-in Security**: The Directus MCP server uses your existing permissions and access policy settings. AI tools can only access what you explicitly allow - just like any other Directus user.
::
diff --git a/content/guides/11.ai/2.mcp/7.local-mcp/0.index.md b/content/guides/11.ai/2.mcp/7.local-mcp/0.index.md
index 4075cec32..a9e92c2c9 100644
--- a/content/guides/11.ai/2.mcp/7.local-mcp/0.index.md
+++ b/content/guides/11.ai/2.mcp/7.local-mcp/0.index.md
@@ -6,7 +6,7 @@ description: The Directus Content MCP Server allows you to interact with your Di
The local MCP server is a standalone Node.js application that provides an alternative way to connect AI tools to your Directus instance using the Model Context Protocol.
-::callout{icon="material-symbols:info" color="warning"}
+::callout{icon="material-symbols:info-outline" color="warning"}
**Remote vs Local MCP**: Directus v11.12+ now includes a [built-in remote MCP server](/guides/ai/mcp/) that's easier to set up and doesn't require Node.js. We highly recommend using the newer remote MCP server. The local MCP server remains available as an alternative for users who prefer a local setup or need specific Node.js-based functionality.
::
diff --git a/content/guides/11.ai/2.mcp/7.local-mcp/1.tools.md b/content/guides/11.ai/2.mcp/7.local-mcp/1.tools.md
index 827495584..4583c7b18 100644
--- a/content/guides/11.ai/2.mcp/7.local-mcp/1.tools.md
+++ b/content/guides/11.ai/2.mcp/7.local-mcp/1.tools.md
@@ -7,7 +7,7 @@ description: Learn about the tools available in the local Directus Content MCP S
The local Directus Content MCP Server provides a set of tools that allow AI assistants to interact with your Directus instance through a Node.js application. These tools enable various operations like reading collections, managing content, working with files, and more.
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
**Note**: The local MCP server provides more granular tools compared to the remote MCP server. For example, it has separate `read-items`, `create-item`, `update-item`, and `delete-item` tools, while the remote MCP combines these into a single `items` tool.
::
diff --git a/content/guides/11.ai/2.mcp/7.local-mcp/2.prompts.md b/content/guides/11.ai/2.mcp/7.local-mcp/2.prompts.md
index 84ff957d1..201f75e55 100644
--- a/content/guides/11.ai/2.mcp/7.local-mcp/2.prompts.md
+++ b/content/guides/11.ai/2.mcp/7.local-mcp/2.prompts.md
@@ -7,11 +7,11 @@ headline: Local MCP
The local MCP server supports the same prompt functionality as the remote MCP server, but requires manual configuration through environment variables and collection setup.
-::callout{icon="material-symbols:info" color="info"}
+::callout{icon="material-symbols:info-outline" color="info"}
**Client Support**: Not all AI clients support prompts. Check the [MCP clients compatibility matrix](https://modelcontextprotocol.io/clients) for your specific client.
::
-::callout{icon="material-symbols:lightbulb" color="primary"}
+::callout{icon="material-symbols:lightbulb-outline" color="primary"}
For detailed information about creating prompts, templating, examples, and best practices, see the main [Prompts guide](/guides/ai/mcp/prompts). This page covers only the local MCP-specific setup differences.
::
diff --git a/content/guides/12.integrations/.navigation.yml b/content/guides/12.integrations/.navigation.yml
index c065d9d22..2f363a9c1 100644
--- a/content/guides/12.integrations/.navigation.yml
+++ b/content/guides/12.integrations/.navigation.yml
@@ -1,4 +1,3 @@
title: Integrations
-icon: directus-integrations
diff --git a/content/guides/12.integrations/1.n8n/0.index.md b/content/guides/12.integrations/1.n8n/0.index.md
index 3fb69545a..0d07841d2 100644
--- a/content/guides/12.integrations/1.n8n/0.index.md
+++ b/content/guides/12.integrations/1.n8n/0.index.md
@@ -9,7 +9,7 @@ technologies:
Connect your Directus instance with n8n to automate workflows, sync data between systems, and build powerful integrations.
-::callout{icon="heroicons-outline:rocket-launch"}
+::callout{icon="material-symbols:rocket-launch-outline"}
**Quick Start**
1. **Install the node**: Click the **+** button in n8n, search for "Directus", and install the verified node (look for the verification badge)
@@ -98,7 +98,7 @@ docker restart
3. Start n8n
4. Reinstall the Directus node through the node palette (search for "Directus")
-::callout{icon="material-symbols:warning"}
+::callout{icon="material-symbols:warning-outline"}
**Important**: After clearing the nodes directory, always reinstall by **searching for "Directus" in the node palette** (the + button), not through Settings → Community Nodes. The node palette search will show the verified Directus node with a shield icon.
::
@@ -135,4 +135,3 @@ Set up automated workflows that trigger when events happen in Directus.
Use raw CRUD operations with Directus filter syntax, complex queries, and advanced query parameters.
-
diff --git a/content/guides/12.integrations/1.n8n/directus-n8n-advanced.md b/content/guides/12.integrations/1.n8n/directus-n8n-advanced.md
index d414ee10d..790ea44a6 100644
--- a/content/guides/12.integrations/1.n8n/directus-n8n-advanced.md
+++ b/content/guides/12.integrations/1.n8n/directus-n8n-advanced.md
@@ -34,7 +34,7 @@ Quick reference of all available raw operations organized by resource type:
---
-::callout{icon="heroicons-outline:light-bulb"}
+::callout{icon="material-symbols:lightbulb-outline"}
**When to Use Raw Operations**
Use raw operations when you need complex filters with logical operators (`_and`, `_or`), relational field filtering, advanced query parameters (aggregation, search, etc.), or full control over the JSON payload structure.
::
diff --git a/content/guides/12.integrations/2.clay/use-clay-templates-with-directus.md b/content/guides/12.integrations/2.clay/use-clay-templates-with-directus.md
index a2fa5f879..de32eaf79 100644
--- a/content/guides/12.integrations/2.clay/use-clay-templates-with-directus.md
+++ b/content/guides/12.integrations/2.clay/use-clay-templates-with-directus.md
@@ -39,7 +39,7 @@ Before using any templates, configure your Directus authentication in Clay:
| **Get Item from Collection** | Search and retrieve records from Directus |
| **Get Related Item Details** | Fetch relational data from Directus |
-::callout{icon="material-symbols:warning" color="warning"}
+::callout{icon="material-symbols:warning-outline" color="warning"}
**Important**
The Directus templates use generic collection names (like "posts" or "users") as examples. You'll need to adapt these to match your specific Directus schema by replacing collection names, adjusting field names, and configuring filters based on your data structure.
::
diff --git a/content/guides/12.integrations/3.zapier/0.index.md b/content/guides/12.integrations/3.zapier/0.index.md
index ed258652a..28f60248d 100644
--- a/content/guides/12.integrations/3.zapier/0.index.md
+++ b/content/guides/12.integrations/3.zapier/0.index.md
@@ -9,7 +9,7 @@ technologies:
Connect your Directus instance with Zapier to automate workflows, sync data between systems, and build powerful integrations with 6,000+ apps.
-::callout{icon="heroicons-outline:rocket-launch"}
+::callout{icon="material-symbols:rocket-launch-outline"}
**Quick Start**
1. **Connect your account**: In Zapier, search for "Directus" and connect your Directus URL and API token
diff --git a/content/guides/12.integrations/3.zapier/actions.md b/content/guides/12.integrations/3.zapier/actions.md
index 7d3e08476..704ae0305 100644
--- a/content/guides/12.integrations/3.zapier/actions.md
+++ b/content/guides/12.integrations/3.zapier/actions.md
@@ -81,7 +81,7 @@ By default, Zapier returns only the **first result** from Find actions. To proce
**Note for Items**: Select the **Collection** before configuring options.
-::callout{icon="heroicons-outline:light-bulb"}
+::callout{icon="material-symbols:lightbulb-outline"}
**Advanced Filtering in Find**
The **Filter (JSON)** field in Find actions supports Directus's complete filter syntax, including logical operators (`_and`, `_or`), relational field filtering, and all filter operators. For complete filter syntax and examples, see the [Directus Filter Rules documentation](https://directus.io/docs/guides/connect/filter-rules).
@@ -127,7 +127,7 @@ To create users, use **Invite User**. This sends an invitation email so the user
- **Role** (dropdown selection)
- **Custom Invite URL** (optional)
-::callout{icon="heroicons-outline:light-bulb"}
+::callout{icon="material-symbols:lightbulb-outline"}
**Direct User Creation**
For direct user creation without invitation, use **User Raw Request** with POST method.
::
@@ -165,7 +165,7 @@ Import a file from a publicly accessible URL.
- **Description** (optional)
- **Folder** (optional)
-::callout{icon="heroicons-outline:light-bulb"}
+::callout{icon="material-symbols:lightbulb-outline"}
**Upload vs Import**
Use **Upload** when you have binary file data from a previous step. Use **Import** when you have a publicly accessible URL to the file.
::
diff --git a/content/guides/12.integrations/3.zapier/advanced.md b/content/guides/12.integrations/3.zapier/advanced.md
index 758c7c5bd..423099a15 100644
--- a/content/guides/12.integrations/3.zapier/advanced.md
+++ b/content/guides/12.integrations/3.zapier/advanced.md
@@ -27,7 +27,7 @@ Quick reference of all available raw request actions:
---
-::callout{icon="heroicons-outline:light-bulb"}
+::callout{icon="material-symbols:lightbulb-outline"}
**When to Use Raw Request Actions**
Use raw request actions when you need full control over HTTP methods, complex query parameters (aggregation, search, etc.), or complete control over the JSON payload structure. For advanced filtering in Find actions, use the Filter (JSON) field instead.
::
diff --git a/content/guides/12.integrations/4.vercel/0.index.md b/content/guides/12.integrations/4.vercel/0.index.md
index 385c9a7fa..4c6b38c8c 100644
--- a/content/guides/12.integrations/4.vercel/0.index.md
+++ b/content/guides/12.integrations/4.vercel/0.index.md
@@ -9,7 +9,7 @@ technologies:
Connect your Directus instance with Vercel to centrally manage deployments, monitor build status, and control multiple frontend projects — all without leaving Directus.
-::callout{icon="heroicons-outline:rocket-launch"}
+::callout{icon="material-symbols:rocket-launch-outline"}
**Quick Start**
1. **Enable Deployment module**: Enable the Deployment module from your Directus project settings
2. **Connect your Vercel account**: Navigate to the Deployment module and add your Vercel API token
@@ -39,7 +39,7 @@ Connect your Directus instance with Vercel to centrally manage deployments, moni
2. Click **Save** to add the projects to the integration
3. You can return to the Vercel integration settings at any time to add or remove projects
-::callout{icon="material-symbols:warning" color="warning"}
+::callout{icon="material-symbols:warning-outline" color="warning"}
**Removing Projects**
Removing a project from the Vercel integration will also permanently delete all deployment history for that project from Directus. This action cannot be undone.
@@ -54,7 +54,7 @@ Once configured, your connected Vercel projects will be listed in the Deployment
- Access deployment controls for each project
- Monitor deployment status and history
-::callout{icon="material-symbols:shield" color="info"}
+::callout{icon="material-symbols:shield-outline" color="info"}
**Permissions**
The Deployment module uses Directus native permissions to control access. See [Deployment Security](/guides/deployments/security) for details on configuring roles and access policies.
diff --git a/content/guides/12.integrations/5.netlify/0.index.md b/content/guides/12.integrations/5.netlify/0.index.md
index 7b5ab7998..e2063790d 100644
--- a/content/guides/12.integrations/5.netlify/0.index.md
+++ b/content/guides/12.integrations/5.netlify/0.index.md
@@ -9,7 +9,7 @@ technologies:
Integrate your Directus instance with Netlify to centrally deploy sites, track build progress, and manage multiple frontend projects — all from within Directus.
-::callout{icon="heroicons-outline:rocket-launch"}
+::callout{icon="material-symbols:rocket-launch-outline"}
**Quick Start**
1. **Enable Deployment module**: Enable the Deployment module in your Directus project settings
2. **Link your Netlify account**: Go to the Deployment module and enter your Netlify Personal Access Token
@@ -39,7 +39,7 @@ Integrate your Directus instance with Netlify to centrally deploy sites, track b
2. Click **Save** to add the selected sites
3. Return to integration settings anytime to add or remove sites
-::callout{icon="material-symbols:warning" color="warning"}
+::callout{icon="material-symbols:warning-outline" color="warning"}
**Removing Sites**
Removing a site from the Netlify integration will permanently delete all deployment history for that site from Directus. This cannot be undone.
@@ -54,7 +54,7 @@ Once set up, your connected Netlify sites appear in the Deployment module. From
- Access deployment controls for each site
- Track deployment status and history
-::callout{icon="material-symbols:shield" color="info"}
+::callout{icon="material-symbols:shield-outline" color="info"}
**Permissions**
The Deployment module uses Directus native permissions to control access. See [Deployment Security](/guides/deployments/security) for details on configuring roles and access policies.
diff --git a/content/guides/12.integrations/6.framer/0.index.md b/content/guides/12.integrations/6.framer/0.index.md
index ddd01560e..444222df9 100644
--- a/content/guides/12.integrations/6.framer/0.index.md
+++ b/content/guides/12.integrations/6.framer/0.index.md
@@ -9,7 +9,7 @@ technologies:
The Directus Framer plugin connects a Framer CMS collection to a Directus collection, letting you pull content into Framer or push edits back to Directus without leaving your project.
-::callout{icon="heroicons-outline:rocket-launch"}
+::callout{icon="material-symbols:rocket-launch-outline"}
**Quick Start**
1. **Open a collection**: In Framer, create or open a regular CMS collection, then open the Directus plugin from it
@@ -112,4 +112,3 @@ A conflict occurs when the same item has been updated in both Framer and Directu
- **Always overwrite (skip conflict screen) checked**: The plugin uses the source side's version for every item and skips the conflict screen.
- **Always overwrite (skip conflict screen) unchecked** (default): Items with changes on both sides are listed individually. For each one, click **Use Framer** to keep the Framer version or **Use Directus** to accept the Directus version.
-
diff --git a/content/guides/13.security/1.best-practices.md b/content/guides/13.security/1.best-practices.md
index b4e935a45..51eb4698f 100644
--- a/content/guides/13.security/1.best-practices.md
+++ b/content/guides/13.security/1.best-practices.md
@@ -159,7 +159,7 @@ If your project applies custom CSS through the theming group in settings, an att
Understand how permissions, policies, and roles compose access control in Directus.
:::
-:::card{title="Security & Limits" icon="i-ph-gear" to="/configuration/security-limits"}
+:::card{title="Security & Limits" icon="material-symbols:settings-outline" to="/configuration/security-limits"}
Environment variables for tokens, cookies, CSP, CORS, rate limiting, and request limits.
:::
@@ -167,7 +167,7 @@ Environment variables for tokens, cookies, CSP, CORS, rate limiting, and request
Set up and require 2FA for Data Studio and API logins.
:::
-:::card{title="Report a Security Issue" icon="i-ph-shield-warning" to="/community/reporting-and-support/security-reporting"}
+:::card{title="Report a Security Issue" icon="material-symbols:shield-lock-outline" to="/community/reporting-and-support/security-reporting"}
How to responsibly disclose a security vulnerability in Directus.
:::
diff --git a/content/index.md b/content/index.md
index 55915c9cc..26a8b4d9d 100644
--- a/content/index.md
+++ b/content/index.md
@@ -1,305 +1,125 @@
---
title: Directus Documentation
-description: Learn how to get started and implement Directus through our developer resources.
+description: The AI-ready backend for your whole team.
navigation: false
---
-## Try a Demo
+## Key features
-::two-up
+Everything you need in one platform.
-#left
-:::tabs
- ::::div{class="pr-6"}
- ---
- label: Local Demo
- ---
- Run Docker locally and use the following command to start the project.
-
- :doc-cli-snippet{command="npx directus-template-cli@latest init"}
-
- You can then select a project directory name, a backend template, frontend framework, and whether to install dependencies automatically.
- ::::
-
- ::::div{class="pr-6"}
- ---
- label: Hosted Demo
- ---
- Try our [hosted demo project](https://sandbox.directus.io). This is a public demo project that is occasionally reset but please don't input sensitive data.
- ::::
-:::
-
-#right
-:video-embed{video-id="96b44cbc-1b14-4bea-87cd-0c4cb34d261d"}
-
-::
-
-## Getting Started
-
-::shiny-grid{class="lg:grid-cols-3"}
- :::shiny-card
- ---
- title: Platform Overview
- description: Choose how to run Directus and get started.
- icon: simple-icons:directus
- to: /getting-started/overview
- ---
- :::
-
- :::shiny-card
- ---
- title: Directus Academy
- description: Learn to use Directus in our video series.
- icon: heroicons-outline:play
- to: https://directus.io/tv/directus-academy
- ---
- :::
-
- :::shiny-card
- ---
- title: How to Deploy Directus
- description: Explore Cloud, Docker, and platform-specific deployment guides.
- icon: heroicons-outline:cloud
- to: /tutorials/self-hosting
- ---
- :::
-::
+:home-features
-:framework-links
+## Get started
-## Features
+The fastest way to try Directus is to spin up a local instance with our template CLI. It scaffolds a Directus project plus a frontend in under a minute — no signup required.
-::shiny-grid{class="lg:grid-cols-2"}
- :::shiny-card
- ---
- title: APIs and Developer Tools
- description: Build with REST, GraphQL, the SDK, realtime, auth, and Flows.
- ---
- :product-link{product="connect"} :product-link{product="realtime"} :product-link{product="auth"} :product-link{product="automate"}
- :::
+:doc-cli-snippet{command="npx directus-template-cli@latest init"}
- :::shiny-card
- ---
- title: Data Studio
- description: A web app for your whole team to manage content, files, users, and dashboards.
- ---
- :product-link{product="insights"} :product-link{product="files"}
- :::
-::
-
-## Directus APIs
+Once you're ready to commit, pick how you want to run Directus:
-::shiny-grid
- :::shiny-card
+::u-page-grid{class="lg:grid-cols-3"}
+ :::u-page-card
---
- title: Quickstart
- description: Learn to connect with Directus.
- icon: heroicons-outline:star
- to: '/getting-started/use-the-api'
+ title: Directus Cloud
+ description: Spin up a managed instance in seconds. No infrastructure to run.
+ icon: material-symbols:cloud-outline
+ to: https://directus.cloud
---
:::
- :::shiny-card
+ :::u-page-card
---
- title: API Reference
- description: Learn how to use the Directus API.
- icon: heroicons-outline:play
- to: '/api'
+ title: Self-Hosted
+ description: Run Directus on your own infrastructure with Docker or your platform of choice.
+ icon: simple-icons:docker
+ to: /self-hosting/overview
---
:::
- :::shiny-card
+ :::u-page-card
---
- title: SDK
- description: Official JavaScript and TypeScript library.
- icon: heroicons-outline:code
- to: '/guides/connect/sdk'
+ title: Local Demo
+ description: Try Directus locally in one command.
+ icon: material-symbols:terminal
+ to: /getting-started/overview
---
:::
::
-## Tutorials
-
-::shiny-grid
- :::shiny-card
- ---
- title: Integrate Your Frontend
- description: Learn how to build a website using Directus as a Headless CMS using various frameworks.
- icon: material-symbols:web
- to: '/frameworks'
- ---
- :::
+## Pick your framework
- :::shiny-card
- ---
- title: Build Projects with Directus
- description: Learn from a variety of different usecases you can build using Directus.
- icon: heroicons-outline:wrench
- to: '/tutorials/projects'
- ---
- :::
+Connect your preferred frontend to Directus. Each guide covers data fetching, authentication, and live preview.
- :::shiny-card
- ---
- title: Tips and Tricks
- description: Small concepts and techniques to get the most from Directus.
- icon: heroicons-outline:light-bulb
- to: '/tutorials/tips-and-tricks'
- ---
- :::
+:framework-links
- :::shiny-card
- ---
- title: Migrate
- description: Techniques and considerations when migrating from other platforms to Directus.
- icon: carbon:migrate
- to: '/tutorials/migration'
- ---
- :::
+## Who's it for
- :::shiny-card
- ---
- title: Extensions
- description: Learn to build extensions from examples that amplify Directus' functionality.
- icon: heroicons-outline:puzzle
- to: '/tutorials/extensions'
- ---
- :::
+Directus is a flexible backend, but here's how teams typically use it.
- :::shiny-card
+::u-page-grid{class="lg:grid-cols-2"}
+ :::u-page-card
---
- title: Workflows
- description: Learn to set up common patterns to build complex workflows and integrations.
- icon: material-symbols:flowchart-outline
- to: '/tutorials/workflows'
+ title: Developers
+ description: APIs, SDKs, extensions, and self-hosting on your own terms.
+ icon: material-symbols:code
---
:::
- :::callout{icon="heroicons-outline:light-bulb" to="/tutorials" class="md:col-span-2 lg:col-span-3"}
- See All Tutorials
- :::
-::
-
-## Releases
-
-::shiny-grid
- :::shiny-card
+ :::u-page-card
---
- title: GitHub Releases
- description: See the complete and latest updates and release notes for Directus.
- icon: simple-icons:github
- to: 'https://github.com/directus/directus/releases'
+ title: Product teams
+ description: Ship features without waiting for a backend rebuild.
+ icon: material-symbols:rocket-launch-outline
---
:::
- :::shiny-card
+ :::u-page-card
---
- title: Breaking Changes
- description: Breaking changes may require action on your part before upgrading.
- icon: heroicons-outline:exclamation-circle
- to: '/releases/breaking-changes'
+ title: Agencies
+ description: One backend, every project, every framework.
+ icon: material-symbols:business-center-outline
---
:::
- :::shiny-card
+ :::u-page-card
---
- title: Changelog
- description: A monthly summary of what's new from the Directus team.
- icon: heroicons-outline:document-text
- to: '/releases/changelog'
+ title: Publishers
+ description: Headless CMS for content-driven sites at any scale.
+ icon: material-symbols:newspaper
---
:::
::
-## Community-Maintained Tooling
-
-::shiny-grid
- :::shiny-card
- ---
- title: Python SDK
- description: Interact with Directus using Python.
- icon: simple-icons:python
- to: 'https://pypi.org/project/directus-sdk-py/'
- ---
- :::
-
- :::shiny-card
- ---
- title: Go SDK
- description: Interact with Directus using Go.
- icon: simple-icons:go
- to: 'https://pkg.go.dev/github.com/altipla-consulting/directus-go#section-readme'
- ---
- :::
-
- :::shiny-card
- ---
- title: Dart SDK
- description: Interact with Directus using Dart.
- icon: simple-icons:dart
- to: 'https://github.com/apstanisic/directus-dart'
- ---
- :::
-
- :::shiny-card
- ---
- title: Nuxt Module
- description: Easily connect your Nuxt application to your Directus project.
- icon: simple-icons:nuxtdotjs
- to: 'https://nuxt.com/modules/directus'
- ---
- :::
-
- :::shiny-card
- ---
- title: Helm Chart
- description: Community-maintained Helm Charts repository for Directus.
- icon: simple-icons:helm
- to: 'https://github.com/directus-labs/helm-chart'
- ---
- :::
-
- :::shiny-card
- ---
- title: PHP SDK
- description: Interact with Directus using PHP.
- icon: simple-icons:php
- to: 'https://github.com/alantiller/directus-php-sdk'
- ---
- :::
-
- :::callout{icon="material-symbols:lightbulb-2-outline" class="lg:col-span-3"}
- These are built and maintained by our awesome community. If you are building tooling and want to include it here, please open a [pull request on GitHub](https://github.com/directus/docs).
- :::
-::
+## How it works
-## Advanced Concepts
+Three concepts power everything in Directus. Start here to build your mental model.
-::shiny-grid
- :::shiny-card
+::u-page-grid{class="lg:grid-cols-3"}
+ :::u-page-card
---
- title: Environment Variables
- description: Configure Directus at an advanced level.
- icon: heroicons-outline:cog
- to: '/configuration/intro'
+ title: Data model
+ description: Collections and fields map directly to your database schema.
+ icon: material-symbols:database-outline
+ to: /guides/data-model/collections
---
:::
- :::shiny-card
+ :::u-page-card
---
- title: Building Extensions
- description: Learn to build Extensions for Directus.
- icon: heroicons-outline:puzzle
- to: '/guides/extensions/overview'
+ title: Permissions
+ description: Define who can do what, down to the field level.
+ icon: material-symbols:verified-user-outline
+ to: /guides/auth/access-control
---
:::
- :::shiny-card
+ :::u-page-card
---
- title: Self-Hosting
- description: Learn self-hosting concepts and deployment paths.
- icon: heroicons-outline:cloud
- to: '/self-hosting/overview'
+ title: Flows
+ description: Trigger logic on events, schedules, or webhooks.
+ icon: material-symbols:bolt-outline
+ to: /guides/flows/operations
---
:::
::
diff --git a/content/releases/1.index.md b/content/releases/0.index.md
similarity index 96%
rename from content/releases/1.index.md
rename to content/releases/0.index.md
index 5cd0f9bed..aa77a436f 100644
--- a/content/releases/1.index.md
+++ b/content/releases/0.index.md
@@ -1,8 +1,8 @@
---
stableId: c27fb12d-c305-4814-bcac-4a8b26e54921
-title: Overview
+title: Releases
description: Releases are how we roll out new features, updates, and fixes to Directus.
-headline: Releases
+icon: material-symbols:tag
---
We release new versions of Directus approximately once a month. While we provide a single version number for each release, Directus has several packages and each package may have different version numbers.
diff --git a/content/tutorials/2.projects/build-a-user-feedback-widget-with-vue-js-.md b/content/tutorials/2.projects/build-a-user-feedback-widget-with-vue-js-.md
index d8b775602..a2fd7a39f 100644
--- a/content/tutorials/2.projects/build-a-user-feedback-widget-with-vue-js-.md
+++ b/content/tutorials/2.projects/build-a-user-feedback-widget-with-vue-js-.md
@@ -465,7 +465,7 @@ Open up the Public role with the [Access Control settings](/guides/auth/access-c
**Create and Update Operations**
-Click the :icon{name="material-symbols:block"} button inside each column and choose :icon{name="material-symbols:check"} All Access.
+Click the :icon{name="material-symbols:block-outline"} button inside each column and choose :icon{name="material-symbols:check"} All Access.
**Read Operation**
diff --git a/content/tutorials/7.workflows/build-content-approval-workflows-with-custom-permissions.md b/content/tutorials/7.workflows/build-content-approval-workflows-with-custom-permissions.md
index 84b9117f1..2ff7c5bc9 100644
--- a/content/tutorials/7.workflows/build-content-approval-workflows-with-custom-permissions.md
+++ b/content/tutorials/7.workflows/build-content-approval-workflows-with-custom-permissions.md
@@ -116,6 +116,6 @@ explicit set of permissions each role has at each stage.
::callout{icon="material-symbols:info-outline"}
-Workflows can be further enhanced with custom [Interfaces](/guides/extensions/app-extensions/interfaces) as well as [flows](/guides/automate/flows).
+Workflows can be further enhanced with custom [Interfaces](/guides/extensions/app-extensions/interfaces) as well as [flows](/guides/flows).
::
diff --git a/content/tutorials/7.workflows/invincible-ai-content-workflows-with-inngest-and-directus.md b/content/tutorials/7.workflows/invincible-ai-content-workflows-with-inngest-and-directus.md
index 78b55f18b..d1d2108f2 100644
--- a/content/tutorials/7.workflows/invincible-ai-content-workflows-with-inngest-and-directus.md
+++ b/content/tutorials/7.workflows/invincible-ai-content-workflows-with-inngest-and-directus.md
@@ -62,7 +62,7 @@ The platform also offers a developer-friendly experience with excellent local de
## The Directus + Inngest Integration: Beyond Flows
-While Directus already includes its own [workflow automation system (Flows)](/guides/automate/flows), Inngest complements it by handling scenarios that Flows wasn't designed for. Directus Flows excels at short-lived automations like sending notifications or processing simple data operations, but AI workflows typically require more resilience and computational power.
+While Directus already includes its own [workflow automation system (Flows)](/guides/flows), Inngest complements it by handling scenarios that Flows wasn't designed for. Directus Flows excels at short-lived automations like sending notifications or processing simple data operations, but AI workflows typically require more resilience and computational power.
Inngest is the perfect companion when you need:
diff --git a/content/tutorials/7.workflows/schedule-future-content-with-directus-automate.md b/content/tutorials/7.workflows/schedule-future-content-with-directus-automate.md
index bae74452b..8934fab2c 100644
--- a/content/tutorials/7.workflows/schedule-future-content-with-directus-automate.md
+++ b/content/tutorials/7.workflows/schedule-future-content-with-directus-automate.md
@@ -12,7 +12,7 @@ description: Learn how to set content to be scheduled on a future date with Dire
This guide explains how to schedule content to be published on a future date for a statically generated site (SSG).
-We'll be using [Flows](/guides/automate/flows) to publish articles when the current date matches the published date.
+We'll be using [Flows](/guides/flows) to publish articles when the current date matches the published date.
First we'll schedule a flow to run at regular intervals.
@@ -63,13 +63,13 @@ field `status` that controls the published state.
### Create and Configure Your Flow
-5. [Create a new flow](/guides/automate/flows)
+5. [Create a new flow](/guides/flows)

Give it a memorable name and short description like `Publish Scheduled Articles`.
-6. [Complete the trigger setup](/guides/automate/triggers)
+6. [Complete the trigger setup](/guides/flows/triggers)

@@ -85,7 +85,7 @@ field `status` that controls the published state.
### Add an Operation to Check The Published Date and Update Data
-7. [Create a new operation](/guides/automate/operations)
+7. [Create a new operation](/guides/flows/operations)

diff --git a/content/tutorials/7.workflows/trigger-netlify-site-builds-with-directus-automate.md b/content/tutorials/7.workflows/trigger-netlify-site-builds-with-directus-automate.md
index 1ff73a978..d973fb1eb 100644
--- a/content/tutorials/7.workflows/trigger-netlify-site-builds-with-directus-automate.md
+++ b/content/tutorials/7.workflows/trigger-netlify-site-builds-with-directus-automate.md
@@ -35,11 +35,11 @@ with them.
### Create and Configure Your Flow
-1. [Create a new flow](/guides/automate/flows)
+1. [Create a new flow](/guides/flows)
Give it a memorable name and short description like `Trigger New Site Build`.
-2. [Complete the trigger setup](/guides/automate/triggers)
+2. [Complete the trigger setup](/guides/flows/triggers)

@@ -62,7 +62,7 @@ with them.
> This step is optional but it is recommended to add a Condition operation to prevent unnecessary builds.
-3. [Create a new Operation](/guides/automate/operations)
+3. [Create a new Operation](/guides/flows/operations)

diff --git a/content/tutorials/7.workflows/trigger-vercel-site-builds-with-directus-automate.md b/content/tutorials/7.workflows/trigger-vercel-site-builds-with-directus-automate.md
index 5fd3619c3..b88205fe5 100644
--- a/content/tutorials/7.workflows/trigger-vercel-site-builds-with-directus-automate.md
+++ b/content/tutorials/7.workflows/trigger-vercel-site-builds-with-directus-automate.md
@@ -33,11 +33,11 @@ with them.
### Create and Configure Your Flow
-1. [Create a new flow](/guides/automate/flows)
+1. [Create a new flow](/guides/flows)
Give it a memorable name and short description like `Trigger New Site Build`.
-2. [Complete the Trigger Setup](/guides/automate/triggers)
+2. [Complete the Trigger Setup](/guides/flows/triggers)

@@ -60,7 +60,7 @@ with them.
> This step is optional but it is recommended to add a Condition operation to prevent unnecessary builds.
-3. [Create a new operation](/guides/automate/operations)
+3. [Create a new operation](/guides/flows/operations)

diff --git a/modules/prerender.ts b/modules/prerender.ts
deleted file mode 100644
index c1aec22d2..000000000
--- a/modules/prerender.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/* Ensure all the API reference docs are prerendered */
-import { spec } from '@directus/openapi';
-import { defineNuxtModule, extendRouteRules, useLogger } from '@nuxt/kit';
-import { withoutTrailingSlash } from 'ufo';
-import mapOasNavigation from '~/utils/mapOasNavigation';
-
-export default defineNuxtModule({
- async setup(_moduleOptions, _nuxt) {
- const logger = useLogger();
-
- logger.info('Prerendering API reference docs...');
- // Use existing helper to map the OAS to a navigation object and just return the paths
- const permalinks = mapOasNavigation(spec).map(item => item.path).flat();
-
- for (const link of permalinks) {
- extendRouteRules(withoutTrailingSlash(link), {
- prerender: true,
- });
- }
-
- logger.info(`Added ${permalinks.length} API reference docs to prerender`);
- },
-});
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 2c6094290..415f8321a 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -1,6 +1,9 @@
import { existsSync, readFileSync } from 'node:fs';
import type { NitroConfig } from 'nitropack';
+const directusLight = JSON.parse(readFileSync('./app/assets/shiki/directus-light.json', 'utf8'));
+const directusDark = JSON.parse(readFileSync('./app/assets/shiki/directus-dark.json', 'utf8'));
+
const BASE_URL = '/docs';
type RedirectStatusCode = 301 | 302 | 307 | 308;
@@ -66,9 +69,9 @@ export default defineNuxtConfig({
},
highlight: {
theme: {
- default: 'github-light',
- light: 'github-light',
- dark: 'github-dark',
+ default: directusLight as never,
+ light: directusLight as never,
+ dark: directusDark as never,
},
langs: [
'bash',
@@ -131,7 +134,15 @@ export default defineNuxtConfig({
transpile: ['shiki'],
},
- routeRules: loadRedirectRouteRules(),
+ routeRules: {
+ ...loadRedirectRouteRules(),
+ '/api/**': { prerender: false },
+ '/docs/api/**': { prerender: false },
+ '/llms-full.txt': { prerender: false },
+ '/docs/llms-full.txt': { prerender: false },
+ },
+
+ sourcemap: false,
future: {
compatibilityVersion: 4,
@@ -141,20 +152,20 @@ export default defineNuxtConfig({
nitro: {
compressPublicAssets: false,
+ externals: {
+ inline: ['unhead'],
+ },
prerender: {
routes: ['/'],
-
crawlLinks: true,
-
- concurrency: 3,
+ concurrency: 2,
retry: 2,
retryDelay: 1000,
},
},
algolia: {
- docSearch: {
- indexName: 'directus_unified',
+ docSearch: { indexName: 'directus_unified',
},
},
@@ -171,12 +182,16 @@ export default defineNuxtConfig({
fonts: {
families: [
{ name: 'Inter', weights: [400, 500, 600, 700], global: true },
- { name: 'Poppins', weights: [400, 500, 600], global: true },
- { name: 'Fira Mono', weights: [400, 500, 600], global: true },
+ { name: 'Source Serif 4', weights: [400, 500, 600], global: true },
+ { name: 'IBM Plex Mono', weights: [400, 500, 600], global: true },
],
},
icon: {
+ serverBundle: {
+ collections: ['material-symbols', 'simple-icons'],
+ externalizeIconsJson: true,
+ },
customCollections: [
{
prefix: 'directus',
diff --git a/package.json b/package.json
index 034da16d4..ad0a06213 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,6 @@
"@directus/sdk": "^21.2.2",
"@docsearch/css": "4.6.2",
"@docsearch/js": "4.6.2",
- "@iconify-json/heroicons-outline": "1.2.1",
"@iconify-json/material-symbols": "1.2.68",
"@iconify-json/simple-icons": "1.2.79",
"@nuxt/content": "3.13.0",
@@ -39,10 +38,10 @@
"posthog-node": "5.29.7",
"sharp": "^0.34.5",
"tailwindcss": "^4.2.4",
- "ufo": "1.6.3"
+ "ufo": "1.6.3",
+ "unhead": "^3.1.0"
},
"devDependencies": {
- "@iconify-json/ph": "^1.2.2",
"@nuxt/eslint": "1.15.2",
"@nuxt/scripts": "1.0.2",
"@types/lodash-es": "4.17.12",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d56123993..006a5ef9b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,9 +23,6 @@ importers:
'@docsearch/js':
specifier: 4.6.2
version: 4.6.2
- '@iconify-json/heroicons-outline':
- specifier: 1.2.1
- version: 1.2.1
'@iconify-json/material-symbols':
specifier: 1.2.68
version: 1.2.68
@@ -49,7 +46,7 @@ importers:
version: 4.0.0(magicast@0.5.2)
'@nuxtjs/seo':
specifier: 5.1.3
- version: 5.1.3(4647370d33bd5943aead907599700ab7)
+ version: 5.1.3(f958a059aa9f5a6866becf12bff13989)
'@takumi-rs/core':
specifier: ^1.1.0
version: 1.1.0
@@ -86,10 +83,10 @@ importers:
ufo:
specifier: 1.6.3
version: 1.6.3
+ unhead:
+ specifier: ^3.1.0
+ version: 3.1.0(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.3))
devDependencies:
- '@iconify-json/ph':
- specifier: ^1.2.2
- version: 1.2.2
'@nuxt/eslint':
specifier: 1.15.2
version: 1.15.2(@typescript-eslint/utils@8.59.0(eslint@9.28.0(jiti@2.6.1))(typescript@6.0.3))(@vue/compiler-sfc@3.5.33)(eslint@9.28.0(jiti@2.6.1))(magicast@0.5.2)(typescript@6.0.3)(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.3))
@@ -370,9 +367,6 @@ packages:
'@emnapi/runtime@1.10.0':
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
- '@emnapi/runtime@1.8.1':
- resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
-
'@emnapi/runtime@1.9.2':
resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==}
@@ -650,15 +644,9 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
- '@iconify-json/heroicons-outline@1.2.1':
- resolution: {integrity: sha512-QNYV4/KsW8Ww9a3B+hxDntS5BwLLbErKpL1V3MkvB8X+ZVTX5VLxjlj8rAEih+GCDWzaiZJOrdO/pagvsuBkXg==}
-
'@iconify-json/material-symbols@1.2.68':
resolution: {integrity: sha512-MGo7A6j+evFoks/kIZAdAKMSKl24ARa19bUvXMw/RVFKuMo2tIc27HZitTuXna858pvhjzMaFq8UrXaKqbQGjA==}
- '@iconify-json/ph@1.2.2':
- resolution: {integrity: sha512-PgkEZNtqa8hBGjHXQa4pMwZa93hmfu8FUSjs/nv4oUU6yLsgv+gh9nu28Kqi8Fz9CCVu4hj1MZs9/60J57IzFw==}
-
'@iconify-json/simple-icons@1.2.79':
resolution: {integrity: sha512-aNyO7Fd1qej9oQfIyohYFRv0lhQLaZ+6UkK1c1qwax0MDPUOZOdq65MlU500kow97pD/W+b2u1And3e25eE24Q==}
@@ -7005,6 +6993,14 @@ packages:
unhead@2.1.13:
resolution: {integrity: sha512-jO9M1sI6b2h/1KpIu4Jeu+ptumLmUKboRRLxys5pYHFeT+lqTzfNHbYUX9bxVDhC1FBszAGuWcUVlmvIPsah8Q==}
+ unhead@3.1.0:
+ resolution: {integrity: sha512-SH1PAjAMspLIoBjAjE/R8hty2NYo7YcIrdu5I+PVfiW4QmmwEG4pgoiKG0MCs6WRSwiatzeha+4lqSqvHW9PEg==}
+ peerDependencies:
+ vite: '>=6.4.2'
+ peerDependenciesMeta:
+ vite:
+ optional: true
+
unicode-emoji-modifier-base@1.0.0:
resolution: {integrity: sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==}
engines: {node: '>=4'}
@@ -7915,11 +7911,6 @@ snapshots:
tslib: 2.8.1
optional: true
- '@emnapi/runtime@1.8.1':
- dependencies:
- tslib: 2.8.1
- optional: true
-
'@emnapi/runtime@1.9.2':
dependencies:
tslib: 2.8.1
@@ -8138,18 +8129,10 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
- '@iconify-json/heroicons-outline@1.2.1':
- dependencies:
- '@iconify/types': 2.0.0
-
'@iconify-json/material-symbols@1.2.68':
dependencies:
'@iconify/types': 2.0.0
- '@iconify-json/ph@1.2.2':
- dependencies:
- '@iconify/types': 2.0.0
-
'@iconify-json/simple-icons@1.2.79':
dependencies:
'@iconify/types': 2.0.0
@@ -8255,7 +8238,7 @@ snapshots:
'@img/sharp-wasm32@0.34.5':
dependencies:
- '@emnapi/runtime': 1.8.1
+ '@emnapi/runtime': 1.10.0
optional: true
'@img/sharp-win32-arm64@0.34.5':
@@ -9226,7 +9209,7 @@ snapshots:
- vite
- vue
- '@nuxtjs/seo@5.1.3(4647370d33bd5943aead907599700ab7)':
+ '@nuxtjs/seo@5.1.3(f958a059aa9f5a6866becf12bff13989)':
dependencies:
'@nuxt/kit': 4.4.2(magicast@0.5.2)
'@nuxtjs/robots': 6.0.8(@nuxt/schema@4.4.2)(magicast@0.5.2)(nuxt@4.4.2(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.33)(better-sqlite3@11.10.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@11.10.0))(esbuild@0.27.7)(eslint@9.28.0(jiti@2.6.1))(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rolldown@1.0.0-beta.53(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-beta.53(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(rollup@4.60.2))(rollup@4.60.2)(srvx@0.11.15)(terser@5.46.2)(typescript@6.0.3)(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.3))(vue-tsc@3.2.7(typescript@6.0.3))(yaml@2.8.3))(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.3))(vue@3.5.33(typescript@6.0.3))(zod@4.3.6)
@@ -9234,8 +9217,8 @@ snapshots:
nuxt: 4.4.2(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.33)(better-sqlite3@11.10.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@11.10.0))(esbuild@0.27.7)(eslint@9.28.0(jiti@2.6.1))(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rolldown@1.0.0-beta.53(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-beta.53(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(rollup@4.60.2))(rollup@4.60.2)(srvx@0.11.15)(terser@5.46.2)(typescript@6.0.3)(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.3))(vue-tsc@3.2.7(typescript@6.0.3))(yaml@2.8.3)
nuxt-link-checker: 5.0.9(ad3672cc71ed4fef9dee73a54522c31c)
nuxt-og-image: 6.4.7(9fe474218c97b47f30882881f0852e2d)
- nuxt-schema-org: 6.0.4(d49a319d2e54ee8f7691b4d98b24824b)
- nuxt-seo-utils: 8.1.11(9275d2bad68344b1070a6ddfde2ef475)
+ nuxt-schema-org: 6.0.4(1a967bc97ce9677f66be35b83ed89595)
+ nuxt-seo-utils: 8.1.11(afd1536e353fe9aacca1e8172ba75d16)
nuxt-site-config: 4.0.8(@nuxt/schema@4.4.2)(magicast@0.5.2)(nuxt@4.4.2(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.6)(@types/node@22.19.17)(@vue/compiler-sfc@3.5.33)(better-sqlite3@11.10.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@11.10.0))(esbuild@0.27.7)(eslint@9.28.0(jiti@2.6.1))(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rolldown@1.0.0-beta.53(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-beta.53(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(rollup@4.60.2))(rollup@4.60.2)(srvx@0.11.15)(terser@5.46.2)(typescript@6.0.3)(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.3))(vue-tsc@3.2.7(typescript@6.0.3))(yaml@2.8.3))(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.3))(vue@3.5.33(typescript@6.0.3))(zod@4.3.6)
nuxtseo-shared: 5.1.3(25023e8fc1951fd485d95c7642f63f09)
transitivePeerDependencies:
@@ -10711,13 +10694,14 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
- '@unhead/bundler@3.0.5(esbuild@0.27.7)(lightningcss@1.32.0)(rolldown@1.0.0-beta.53(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(typescript@6.0.3)(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.3))':
+ '@unhead/bundler@3.0.5(esbuild@0.27.7)(lightningcss@1.32.0)(rolldown@1.0.0-beta.53(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(typescript@6.0.3)(unhead@3.1.0(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.3)))(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.3))':
dependencies:
'@vitejs/devtools-kit': 0.1.15(typescript@6.0.3)(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.3))
magic-string: 0.30.21
oxc-parser: 0.127.0
oxc-walker: 0.7.0(oxc-parser@0.127.0)
ufo: 1.6.3
+ unhead: 3.1.0(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.3))
unplugin: 3.0.0
optionalDependencies:
esbuild: 0.27.7
@@ -13857,7 +13841,7 @@ snapshots:
- vue
- zod
- nuxt-schema-org@6.0.4(d49a319d2e54ee8f7691b4d98b24824b):
+ nuxt-schema-org@6.0.4(1a967bc97ce9677f66be35b83ed89595):
dependencies:
'@nuxt/kit': 4.4.2(magicast@0.5.2)
'@unhead/schema-org': 2.1.13(@unhead/vue@2.1.13(vue@3.5.33(typescript@6.0.3)))
@@ -13868,6 +13852,7 @@ snapshots:
pkg-types: 2.3.0
optionalDependencies:
'@unhead/vue': 2.1.13(vue@3.5.33(typescript@6.0.3))
+ unhead: 3.1.0(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.3))
zod: 4.3.6
transitivePeerDependencies:
- '@azure/app-configuration'
@@ -13925,10 +13910,10 @@ snapshots:
- yjs
- yup
- nuxt-seo-utils@8.1.11(9275d2bad68344b1070a6ddfde2ef475):
+ nuxt-seo-utils@8.1.11(afd1536e353fe9aacca1e8172ba75d16):
dependencies:
'@nuxt/kit': 4.4.2(magicast@0.5.2)
- '@unhead/bundler': 3.0.5(esbuild@0.27.7)(lightningcss@1.32.0)(rolldown@1.0.0-beta.53(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(typescript@6.0.3)(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.3))
+ '@unhead/bundler': 3.0.5(esbuild@0.27.7)(lightningcss@1.32.0)(rolldown@1.0.0-beta.53(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(typescript@6.0.3)(unhead@3.1.0(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.3)))(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.3))
citty: 0.2.2
defu: 6.1.7
escape-string-regexp: 5.0.0
@@ -15776,6 +15761,13 @@ snapshots:
dependencies:
hookable: 6.1.1
+ unhead@3.1.0(vite@7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.3)):
+ dependencies:
+ hookable: 6.1.1
+ unplugin: 3.0.0
+ optionalDependencies:
+ vite: 7.3.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.8.3)
+
unicode-emoji-modifier-base@1.0.0: {}
unicorn-magic@0.1.0:
diff --git a/public/img/mountains.avif b/public/img/mountains.avif
new file mode 100644
index 000000000..7936b54e6
Binary files /dev/null and b/public/img/mountains.avif differ
diff --git a/public/img/tutorials/railway.png b/public/img/tutorials/railway.png
new file mode 100644
index 000000000..e41536139
Binary files /dev/null and b/public/img/tutorials/railway.png differ
diff --git a/redirects.json b/redirects.json
index 3ae3175cb..5c2740f18 100644
--- a/redirects.json
+++ b/redirects.json
@@ -1,238 +1,254 @@
{
- "/tutorials/getting-started": {
- "to": "/tutorials",
- "statusCode": 301
- },
- "/tutorials/getting-started/create-reusable-blocks-with-many-to-any-relationships": {
- "to": "/tutorials/projects/create-reusable-blocks-with-many-to-any-relationships",
- "statusCode": 301
- },
- "/tutorials/getting-started/fetch-data-from-directus-in-android-with-kotlin": {
- "to": "/frameworks/kotlin/data-fetching",
- "statusCode": 301
- },
- "/tutorials/getting-started/fetch-data-from-directus-in-ios-with-swift": {
- "to": "/frameworks/swift/data-fetching",
- "statusCode": 301
- },
- "/tutorials/getting-started/fetch-data-from-directus-with-angular": {
- "to": "/frameworks/angular/data-fetching",
- "statusCode": 301
- },
- "/tutorials/getting-started/fetch-data-from-directus-with-astro": {
- "to": "/frameworks/astro/data-fetching",
- "statusCode": 301
- },
- "/tutorials/getting-started/fetch-data-from-directus-with-django": {
- "to": "/frameworks/django/data-fetching",
- "statusCode": 301
- },
- "/tutorials/getting-started/fetch-data-from-directus-with-eleventy-3": {
- "to": "/frameworks/eleventy/data-fetching",
- "statusCode": 301
- },
- "/tutorials/getting-started/fetch-data-from-directus-with-flask": {
- "to": "/frameworks/flask/data-fetching",
- "statusCode": 301
- },
- "/tutorials/getting-started/fetch-data-from-directus-with-flutter": {
- "to": "/frameworks/flutter/data-fetching",
- "statusCode": 301
- },
- "/tutorials/getting-started/fetch-data-from-directus-with-laravel": {
- "to": "/frameworks/laravel/data-fetching",
- "statusCode": 301
- },
- "/tutorials/getting-started/fetch-data-from-directus-with-nextjs": {
- "to": "/frameworks/nextjs/data-fetching",
- "statusCode": 301
- },
- "/tutorials/getting-started/fetch-data-from-directus-with-nuxt": {
- "to": "/frameworks/nuxt/data-fetching",
- "statusCode": 301
- },
- "/tutorials/getting-started/fetch-data-from-directus-with-solidstart": {
- "to": "/frameworks/solidstart/data-fetching",
- "statusCode": 301
- },
- "/tutorials/getting-started/fetch-data-from-directus-with-spring-boot": {
- "to": "/frameworks/spring-boot/data-fetching",
- "statusCode": 301
- },
- "/tutorials/getting-started/fetch-data-from-directus-with-sveltekit": {
- "to": "/frameworks/sveltekit/data-fetching",
- "statusCode": 301
- },
- "/tutorials/getting-started/implement-directus-auth-with-ios": {
- "to": "/frameworks/swift/authentication",
- "statusCode": 301
- },
- "/tutorials/getting-started/implement-multilingual-content-with-directus-and-svelte-kit": {
- "to": "/frameworks/sveltekit/internationalization",
- "statusCode": 301
- },
- "/tutorials/getting-started/implementing-live-preview-in-astro": {
- "to": "/frameworks/astro/live-preview",
- "statusCode": 301
- },
- "/tutorials/getting-started/implementing-live-preview-in-next-js": {
- "to": "/frameworks/nextjs/live-preview",
- "statusCode": 301
- },
- "/tutorials/getting-started/implementing-live-preview-in-nuxt": {
- "to": "/frameworks/nuxt/live-preview",
- "statusCode": 301
- },
- "/tutorials/getting-started/implementing-live-preview-in-react": {
- "to": "/frameworks/react/live-preview",
- "statusCode": 301
- },
- "/tutorials/getting-started/implementing-live-preview-in-sveltekit": {
- "to": "/frameworks/sveltekit/live-preview",
- "statusCode": 301
- },
- "/tutorials/getting-started/implementing-multilingual-content-using-directus-and-astro": {
- "to": "/frameworks/astro/multilingual-content",
- "statusCode": 301
- },
- "/tutorials/getting-started/implementing-multilingual-content-using-directus-and-next": {
- "to": "/frameworks/nextjs/multilingual-content",
- "statusCode": 301
- },
- "/tutorials/getting-started/implementing-multilingual-content-using-directus-and-nuxt": {
- "to": "/frameworks/nuxt/multilingual-content",
- "statusCode": 301
- },
- "/tutorials/getting-started/implementing-multilingual-content-using-directus-and-sveltekit": {
- "to": "/frameworks/sveltekit/multilingual-content",
- "statusCode": 301
- },
- "/tutorials/getting-started/integrating-the-directus-visual-editor-with-nextjs": {
- "to": "/frameworks/nextjs/visual-editor",
- "statusCode": 301
- },
- "/tutorials/getting-started/integrating-the-directus-visual-editor-with-nuxt": {
- "to": "/frameworks/nuxt/visual-editor",
- "statusCode": 301
- },
- "/tutorials/getting-started/integrating-the-directus-visual-editor-with-sveltekit": {
- "to": "/frameworks/sveltekit/visual-editor",
- "statusCode": 301
- },
- "/tutorials/getting-started/rendering-dynamic-blocks-using-astro": {
- "to": "/frameworks/astro/reusable-blocks",
- "statusCode": 301
- },
- "/tutorials/getting-started/rendering-dynamic-blocks-using-next": {
- "to": "/frameworks/nextjs/reusable-blocks",
- "statusCode": 301
- },
- "/tutorials/getting-started/rendering-dynamic-blocks-using-nuxt": {
- "to": "/frameworks/nuxt/reusable-blocks",
- "statusCode": 301
- },
- "/tutorials/getting-started/rendering-dynamic-blocks-using-sveltekit": {
- "to": "/frameworks/sveltekit/reusable-blocks",
- "statusCode": 301
- },
- "/tutorials/getting-started/set-up-live-preview-with-next-js": {
- "to": "/frameworks/nextjs/live-preview-draft-mode",
- "statusCode": 301
- },
- "/tutorials/getting-started/set-up-live-preview-with-nuxt": {
- "to": "/frameworks/nuxt/live-preview-setup",
- "statusCode": 301
- },
- "/tutorials/getting-started/submit-forms-using-directus-and-nextjs": {
- "to": "/frameworks/nextjs/forms",
- "statusCode": 301
- },
- "/tutorials/getting-started/submit-forms-using-directus-and-nuxt": {
- "to": "/frameworks/nuxt/forms",
- "statusCode": 301
- },
- "/tutorials/getting-started/submit-forms-using-directus-and-sveltekit": {
- "to": "/frameworks/sveltekit/forms",
- "statusCode": 301
- },
- "/tutorials/getting-started/using-authentication-in-astro": {
- "to": "/frameworks/astro/authentication",
- "statusCode": 301
- },
- "/tutorials/getting-started/using-authentication-in-next-js": {
- "to": "/frameworks/nextjs/authentication",
- "statusCode": 301
- },
- "/tutorials/getting-started/using-authentication-in-nuxt": {
- "to": "/frameworks/nuxt/authentication",
- "statusCode": 301
- },
- "/tutorials/getting-started/using-authentication-in-react": {
- "to": "/frameworks/react/authentication",
- "statusCode": 301
- },
- "/tutorials/getting-started/using-authentication-in-sveltekit": {
- "to": "/frameworks/sveltekit/authentication",
- "statusCode": 301
- },
- "/tutorials/migration/migrate-from-nuxt-content-to-directus": {
- "to": "/frameworks/nuxt/migrate-from-nuxt-content",
- "statusCode": 301
- },
- "/tutorials/projects/build-forms-dynamically-using-directus-and-astro": {
- "to": "/frameworks/astro/dynamic-forms",
- "statusCode": 301
- },
- "/tutorials/projects/build-forms-dynamically-using-directus-and-next": {
- "to": "/frameworks/nextjs/dynamic-forms",
- "statusCode": 301
- },
- "/tutorials/projects/build-forms-dynamically-using-directus-and-nuxt": {
- "to": "/frameworks/nuxt/dynamic-forms",
- "statusCode": 301
- },
- "/tutorials/projects/build-forms-dynamically-using-directus-and-sveltekit": {
- "to": "/frameworks/sveltekit/dynamic-forms",
- "statusCode": 301
- },
- "/tutorials/projects/create-a-cms-using-directus-and-astro": {
- "to": "/frameworks/astro/build-a-cms",
- "statusCode": 301
- },
- "/tutorials/projects/create-a-cms-using-directus-and-nextjs": {
- "to": "/frameworks/nextjs/build-a-cms",
- "statusCode": 301
- },
- "/tutorials/projects/create-a-cms-using-directus-and-nuxt": {
- "to": "/frameworks/nuxt/build-a-cms",
- "statusCode": 301
- },
- "/tutorials/projects/create-a-cms-using-directus-and-sveltekit": {
- "to": "/frameworks/sveltekit/build-a-cms",
- "statusCode": 301
- },
- "/tutorials/projects/create-dynamic-pages-for-a-cms-using-directus-and-astro": {
- "to": "/frameworks/astro/dynamic-pages",
- "statusCode": 301
- },
- "/tutorials/projects/create-dynamic-pages-for-a-cms-using-directus-and-nextjs": {
- "to": "/frameworks/nextjs/dynamic-pages",
- "statusCode": 301
- },
- "/tutorials/projects/create-dynamic-pages-for-a-cms-using-directus-and-nuxt": {
- "to": "/frameworks/nuxt/dynamic-pages",
- "statusCode": 301
- },
- "/tutorials/projects/create-dynamic-pages-for-a-cms-using-directus-and-sveltekit": {
- "to": "/frameworks/sveltekit/dynamic-pages",
- "statusCode": 301
- },
- "/tutorials/tips-and-tricks/implement-pagination-and-infinite-scrolling-in-next-js-": {
- "to": "/frameworks/nextjs/pagination-infinite-scroll",
- "statusCode": 301
- },
- "/tutorials/workflows/combine-live-preview-and-content-versioning-with-next-js": {
- "to": "/frameworks/nextjs/live-preview-content-versioning",
- "statusCode": 301
- }
+ "/guides/automate/data-chain": {
+ "to": "/guides/flows/data-chain",
+ "statusCode": 301
+ },
+ "/guides/automate/flows": {
+ "to": "/guides/flows",
+ "statusCode": 301
+ },
+ "/guides/automate/operations": {
+ "to": "/guides/flows/operations",
+ "statusCode": 301
+ },
+ "/guides/automate/triggers": {
+ "to": "/guides/flows/triggers",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started": {
+ "to": "/tutorials",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/create-reusable-blocks-with-many-to-any-relationships": {
+ "to": "/tutorials/projects/create-reusable-blocks-with-many-to-any-relationships",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/fetch-data-from-directus-in-android-with-kotlin": {
+ "to": "/frameworks/kotlin/data-fetching",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/fetch-data-from-directus-in-ios-with-swift": {
+ "to": "/frameworks/swift/data-fetching",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/fetch-data-from-directus-with-angular": {
+ "to": "/frameworks/angular/data-fetching",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/fetch-data-from-directus-with-astro": {
+ "to": "/frameworks/astro/data-fetching",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/fetch-data-from-directus-with-django": {
+ "to": "/frameworks/django/data-fetching",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/fetch-data-from-directus-with-eleventy-3": {
+ "to": "/frameworks/eleventy/data-fetching",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/fetch-data-from-directus-with-flask": {
+ "to": "/frameworks/flask/data-fetching",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/fetch-data-from-directus-with-flutter": {
+ "to": "/frameworks/flutter/data-fetching",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/fetch-data-from-directus-with-laravel": {
+ "to": "/frameworks/laravel/data-fetching",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/fetch-data-from-directus-with-nextjs": {
+ "to": "/frameworks/nextjs/data-fetching",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/fetch-data-from-directus-with-nuxt": {
+ "to": "/frameworks/nuxt/data-fetching",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/fetch-data-from-directus-with-solidstart": {
+ "to": "/frameworks/solidstart/data-fetching",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/fetch-data-from-directus-with-spring-boot": {
+ "to": "/frameworks/spring-boot/data-fetching",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/fetch-data-from-directus-with-sveltekit": {
+ "to": "/frameworks/sveltekit/data-fetching",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/implement-directus-auth-with-ios": {
+ "to": "/frameworks/swift/authentication",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/implement-multilingual-content-with-directus-and-svelte-kit": {
+ "to": "/frameworks/sveltekit/internationalization",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/implementing-live-preview-in-astro": {
+ "to": "/frameworks/astro/live-preview",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/implementing-live-preview-in-next-js": {
+ "to": "/frameworks/nextjs/live-preview",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/implementing-live-preview-in-nuxt": {
+ "to": "/frameworks/nuxt/live-preview",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/implementing-live-preview-in-react": {
+ "to": "/frameworks/react/live-preview",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/implementing-live-preview-in-sveltekit": {
+ "to": "/frameworks/sveltekit/live-preview",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/implementing-multilingual-content-using-directus-and-astro": {
+ "to": "/frameworks/astro/multilingual-content",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/implementing-multilingual-content-using-directus-and-next": {
+ "to": "/frameworks/nextjs/multilingual-content",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/implementing-multilingual-content-using-directus-and-nuxt": {
+ "to": "/frameworks/nuxt/multilingual-content",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/implementing-multilingual-content-using-directus-and-sveltekit": {
+ "to": "/frameworks/sveltekit/multilingual-content",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/integrating-the-directus-visual-editor-with-nextjs": {
+ "to": "/frameworks/nextjs/visual-editor",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/integrating-the-directus-visual-editor-with-nuxt": {
+ "to": "/frameworks/nuxt/visual-editor",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/integrating-the-directus-visual-editor-with-sveltekit": {
+ "to": "/frameworks/sveltekit/visual-editor",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/rendering-dynamic-blocks-using-astro": {
+ "to": "/frameworks/astro/reusable-blocks",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/rendering-dynamic-blocks-using-next": {
+ "to": "/frameworks/nextjs/reusable-blocks",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/rendering-dynamic-blocks-using-nuxt": {
+ "to": "/frameworks/nuxt/reusable-blocks",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/rendering-dynamic-blocks-using-sveltekit": {
+ "to": "/frameworks/sveltekit/reusable-blocks",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/set-up-live-preview-with-next-js": {
+ "to": "/frameworks/nextjs/live-preview-draft-mode",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/set-up-live-preview-with-nuxt": {
+ "to": "/frameworks/nuxt/live-preview-setup",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/submit-forms-using-directus-and-nextjs": {
+ "to": "/frameworks/nextjs/forms",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/submit-forms-using-directus-and-nuxt": {
+ "to": "/frameworks/nuxt/forms",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/submit-forms-using-directus-and-sveltekit": {
+ "to": "/frameworks/sveltekit/forms",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/using-authentication-in-astro": {
+ "to": "/frameworks/astro/authentication",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/using-authentication-in-next-js": {
+ "to": "/frameworks/nextjs/authentication",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/using-authentication-in-nuxt": {
+ "to": "/frameworks/nuxt/authentication",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/using-authentication-in-react": {
+ "to": "/frameworks/react/authentication",
+ "statusCode": 301
+ },
+ "/tutorials/getting-started/using-authentication-in-sveltekit": {
+ "to": "/frameworks/sveltekit/authentication",
+ "statusCode": 301
+ },
+ "/tutorials/migration/migrate-from-nuxt-content-to-directus": {
+ "to": "/frameworks/nuxt/migrate-from-nuxt-content",
+ "statusCode": 301
+ },
+ "/tutorials/projects/build-forms-dynamically-using-directus-and-astro": {
+ "to": "/frameworks/astro/dynamic-forms",
+ "statusCode": 301
+ },
+ "/tutorials/projects/build-forms-dynamically-using-directus-and-next": {
+ "to": "/frameworks/nextjs/dynamic-forms",
+ "statusCode": 301
+ },
+ "/tutorials/projects/build-forms-dynamically-using-directus-and-nuxt": {
+ "to": "/frameworks/nuxt/dynamic-forms",
+ "statusCode": 301
+ },
+ "/tutorials/projects/build-forms-dynamically-using-directus-and-sveltekit": {
+ "to": "/frameworks/sveltekit/dynamic-forms",
+ "statusCode": 301
+ },
+ "/tutorials/projects/create-a-cms-using-directus-and-astro": {
+ "to": "/frameworks/astro/build-a-cms",
+ "statusCode": 301
+ },
+ "/tutorials/projects/create-a-cms-using-directus-and-nextjs": {
+ "to": "/frameworks/nextjs/build-a-cms",
+ "statusCode": 301
+ },
+ "/tutorials/projects/create-a-cms-using-directus-and-nuxt": {
+ "to": "/frameworks/nuxt/build-a-cms",
+ "statusCode": 301
+ },
+ "/tutorials/projects/create-a-cms-using-directus-and-sveltekit": {
+ "to": "/frameworks/sveltekit/build-a-cms",
+ "statusCode": 301
+ },
+ "/tutorials/projects/create-dynamic-pages-for-a-cms-using-directus-and-astro": {
+ "to": "/frameworks/astro/dynamic-pages",
+ "statusCode": 301
+ },
+ "/tutorials/projects/create-dynamic-pages-for-a-cms-using-directus-and-nextjs": {
+ "to": "/frameworks/nextjs/dynamic-pages",
+ "statusCode": 301
+ },
+ "/tutorials/projects/create-dynamic-pages-for-a-cms-using-directus-and-nuxt": {
+ "to": "/frameworks/nuxt/dynamic-pages",
+ "statusCode": 301
+ },
+ "/tutorials/projects/create-dynamic-pages-for-a-cms-using-directus-and-sveltekit": {
+ "to": "/frameworks/sveltekit/dynamic-pages",
+ "statusCode": 301
+ },
+ "/tutorials/tips-and-tricks/implement-pagination-and-infinite-scrolling-in-next-js-": {
+ "to": "/frameworks/nextjs/pagination-infinite-scroll",
+ "statusCode": 301
+ },
+ "/tutorials/workflows/combine-live-preview-and-content-versioning-with-next-js": {
+ "to": "/frameworks/nextjs/live-preview-content-versioning",
+ "statusCode": 301
+ }
}
diff --git a/shared/utils/docsSections.ts b/shared/utils/docsSections.ts
new file mode 100644
index 000000000..76ec60bc7
--- /dev/null
+++ b/shared/utils/docsSections.ts
@@ -0,0 +1,128 @@
+export type DocsSectionId
+ = | 'getting-started'
+ | 'guides'
+ | 'deploy'
+ | 'tutorials'
+ | 'frameworks'
+ | 'reference'
+ | 'api'
+ | 'community';
+
+export type DocsGroupId = 'docs' | 'reference' | 'legacy-reference' | 'examples';
+
+export interface DocsSection {
+ id: DocsSectionId;
+ label: string;
+ to: string;
+ prefixes: string[];
+ icon: string;
+}
+
+export interface DocsGroup {
+ id: DocsGroupId;
+ label: string;
+ to: string;
+ icon: string;
+ sectionIds: DocsSectionId[];
+}
+
+export const docsSections: DocsSection[] = [
+ {
+ id: 'getting-started',
+ label: 'Get Started',
+ to: '/getting-started/overview',
+ prefixes: ['/getting-started'],
+ icon: 'material-symbols:rocket-launch-outline',
+ },
+ {
+ id: 'guides',
+ label: 'Guides',
+ to: '/guides/data-model/collections',
+ prefixes: ['/guides'],
+ icon: 'material-symbols:menu-book-outline',
+ },
+ {
+ id: 'deploy',
+ label: 'Hosting',
+ to: '/cloud/getting-started/introduction',
+ prefixes: ['/cloud', '/self-hosting', '/configuration'],
+ icon: 'material-symbols:cloud-outline',
+ },
+ {
+ id: 'frameworks',
+ label: 'Frameworks',
+ to: '/frameworks',
+ prefixes: ['/frameworks'],
+ icon: 'material-symbols:stacks-outline',
+ },
+ {
+ id: 'api',
+ label: 'API Reference',
+ to: '/api',
+ prefixes: ['/api'],
+ icon: 'material-symbols:code',
+ },
+ {
+ id: 'reference',
+ label: 'Reference',
+ to: '/reference/interfaces',
+ prefixes: ['/reference'],
+ icon: 'material-symbols:bookmarks-outline',
+ },
+ {
+ id: 'tutorials',
+ label: 'Tutorials',
+ to: '/tutorials',
+ prefixes: ['/tutorials'],
+ icon: 'material-symbols:article-outline',
+ },
+ {
+ id: 'community',
+ label: 'Community',
+ to: '/community/overview/welcome',
+ prefixes: ['/community', '/releases'],
+ icon: 'material-symbols:groups-outline',
+ },
+];
+
+export const docsGroups: DocsGroup[] = [
+ {
+ id: 'docs',
+ label: 'Docs',
+ to: '/getting-started/overview',
+ icon: 'material-symbols:menu-book-outline',
+ sectionIds: ['getting-started', 'guides', 'deploy', 'frameworks', 'community'],
+ },
+ {
+ id: 'reference',
+ label: 'API ',
+ to: '/api',
+ icon: 'material-symbols:code',
+ sectionIds: ['api'],
+ },
+ // {
+ // id: 'legacy-reference',
+ // label: 'Reference',
+ // to: '/reference/interfaces',
+ // icon: 'material-symbols:bookmarks-outline',
+ // sectionIds: ['reference'],
+ // },
+ {
+ id: 'examples',
+ label: 'Tutorials',
+ to: '/tutorials',
+ icon: 'material-symbols:article-outline',
+ sectionIds: ['tutorials'],
+ },
+];
+
+export const matchesPrefix = (path: string, prefix: string) =>
+ path === prefix || path.startsWith(`${prefix}/`);
+
+export const findSectionByPath = (path: string): DocsSection | null =>
+ docsSections.find(section =>
+ section.prefixes.some(prefix => matchesPrefix(path, prefix)),
+ ) ?? null;
+
+export const findGroupBySectionId = (sectionId: DocsSectionId): DocsGroup | null =>
+ docsGroups.find(group => group.sectionIds.includes(sectionId)) ?? null;
diff --git a/shared/utils/frameworks.ts b/shared/utils/frameworks.ts
new file mode 100644
index 000000000..dfc10e2fa
--- /dev/null
+++ b/shared/utils/frameworks.ts
@@ -0,0 +1,52 @@
+export interface Framework {
+ slug: string;
+ label: string;
+ icon: string;
+ description: string;
+}
+
+export const frameworks: Framework[] = [
+ { slug: 'nextjs', label: 'Next.js', icon: 'simple-icons:nextdotjs', description: 'React framework for production.' },
+ { slug: 'nuxt', label: 'Nuxt', icon: 'simple-icons:nuxt', description: 'Intuitive Vue framework.' },
+ { slug: 'sveltekit', label: 'SvelteKit', icon: 'simple-icons:svelte', description: 'The fastest way to build Svelte apps.' },
+ { slug: 'astro', label: 'Astro', icon: 'simple-icons:astro', description: 'The web framework for content-driven websites.' },
+ { slug: 'react', label: 'React', icon: 'simple-icons:react', description: 'The library for web and native user interfaces.' },
+ { slug: 'vue', label: 'Vue', icon: 'simple-icons:vuedotjs', description: 'The progressive JavaScript framework.' },
+ { slug: 'angular', label: 'Angular', icon: 'simple-icons:angular', description: 'Web development platform built on TypeScript.' },
+ { slug: 'solidstart', label: 'SolidStart', icon: 'simple-icons:solid', description: 'Fine-grained reactive framework.' },
+ { slug: 'eleventy', label: 'Eleventy', icon: 'simple-icons:eleventy', description: 'A simpler static site generator.' },
+ { slug: 'flutter', label: 'Flutter', icon: 'simple-icons:flutter', description: 'Build apps for any screen.' },
+ { slug: 'kotlin', label: 'Android (Kotlin)', icon: 'simple-icons:android', description: 'Native Android development with Kotlin.' },
+ { slug: 'swift', label: 'iOS (Swift)', icon: 'simple-icons:swift', description: 'Native iOS development with Swift.' },
+ { slug: 'laravel', label: 'Laravel', icon: 'simple-icons:laravel', description: 'PHP framework for web artisans.' },
+ { slug: 'django', label: 'Django', icon: 'simple-icons:django', description: 'High-level Python web framework.' },
+ { slug: 'flask', label: 'Flask', icon: 'simple-icons:flask', description: 'Python micro web framework.' },
+ { slug: 'spring-boot', label: 'Spring Boot', icon: 'simple-icons:springboot', description: 'Production-grade Java applications.' },
+];
+
+export const getFramework = (slug: string): Framework | undefined =>
+ frameworks.find(f => f.slug === slug);
+
+const groupLabels: Record = {
+ '1.getting-started': 'Getting Started',
+ '2.projects': 'Projects',
+ '3.tips-and-tricks': 'Tips & Tricks',
+ '4.migration': 'Migration',
+ '5.extensions': 'Extensions',
+ '6.self-hosting': 'Self-Hosting',
+ '7.workflows': 'Workflows',
+};
+
+export const tutorialGroupLabel = (stem: string | undefined): string => {
+ if (!stem) return 'Other';
+ const segments = stem.split('/');
+ const folder = segments[1];
+ return (folder && groupLabels[folder]) || 'Other';
+};
+
+export const tutorialGroupOrder = (stem: string | undefined): number => {
+ if (!stem) return 99;
+ const folder = stem.split('/')[1] ?? '';
+ const match = folder.match(/^(\d+)\./);
+ return match ? Number(match[1]) : 99;
+};