-
Notifications
You must be signed in to change notification settings - Fork 28
NEX-200: Update next.js to version 15, switch to @fortedigital/nextjs-cache-handler #313
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9f04dba
580d30d
32c820e
90c5cc1
1693765
44d3fa9
fc8eee9
e85281d
1f869dc
3438ca9
0354d66
0b77fc4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,188 @@ | ||||||
| # Copilot Instructions | ||||||
|
|
||||||
| ## Architecture Overview | ||||||
|
|
||||||
| This is a **decoupled (headless) Drupal + Next.js** multilingual starter template by Wunder. The repository contains two separate applications: | ||||||
|
|
||||||
| - **`drupal/`** — Drupal 10.4 (PHP 8.3) backend: content management, GraphQL API, Elasticsearch indexing, OAuth authentication | ||||||
| - **`next/`** — Next.js 15 (App Router, TypeScript) frontend: consumes Drupal via GraphQL, handles routing, rendering, and user auth | ||||||
|
|
||||||
| Supporting services: MariaDB 10.11, Elasticsearch 8.9, Redis 4.7. | ||||||
|
|
||||||
| ### Data Flow | ||||||
|
|
||||||
| 1. Next.js issues GraphQL queries to `NEXT_PUBLIC_DRUPAL_BASE_URL/graphql` using Simple OAuth credentials | ||||||
| 2. Pages are statically generated with on-demand ISR — Drupal POSTs to `/api/revalidate` when content is saved | ||||||
| 3. Search queries are proxied through Drupal's `wunder_search` module to Elasticsearch | ||||||
| 4. Frontend user auth uses Auth.js (next-auth v5 beta); users are created in Drupal via REST | ||||||
|
|
||||||
| ### Custom Drupal Modules (`drupal/web/modules/custom/`) | ||||||
|
|
||||||
| | Module | Purpose | | ||||||
| |---|---| | ||||||
| | `wunder_next` | Configures the next-drupal integration and environment setup | | ||||||
| | `wunder_search` | Elasticsearch indexing and search proxy | | ||||||
| | `wunder_democontent` | Demo content migration | | ||||||
| | `wunder_sitemap` | XML sitemap generation | | ||||||
|
|
||||||
| --- | ||||||
|
|
||||||
| ## Local Development (DDEV) | ||||||
|
|
||||||
| **All npm commands must be run with `ddev` prefix and from the `next/` directory.** | ||||||
|
|
||||||
| ```bash | ||||||
| # Initial setup (idempotent — safe to re-run; use -c flag for a clean slate) | ||||||
| ./setup-ddev.sh | ||||||
| ./setup-ddev.sh -c # drop everything and start fresh | ||||||
|
|
||||||
| # URLs after setup | ||||||
| # Backend: https://next-drupal-starterkit.ddev.site | ||||||
| # Frontend: https://frontend.ddev.site | ||||||
| ``` | ||||||
|
|
||||||
| > `localhost:3000` does not work in DDEV — use the DDEV URL above. | ||||||
|
|
||||||
| --- | ||||||
|
|
||||||
| ## Commands | ||||||
|
|
||||||
| ### Next.js (`cd next` first) | ||||||
|
|
||||||
| ```bash | ||||||
| ddev npm run dev # Dev server + GraphQL codegen in watch mode | ||||||
| ddev npm run dev:nocodegen # Dev server only | ||||||
| ddev npm run build # graphql-codegen + Next.js build | ||||||
| ddev npm run lint # ESLint check | ||||||
| ddev npm run lint:fix # ESLint auto-fix | ||||||
| ddev npm run graphql-codegen # Regenerate GraphQL types from Drupal schema | ||||||
| ddev npm run storybook # Storybook on port 6006 | ||||||
| ``` | ||||||
|
|
||||||
| ### Cypress E2E Tests (`cd next` first) | ||||||
|
|
||||||
| ```bash | ||||||
| ddev npm run build && ddev npm run start # Required before running tests | ||||||
| ddev npm run cypress:run # Run all tests headlessly | ||||||
| ddev npm run cypress:run -- --spec "cypress/e2e/my.cy.ts" # Single spec | ||||||
| ``` | ||||||
|
|
||||||
| One-time Cypress setup inside DDEV: | ||||||
| ```bash | ||||||
| ddev npx cypress install | ||||||
| ddev exec ./install-cypress-deps.sh | ||||||
| ``` | ||||||
|
|
||||||
| ### Drupal | ||||||
|
|
||||||
| ```bash | ||||||
| ddev drush cr # Clear caches | ||||||
| ddev drush uli # One-time login link | ||||||
| ddev drush mim --group=demo_content # Import demo content | ||||||
| ddev drush eshd -y && ddev drush eshs # Reset and reindex Elasticsearch | ||||||
| ddev composer install # Install PHP dependencies | ||||||
| ``` | ||||||
|
|
||||||
| ### PHP Code Quality (`cd drupal` first) | ||||||
|
|
||||||
| ```bash | ||||||
| ddev grumphp run # All checks: phpcs, phpstan, yaml/json lint, phpunit | ||||||
| ddev npm run lint # ESLint + Stylelint for custom modules/themes | ||||||
| ddev phpunit web/modules/custom/wunder_next/tests/ # Single module tests | ||||||
| ``` | ||||||
|
|
||||||
| PHP linting targets only `web/modules/custom/` and `web/themes/custom/` (configured in `grumphp.yml`). Standard: `WunderDrupal` + `WunderSecurity` (see `phpcs.xml`). PHPStan level 5 (see `phpstan.neon`). | ||||||
|
|
||||||
| --- | ||||||
|
|
||||||
| ## Key Conventions | ||||||
|
|
||||||
| ### Environment Variables | ||||||
|
|
||||||
| All Next.js env vars are declared in `next/env.ts` using `@t3-oss/env-nextjs` with Zod validation. **When adding a new env var**, you must register it in four places: | ||||||
|
|
||||||
| 1. `.ddev/config.yaml` (local dev) | ||||||
| 2. `next/env.ts` — twice: in the `server`/`client` schema and in `runtimeEnv` | ||||||
| 3. `.circleci/config.yml` | ||||||
| 4. `silta/silta-next.yml` | ||||||
|
|
||||||
| Import as: `import { env } from "@/env";` — never use `process.env` directly in app code. | ||||||
|
|
||||||
| ### GraphQL & TypeScript Types | ||||||
|
|
||||||
| GraphQL queries live in `next/src/lib/graphql/` and `next/src/lib/gql/` (generated). **Never edit files in `src/lib/gql/`** — they are gitignored and regenerated by `graphql-codegen`. | ||||||
|
|
||||||
| When you add or modify a query/fragment, the types regenerate automatically on `dev` and `build`. If the **Drupal schema itself changes**, stop and restart `dev` (or re-run `graphql-codegen`) to re-fetch the schema, then also run `ddev drush cr`. | ||||||
|
|
||||||
| ### TypeScript | ||||||
|
|
||||||
| TypeScript is intentionally loose (`strict: false`, `strictNullChecks: false`) to lower the barrier for entry. The `@/*` alias resolves to `next/src/*`. | ||||||
|
|
||||||
| ### Multilingual | ||||||
|
|
||||||
| The frontend supports three locales: `en` (default), `fi`, `sv`. All routes are nested under the `[locale]` segment in `next/src/app/[locale]/`. Translations are managed via `next-intl`. Middleware handles locale detection and route protection. | ||||||
|
|
||||||
| ### Tailwind & Components | ||||||
|
|
||||||
| Styling uses Tailwind CSS 3 with shadcn/ui (Radix UI primitives + `class-variance-authority`). Theme tokens (colors, radius) are CSS custom properties defined via Tailwind's config (`next/tailwind.config.js`). Component config is at `next/components.json`. | ||||||
|
|
||||||
| ### Silta Deployment | ||||||
|
|
||||||
| `silta/` contains Helm/Kubernetes values for Wunder's [Silta](https://wunderio.github.io/silta/) hosting. CircleCI handles CI/CD (`circleci/config.yml`). There are no GitHub Actions workflows — all CI runs on CircleCI. | ||||||
|
|
||||||
| ### Drupal Recipes | ||||||
|
|
||||||
| Drupal setup is automated via a recipe in `drupal/recipes/`. Running `./setup-ddev.sh` applies it automatically. When modifying Drupal config, export with `ddev drush cex` — config is tracked in `drupal/config/`. | ||||||
|
|
||||||
| ### Redis Caching | ||||||
|
|
||||||
| The cache handler package is `@fortedigital/nextjs-cache-handler` (`next/cache-handler.mjs`). It replaced `@neshca/cache-handler` during the Next.js 15 upgrade. It is **only active when `NODE_ENV=production`** — in development, no custom cache handler is used. | ||||||
|
|
||||||
| **Behaviour:** | ||||||
| - Cache keys are prefixed with `cache-{buildId}:` so deploys don't serve stale entries from a previous build | ||||||
| - If Redis is unavailable or the connection times out (3 s), it **falls back silently to an in-memory LRU** handler — the app keeps working | ||||||
|
||||||
| - If Redis is unavailable or the connection times out (3 s), it **falls back silently to an in-memory LRU** handler — the app keeps working | |
| - Redis connectivity (including timeouts and any fallbacks) is handled by `@fortedigital/nextjs-cache-handler` / `@redis/client`; there is currently **no custom 3 s connect-timeout or guaranteed automatic fallback to an in-memory LRU** implemented in `next/cache-handler.mjs` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,17 @@ | ||
| // @ts-check | ||
|
|
||
| import { CacheHandler } from "@neshca/cache-handler"; | ||
| import createLruHandler from "@neshca/cache-handler/local-lru"; | ||
| import createRedisHandler from "@neshca/cache-handler/redis-strings"; | ||
| import { createClient } from "redis"; | ||
| import { CacheHandler } from "@fortedigital/nextjs-cache-handler"; | ||
| import createLruHandler from "@fortedigital/nextjs-cache-handler/local-lru"; | ||
| import createRedisHandler from "@fortedigital/nextjs-cache-handler/redis-strings"; | ||
| import { createClient } from "@redis/client"; | ||
|
|
||
| // This should match the REVALIDATE_LONG value in src/lib/constants.ts | ||
| const defaultStaleAge = 60 * 10; | ||
|
|
||
| CacheHandler.onCreation(async ({ buildId }) => { | ||
| /** @type {import("redis").RedisClientType | undefined} */ | ||
| let client; | ||
| /** @type {import("@neshca/cache-handler").Handler | undefined} */ | ||
| let handler; | ||
| /** @type {any} */ | ||
| let handler = createLruHandler(); | ||
|
|
||
|
Comment on lines
11
to
15
|
||
| if ( | ||
| // Do not create the Redis handler during the build phase. | ||
|
|
@@ -68,7 +67,6 @@ CacheHandler.onCreation(async ({ buildId }) => { | |
| } | ||
|
|
||
| if (client?.isReady) { | ||
| /** @type {import("@neshca/cache-handler/redis-strings").CreateRedisStringsHandlerOptions} */ | ||
| const redisHandlerOptions = { | ||
| client, | ||
| keyPrefix: `cache-${buildId}:`, | ||
|
Comment on lines
69
to
72
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| /// <reference types="next" /> | ||
| /// <reference types="next/image-types/global" /> | ||
| /// <reference path="./.next/types/routes.d.ts" /> | ||
|
|
||
| // NOTE: This file should not be edited | ||
| // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. | ||
| // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. |
Uh oh!
There was an error while loading. Please reload this page.