The official website for the Catholic Digital Commons Foundation (CDCF), built with Next.js and headless WordPress. This site serves as the public-facing portal for the foundation, showcasing projects, community resources, news, and governance information.
- Framework: Next.js 15 (App Router, React Server Components)
- CMS: WordPress (headless) with WPGraphQL + ACF + Polylang
- Styling: Tailwind CSS v4 + custom CDCF brand system
- i18n: next-intl (UI chrome) + Polylang (CMS content)
- Translation Management: Weblate (for
messages/*.jsonUI strings) - Development: Docker Compose (WordPress + MariaDB + Next.js + Nginx)
- Production: Native on Plesk (WordPress + Next.js standalone) with GitHub Actions CI/CD
- Node.js 22+
- npm 10+
- Docker & Docker Compose (for local development)
# Clone the repository
git clone https://github.com/CatholicOS-org/cdcf-website.git
cd cdcf-website
# Install frontend dependencies
npm install
# Copy environment template
cp .env.local.example .env.localEdit .env.local:
| Variable | Description |
|---|---|
WP_GRAPHQL_URL |
WordPress GraphQL endpoint (e.g. http://localhost/graphql or http://wordpress/graphql in Docker) |
WP_PREVIEW_SECRET |
Shared secret for Next.js draft mode preview |
WP_DB_ROOT_PASSWORD |
MariaDB root password |
WP_DB_NAME |
WordPress database name (default: wordpress) |
WP_DB_USER |
WordPress database user (default: wordpress) |
WP_DB_PASSWORD |
WordPress database password |
The recommended way to develop is with Docker, which starts WordPress, MariaDB, Next.js, and Nginx together:
docker compose up --build- Next.js frontend: http://localhost (via Nginx) or http://localhost:3000 (direct)
- WordPress admin: http://localhost/wp-admin
- GraphQL endpoint: http://localhost/graphql
If WordPress is already running elsewhere (e.g. a staging server), you can run just the Next.js frontend:
# Set WP_GRAPHQL_URL in .env.local to point at your WordPress instance
npm run devOpen http://localhost:3000.
Using Docker (automatic): The wp-init service in docker-compose.yml runs wordpress/init.sh on first boot, which automatically:
- Installs WordPress core with admin credentials from env vars
- Installs and activates all required plugins (WPGraphQL, ACF, WPGraphQL for ACF, Polylang, WPGraphQL Polylang)
- Activates the
cdcf-headlesstheme - Configures all 6 Polylang languages
- Creates all pages (Home, About, Projects, Community, Blog, Contact) with correct templates
- Seeds ACF field content and sample CPT entries (projects, team members, stat items, etc.)
- Optionally bulk-translates all content if
OPENAI_API_KEYis set in.env
The script is idempotent — if WordPress is already installed, it skips everything.
Manual setup (without Docker): If installing WordPress natively (e.g. via Plesk), you need to:
-
Install and activate required plugins:
- WPGraphQL
- Advanced Custom Fields (ACF)
- WPGraphQL for ACF (download from GitHub releases)
- Polylang
- WPGraphQL Polylang (download from GitHub releases)
-
Activate the headless theme: copy
wordpress/themes/cdcf-headless/intowp-content/themes/and activate CDCF Headless in Appearance > Themes -
Configure Polylang languages: go to Languages > Settings and add: English (default), Italian, Spanish, French, Portuguese, German
-
Create pages with templates:
- Create pages for Home, About, Projects, Community, Blog, Contact
- Assign the corresponding page template to each (e.g. Home page → "Home" template)
- Fill in the ACF fields (hero section, CTA, etc.) that appear for each template
-
Create content:
- Add projects, team members, sponsors, community channels, and stat items as CPT entries
- Link them to pages via the relationship fields in each page template's ACF group
npm run build
npm startcdcf-website/
├── app/
│ ├── [lang]/ # i18n dynamic segment
│ │ ├── layout.tsx # Root layout with providers
│ │ └── [[...slug]]/ # Catch-all page renderer
│ │ └── page.tsx
│ └── api/
│ ├── preview/route.ts # Draft mode endpoint for WP previews
│ └── revalidate/route.ts # On-demand ISR webhook
├── components/
│ ├── Header.tsx # Site header with nav + language switcher
│ ├── Footer.tsx # Multi-column footer
│ ├── Logo.tsx # SVG logo wrapper
│ ├── LanguageSwitcher.tsx # Locale dropdown
│ └── sections/ # Page section components
│ ├── PageRenderer.tsx # Template-based section orchestrator
│ ├── HeroBanner.tsx # Full-width hero section
│ ├── TextSection.tsx # Text block with heading + body
│ ├── RichContent.tsx # Two-column text + image layout
│ ├── CallToAction.tsx # CTA banner / card / inline
│ ├── StatsBar.tsx # Statistics counter row
│ ├── ProjectGrid.tsx # Project card grid
│ ├── CommunitySection.tsx # Community channel cards
│ ├── GovernanceSection.tsx # Team member grid
│ ├── BlogFeed.tsx # Blog post listing
│ └── SponsorGrid.tsx # Sponsor logos grid
├── lib/
│ └── wordpress/
│ ├── client.ts # wpQuery() GraphQL fetch wrapper
│ ├── queries.ts # GraphQL query strings
│ ├── types.ts # TypeScript interfaces for WP data
│ └── api.ts # Typed API functions (getPage, getPosts, etc.)
├── src/
│ └── i18n/
│ ├── routing.ts # Locale list + routing config
│ ├── request.ts # Per-request message loading
│ └── navigation.ts # Typed navigation helpers
├── messages/ # UI translation strings (managed via Weblate)
│ ├── en.json # English (source)
│ ├── it.json # Italian
│ ├── es.json # Spanish
│ ├── fr.json # French
│ ├── pt.json # Portuguese
│ └── de.json # German
├── css/
│ └── globals.css # Tailwind imports + brand utilities
├── public/
│ └── logo.svg # CDCF globe/cross logo
├── wordpress/
│ └── themes/
│ └── cdcf-headless/ # Headless WordPress theme
│ ├── style.css # Theme metadata
│ ├── index.php # Redirect to Next.js frontend
│ └── functions.php # CPTs, ACF fields, Polylang, CORS, preview
├── nginx/
│ └── default.conf # Nginx reverse proxy (Next.js + WordPress)
├── .github/
│ └── workflows/
│ └── deploy.yml # CI/CD pipeline
├── Dockerfile # Multi-stage Next.js Docker build
├── docker-compose.yml # WordPress + MariaDB + Next.js + Nginx
├── tailwind.config.ts # Brand colors + fonts
├── next.config.ts # Next.js configuration
└── package.json
- WordPress manages all CMS content — pages, posts, projects, team members, sponsors, etc.
- ACF field groups are registered programmatically in
functions.phpand provide structured fields for each page template (hero section, CTA, relationships to CPTs). - WPGraphQL exposes all content (including ACF fields and Polylang translations) via a
/graphqlendpoint. - Next.js fetches content from the GraphQL API at build/request time using the
lib/wordpress/client library. - PageRenderer maps page templates to fixed section layouts — each template renders its sections in a predetermined order using data from ACF fields and related CPTs.
- In development, Nginx routes requests on a single
localhostdomain: WordPress paths (/wp-admin,/graphql,/wp-content) go to WordPress; everything else goes to Next.js. In production, WordPress and Next.js run on separate subdomains managed by Plesk.
| Page Template | Sections (fixed order) |
|---|---|
| Home | Hero, Stats, Featured Projects, Sponsors, CTA |
| About | Hero, Content, Team/Governance, CTA |
| Projects | Hero, Project Grid, CTA |
| Community | Hero, Channels, Team/Governance, CTA |
| Blog | Hero, Blog Feed |
| Contact | Hero, Content, CTA |
| CPT | Purpose | Key ACF Fields |
|---|---|---|
project |
Foundation projects | status, repoUrl, projectUrl, license, category |
team_member |
Team/governance members | role, title, linkedinUrl, githubUrl |
sponsor |
Sponsors and partners | tier, sponsorUrl |
community_channel |
Community platforms | icon, channelUrl, description |
stat_item |
Statistics counters | icon, number, label |
- Navigate to
/wp-adminand log in with your WordPress credentials - Edit pages: Go to Pages, select a page, and fill in the ACF fields (hero, CTA, relationships)
- Create projects: Go to Projects > Add New, fill in title, description, featured image, and ACF fields (status, repo URL, etc.)
- Manage team: Go to Team Members > Add New, fill in name, bio, photo, and role/social links
- Publish: Save/publish in WordPress. Changes appear on the frontend after ISR revalidation (default: 60 seconds) or immediately via the revalidation webhook.
- Install and configure Polylang in WordPress
- When editing any page or CPT entry, use the Polylang language meta box to create translations
- Each translation is a separate WordPress post linked to the original
- The Next.js frontend automatically fetches the correct translation based on the URL locale
This project uses a dual i18n system:
- Source files:
messages/*.json - Source language: English (
messages/en.json) - Workflow:
- Developers modify
messages/en.jsonand push tomain - Weblate watches the repo and picks up new/changed strings
- Translators translate via the Weblate web UI
- Weblate pushes translations to the
l10n-weblatebranch - PR from
l10n-weblate→mainfor review - Merge triggers rebuild and deploy
- Developers modify
- Content translations are managed in WordPress using Polylang
- Each page/post/CPT can have independent translations per locale
- Translations are fetched at render time via WPGraphQL Polylang based on the URL locale
WordPress is configured to redirect preview links to the Next.js draft mode endpoint:
GET /api/preview?secret=YOUR_SECRET&slug=about&type=page
This enables Next.js draft mode, which fetches the latest revision from WordPress (bypassing ISR cache).
When content is published in WordPress, a webhook can trigger immediate cache invalidation:
curl -X POST http://localhost:3000/api/revalidate \
-H "Content-Type: application/json" \
-d '{"secret": "YOUR_SECRET", "path": "/about"}'You can set this up as a WordPress publish hook (e.g. via the WP Webhooks plugin or a custom save_post action).
- Add the locale code to
src/i18n/routing.tsin thelocalesarray - Create
messages/<locale>.json(copy frommessages/en.json) - Add the locale label in
components/LanguageSwitcher.tsx→localeLabels - Add the locale mapping in
lib/wordpress/api.ts→LOCALE_MAP - Add the language in WordPress via Polylang settings
- Configure the new language in Weblate for UI string translation
Production runs natively on a Plesk-managed server (no Docker) with two subdomains:
catholicdigitalcommons.org— Next.js frontend (standalone build running via Node.js)cms.catholicdigitalcommons.org— WordPress admin backend (PHP-FPM managed by Plesk)
WordPress and Next.js share the same MariaDB instance already running on the server. Plesk manages Nginx, SSL certificates, and PHP-FPM.
The Next.js app fetches content from WordPress via WP_GRAPHQL_URL=https://cms.catholicdigitalcommons.org/graphql. The WordPress theme's CORS headers (registered in functions.php) allow cross-origin GraphQL requests from the frontend subdomain.
The deploy workflow (.github/workflows/deploy.yml) triggers when a GitHub release is published. It builds the Next.js standalone output in CI, then SCPs the artifacts to the VPS (no repo clone needed on the server).
Required GitHub Secrets:
| Secret | Description |
|---|---|
WP_GRAPHQL_URL |
WordPress GraphQL endpoint (e.g. https://cms.catholicdigitalcommons.org/graphql) |
WP_PREVIEW_SECRET |
Shared secret for preview/revalidation |
VPS_HOST |
VPS IP address or hostname |
VPS_USERNAME |
SSH username |
VPS_SSH_KEY |
SSH private key for deployment |
VPS_APP_DIR |
Directory on the VPS where the Next.js standalone app runs |
WP_THEME_DIR |
WordPress theme directory (e.g. /var/www/vhosts/.../wp-content/themes/cdcf-headless) |
Docker Compose is used for local development to run the full stack:
# Build and run all services
docker compose up --build -d
# View logs
docker compose logs -f
# Stop all services
docker compose downData is persisted in Docker named volumes (db_data for MariaDB, wordpress_data for WordPress uploads/plugins).
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Make your changes
- Test locally with
npm run devandnpm run build - Submit a pull request
All rights reserved. Copyright Catholic Digital Commons Foundation.