Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions cms-i18n/directus/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ services:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_DATABASE}
healthcheck:
test: ["CMD", "pg_isready", "-U", "${DB_USER}", "-d", "${DB_DATABASE}", "-h", "localhost"]
test: ['CMD', 'pg_isready', '-U', '${DB_USER}', '-d', '${DB_DATABASE}', '-h', 'localhost']
interval: 10s
timeout: 5s
retries: 5
Expand All @@ -20,7 +20,7 @@ services:
cache:
image: redis:6
healthcheck:
test: ["CMD-SHELL", "[ $$(redis-cli ping) = 'PONG' ]"]
test: ['CMD-SHELL', "[ $$(redis-cli ping) = 'PONG' ]"]
interval: 10s
timeout: 5s
retries: 5
Expand All @@ -42,17 +42,17 @@ services:
environment:
SECRET: ${DIRECTUS_SECRET}

DB_CLIENT: "pg"
DB_HOST: "database"
DB_PORT: "5432"
DB_CLIENT: 'pg'
DB_HOST: 'database'
DB_PORT: '5432'
DB_DATABASE: ${DB_DATABASE}
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}

CACHE_ENABLED: ${CACHE_ENABLED}
CACHE_AUTO_PURGE: ${CACHE_AUTO_PURGE}
CACHE_STORE: "redis"
REDIS: "redis://cache:6379"
CACHE_STORE: 'redis'
REDIS: 'redis://cache:6379'

ADMIN_EMAIL: ${ADMIN_EMAIL}
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
Expand Down
1 change: 1 addition & 0 deletions cms-i18n/nuxt/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
.cache/
.output/
dist/
.pnpm-store
76 changes: 63 additions & 13 deletions cms-i18n/nuxt/README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
# Nuxt 3 CMS Template with Directus Integration
# Nuxt 4 CMS Template with Directus Integration & i18n Support

<div align="center">
<img src="public/images/thumbnail.png" alt="Nuxt 3 CMS Template Thumbnail" width="800" height="auto" />
<img src="public/images/thumbnail.png" alt="Nuxt 4 CMS Template with i18n Thumbnail" width="800" height="auto" />
</div>

This is a **Nuxt 3-based CMS Template** that is fully integrated with [Directus](https://directus.io/), offering a CMS
solution for managing and delivering content seamlessly. The template leverages modern technologies like **Nuxt 3's
file-based routing system**, **Tailwind CSS**, and **Shadcn Vue components**, providing a complete and scalable starting
point for building CMS-powered web applications.
This is a **Nuxt 4-based CMS Template with Internationalization (i18n) support** that is fully integrated with
[Directus](https://directus.io/), offering a CMS solution for managing and delivering multilingual content seamlessly.
The template leverages modern technologies like **Nuxt 4's file-based routing system**, **Tailwind CSS**, **Shadcn Vue
components**, and **built-in i18n support**, providing a complete and scalable starting point for building multilingual
CMS-powered web applications.

> **Note**: This is the i18n-enabled version of the Nuxt CMS template. For a single-language version, see the
> [standard Nuxt CMS template](../nuxt/README.md).

## **Features**

- **Nuxt 3 File-Based Routing**: Uses Nuxt's built-in routing system with dynamic page handling.
- **Full Directus Integration**: Directus API integration for fetching and managing relational data.
- **Nuxt 4 File-Based Routing**: Uses Nuxt's built-in routing system with dynamic page handling.
- **Internationalization (i18n)**: Built-in support for multiple languages with locale-based routing, automatic
translation fetching from Directus, and language switcher component.
- **Full Directus Integration**: Directus API integration for fetching and managing relational data with translation
support.
- **Locale-Aware Content**: Automatic content translation based on URL locale prefixes (e.g., `/en/`, `/es/`) with
fallback to default locale.
- **Tailwind CSS**: Fully integrated for rapid UI styling.
- **TypeScript**: Ensures type safety and reliable code quality.
- **Shadcn Vue Components**: Pre-built, customizable UI components for modern design systems.
Expand All @@ -35,7 +44,7 @@ you’ll find `pnpm` very similar in usage. You can still use `npm` if you prefe

### **Draft Mode Overview**

Directus allows you to work on unpublished content using **Draft Mode**. This Nuxt 3 template is configured to support
Directus allows you to work on unpublished content using **Draft Mode**. This Nuxt 4 template is configured to support
Directus Draft Mode out of the box, enabling live previews of unpublished or draft content as you make changes.

### **Live Preview Setup**
Expand Down Expand Up @@ -71,14 +80,42 @@ https://yourwebsite.com/blog/some-post?preview=true

---

## **Internationalization (i18n)**

### **How It Works**

This template includes built-in internationalization support:

- **Locale-Based Routing**: URLs automatically include locale prefixes (e.g., `/en/about`, `/es/about`) with the default
locale (en-US) using clean URLs without a prefix.
- **Directus Translation Integration**: Translations are stored in Directus `{collection}_translations` tables and
automatically fetched based on the current locale.
- **Automatic Content Merging**: Translations are automatically merged onto base content objects, so components can use
`item.title` directly without checking for translations.
- **Language Switcher**: Built-in `LanguageSwitcher` component for easy language selection.
- **SSR & Client Support**: Locale detection works on both server-side (via middleware) and client-side (via route
detection).

### **Directus Setup for i18n**

The i18n schema (languages collection, translation tables, etc.) is included in the Directus template located in
`../directus/template/`. Apply it to your Directus instance using the
[Directus Template CLI](https://github.com/directus/template-cli):

```bash
npx directus-template-cli@latest apply <path-to-template>
```

---

## **Getting Started**

### Prerequisites

To set up this template, ensure you have the following:

- **Node.js** (16.x or newer)
- **npm** or **pnpm**
- **Node.js** (20.x or newer)
- **pnpm** (8.6.0 or newer) or **npm**
- Access to a **Directus** instance ([cloud or self-hosted](../../README.md))

## ⚠️ Directus Setup Instructions
Expand All @@ -92,9 +129,9 @@ For instructions on setting up Directus, choose one of the following:

You can instantly deploy this template using one of the following platforms:

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/directus-labs/starters/tree/main/cms/nuxt&env=DIRECTUS_URL,NUXT_PUBLIC_SITE_URL,DIRECTUS_SERVER_TOKEN,NUXT_PUBLIC_ENABLE_VISUAL_EDITING)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/directus-labs/starters/tree/main/cms-i18n/nuxt&env=DIRECTUS_URL,NUXT_PUBLIC_SITE_URL,DIRECTUS_SERVER_TOKEN,NUXT_PUBLIC_ENABLE_VISUAL_EDITING)

[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/directus-labs/starters&branch=main&create_from_path=cms/nuxt)
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/directus-labs/starters&branch=main&create_from_path=cms-i18n/nuxt)

