Add Drupal headless stack with Next.js frontend

- Add Next.js frontend service (nextjs) with Dockerfile and source
- Update docker-compose.yml: image names, Drupal 11.3.3, nextjs service
- Add docker-compose.override.yml.disabled for dev hot-reload
- Add install-headless-modules.sh for OAuth/JSON:API module setup
- Add README.md with full setup and configuration guide
- Update nginx/Dockerfile and nginx.conf.template for cms. subdomain
- Update drupal/Dockerfile PHP-FPM build args
- Gitignore **/.vscode/ to prevent IDE workspace files from being tracked
This commit is contained in:
rnsrk 2026-03-30 11:14:17 +02:00
parent 71a8dac389
commit f8b8f53d54
85 changed files with 7802 additions and 17 deletions

View file

@ -0,0 +1,76 @@
import type { DrupalMenuItem } from "next-drupal"
import { MainNavClient } from "./main-nav-client"
const drupalBaseUrl = process.env.NEXT_PUBLIC_DRUPAL_BASE_URL ?? ""
interface RawMenuItem {
id: string
parent: string
title: string
url: string
enabled?: boolean
weight?: string | number
}
function buildMenuTree(
items: RawMenuItem[],
parentId: string
): DrupalMenuItem[] {
return items
.filter((item) => (item.parent || "") === parentId && item.enabled !== false)
.sort((a, b) => Number(a.weight ?? 0) - Number(b.weight ?? 0))
.map((item) => {
const children = buildMenuTree(items, item.id)
return {
...item,
items: children.length ? children : undefined,
} as DrupalMenuItem
})
}
const FALLBACK_MENU: DrupalMenuItem[] = [
{
id: "home",
title: "Home",
url: "/",
enabled: true,
items: undefined,
} as DrupalMenuItem,
]
async function getMainMenu(): Promise<DrupalMenuItem[]> {
if (!drupalBaseUrl) return FALLBACK_MENU
try {
const url = `${drupalBaseUrl.replace(/\/$/, "")}/jsonapi/menu_items/main`
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 5000)
const res = await fetch(url, {
headers: { Accept: "application/vnd.api+json" },
next: { revalidate: 60 },
signal: controller.signal,
})
clearTimeout(timeoutId)
if (!res.ok) {
if (res.status !== 404) {
console.warn(`[MainNav] Menu fetch returned ${res.status}, using fallback nav.`)
}
return FALLBACK_MENU
}
const json = await res.json()
const items: RawMenuItem[] = json.data ?? []
return buildMenuTree(items, "")
} catch (error) {
if ((error as Error).name !== "AbortError") {
console.warn("[MainNav] CMS unreachable, using fallback nav:", (error as Error).message)
}
return FALLBACK_MENU
}
}
export async function MainNav() {
const menuItems = await getMainMenu()
return <MainNavClient menuItems={menuItems} />
}