Handoff: Strapi CMS + Project Page V2¶
Session date: 2026-05-31
What was built¶
1. Strapi v5 CMS scaffold — feat/strapi-cms (open PR)¶
Branch: feat/strapi-cms
Files added:
| Path | Purpose |
|---|---|
cms/ |
Full Strapi v5 app in plain JS (no TypeScript, avoids compile-before-config problem) |
cms/config/database.js |
Postgres connection via DATABASE_CLIENT=postgres, DATABASE_SCHEMA=strapi |
cms/config/server.js |
Host/port/app keys |
cms/config/admin.js |
Admin JWT, API token salt |
cms/config/middlewares.js |
CORS origins (localhost + PUBLIC_URL) |
cms/src/index.js |
Bootstrap: seeds iac-platform-framework entry + public read permissions on first start |
cms/src/api/project-page/ |
project-page collection type (one entry per slug) |
cms/src/api/hero-section/ |
hero-section singleton |
cms/src/api/about-section/ |
about-section singleton |
cms/src/components/shared/ |
6 reusable components: meta-field, architecture-section, architecture-bullet, code-snippet, diagram-callout, design-decision |
Dockerfile.strapi |
Multi-stage production build |
shared/cms.types.ts |
TypeScript types for Strapi REST responses |
client/src/hooks/useProjectPageCMS.ts |
React Query hook — fetches from /cms/api/project-pages?filters[slug][$eq]={slug}&populate=*, falls back to static projectV2Data on error |
scripts/postgres-init/01-strapi-schema.sql |
CREATE SCHEMA IF NOT EXISTS strapi — runs automatically on postgres container init |
Files modified:
| Path | Change |
|---|---|
client/src/pages/project-page-v2.tsx |
Replaced projectV2Data[slug] direct lookup with useProjectPageCMS(slug) hook |
docker-compose.yml |
Added strapi service (node:22-alpine, source mount, named volumes); postgres gets init SQL volume; frontend gets CMS_URL=http://portfolio-strapi:1337 |
nginx.default.conf.template |
Added /cms/ proxy location using ${CMS_URL} |
Dockerfile.frontend |
Added CMS_URL to envsubst substitution list |
server/index.ts |
Added /cms proxy middleware (http-proxy-middleware) for dev server |
vite.config.ts |
Added /cms proxy rewrite |
cms/package.json |
Strapi 5.47.0 + admin + users-permissions + provider-upload-local + pg |
Key design decisions:
- Plain JS config files (not TypeScript) — Strapi v5
developcommand cleansdist/before recompiling, so TS configs must live indist/config/after compilation. Skipping TypeScript for the Strapi app avoids this complexity entirely. placeholderData(notinitialData) in the React Query hook —initialDatamarks the query as fresh and suppresses the CMS fetch when static data exists.placeholderDatashows static data immediately but still fires the background fetch.- CMS fetch errors fall back to static
projectV2Data— pages always render even when Strapi is unreachable.
2. Feature flag + V2 migration for all 7 projects — PR #485 (open)¶
Branch: feat/project-page-v2-migration
project-page-v2flag added to backend (feature_flags.py,FeatureFlagsResponse, GET/PATCH endpoints)projectPageV2: booleanadded to frontendFeatureFlagsinterface anddefaultFlagsProjectRouteinApp.tsxnow gates onflags.projectPageV2— flag off = V1 for everyoneFEATURE_PROJECT_PAGE_V2=trueindocker-compose.yml(local dev default)- All 7 slugs added to
projectV2Datawith titleLines, tagline, overviewBody, metaFields:
| Slug | Title lines |
|---|---|
iac-platform-framework |
Infrastructure-as-Code / Platform Framework (full bespoke content) |
genai-framework |
Scalable GenAI Framework / for Unstructured Data |
data-platform-migration |
Enterprise Data Platform / Migrations |
realtime-bi-integration |
Real-time BI / Integration |
metadata-ingestion |
Metadata-Driven / Ingestion Framework |
azure-batch-optimization |
Azure Batch / Optimisation |
healthcare-cost-analysis |
Healthcare Cost / Analysis |
3. Azure infrastructure for Strapi — PR #486 (open)¶
Branch: feat/strapi-infra
container_apps.bicep¶
- New params:
deploy_cms,cms_custom_domain,deploy_cms_custom_domain - New MI:
dna-{env}-portfolio-we-id-cms(conditional ondeploy_cms) cms_managed_cert— managed TLS cert forcms.*.sven-relijveld.comcms_app— Strapi CA:- Name:
dna-{env}-portfolio-we-ca-cms - External ingress, port 1337, single revision mode
minReplicas: 0,maxReplicas: 1- Reads 5 KV secrets:
strapi-app-keys,strapi-api-token-salt,strapi-admin-jwt-secret,strapi-jwt-secret,database-url MANAGED_IDENTITY_CLIENT_ID→ CMS MI client ID for Postgres authPUBLIC_URL→ custom domain if set, otherwise ACA FQDN- Frontend CA:
CMS_URLenv var injected whendeploy_cms=true - RBAC: CMS MI → Key Vault Secrets User + AcrPull
- Outputs:
cms_app_name,cms_fqdn,cms_url,cms_identity_name,cms_principal_id
key_vault.bicep¶
4 Strapi secrets generated deterministically from KV resource ID (stable across redeploys):
| Secret name | Value |
|---|---|
strapi-app-keys |
4-part comma-separated uniqueString(kv.id, salt) |
strapi-api-token-salt |
uniqueString(kv.id, 'strapi-api-token-salt') |
strapi-admin-jwt-secret |
uniqueString(kv.id, 'strapi-admin-jwt-secret') |
strapi-jwt-secret |
uniqueString(kv.id, 'strapi-jwt-secret') |
main.bicep / bicepparams¶
- New params wired through to container_apps module
parameters.dev.bicepparam:cms_custom_domain=cms.dev.sven-relijveld.com,deploy_cms=falseparameters.prod.bicepparam:cms_custom_domain=cms.sven-relijveld.com,deploy_cms=false
app_configuration.bicep / shared.bicep¶
All 4 feature flags now fully seeded in Bicep — no manual portal work:
| Flag | dev | prod |
|---|---|---|
cv-generation |
true |
true |
portfolio-content-api |
true |
true |
hero-role-cycler |
true |
true (synced from live prod value) |
project-page-v2 |
true |
false (flip project_page_v2_prod_enabled=true when ready) |
Controlled via parameters.shared.bicepparam — push to shared.bicep to change prod values.
deploy-application.yml¶
- Builds and pushes
portfolio-cmsimage on every deploy run - Extracts
cms_app+cms_urlfrom deployment outputs - Post-deploy step: initialises
strapiPostgres schema via backend CA exec - Smoke test: probes
CMS_URL/_healthwhen CMS is deployed
Merge order¶
Merge in this order to avoid dependency issues:
- PR #486 (
feat/strapi-infra) — no app code dependency, safe to merge first feat/strapi-cms— Strapi scaffold; safe to merge alongside #486- PR #485 (
feat/project-page-v2-migration) — feature flag + all-project V2 migration
Activation sequence (after all PRs merged)¶
Dev¶
1. Run deploy-infrastructure-shared.yml
→ Creates project-page-v2 flag in App Config (dev=true, prod=false)
2. Run deploy-application (push to main or workflow_dispatch)
→ Builds portfolio-cms image and pushes to dev ACR
→ project-page-v2 flag now live in dev — V2 pages active on dev frontend
3. Edit infra/parameters.dev.bicepparam:
param deploy_cms = true
Push to infra/** → triggers Deploy Infrastructure on dev
→ Creates dna-dev-portfolio-we-ca-cms Container App
→ Creates strapi schema in Postgres
→ Injects CMS_URL into frontend CA
4. Add DNS record in shared DNS zone:
CNAME cms.dev.sven-relijveld.com → <cms-ca-fqdn>
(fqdn from Azure portal or: az containerapp show --name dna-dev-portfolio-we-ca-cms --rg dna-dev-portfolio-westeurope-rg --query properties.configuration.ingress.fqdn -o tsv)
5. Edit infra/parameters.dev.bicepparam:
param deploy_cms_custom_domain = true
Push → binds managed TLS cert
6. Open https://cms.dev.sven-relijveld.com/admin
→ Create first Strapi admin account
→ iac-platform-framework entry already seeded by bootstrap
7. Verify https://dev.sven-relijveld.com/project/iac-platform-framework
→ Network tab should show /cms/api/project-pages → 200
8. Add Strapi entries for the 6 remaining project slugs via admin UI
(or populate via API — see cms/src/index.js bootstrap for the data shape)
Prod¶
1. Edit infra/parameters.prod.bicepparam:
param deploy_cms = true
Push to infra/** on main (or v* tag) → triggers prod infra deploy
2. Add DNS: CNAME cms.sven-relijveld.com → <prod-cms-ca-fqdn>
3. Set deploy_cms_custom_domain = true → re-run infra
4. Create Strapi admin at https://cms.sven-relijveld.com/admin
5. Migrate all 7 project entries from dev Strapi to prod
(use Strapi Data Transfer: strapi transfer --to https://cms.sven-relijveld.com)
6. Flip project-page-v2 on prod:
Edit infra/parameters.shared.bicepparam:
param project_page_v2_prod_enabled = true
Push to shared infra (deploy-infrastructure-shared.yml)
What's next in code¶
| Phase | Scope | Effort |
|---|---|---|
| Frontend singletons | Replace hardcoded hero/about/footer/contact strings with Strapi API calls + static fallback. Add useHeroSection, useAboutSection hooks. |
~3h |
| Blog / speaking / announcements | New Strapi collection types + frontend routes /blog, /blog/:slug, /speaking, /speaking/:slug, /announcements |
~4h |
| Case study join | Strapi case-study collection (slug joins to Postgres project), merge on V2 page |
~2h |
| V1 deprecation | Delete project-detail.tsx + V1 branch in App.tsx once all slugs confirmed stable on prod |
~30min |
| Strapi Postgres schema grant | Add GRANT CREATE ON DATABASE portfolio TO strapi_mi to the post-deploy AAD admin step in deploy-infrastructure.yml — currently done manually via postgres-init SQL on first start |
~30min |
Recommended order¶
- Activate on dev (follow activation sequence above)
- Verify all 7 project pages end-to-end
- Activate on prod
- Frontend singletons (hero/about/footer)
- Blog + speaking routes (enables Strapi as the primary content authoring tool)
- V1 deprecation