QA & Troubleshooting: Sync, Beacons, and Verification
Ensure your learning content publishes correctly and renders as expected. This module covers quality assurance workflows for content authors, including sync troubleshooting, using debug beacons to verify content, validation techniques, and resolving common issues that arise during the content lifecycle.
Learning Objectives
- Troubleshoot HubDB sync failures and API errors
- Use debug beacons to verify content rendering
- Validate metadata and content completeness
- Identify and resolve common authoring issues
- Test content across different environments
- Apply systematic QA workflows before publishing
- Debug Cloudflare WAF blocks and content restrictions
Prerequisites
- Completion of "Authoring Basics: Modules, Front Matter, and Sync" module
- Familiarity with the content sync workflow (
npm run sync:content
) - Access to
.env
configuration with HubSpot tokens - Basic understanding of HTTP status codes
- Command-line proficiency (grep, curl, etc.)
Scenario: Publishing a Module with Confidence
You've authored a new module about Kubernetes troubleshooting and you're ready to publish. You'll run through a comprehensive QA workflow to catch issues before they reach learners, troubleshoot sync problems, verify rendering with debug mode, and confirm everything works correctly in production.
Step 1: Pre-Sync Validation Checklist
Before syncing, validate your module locally to catch common issues:
# Navigate to your module directory
cd content/modules/kubernetes-troubleshooting
# Verify front matter is valid YAML
head -30 README.md
Run through this checklist:
# 1. Check slug matches directory name
echo "Directory: $(basename $(pwd))"
grep "^slug:" README.md
# 2. Verify description length (aim for 120-160 chars)
grep "^description:" README.md | wc -c
# 3. Check all code blocks have language hints (should return empty)
grep -n '^```$' README.md
# 4. Verify required front matter fields
grep -E "^(title|slug|difficulty|estimated_minutes|tags|description):" README.md
# 5. Look for common YAML errors
head -30 README.md | grep -E "(^[^#].*:.*:)|(^[^-#].* -)"
Pre-sync validation checklist:
- ✅ Slug in front matter matches directory name exactly
- ✅ Description is 120-160 characters
- ✅ All code blocks specify language (
bash,
yaml, etc.) - ✅ Required fields present: title, slug, difficulty, estimated_minutes, tags, description
- ✅ YAML syntax correct (no unquoted colons, proper array formatting)
- ✅ No trailing spaces or tabs after front matter closing
---
- ✅ Tags are an array, not comma-separated string
- ✅ Estimated minutes is a number, not a string
Step 2: Check for Content Restrictions
Certain content patterns can trigger HubSpot's Cloudflare WAF (Web Application Firewall), causing sync failures:
# Check for potentially problematic patterns
cd /home/ubuntu/afewell-hh/hh-learn
# Look for raw Host: headers
grep -n "^Host:" content/modules/kubernetes-troubleshooting/README.md
# Look for wget commands (prefer curl)
grep -n "wget " content/modules/kubernetes-troubleshooting/README.md
# Look for raw IP addresses in headers
grep -n "X-Forwarded-For:" content/modules/kubernetes-troubleshooting/README.md
Common WAF triggers to avoid:
- Raw HTTP headers like
Host: example.com
wget
commands (prefercurl
instead)- IP addresses in header examples
- Suspicious URL patterns
- Large blocks of base64-encoded data
Safer alternatives:
# ❌ Avoid
wget http://example.com/file.tar.gz
curl -H "Host: example.com" http://127.0.0.1
# ✅ Prefer
curl -o file.tar.gz https://example.com/file.tar.gz
curl --resolve example.com:80:127.0.0.1 http://example.com
Step 3: Run the Content Sync
Execute the sync and capture output for analysis:
# Ensure you're in repository root
cd /home/ubuntu/afewell-hh/hh-learn
# Run sync and capture output
npm run sync:content 2>&1 | tee sync-output.log
Expected success output:
🔄 Starting content sync to HubDB...
✓ Updated: Kubernetes Troubleshooting
✅ Sync complete! Table published.
Summary: 1 succeeded, 0 failed
Watch for these indicators:
✅ Success markers:
✓ Updated:
followed by your module titleSummary: N succeeded, 0 failed
Table published
confirmation
❌ Failure markers:
Error:
messages with stack tracesCannot parse content
Authentication credentials not found
requiredGranularScopes
Cloudflare block detected
Step 4: Troubleshoot Sync Failures
If sync fails, diagnose the issue systematically:
# Check the full error message
cat sync-output.log | grep -A 10 "Error:"
# Verify environment variables are set
grep -E "HUBSPOT_PRIVATE_APP_TOKEN|HUBDB_MODULES_TABLE_ID" .env
# Test HubSpot API connectivity
TOKEN=$(grep HUBSPOT_PRIVATE_APP_TOKEN .env | cut -d'=' -f2)
TABLE_ID=$(grep HUBDB_MODULES_TABLE_ID .env | cut -d'=' -f2)
curl -H "Authorization: Bearer $TOKEN" \
"https://api.hubapi.com/cms/v3/hubdb/tables/${TABLE_ID}" \
2>&1 | head -20
Common sync errors and solutions:
Error: Authentication credentials not found
(401)
# Symptom: API returns 401
# Cause: Missing or invalid token
# Fix: Verify token is set and valid
grep HUBSPOT_PRIVATE_APP_TOKEN .env
# Should show: HUBSPOT_PRIVATE_APP_TOKEN=pat-na1-...
# If missing, add it:
echo "HUBSPOT_PRIVATE_APP_TOKEN=pat-na1-your-token-here" >> .env
Error: requiredGranularScopes: ["hubdb"]
# Symptom: API returns 403 with scopes error
# Cause: Private app lacks HubDB permissions
# Fix:
# 1. Visit HubSpot → Settings → Integrations → Private Apps
# 2. Select your app
# 3. Enable "HubDB" read and write scopes
# 4. Generate new token
# 5. Update .env with new token
Error: Cannot parse content. No Content-Type defined.
# Symptom: Sync fails with parse error for specific module
# Cause: Content triggers Cloudflare WAF
# Fix: Identify problematic content
grep -n "Host:" content/modules/your-module/README.md
grep -n "wget" content/modules/your-module/README.md
# Rewrite to avoid triggers (see Step 2 above)
Error: Cloudflare block detected
# Symptom: HubSpot API rejects HTML due to WAF rules
# Cause: Suspicious strings in rendered content
# Fix: Simplify content
# - Use curl --resolve instead of raw Host: headers
# - Replace wget with curl
# - Avoid large IP/CIDR blocks in examples
# - Split very large modules into smaller ones
# Temporary workaround: Upload directly via HubSpot UI
# (Document this in your PR)
Step 5: Verify HubDB Row Creation
Confirm your module was created/updated in HubDB:
# Check HubDB via API
TOKEN=$(grep HUBSPOT_PRIVATE_APP_TOKEN .env | cut -d'=' -f2)
TABLE_ID=$(grep HUBDB_MODULES_TABLE_ID .env | cut -d'=' -f2)
# List rows (filter by slug)
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.hubapi.com/cms/v3/hubdb/tables/${TABLE_ID}/rows" \
| jq '.results[] | select(.values.slug == "kubernetes-troubleshooting") | {id: .id, title: .values.title, slug: .values.slug}'
Expected output:
{
"id": "197554690681",
"title": "Kubernetes Troubleshooting",
"slug": "kubernetes-troubleshooting"
}
If your module doesn't appear:
- Sync may have failed silently
- Slug might be incorrect
- Table might not be published
Step 6: Use Debug Beacons to Verify Content
The Hedgehog Learn platform includes debug beacons that display metadata when you append ?debug=1
to any module URL:
# Construct debug URL for your module
MODULE_SLUG="kubernetes-troubleshooting"
echo "Debug URL: https://hedgehog.cloud/learn/${MODULE_SLUG}?debug=1"
# Open in browser or fetch with curl
curl -s "https://hedgehog.cloud/learn/${MODULE_SLUG}?debug=1" | grep -A 20 "debug-info"
Debug beacon displays:
- Module slug
- Module title
- Estimated minutes
has full_content? true/false
- Tags
- Difficulty
- Metadata fields
Interpreting debug output:
✅ Good beacon:
Module: kubernetes-troubleshooting
Title: Kubernetes Troubleshooting
has full_content? true
Estimated minutes: 45
Tags: kubernetes, troubleshooting, debugging, k8s
❌ Problem beacon:
Module: kubernetes-troubleshooting
Title: Kubernetes Troubleshooting
has full_content? false <-- PROBLEM!
Estimated minutes: 0 <-- PROBLEM!
Tags: (empty) <-- PROBLEM!
If has full_content? false
:
- Sync may have failed
- Content was empty or not processed
- HubDB row exists but
full_content
column is empty
Step 7: Validate Full Content Rendering
Check that your module content renders completely:
# Visit the live URL (without debug param)
echo "Live URL: https://hedgehog.cloud/learn/kubernetes-troubleshooting"
Visual validation checklist:
- ✅ Title displays correctly
- ✅ Learning objectives section present
- ✅ Prerequisites section present
- ✅ All scenario steps render with commands
- ✅ Code blocks have syntax highlighting
- ✅ Concepts section present
- ✅ Troubleshooting section present
- ✅ Resources section present
- ✅ Images load (if any)
- ✅ Links are clickable
- ✅ No raw HTML or broken Markdown
Check for rendering issues:
# Fetch rendered page
curl -s "https://hedgehog.cloud/learn/kubernetes-troubleshooting" > rendered.html
# Look for raw Markdown that didn't render
grep "^##" rendered.html # Should be empty
grep "^```" rendered.html # Should be empty
# Check if content is present
grep -c "<p>" rendered.html # Should be > 0
grep -c "<code>" rendered.html # Should be > 0 if you have code blocks
Step 8: Test Metadata Completeness
Verify all metadata fields are populated and correct:
# Check meta tags in rendered HTML
curl -s "https://hedgehog.cloud/learn/kubernetes-troubleshooting" | \
grep -E "<meta (name|property)=" | head -10
# Verify specific tags
# Title tag
curl -s "https://hedgehog.cloud/learn/kubernetes-troubleshooting" | \
grep "<title>"
# Description meta tag
curl -s "https://hedgehog.cloud/learn/kubernetes-troubleshooting" | \
grep 'meta name="description"'
# Open Graph tags
curl -s "https://hedgehog.cloud/learn/kubernetes-troubleshooting" | \
grep 'property="og:'
Expected meta tags:
<title>Kubernetes Troubleshooting | Hedgehog Learn</title>
<meta name="description" content="Your 120-160 char description...">
<meta property="og:title" content="Kubernetes Troubleshooting">
<meta property="og:description" content="...">
<meta property="og:url" content="https://hedgehog.cloud/learn/kubernetes-troubleshooting">
Step 9: Test Social Share Previews
Verify your module generates correct social previews:
# Test Open Graph with Facebook debugger
echo "Facebook Debugger: https://developers.facebook.com/tools/debug/"
echo "Enter URL: https://hedgehog.cloud/learn/kubernetes-troubleshooting"
# Test Twitter Card
echo "Twitter Validator: https://cards-dev.twitter.com/validator"
echo "Enter URL: https://hedgehog.cloud/learn/kubernetes-troubleshooting"
Social preview checklist:
- ✅ Title matches your module title
- ✅ Description matches your
description
field - ✅ Image displays (custom or default)
- ✅ Image is 1200 × 630 pixels
- ✅ No errors in debugger tools
If social image doesn't appear:
- Check
social_image
field in front matter - Verify image URL is publicly accessible (HTTPS)
- Use "Scrape Again" to refresh platform cache
Step 10: Validate Module in List View
Check that your module appears correctly in the main /learn
list:
# Visit list page
echo "List page: https://hedgehog.cloud/learn"
# Check if your module appears in API
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.hubapi.com/cms/v3/hubdb/tables/${TABLE_ID}/rows" \
| jq '.results[] | select(.values.slug == "kubernetes-troubleshooting") | {title: .values.title, difficulty: .values.difficulty, tags: .values.tags}'
List view validation:
- ✅ Module card appears with correct title
- ✅ Description displays (truncated to fit card)
- ✅ Difficulty badge shows correct level
- ✅ Estimated time displays
- ✅ Tags render correctly
- ✅ Card is clickable and links to correct URL
If module doesn't appear in list:
- Table might not be published
- Module might be marked as archived
- Filtering might hide it (check tag filters)
Step 11: Test Cross-Browser Compatibility
Verify your module renders correctly across browsers:
# Automated testing with multiple user agents
USER_AGENTS=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0"
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Safari/605.1.15"
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Firefox/121.0"
)
for UA in "${USER_AGENTS[@]}"; do
echo "Testing with: $UA"
curl -s -A "$UA" "https://hedgehog.cloud/learn/kubernetes-troubleshooting" \
-o /dev/null -w "HTTP %{http_code}\n"
done
Expected: All should return HTTP 200
Manual testing checklist:
- ✅ Chrome/Edge: All content renders, code highlighting works
- ✅ Firefox: Same behavior as Chrome
- ✅ Safari: No layout issues, fonts render correctly
- ✅ Mobile browsers: Content is responsive, readable on small screens
Step 12: Verify Links and References
Check that all links in your content are valid:
# Extract all links from your module
grep -oP '(?<=\[)[^\]]+(?=\]\([^\)]+\))' content/modules/kubernetes-troubleshooting/README.md
# Extract URLs
grep -oP '(?<=\]\()[^\)]+(?=\))' content/modules/kubernetes-troubleshooting/README.md | sort -u > links.txt
# Test each link (basic check)
while read url; do
if [[ $url == http* ]]; then
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -L "$url")
echo "$STATUS - $url"
fi
done < links.txt
Link validation checklist:
- ✅ All external links return 200 status
- ✅ Internal module references resolve correctly
- ✅ Links open in appropriate target (same/new window)
- ✅ No broken anchor links
- ✅ HTTPS preferred over HTTP
Step 13: Test Module Progress Tracking
Verify progress buttons work correctly (if applicable):
# Visit module page
echo "Module URL: https://hedgehog.cloud/learn/kubernetes-troubleshooting"
# In browser:
# 1. Click "Mark as started" button
# 2. Check browser localStorage for progress entry
# 3. Click "Mark complete" button
# 4. Verify completion is tracked
Browser console check:
// Check localStorage
localStorage.getItem('hhProgressState')
// Should show JSON with your module slug
Progress tracking checklist:
- ✅ "Mark as started" button visible
- ✅ "Mark complete" button visible
- ✅ Buttons are keyboard accessible
- ✅ Progress persists in localStorage
- ✅ If accessed from pathway, "Back to pathway" link appears
Step 14: Run Final QA Checklist
Before marking complete, run through the comprehensive QA checklist:
# Generate QA report
cat > qa-report.txt << EOF
Module QA Report: kubernetes-troubleshooting
Date: $(date +%Y-%m-%d)
✅ Pre-Sync Validation
- Slug matches directory: $(basename $(pwd))
- Front matter valid YAML
- All code blocks have language hints
- Description length: $(grep "^description:" README.md | wc -c) chars
✅ Sync Process
- Sync completed successfully
- HubDB row created/updated
- Table published
✅ Debug Beacon
- has full_content? true
- All metadata fields populated
- Estimated minutes: $(grep "^estimated_minutes:" README.md | cut -d' ' -f2)
✅ Content Rendering
- Title displays correctly
- All sections present and render
- Code blocks have syntax highlighting
- Images load (if applicable)
- Links are functional
✅ Metadata & SEO
- Meta description tag present
- Open Graph tags present
- Twitter Card tags present
- Social image displays (if set)
✅ List View
- Module appears in /learn list
- Card displays correct title and description
- Difficulty and time display
- Tags render
✅ Cross-Browser
- Chrome: OK
- Firefox: OK
- Safari: OK
- Mobile: OK
✅ Final Approval
- Ready for production
- No known issues
- Documentation updated
QA approved by: $(whoami)
EOF
cat qa-report.txt
Final acceptance criteria:
- ✅
full_content
populated and published for the module - ✅ Page renders correctly and shows "has full_content? true" in
?debug=1
banner - ✅
estimated_minutes
,tags
, and relevant metadata set - ✅ Links validated; screenshots added if helpful
- ✅ No Cloudflare WAF blocks or sync errors
- ✅ Social previews work on Facebook/Twitter/LinkedIn
- ✅ Module appears in list view with correct data
Concepts & Deep Dive
The Content Sync Architecture
Understanding how content flows from Markdown to HubDB to live pages:
1. Local Markdown Files
content/modules/<slug>/README.md
↓
Front matter (YAML) + Body (Markdown)
2. Sync Script Processing
npm run sync:content
↓
- Parse front matter
- Convert Markdown → HTML
- Build HubDB payload
- Validate required fields
3. HubDB API
POST/PATCH to HubSpot API
↓
- Create/update row by slug
- Store HTML in full_content column
- Publish table
4. HubSpot Template Rendering
Template reads HubDB row
↓
- Inject HTML into page
- Add meta tags from metadata
- Apply styling and scripts
5. Cloudflare & CDN
Request → Cloudflare → HubSpot → Response
↓
WAF rules applied, caching active
Debug Beacon Implementation
Debug beacons are conditional rendering in the template:
<!-- In module-page.html template -->
Beacons help you verify:
- HubDB data is correct
- Fields are populated
- Template is reading correct values
- Content is present (not empty)
HubDB Row Upsert Logic
The sync script uses "upsert" logic (update or insert):
// Pseudo-code for sync logic
const existingRow = await findRowBySlug(slug);
if (existingRow) {
// Update existing row
await updateRow(existingRow.id, payload);
} else {
// Create new row
await createRow(payload);
}
This means:
- Re-syncing is safe (idempotent)
- Slug is the unique identifier
- Changing slug creates a new row
- Old slug row remains unless deleted
Cloudflare WAF Rules
HubSpot uses Cloudflare's Web Application Firewall to protect against malicious content. Common triggers:
Suspicious patterns:
- Raw HTTP request/response headers
- SQL injection keywords
- XSS patterns (script tags in unusual contexts)
- IP address enumeration
- Large POST bodies with binary data
Why content gets blocked:
- Educational content often includes examples of HTTP headers, curl commands, and network debugging
- WAF can't distinguish between example and attack
- Sync fails with
Cannot parse content
error
Mitigation strategies:
- Rewrite examples to be less "suspicious"
- Use
curl --resolve
instead of rawHost:
headers - Prefer
curl
overwget
- Avoid large blocks of IPs or CIDR ranges
- Split very long modules into smaller ones
- Document WAF issues in PRs
The HubDB Modules Table Schema
Understanding the table structure helps with debugging:
Column | Type | Usage | Notes |
---|---|---|---|
hs_name |
System | Module title | Required for page routing |
hs_path |
System | URL slug | Drives /learn/<slug> routing |
slug |
Text | Duplicate slug | For cross-table references |
title |
Text | Duplicate title | For HubL filtering |
meta_description |
Text | SEO description | From description field |
difficulty |
Select | Beginner/Intermediate/Advanced | IDs: 1/2/3 |
estimated_minutes |
Number | Completion time | Integer value |
tags |
Text | Comma-separated topics | From array in front matter |
full_content |
Rich Text | HTML body | From Markdown conversion |
display_order |
Number | Sorting weight | From order field |
Key points:
hs_path
must be unique (enforced by HubDB)full_content
is HTML, not Markdown- Table must be published for changes to appear
- Select field (
difficulty
) uses numeric IDs
Sync Script Retry Logic
The sync script includes automatic retry for transient errors:
// Pseudo-code for retry logic
async function syncWithRetry(module, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await syncModule(module);
return; // Success
} catch (error) {
if (error.status === 429 || error.status === 403) {
// Rate limit or transient block
const delay = 1000 * Math.pow(2, attempt); // Exponential backoff
await sleep(delay);
} else {
throw error; // Permanent error, don't retry
}
}
}
}
Retry triggers:
- 429 (Too Many Requests)
- 403 (Temporary Cloudflare block)
No retry for:
- 401 (Authentication - permanent)
- 400 (Bad Request - content issue)
- 404 (Not Found - config issue)
Meta Tag Priority
When multiple sources provide metadata, priority order:
Front matter (highest priority)
description
→<meta name="description">
social_image
→ Open Graph/Twitter image
HubDB row data
meta_description
column- Other metadata fields
Template defaults
- Default social image from
constants.json
- Fallback descriptions
- Default social image from
HubSpot CMS settings (lowest priority)
- Global site metadata
Troubleshooting
Debug Beacon Shows has full_content? false
Symptom: Page loads but shows no content, debug beacon reports false
Cause: HubDB full_content
column is empty
Fix:
# 1. Check if sync reported success
npm run sync:content 2>&1 | grep -E "(succeeded|failed)"
# 2. Verify content is present in README
wc -l content/modules/your-module/README.md
# Should be > 50 lines typically
# 3. Check for front matter issues
head -30 content/modules/your-module/README.md
# Ensure closing --- is present
# 4. Re-sync
npm run sync:content
# 5. Verify in HubDB via API
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.hubapi.com/cms/v3/hubdb/tables/${TABLE_ID}/rows?slug=your-module" \
| jq '.results[0].values.full_content' | head -20
# Should show HTML content, not null
Module Not Appearing in List
Symptom: Sync succeeds, debug beacon works, but module invisible on /learn
Cause: Table not published, archived flag, or filtering
Fix:
# 1. Check if table is published
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.hubapi.com/cms/v3/hubdb/tables/${TABLE_ID}" \
| jq '.published'
# Should be true
# 2. Check for archived flag
grep "^archived:" content/modules/your-module/README.md
# Should not exist or be false
# 3. Check display_order
grep "^order:" content/modules/your-module/README.md
# Very high values might push it out of view
# 4. Clear browser cache and hard refresh
# Ctrl+Shift+R (Windows/Linux) or Cmd+Shift+R (Mac)
Sync Hangs or Times Out
Symptom: Sync script runs but never completes
Cause: Network issue, rate limiting, or very large content
Fix:
# 1. Check network connectivity
curl -I https://api.hubapi.com/cms/v3/hubdb/tables
# Should return 401 (auth required but endpoint reachable)
# 2. Check content size
wc -c content/modules/your-module/README.md
# > 50KB might cause issues
# 3. Kill sync and retry with single module
# Edit sync script temporarily to process only your module
# 4. Check for infinite loops in content
# Malformed Markdown might cause parser hangs
Code Blocks Render Without Syntax Highlighting
Symptom: Code appears as plain text, no colors
Cause: Missing language hint in code fence
Fix:
# Find code blocks without language
grep -n '^```$' content/modules/your-module/README.md
# Add language to each:
# Before:
# ```
# kubectl get pods
# ```
# After:
# ```bash
# kubectl get pods
# ```
# Re-sync
npm run sync:content
Social Preview Shows Default Image
Symptom: Custom social image doesn't appear in preview
Cause: Image URL not accessible or syntax error
Fix:
# 1. Verify social_image field syntax
grep "^social_image:" content/modules/your-module/README.md
# Should show: social_image: "https://..."
# 2. Test URL accessibility
IMAGE_URL=$(grep "^social_image:" content/modules/your-module/README.md | cut -d' ' -f2 | tr -d '"')
curl -I "$IMAGE_URL"
# Should return HTTP 200
# 3. Check image dimensions
curl -s "$IMAGE_URL" -o temp.jpg
file temp.jpg
# Should show 1200 x 630
# 4. Clear platform cache
# Use Facebook debugger "Scrape Again"
# 5. Re-sync
npm run sync:content
Metadata Missing in Search Results
Symptom: Module appears in list but description is generic
Cause: Description field missing or not synced
Fix:
# 1. Verify description in front matter
grep "^description:" content/modules/your-module/README.md
# 2. Check character count
grep "^description:" content/modules/your-module/README.md | wc -c
# Should be 120-160
# 3. Ensure quotes around description
# Correct:
description: "Learn Kubernetes networking..."
# Incorrect:
description: Learn Kubernetes networking...
# 4. Re-sync
npm run sync:content
Tags Not Displaying
Symptom: Module has tags in front matter but none display
Cause: Tags not formatted as array
Fix:
# Check tag format
grep -A 5 "^tags:" content/modules/your-module/README.md
# ❌ Wrong format (comma-separated string)
tags: kubernetes, networking, troubleshooting
# ✅ Correct format (YAML array)
tags:
- kubernetes
- networking
- troubleshooting
# Re-sync after fixing
npm run sync:content
Resources
- Course Authoring Guide - Complete authoring reference
- Content Sync Runbook - Technical sync documentation
- Module README Template - Starting template
- HubDB API Documentation - Official HubSpot HubDB API reference
- Facebook Sharing Debugger - Test Open Graph previews
- Twitter Card Validator - Test Twitter Card previews
- LinkedIn Post Inspector - Test LinkedIn previews
- JSON Query Tool (jq) - Command-line JSON processing
- YAML Linter - Online YAML validation
- Cloudflare WAF Documentation - Understanding WAF rules