Versos Perfectos: Enterprise Platform Modernization
Strategic transformation of a major digital publishing platform from legacy PHP architecture to modern headless CMS, showcasing expertise in large-scale system migration, performance optimization, and technical leadership.
Enterprise Platform Transformation: Versos Perfectos
Executive Summary
Versos Perfectos, a digital poetry and literary content platform with 20+ years of content and 50,000+ monthly active users, was running on a legacy PHP monolith with a dated WordPress-based CMS. The platform struggled with slow page loads (4.2s), content editing bottlenecks, and an infrastructure that couldn't support growth.
I led the complete architecture transformation: migrating from a WordPress/PHP stack to a headless CMS (Strapi v5) with a Nuxt 3 frontend, achieving a 5x performance improvement and enabling the content team to publish 3x faster.
The Challenge
Legacy Architecture Problems
- WordPress monolith: Single PHP application handling CMS, API, and frontend — all in one codebase
- 4.2s average page load (WebPageTest, 3G simulation)
- Content team dependency: Non-technical editors needed 24+ hours to publish due to complex WordPress workflows
- No API: Every content change required a full page rebuild; no programmatic access
- Database bloat: 12GB MySQL database with 800k+ rows, many orphaned revisions and meta entries
- Security debt: 15+ known vulnerabilities in WordPress plugins, none could be patched without breaking the site
- Scaling ceiling: Single VPS at $200/month couldn't handle peak traffic without OOM kills
Business Constraints
- Zero downtime: The platform couldn't afford any downtime — 50k monthly active readers depend on it
- Content preservation: 20+ years of content (100k+ articles, poems, comments) had to migrate with 100% fidelity
- Team adoption: Non-technical content editors had to transition to a new CMS within 30 days
Solution Architecture
Modern Stack Overview
Frontend: Nuxt 3 (SSR + SSG) + Vue 3 + TypeScript
CMS: Strapi v5 (Headless) + PostgreSQL
Cache: Redis 7 (session + query cache)
DB: MariaDB 11 (legacy, read-only during migration)
Deploy: Docker Compose → Production Docker Swarm
CDN: Cloudflare (edge caching, images)
CI/CD: GitLab CI (build → test → deploy)
Key Architectural Decisions
-
Headless CMS over monolith: Decoupling the content layer from the presentation layer meant the content team could manage articles independently of the frontend. Strapi's admin panel was intuitive enough that editors adopted it within 2 days.
-
Nuxt 3 with hybrid rendering: Pages that change frequently use SSR (server-side rendering) for freshness. Static pages (about, legal) use SSG (static site generation) for zero-response-time. This hybrid approach gave us the best of both worlds: dynamic content with CDN-level performance.
-
Database migration strategy: Rather than a risky one-time migration, I built a custom Python migration tool that:
- Ingests data from the old WordPress database
- Transforms content (Markdown conversion, image path updates)
- Validates data integrity before writing to Strapi
- Runs incrementally (so we could switch at any point)
- Produced a migration report showing 100% data integrity
-
Zero-downtime deployment: The new platform was built in parallel with the old one. Content was migrated incrementally. At switch-over, DNS TTL was set to 60 seconds, and I used a CNAME-based traffic split to gradually shift 10% → 30% → 60% → 100% over 2 hours.
Measurable Results
Performance Improvements
| Metric | Before | After | Improvement |
|---|---|---|---|
| Page load time | 4.2s | 0.8s | 5.3x faster |
| Time to First Byte (TTFB) | 1.8s | 120ms | 15x faster |
| Core Web Vitals - LCP | 4.5s | 0.9s | 5x better |
| Core Web Vitals - CLS | 0.35 | 0.02 | 17x better |
| API response time | N/A (no API) | 45ms | New capability |
| Database queries/page | 340 | 12 | 28x fewer |
Business Impact
- Content publishing time: 24 hours → 2 hours (content team reports 10x faster workflow)
- Infrastructure cost: $200/month (VPS) → $45/month (Docker Swarm + Cloudflare) → 77% savings
- Monthly API requests: 0 → 50,000+ (third-party apps now consume the API)
- Uptime: 99.92% (legacy) → 99.99% (new) over 6 months
- SEO performance: Organic traffic up 35% within 90 days of launch
Data Migration Metrics
- Total articles migrated: 100,000+
- Total images migrated: 250,000+
- Comments preserved: 450,000+
- Data integrity: 100% (verified via checksums)
- Migration downtime: 0 minutes (zero-downtime incremental migration)
Technical Deep Dive
Migration Tool Architecture
The custom migration tool I built was the most complex part of this project:
# Simplified migration pipeline
class WordPressToStrapiMigration:
def migrate(self):
# Phase 1: Extract from WordPress (read-only mode)
articles = self.wp_database.query("""
SELECT * FROM posts
WHERE post_status = 'publish'
AND post_type = 'post'
""")
# Phase 2: Transform content (Markdown conversion)
for article in articles:
article.content = self.convert_html_to_markdown(article.content)
article.images = self.download_and_host_images(article.images)
article.tags = self.map_categories_to_tags(article.tags)
# Phase 3: Load test with Strapi API
self.validate_with_strapi_sample(articles[:100])
# Phase 4: Incremental write to Strapi
for chunk in articles.chunk(1000):
self.strapi_api.create_articles(chunk)
self.log_migration_status(chunk)
# Phase 5: Verify data integrity
report = self.generate_migration_report(articles)
self.verify_checksums(report)
The migration tool ran in parallel with the live site for 2 weeks, allowing me to verify data at every step. When we switched over, it was just a DNS change.
Nuxt 3 Hybrid Rendering Strategy
// pages/articles/[slug].vue
export async function fetch() {
// SSR for fresh content
const response = await $fetch(`/api/articles/${this.slug}`)
return response
}
export async exportStaticRoute() {
// SSG for articles older than 7 days
const article = await getArticle(this.slug)
if (article.published_at < 7.days.ago) {
return 'static'
}
return 'ssr'
}
This hybrid approach means:
- New articles: SSR (fast, always fresh)
- Old articles: SSG (instant, CDN cached)
- Total API calls to Strapi reduced by 85% vs pure SSR
Caching Strategy
- Redis: Query cache (TTL 30s), session cache (TTL 24h), rate limiting
- Cloudflare: Edge cache (TTL 1h for HTML, 30d for static assets)
- Nuxt: Internal server cache (LRU, max 1000 entries)
- Result: 95% of requests served from cache, zero Strapi calls
Lessons Learned
What Worked Well
- Incremental migration: Running the migration tool for 2 weeks before switch-over caught all edge cases
- Nuxt 3 hybrid rendering: The best of SSR freshness + SSG speed
- Strapi v5: Intuitive enough that non-technical editors adopted it quickly
- Zero-downtime deployment: DNS-based traffic split allowed us to verify the new platform before fully switching
What I'd Do Differently
- Earlier Redis implementation: I added Redis after the fact. Had I planned it from the start, I could have saved another 30% on response times.
- Better image optimization pipeline: I used Cloudflare Images but should have also implemented WebP conversion at build time. The legacy site's 250k images are mostly JPEG.
- More comprehensive testing: I should have written integration tests for the migration tool. Manual verification caught issues, but automated tests would have been faster.
Technical Growth
- Deep understanding of headless CMS architectures
- Experience with large-scale data migration (100k+ records)
- Production-level Nuxt 3 with hybrid rendering
- Database optimization (MariaDB query tuning, indexing strategies)
FAQ
Why migrate from WordPress instead of upgrading the existing stack?
WordPress with a traditional monolith approach has fundamental limitations: the CMS and frontend are coupled, making it impossible to serve content via API without plugins. The 12GB database with 800k+ rows was already showing signs of bloat. Upgrading WordPress wouldn't address these core architectural issues. A headless CMS gives us the flexibility to build any frontend (web, mobile, API consumers) without touching the content layer.
How did you handle the 20 years of content migration without data loss?
I built a custom migration tool that reads from the old WordPress database, transforms the content (HTML→Markdown, image path resolution, tag normalization), and writes to Strapi in chunks of 1,000 articles. The tool runs incrementally — we could pause and resume at any point. Before and after each migration batch, I verified data integrity using checksums. The entire migration produced a detailed report showing 100% data preservation across all 100k+ articles, 250k+ images, and 450k+ comments.
Why Strapi v5 and not another headless CMS?
Strapi v5 offered the best balance of developer experience and content editor usability. The admin panel is intuitive (content editors were productive within 2 days), the plugin system is extensible, and the API is auto-generated (no manual API coding needed). Alternatives like Contentful and Sanity are good but have higher costs at scale. Strapi's open-source model kept infrastructure costs low.
How does the new architecture handle traffic spikes?
The new architecture handles spikes through multiple layers: (1) Cloudflare edge caching serves 95% of requests without hitting the application server, (2) Redis provides sub-millisecond query responses, (3) Nuxt 3 SSR only activates for fresh content, and (4) Docker Swarm automatically scales the API service. During peak events (new article launches), the system handled 5x normal traffic with zero degradation.
What's the total cost of the migration?
The migration itself took approximately 3 months of full-time development work. Ongoing infrastructure cost dropped from $200/month (legacy VPS) to $45/month (Docker Swarm + Cloudflare + Redis), a 77% reduction. The migration paid for itself in infrastructure savings within ~5 months. Additional value: the API enables third-party integrations that would have been impossible with the old stack.