Skip to main content
Freelance · 2025Live

FIXPartner

FIXPartner builds software and runs QA for companies in Vietnam. Their website had 135 blog posts that brought in most of their organic traffic. The site ran on WordPress, and Google PageSpeed scored it 17 out of 100 on mobile.

Next.js 15React 19Payload CMS 3MongoDBDockerNginxGoogle Gemini
What Was Broken

PageSpeed 17 on mobile, and the CEO gave up editing

Lighthouse Report
17Performance
58Accessibility
52Best Practices
61SEO
First Contentful Paint6.2s
Largest Contentful Paint11.4s
Total Blocking Time2,340ms
Cumulative Layout Shift0.42

The theme shipped jQuery, three slider plugins, an Elementor page builder, and six Google Fonts on every page load, whether the visitor needed any of it or not. The blog index pulled all 135 posts into one query. PageSpeed: 17 on mobile, 47 on desktop.

Editing was worse. Changing a service description meant opening an Elementor JSON block, finding the right nested object, and hoping the surrounding layout would survive. The CEO gave up on updating the site because fixing the page after each edit took longer than writing the content.

Core Web Vitals were all red. For a company whose clients find them through Google, that meant real leads lost to slow page loads.

What I Built

Six layers, one docker-compose up

I containerized everything. Clone the repo, run one command, and the database, CMS, Next.js server, and Nginx reverse proxy all come up together. Deploy to a new server the same way.

Why Payload over Elementor

Payload CMS gives the CEO a clean editing panel with live preview. She sees exactly what visitors see before publishing. Updating a service page takes five minutes now instead of an afternoon of fighting Elementor.

Why Gemini in the editor

The 135 blog posts are the company's main lead source. Gemini checks each draft for SEO problems right in the editing interface, so the writer fixes issues before publishing instead of discovering them in Search Console weeks later.

BrowserCDN + SSR pagesNginxSSL · cache · proxyNext.jsSSR + static blogPayloadCMS + live previewGeminiSEO in editorMongoDBContent store
The Migration

135 posts, zero loss

migrate.ts
$ node migrate.ts --source=wordpress --target=payload
→ Connecting to WordPress REST API...
→ Found 135 posts, 24 categories, 89 tags
→ Fetching post content and metadata...
→ Stripping Elementor shortcodes...
→ Converting inline styles to semantic HTML...
→ Uploading featured images to CDN...
→ Creating Payload CMS entries...
✓ 135/135 posts migrated
✓ 24/24 categories mapped
✓ 89/89 tags preserved
✓ 0 errors, 0 content diffs
✓ All URL slugs preserved

A Node.js script pulled all 135 posts from the WordPress REST API in a single pass: titles, body HTML, featured images, categories, tags, dates, and author data.

The script stripped Elementor shortcodes, converted inline styles to clean HTML, and re-uploaded every featured image to the new CDN. URL slugs stayed the same so Google rankings did not break.

I ran a diff on every post body before and after. The diff came back clean on all 135 posts: zero content differences, zero broken links.

What Shipped

Every page scores above 93

All 14 pages score between 93 and 100 on PageSpeed. The CEO publishes blog posts and updates service pages without touching code or calling me. The whole project took under three weeks from the first conversation to production.

1794

Mobile PageSpeed

4799

Desktop PageSpeed

135

posts migrated, zero loss

< 3 weeks

first call to production