| name | decap-cms-expert |
| description | Use this skill when working with Decap CMS (formerly Netlify CMS) configuration, OAuth authentication, collection setup, or editorial workflow. Triggers include CMS configuration issues, GitHub backend authentication problems, collection schema design, field widget configuration, or media library integration. Critical for Triunghi.md's headless CMS architecture. |
Decap CMS Expert
Overview
Provide specialized Decap CMS expertise for headless CMS configuration, GitHub backend integration, OAuth authentication flows, collection management, and editorial workflows. Handle complex Decap CMS challenges including OAuth postMessage patterns, multilingual collections, and field widget customization.
Core Capabilities
1. OAuth Authentication Flow (CRITICAL)
The Problem: Decap CMS OAuth with GitHub requires precise postMessage communication. Incorrect message formats cause silent failures where the popup closes but login doesn't complete.
OAuth Setup Requirements
GitHub OAuth App Configuration:
Client ID: Ov23liSvb4wITabAOGoo
Homepage URL: https://triunghi.md/admin
Authorization Callback URL: https://triunghi.md/api/auth
CRITICAL: Callback URL must match EXACTLY.
Cloudflare Environment Variables:
Variable: GITHUB_CLIENT_SECRET
Type: Secret/Encrypted
Location: Dashboard → Project → Settings → Environment Variables
OAuth Endpoint Implementation
File: functions/api/auth.js (at project root, NOT in static/)
INCORRECT postMessage (DOES NOT WORK):
// ❌ Decap CMS ignores JSON objects
window.opener.postMessage({
type: 'authorization_grant',
token: 'xxx'
}, origin);
CORRECT postMessage (String Format):
// ✅ Decap CMS expects STRING format
window.opener.postMessage(
'authorization:github:success:' + JSON.stringify({
token: 'xxx',
provider: 'github'
}),
'https://triunghi.md'
);
Two-Step Handshake (MANDATORY)
Wrong: Just sending success message
// ❌ CMS ignores this - user stares at login button forever
window.opener.postMessage(
'authorization:github:success:' + JSON.stringify({...}),
'https://triunghi.md'
);
Correct: Handshake then success
// ✅ Two-step handshake
// Step 1: Tell CMS auth flow started
window.opener.postMessage("authorizing:github", "https://triunghi.md");
// Step 2: Send success after small delay
setTimeout(() => {
window.opener.postMessage(
'authorization:github:success:' + JSON.stringify({
token: accessToken,
provider: 'github'
}),
'https://triunghi.md'
);
window.close();
}, 100);
Origin Handling
Wrong:
// ❌ Using window.location.origin or '*'
window.opener.postMessage(message, window.location.origin); // Fails
window.opener.postMessage(message, '*'); // Security risk
Correct:
// ✅ Explicit domain origin
window.opener.postMessage(message, 'https://triunghi.md');
Complete OAuth Function
// functions/api/auth.js
export async function onRequest(context) {
const { request, env } = context;
const url = new URL(request.url);
const code = url.searchParams.get('code');
if (!code) {
// Redirect to GitHub OAuth
const clientId = 'Ov23liSvb4wITabAOGoo';
const redirectUri = 'https://triunghi.md/api/auth';
const githubAuthUrl = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=repo,user`;
return Response.redirect(githubAuthUrl, 302);
}
// Exchange code for token
const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
client_id: 'Ov23liSvb4wITabAOGoo',
client_secret: env.GITHUB_CLIENT_SECRET,
code: code
})
});
const tokenData = await tokenResponse.json();
// Return HTML with two-step postMessage
return new Response(`
<!DOCTYPE html>
<html>
<head><title>Authentication Success</title></head>
<body>
<p>Authentication successful. Closing window...</p>
<script>
(function() {
// Step 1: Handshake
window.opener.postMessage("authorizing:github", "https://triunghi.md");
// Step 2: Success
setTimeout(function() {
window.opener.postMessage(
'authorization:github:success:' + JSON.stringify({
token: '${tokenData.access_token}',
provider: 'github'
}),
'https://triunghi.md'
);
window.close();
}, 100);
})();
</script>
</body>
</html>
`, {
headers: { 'Content-Type': 'text/html' }
});
}
Debugging OAuth
Add to static/admin/index.html:
window.addEventListener('message', function(event) {
console.log('🎯 CMS: Received postMessage from:', event.origin);
console.log('🔍 CMS: Message data:', event.data);
console.log('🔍 CMS: Message type:', typeof event.data);
});
Common Failure Symptoms:
- Popup closes, but user still sees login screen = Message format issue
- No postMessage received = Origin mismatch
- Console shows messages but CMS doesn't respond = Missing handshake
2. Collection Configuration
Multilingual Collections
Pattern: Separate collections for each language
collections:
# Romanian News
- name: "news_ro"
label: "Știri (RO)"
folder: "content/ro/news"
create: true
slug: "{{slug}}"
path: "{{slug}}/index"
media_folder: ""
public_folder: ""
format: "frontmatter"
extension: "md"
fields:
- {label: "Title", name: "title", widget: "string"}
- {label: "Summary", name: "summary", widget: "text"}
- {label: "Publish Date", name: "publishDate", widget: "datetime"}
- {label: "Draft", name: "draft", widget: "boolean", default: false}
- label: "Categories"
name: "categories"
widget: "select"
multiple: true
options: ["local", "frontiera-transport", "economie-zel", "servicii-publice", "educatie-sanatate", "national", "ue-romania"]
- label: "Formats"
name: "formats"
widget: "select"
multiple: false
options: ["stire", "analiza", "explainer", "opinie", "factcheck"]
- label: "Body"
name: "body"
widget: "markdown"
# Russian News
- name: "news_ru"
label: "Новости (RU)"
folder: "content/ru/news"
# ... similar structure
Page Bundles
For media with articles:
folder: "content/ro/news"
path: "{{slug}}/index" # Creates article-slug/index.md
media_folder: "" # Media in same folder as index.md
public_folder: ""
Nested Objects
For Cutia Ungheni:
- label: "Cutia Ungheni"
name: "cutia_ungheni"
widget: "object"
required: false
fields:
- {label: "Impact Local", name: "impact_local", widget: "text"}
- {label: "Ce se schimbă", name: "ce_se_schimba", widget: "text"}
- {label: "Termene", name: "termene", widget: "text"}
List Fields
For authors:
- label: "Authors"
name: "authors"
widget: "list"
allow_add: true
field: {label: "Author", name: "author", widget: "string"}
3. Field Widgets
Common Widgets
String: Single-line text
- {label: "Title", name: "title", widget: "string", required: true}
Text: Multi-line text
- {label: "Summary", name: "summary", widget: "text", required: true}
Markdown: Rich text editor
- {label: "Body", name: "body", widget: "markdown"}
DateTime: Date/time picker
- label: "Publish Date"
name: "publishDate"
widget: "datetime"
format: "YYYY-MM-DDTHH:mm:ssZ"
dateFormat: "DD.MM.YYYY"
timeFormat: "HH:mm"
pickerUtc: false
Boolean: Checkbox
- {label: "Draft", name: "draft", widget: "boolean", default: false}
- {label: "Featured", name: "featured", widget: "boolean", default: false}
Select: Dropdown (single)
- label: "Format"
name: "formats"
widget: "select"
multiple: false
options: ["stire", "analiza", "explainer", "opinie", "factcheck"]
Select: Multi-select
- label: "Categories"
name: "categories"
widget: "select"
multiple: true
options: ["local", "national", "ue-romania"]
Relation: Reference other content
- label: "Author"
name: "author"
widget: "relation"
collection: "authors"
searchFields: ["name"]
valueField: "slug"
displayFields: ["name"]
Image: Image upload
- {label: "Featured Image", name: "featured_image", widget: "image"}
List: Array of items
- label: "Tags"
name: "tags"
widget: "list"
allow_add: true
field: {label: "Tag", name: "tag", widget: "string"}
Object: Nested fields
- label: "Author Info"
name: "author_info"
widget: "object"
fields:
- {label: "Name", name: "name", widget: "string"}
- {label: "Email", name: "email", widget: "string"}
4. Media Library Integration
Cloudinary Configuration
media_library:
name: cloudinary
config:
cloud_name: your_cloud_name
api_key: your_api_key
Local Media Fallback
media_folder: "static/uploads"
public_folder: "/uploads"
Per-Collection Media
collections:
- name: "news_ro"
folder: "content/ro/news"
media_folder: "" # Store in page bundle
public_folder: ""
5. Editorial Workflow
Enable Workflow
publish_mode: editorial_workflow
Workflow States:
- Draft - Work in progress
- In Review - Ready for review
- Ready - Approved for publishing
Workflow Configuration
publish_mode: editorial_workflow
# Creates PR branches: cms/collection-name/slug
GitHub Backend:
- Draft = New branch created
- In Review = PR opened
- Ready = Ready to merge
- Publish = PR merged to main
6. Backend Configuration
GitHub Backend
backend:
name: github
repo: owner/repo-name
branch: main
base_url: https://triunghi.md
auth_endpoint: /api/auth
Key Fields:
repo: GitHub repository (format:owner/repo)branch: Target branch for commitsbase_url: Site URL for OAuth redirectauth_endpoint: Custom OAuth endpoint path
7. Validation & Previews
Field Validation
- label: "Email"
name: "email"
widget: "string"
pattern: ['^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', 'Must be a valid email']
Custom Previews
# static/admin/index.html
<script>
CMS.registerPreviewStyle("/admin/preview.css");
CMS.registerPreviewTemplate("news_ro", NewsPreview);
</script>
8. Common Issues & Solutions
Issue: Login popup closes but no login
Cause: Incorrect postMessage format Solution: Use string format with two-step handshake (see OAuth section)
Issue: Collections not showing
Cause: Incorrect folder path or missing content
Solution: Verify folder path matches actual content directory
Issue: Media not uploading
Cause: Cloudinary config missing or incorrect
Solution: Check media_library configuration and API keys
Issue: Changes not appearing on site
Cause: Commit succeeded but build didn't trigger Solution: Check Cloudflare Pages deployment logs
Issue: DateTime timezone issues
Cause: UTC vs local time mismatch
Solution: Set pickerUtc: false and use Europe/Chisinau timezone
Resources
references/
Contains Decap CMS reference documentation:
oauth-flow-complete.md- Complete OAuth implementation guidecollection-examples.md- Collection configuration patternswidget-reference.md- All field widget types and options
When to Use This Skill
Trigger this skill for:
- OAuth authentication issues
- Collection configuration
- Field widget setup
- Media library integration
- Editorial workflow configuration
- GitHub backend setup
- Custom preview templates
- Decap CMS debugging
- Any CMS-related development task
Essential for maintaining the Triunghi.md headless CMS editorial system.