Date: 21 January 2026 Objective: Make optional collections effectively disabled by default, wire Navigation Settings to real behavior, and add Archive-item-based custom content type templates with enable/seed flows.
- Added collection enablement resolver based on Navigation Settings overrides + default-enabled slugs
- Disabled collections are hidden from admin nav, blocked by access rules, and cannot be seeded
- Defaults now align with Page/Post + taxonomy + archive items as enabled
- Navigation endpoint now applies enablement rules and always shows section manager links
- Collection Manager defaults reflect enablement rules (not just admin hidden)
- Added archive icon mapping for archive items
- Added templates for Services, Courses, Case Studies, Galleries, FAQs, Testimonials
- Enabled/Available handling shown in Collections manager without changing layout
- New content-type seed endpoint for template seed items
- Related content blocks renamed from “Artifacts” to “Related Items”
- People/Places admin group moved to Collections
- Removed stray museum references from runtime UI text where applicable
Enablement + Navigation
apps/cms/src/lib/collectionVisibility.tsapps/cms/src/lib/navigationConfig.tsapps/cms/src/endpoints/navigation.tsapps/cms/src/components/CollectionManagerField.tsx
Access Rules + Seeding
apps/cms/src/collections/ArchiveItems.tsapps/cms/src/collections/People.tsapps/cms/src/collections/Places.tsapps/cms/src/collections/Events.tsapps/cms/src/collections/Products.tsapps/cms/src/collections/ProductCategories.tsapps/cms/src/collections/ProductCollections.tsapps/cms/src/collections/Categories.tsapps/cms/src/collections/Tags.tsapps/cms/src/collections/ContentTypes.tsapps/cms/src/collections/CustomItems.tsapps/cms/src/endpoints/seed.ts
Templates + UI
apps/cms/src/collection-templates/types.tsapps/cms/src/collection-templates/templates/service.tsapps/cms/src/collection-templates/templates/course.tsapps/cms/src/collection-templates/templates/case-study.tsapps/cms/src/collection-templates/templates/gallery.tsapps/cms/src/collection-templates/templates/faq.tsapps/cms/src/collection-templates/templates/testimonial.tsapps/cms/src/collection-templates/templates/index.tsapps/cms/src/components/SectionCollectionTemplates.tsxapps/cms/src/endpoints/collectionTemplates.ts
Label/UI Cleanups
apps/web/src/components/PersonRenderer.tsxapps/web/src/components/PlaceRenderer.tsxapps/cms/src/components/PersonRenderer.tsxapps/cms/src/components/PlaceRenderer.tsxapps/cms/src/admin/navData.tsapps/cms/src/admin/TwoPanelNav.tsx
- No tests run during this update.
- Preset rename cleanup (museum → archive) is tracked in a separate commit.
Date: 19 January 2026 Objective: Implement a comprehensive shared taxonomy system with hierarchical categories across all content collections.
- Added parent field to Categories collection for unlimited nesting depth
- Updated nestedDocsPlugin configuration to support category hierarchies
- Categories now support parent-child relationships (e.g., History > Ancient History > Ancient Rome)
- Categories and Tags are now shared across:
- Posts
- Archive Items
- Events
- People
- Custom Items
- Separate taxonomy for ecommerce (Product Categories & Product Collections)
- New "Taxonomy" section in admin navigation (separate from Content)
- Categories and Tags moved from "Content" group to "Taxonomy" group
- Improved mobile accessibility with dedicated taxonomy section
- Categories and Tags now count usage across all collections
- Sidebar displays breakdown by collection type:
- Total Items
- Posts Count
- Archive Items Count
- Events Count
- People Count
- Custom Items Count
- Updated revalidation hooks for:
- Archive Items
- Events
- People
- All collections now properly invalidate taxonomy-related cache tags
- Cross-collection filtering support on frontend
Collections:
apps/cms/src/collections/Categories.ts- Added parent field, hierarchical support, cross-collection countingapps/cms/src/collections/Tags.ts- Added cross-collection countingapps/cms/src/collections/ArchiveItems.ts- Added taxonomy revalidation hooksapps/cms/src/collections/Events.ts- Added taxonomy revalidation hooksapps/cms/src/collections/People.ts- Added taxonomy revalidation hooks
Configuration:
apps/cms/src/payload.config.ts- Updated nestedDocsPlugin for categoriesapps/cms/src/admin/navData.ts- Added support for nested navigation items
Templates:
- Event and Archive Item templates already had taxonomy fields (no changes needed)
- People collection already had taxonomy fields (no changes needed)
✅ Unified Content Discovery - Find all content about a topic across different types ✅ Hierarchical Organisation - Nested categories prevent flat, overwhelming lists ✅ Better Mobile UX - Dedicated Taxonomy section accessible on all devices ✅ Cross-Collection Filtering - Filter by category/tag across Posts, Events, People, etc. ✅ Flexible Customisation - Custom Content Types automatically inherit taxonomy ✅ Improved Analytics - See exactly where each category/tag is used
- Create comprehensive seed data demonstrating hierarchical categories
- Add "Manage Categories/Tags" buttons to collection list pages (mobile UX enhancement)
- Implement frontend category archive pages with cross-collection content
- Add breadcrumb navigation for hierarchical categories
- Update documentation with taxonomy usage examples
Objective: Consolidate the separate Payload CMS (apps/cms) and Next.js frontend (apps/web) applications into a single unified Next.js application.
Requested by: User Date Started: 16 January 2026
headless-cms/
├── apps/
│ ├── cms/ # Payload CMS on port 3000
│ └── web/ # Next.js frontend on port 3001
Problems with the two-app approach:
-
Unnecessary Complexity - Two separate Next.js apps means double the configuration, dependencies, and maintenance burden.
-
Network Overhead - The frontend had to make REST API calls to the CMS for all content, adding latency and complexity.
-
Revalidation Complexity - Required webhook-based revalidation where the CMS would POST to the frontend's
/api/revalidateendpoint to trigger cache invalidation. -
Deployment Overhead - Two separate deployments to manage, with potential CORS issues and inter-service communication concerns.
-
Development Experience - Running two dev servers simultaneously, managing environment variables for cross-app communication.
headless-cms/
├── apps/
│ └── cms/ # Payload CMS + Frontend on port 3000
│ ├── src/app/(payload)/ # Admin panel routes
│ └── src/app/(frontend)/ # Public website routes
Benefits of consolidation:
-
Direct Database Access - Frontend uses Payload's Local API (
getPayload()) for zero-network-overhead data fetching. -
Simplified Revalidation - Can call
revalidatePath()andrevalidateTag()directly in Payload hooks without webhooks. -
Single Deployment - One application to deploy, configure, and monitor.
-
Shared Code - Components, utilities, and types naturally shared between admin and frontend.
-
Better DX - Single dev server, unified configuration, simpler debugging.
-
Created frontend route group -
apps/cms/src/app/(frontend)/with its own layout -
Created Payload Local API utilities -
apps/cms/src/lib/payload-api.tswith functions:getPages,getPageBySluggetPosts,getPostBySluggetArtifacts,getArtifactBySluggetPeople,getPlaces,getMuseumCollections,getCustomItemsgetHeader,getFooter,getSettings
-
Created revalidation utilities -
apps/cms/src/lib/revalidate.tswith direct cache invalidation functions -
Updated collection hooks - Modified
Pages.ts,Posts.ts,ArchiveItems.tsto use direct revalidation instead of webhook calls -
Updated global hooks - Modified
Header.ts,Footer.ts,Settings.tsfor direct revalidation -
Copied frontend components - All renderers (Page, Post, Archive Item, etc.) and block components
-
Created frontend pages:
/- Home page/[slug]- Dynamic pages/blogand/blog/[slug]- Blog listing and detail/archive-itemsand/archive-items/[slug]- Archive items listing and detail
-
Created API routes:
/api/draft- Draft mode for preview/api/exit-draft- Exit draft mode/api/revalidate- External revalidation endpoint (for compatibility)
-
Updated Next.js config - Added
transpilePackages, updated headers for preview iframe support -
Updated tsconfig - Added
@payload-configpath alias
The live preview iframe is now showing content, but there are still issues to resolve:
- Preview URL Configuration - Need to verify the correct preview URLs are being generated
- Content Rendering - Need to test that all block types render correctly in preview mode
- Real-time Updates - Need to verify live preview updates work when editing content
-
X-Frame-Options Header - Preview was blocked because Next.js set
X-Frame-Options: DENY. Fixed by changing toSAMEORIGINfor all routes innext.config.mjs. -
Tailwind v4 Configuration - Build was failing with "require is not defined" error because the old
tailwind.config.tswas being loaded via@configdirective. Fixed by:- Updating
globals.cssto use Tailwind v4's@plugindirective for typography - Adding
@themeblock with custom colour and font variables - Removing the
@config "../../tailwind.config.ts"directive
- Updating
Error: You're importing a component that needs "revalidatePath". That only works in a Server Component
Resolution: This was addressed by restructuring how components are imported and ensuring proper server/client boundaries.
- Test live preview functionality end-to-end
- Verify revalidation works correctly after content changes
- Test draft mode for unpublished content
- Debug any remaining preview issues
- Create remaining frontend pages (people, places, collections, items)
- Copy and update
not-found.tsxanderror.tsxfor all routes - Test all content types render correctly
- Verify image optimisation works
- Remove or archive
apps/webdirectory after full migration - Remove unused
tailwind.config.tsfile (no longer needed with Tailwind v4) - Update environment variable documentation
- Clean up any unused dependencies
Note: When this migration is complete, the following documentation must be updated:
- Update architecture diagram
- Remove references to separate web app
- Update development instructions (single
pnpm devcommand) - Update deployment instructions
- Update environment variables section
- Document the new unified architecture
- Update folder structure documentation
- Document the
(frontend)and(payload)route groups - Update any references to REST API (now uses Local API)
- Document revalidation approach (direct vs webhook)
apps/cms/src/app/(frontend)/layout.tsxapps/cms/src/app/(frontend)/page.tsxapps/cms/src/app/(frontend)/[slug]/page.tsxapps/cms/src/app/(frontend)/blog/page.tsxapps/cms/src/app/(frontend)/blog/[slug]/page.tsxapps/cms/src/app/(frontend)/archive-items/page.tsxapps/cms/src/app/(frontend)/archive-items/[slug]/page.tsxapps/cms/src/app/(frontend)/not-found.tsxapps/cms/src/app/(frontend)/error.tsxapps/cms/src/app/(frontend)/api/draft/route.tsapps/cms/src/app/(frontend)/api/exit-draft/route.tsapps/cms/src/app/(frontend)/api/revalidate/route.tsapps/cms/src/lib/payload-api.tsapps/cms/src/lib/revalidate.tsapps/cms/src/components/RenderBlocksClient.tsx
apps/cms/src/payload.config.ts- No changes yet neededapps/cms/src/collections/Pages.ts- Updated revalidation hooksapps/cms/src/collections/Posts.ts- Updated revalidation hooksapps/cms/src/collections/Artifacts.ts- Updated revalidation hooksapps/cms/src/globals/Header.ts- Updated revalidation hooksapps/cms/src/globals/Footer.ts- Updated revalidation hooksapps/cms/src/globals/Settings.ts- Updated revalidation hooksapps/cms/src/utils/preview.ts- Updated URLs for same-originapps/cms/next.config.mjs- Added transpilePackages, updated headers to allow SAMEORIGIN for iframe previewapps/cms/tsconfig.json- Added @payload-config aliasapps/cms/src/components/blocks/ArchiveBlock.tsx- Updated importsapps/cms/src/styles/globals.css- Updated for Tailwind v4 (removed @config, added @plugin and @theme)
- All block components (
HeroBlock,ContentBlock, etc.) - All renderer components (
PageRenderer,PostRenderer, etc.) Header.tsx,Footer.tsxRichText.tsxstyles/globals.css
The new architecture uses Next.js route groups:
(payload)- Admin panel routes (existing)(frontend)- Public website routes (new)
Route groups don't affect URLs - they're for organisation and can have separate layouts.
Before (REST):
const response = await fetch(`${CMS_URL}/api/pages?where[slug][equals]=${slug}`)
const data = await response.json()After (Local API):
const payload = await getPayload({ config: configPromise })
const result = await payload.find({
collection: 'pages',
where: { slug: { equals: slug } },
})Before (Webhook):
// In Payload hook
await fetch(`${SITE_URL}/api/revalidate`, {
method: 'POST',
body: JSON.stringify({ collection: 'pages', slug }),
})After (Direct):
// In Payload hook
import { revalidatePage } from '../lib/revalidate'
revalidatePage(slug)