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 16 (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 and staging both run natively on the same Plesk-managed server (no Docker) on three subdomains:
catholicdigitalcommons.org— production Next.js frontend (standalone build, Node.js)staging.catholicdigitalcommons.org— staging Next.js frontend (separate standalone build, same Node.js)cms.catholicdigitalcommons.org— WordPress admin backend (PHP-FPM managed by Plesk)
Staging shares the production WordPress backend. That keeps the staging environment lightweight (no second WP install or DB), but it also means staging-only theme/plugin testing isn't possible — both environments see the same CMS code at any moment. The deploy workflow only ships the Next.js bundle to staging for that reason.
proxy.ts sets X-Robots-Tag: noindex, nofollow on every response from any host other than catholicdigitalcommons.org / www.catholicdigitalcommons.org, so staging (and any preview / one-off subdomain) is never indexed by search engines.
The Next.js apps fetch 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 both the production and staging frontends.
Per-environment runtime env vars (WP_GRAPHQL_URL, WP_PREVIEW_SECRET, etc.) are configured in the Plesk panel for each Node.js app, not in .env.local files on disk. NEXT_PUBLIC_SITE_URL is per-environment too but is baked into the client bundle at build time (different value per workflow run, see below).
The deploy workflow (.github/workflows/deploy.yml) is unified for both environments:
release: published— automatically deploys to productionworkflow_dispatch— user picksproductionorstaging(default:staging)
Production-only steps (WP theme + plugin tarballs, plugin activation) are gated behind an environment check. Staging deploys ship only the Next.js bundle so they can't overwrite production WordPress code.
Triggering deploys from the command line (no UI dropdown needed):
# Deploy to staging (matches the dropdown default)
gh workflow run deploy.yml --ref main -f environment=staging
# Deploy to production
gh workflow run deploy.yml --ref main -f environment=production
# No -f → uses the workflow default (staging)
gh workflow run deploy.yml --ref mainA separate workflow (.github/workflows/pr-build.yml) runs next build on every pull request as a required check, so build-only failures (file-name conflicts, missing imports, type errors) are caught pre-merge instead of at deploy time.
Required GitHub repository configuration:
Secrets (encrypted, used as credentials):
| Secret | Description |
|---|---|
VPS_HOST |
VPS IP address or hostname |
VPS_USERNAME |
SSH username |
VPS_SSH_KEY |
SSH private key for deployment |
WP_APP_USERNAME |
WordPress application-password username (for plugin activation) |
WP_APP_PASSWORD |
WordPress application password |
Variables (plain config, visible in workflow logs):
| Variable | Description |
|---|---|
WP_GRAPHQL_URL |
WordPress GraphQL endpoint (e.g. https://cms.catholicdigitalcommons.org/graphql) |
WP_REST_URL |
WordPress REST root (e.g. https://cms.catholicdigitalcommons.org/wp-json) |
NEXT_PUBLIC_SITE_URL_PROD |
Public URL of the production site (e.g. https://catholicdigitalcommons.org) |
NEXT_PUBLIC_SITE_URL_STAGING |
Public URL of the staging site (e.g. https://staging.catholicdigitalcommons.org) |
VPS_APP_DIR |
Production Next.js app directory on the VPS |
VPS_STAGING_APP_DIR |
Staging Next.js app directory on the VPS |
WP_THEME_DIR |
WordPress theme directory (e.g. /var/www/vhosts/.../wp-content/themes) |
WP_PLUGINS_DIR |
WordPress plugins directory (e.g. /var/www/vhosts/.../wp-content/plugins) |
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.