From fed9c97ddb914d1faa536012b91be6e9eb7598b7 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Mon, 11 May 2026 11:12:21 +0300 Subject: [PATCH] feat: URL-driven catchall routing, drop sidebar nav, split export build - app/[[...slug]]/page.tsx replaces app/page.tsx; activeSlug from URL params - SidebarNav and MobileNav removed from main layout (sections accordion is the nav) - next.config.ts: output:export controlled by STATIC_EXPORT env var instead of NODE_ENV - package.json: yarn build is standard Next.js build; yarn export is STATIC_EXPORT=true - Mock API route: force-static + generateStaticParams for output:export compatibility --- app/[[...slug]]/page.tsx | 50 +++++++++++++++++++ .../collections/[collection]/records/route.ts | 6 ++- next.config.ts | 7 +-- package.json | 1 + 4 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 app/[[...slug]]/page.tsx diff --git a/app/[[...slug]]/page.tsx b/app/[[...slug]]/page.tsx new file mode 100644 index 0000000..7505816 --- /dev/null +++ b/app/[[...slug]]/page.tsx @@ -0,0 +1,50 @@ +import { notFound } from 'next/navigation'; +import type { SectionRecord } from '$entities/Section'; +import { getCollection } from '$shared/api'; +import { SectionFactory } from '$widgets/SectionFactory'; +import { SectionsAccordion } from '$widgets/SectionsAccordion'; + +/** + * Optional catchall: `/` → first section, `/:slug` → that section. + */ +export async function generateStaticParams() { + const { items: sections } = await getCollection('sections', { + sort: 'order', + }); + return [{}, ...sections.map((s) => ({ slug: [s.slug] }))]; +} + +type Props = { + params: Promise<{ slug?: string[] }>; +}; + +/** + * Portfolio page — one route per section, sections list always visible. + */ +export default async function SectionPage({ params }: Props) { + const { slug } = await params; + + const { items: sections } = await getCollection('sections', { + sort: 'order', + }); + + if (sections.length === 0) { + notFound(); + } + + const activeSlug = slug?.[0] ?? sections[0].slug; + + if (!sections.some((s) => s.slug === activeSlug)) { + notFound(); + } + + return ( +
+ + {sections.map((s) => ( + + ))} + +
+ ); +} diff --git a/app/api/collections/[collection]/records/route.ts b/app/api/collections/[collection]/records/route.ts index 8fb18ed..2c24ab6 100644 --- a/app/api/collections/[collection]/records/route.ts +++ b/app/api/collections/[collection]/records/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; -export const dynamic = 'force-dynamic'; +export const dynamic = 'force-static'; const base = { created: '', updated: '' }; @@ -241,6 +241,10 @@ const FIXTURES: Record = { ], }; +export function generateStaticParams() { + return Object.keys(FIXTURES).map((collection) => ({ collection })); +} + /** * Mock API route handler for PocketBase collection records. * Returns fixture data shaped as a PocketBase list response. diff --git a/next.config.ts b/next.config.ts index 31d6a9f..9bbdb8d 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,10 +1,11 @@ import type { NextConfig } from 'next'; -const isExport = process.env.NODE_ENV === 'production'; +/* output: 'export' is opt-in via STATIC_EXPORT=true. + * Set this in CI/deploy — not locally — so the mock API route works + * during development and local builds. */ +const isExport = process.env.STATIC_EXPORT === 'true'; const nextConfig: NextConfig = { - /* output: 'export' only applies at build time — enabling it in dev mode - * breaks route handlers (incompatible with force-dynamic in Next.js 16) */ ...(isExport ? { output: 'export' } : {}), images: { unoptimized: true }, }; diff --git a/package.json b/package.json index 70adbe8..8ec288a 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "next dev", "build": "next build", + "export": "STATIC_EXPORT=true next build", "start": "next start", "lint": "biome lint --write .", "format": "biome format --write .",