### **Environment Variables**

Expand Down Expand Up @@ -193,9 +230,22 @@ app/ # Main Nuxt application folder
│ │ ├── search/index.ts # Search functionality
│ │ ├── users/[id].ts # Fetches user data
│ │ ├── authenticated-user.ts # Auth check endpoint
│ ├── middleware/ # Server middleware
│ │ ├── locale.ts # Locale detection and context setup
│ ├── utils/ # Backend utilities
│ │ ├── directus-server.ts # Directus server-side utilities
│ │ ├── directus-utils.ts # General Directus helpers
│ │ ├── directus-i18n.ts # i18n-specific Directus utilities
│ │ ├── i18n.ts # i18n helper functions
│ ├── shared/ # Shared backend logic
│ │ ├── types/schema.ts # Directus schema types
├── middleware/ # Client-side middleware
│ ├── locale.global.ts # Global locale middleware
├── app/
│ ├── lib/
│ │ ├── i18n/ # i18n configuration and utilities
│ │ │ ├── config.ts # Locale configuration
│ │ │ ├── utils.ts # Locale path utilities
│ ├── composables/
│ │ ├── useLocale.ts # Locale composable for components
```
1 change: 0 additions & 1 deletion cms-i18n/nuxt/app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
useHead({
meta: [{ charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }],
link: [{ rel: 'icon', href: '/favicon.ico' }],
htmlAttrs: { lang: 'en' },
});
</script>

Expand Down
17 changes: 15 additions & 2 deletions cms-i18n/nuxt/app/components/Footer.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
<script setup lang="ts">
import { DEFAULT_LOCALE, type Locale } from '~/lib/i18n/config';
import { localizeLink } from '~/lib/i18n/utils';

export interface SocialLink {
service: string;
url: string;
Expand All @@ -23,6 +26,7 @@ export interface FooterProps {
description?: string | null;
social_links?: SocialLink[];
};
locale?: Locale;
}

const props = defineProps<FooterProps>();
Expand All @@ -39,14 +43,23 @@ const lightLogoUrl = computed(() =>
const darkLogoUrl = computed(() =>
props.globals.logo_dark_mode ? `${runtimeConfig.public.directusUrl}/assets/${props.globals.logo_dark_mode}` : '',
);

// Get the current locale from props or default
const currentLocale = computed<Locale>(() => props.locale || DEFAULT_LOCALE);

// Helper to localize internal paths
// Note: localizeLink returns undefined for invalid paths
// However, NuxtLink requires a valid string for the :to prop, so we fallback to '/' to prevent
// navigation errors when paths are missing or invalid.
const localize = (path: string | null | undefined) => localizeLink(path, currentLocale.value) || '/';
</script>

<template>
<footer v-if="globals" ref="footerRef" class="bg-gray dark:bg-[var(--background-variant-color)] py-16">
<Container class="text-foreground dark:text-white">
<div class="flex flex-col md:flex-row justify-between items-start gap-8 pt-8">
<div class="flex-1">
<NuxtLink to="/" class="inline-block transition-opacity hover:opacity-70">
<NuxtLink :to="localize('/')" class="inline-block transition-opacity hover:opacity-70">
<img
v-if="lightLogoUrl"
:src="lightLogoUrl"
Expand Down Expand Up @@ -90,7 +103,7 @@ const darkLogoUrl = computed(() =>
<li v-for="item in props.navigation.items" :key="item.id">
<NuxtLink
v-if="item.page?.permalink"
:to="item.page.permalink"
:to="localize(item.page.permalink)"
class="text-nav font-medium hover:underline"
>
{{ item.title }}
Expand Down
Loading