DEV Community

Cover image for The frontend bug that hid 170 custom SEO titles for 3 months (and 3 weeks of shipping after finding it)
Serhii Kalyna
Serhii Kalyna

Posted on

The frontend bug that hid 170 custom SEO titles for 3 months (and 3 weeks of shipping after finding it)

The moment I found it

Sunday evening, week 21 of the Convertify build. I was in the middle of deploying five new SEO-optimized titles to sibling landing pages on my free image converter. SQL transaction open, five UPDATEs staged, one COMMIT away from shipping the week's biggest experiment.

Before I hit COMMIT, I did something I hadn't done in a while: I opened one of the pages I was about to update in the browser and hit view-source.

The title in the HTML wasn't the one in my database. It was the generic fallback template: Convert HEIC to JPG Online Free | Convertify. My custom title from the DB, the one carefully tuned for that page's specific intent, was nowhere to be seen.

First reaction: "I broke something with a recent deploy."

Second reaction, after git blame: this had been broken since March.

Which meant that for roughly three months, every custom SEO title I had written for the ~170 tool pages on this site had never rendered in a single search engine result. Not one.

Rest of Sunday was the fix. Rest of the following week was catching up on shipping that the fix unlocked. Here is the story.

The bug

Convertify runs on Next.js 16.2 SSG with a Rust backend + Postgresql . Landing page metadata (title, meta_description, h1, JSONB content) comes from a landing_pages table, one row per URL.

Eight route files were reading the title field wrong. One-word wrong.

// Before (broken across 8 files):
export const metadata: Metadata = {
  title: landing?.meta_title
    ?? `Convert ${from} to ${to} Online Free | Convertify`,
  description: landing?.meta_description ?? "...",
}

// After (one-word fix):
export const metadata: Metadata = {
  title: landing?.title
    ?? `Convert ${from} to ${to} Online Free | Convertify`,
  description: landing?.meta_description ?? "...",
}
Enter fullscreen mode Exit fullscreen mode

The DB column is title, not meta_title. There is no meta_title column on landing_pages. So landing?.meta_title was always undefined, and every page silently rendered the fallback template.

Files affected: app/[slug], app/compress/[format], app/crop-image, app/images-to-pdf, app/remove-background, app/remove-signature-background, app/resize-image, app/webp-remove-background.

Not affected: app/blogs/[slug]. The blog_posts table has both a title and a meta_title column, so post.meta_title ?? post.title worked correctly there. That is where the wrong pattern came from originally: I wrote the correct blog version first, copy-pasted it to landing pages, forgot that landing_pages has a different schema.

meta_description and h1 both worked. The DB column names for those matched what the frontend read. Only title was off by one word.

Three months. ~170 pages. Zero custom titles reaching Google.

The fix was landing?.meta_title becoming landing?.title in eight files. Under two minutes of typing.

Three weeks, three deploys

Once the bug was patched, the pipeline that had been silently dead was live. Every SEO change I had made in the previous three months would start propagating on the next crawl. This changed the priority of everything queued.

Sunday: the CTR anomaly template

The reason I was deploying five new titles in the first place: one page, /heif-to-jpg, was showing an anomaly in Google Search Console. Position 37.7, but click-through rate 4.5%. Typical CTR at position 37 is 0.5% or lower. Five to ten times above expected.

Query breakdown explained it. The page was picking up hif to jpg (single F typo) at CTR 12.5%, hif to jpg converter at 25%, and one very long tail hif file to jpg converter software free download at 100% (n=1, but still). The word hif is a typo variant that competitors don't optimize for, and my title happened to include both HEIF and HIF explicitly. Compound low-difficulty targeting, unintentional but effective.

I extracted the pattern: compound low-KD title targeting canonical and typo simultaneously, with device brand meta descriptions (Sony/Canon/Samsung/Windows Camera) as trust anchors. Applied it to five sibling pages: /heic-to-jpg (iPhone/Windows/Mac angle), /heic-to-png (Lossless Photoshop/Figma), /avif-to-jpg (Chrome/web), /tiff-to-jpg (Photo/Print), /webp-to-png (Keeps Transparency).

SQL deploy pattern I use for every landing page change:

BEGIN;

UPDATE landing_pages SET
  title = 'Convert HEIC to JPG Free, iPhone Windows Mac | Convertify',
  meta_description = '...',
  updated_at = NOW()
WHERE slug = 'heic-to-jpg';

-- four more UPDATEs

SELECT slug, title
FROM landing_pages
WHERE slug IN ('heic-to-jpg', 'heic-to-png', 'avif-to-jpg',
               'tiff-to-jpg', 'webp-to-png');

COMMIT;
Enter fullscreen mode Exit fullscreen mode

Verify before commit. Rollback available up to the last statement. No full-payload replacement, only the fields I intend to change. This pattern has saved me from three separate bad deploys over the last six months.

Monday: /resize-image full parity ship

Convertify already had /resize-image and /crop-image as separate pages. The problem: /resize-image only did width-based proportional resize, no way to fit an image into a specific WxH box without cropping. Users who wanted "fit this into 1080x1080 without losing any of the photo" had no path.

Backend changes: three fit modes (proportional, padding, crop), a normalize_format() helper handling 14 format aliases (jpg/jpeg, tiff/tif, heif/hif, fits/fit/fts, etc), a format_to="keep" sentinel that preserves origin format per-file for mixed uploads. Padding mode uses vipsthumbnail --size WxH plus vips gravity centre with a background color (transparent for alpha formats, white for JPG).

Frontend: new FitModePicker toggle switch component (role=switch, aria-checked, keyboard accessible), rendered only on /resize-image via early return null on other pages. Full refactor of the existing ResizePicker to remove dead branches from an earlier lock-toggle experiment.

Content: 15 SQL point-updates via jsonb_set / jsonb_insert / jsonb concat operators. No full replace. 11 sections (was 8), 17 FAQ items (was 12), schema_faq mirrored after each faq change, schema_howto 4-step flow shipped.

The strategic choice was the interesting part. Head term resize image online has DR 70+ competitors. But of the eight competitors on that SERP, only two offered fit-with-padding, and none had a custom color picker. So I positioned the page as a defensive niche ("No Cropping, Any Size") rather than fighting the head term.

Baseline locked before deploy: 2294 impressions,average position 69.4 (the biggest impression pool on the whole domain). Checkpoint set for day 21 after deploy.

Tuesday: WEBP casing audit and one durable lesson

Global cleanup of WEBP (all caps) versus WebP (mixed case) inconsistencies across six pages. Priority tier: title, meta_title, h1, meta_description on three pages. Body tier: sections and faq JSONB on two pages.

Ran the priority-tier fixes. Then ran a "safety audit" to catch anything missed. The safety audit surfaced a whole second round: three more pages had WEBP casing in their schema_howto field, which my original audit checklist had omitted. My audit had covered 7 of the 9 possible content fields.

Permanent rule after this session: casing and style audits MUST check all 9 landing_pages content fields (title, meta_title, h1, meta_description, intro, sections, faq, schema_faq, schema_howto). Not 7. Not 8. All 9. Written into the audit template that I now never skip.

Also caught legitimate uppercase that must be preserved: /webp-to-jpg sections reference "bytes 8 to 11 read WEBP" (RIFF FourCC magic bytes literal per WebP spec), /webp-to-pdf sections mention SaveAnimatedWEBP (literal ComfyUI API node identifier). Neither was a violation. Spec references keep their canonical casing.

Research-driven titles beat guess-and-check

Later the same Tuesday, third task: fix the CTR on /blogs/why-wont-heic-files-open. Baseline was Bing position 6.88 with 26 impressions over 28 days. Google 59 impressions. Top-of-page-3 territory, meaning one CTR fix could plausibly move it to top-of-page-2 territory.

Old approach for a blog CTR fix: draft two title variants by intuition, pick one by gut, deploy, wait a month, see if it moves.

New approach: launch an advanced research task with SERP analysis across 10 target queries on both Bing and Google. Get per-position framing patterns from real top-5 competitors. Identify which framing dominates position 1-3 (cause-first "missing HEVC codec" plus dual-fix). Identify what search engine rewards which signal (Bing rewards literal exact-match keywords in title, H1, and meta more strongly than Google). Identify gaps (numeric hooks and free-converter USP were underexploited by the top 5). Then pick from data-backed variants.

Deploy chose Variant 3 (numeric plus free-converter USP, query-phrase-first):

  • H1: Why HEIC Files Won't Open: Windows, Mac, Android & Photoshop Fixes (2026)
  • meta_title (58 chars): HEIC Won't Open? 3 Fixes and a Free Converter | Convertify
  • meta_description (138 chars): Three quick fixes for HEIC files that won't open, plus a free browser converter that turns HEIC to JPG in seconds with no install or upload.

Body untouched (Google likes stability on 4-month-old posts). IndexNow ping for fast Bing reindex. Checkpoint set for day 28.

The framing shift between "guess two variants" and "SERP-analyze first, then pick from data-backed variants" is not a small tweak. It changed which variant I would have picked. Guess-and-check would have shipped a title with the query in reversed order (which the research showed Bing penalizes).

Meta-lesson: verify what reaches production

The generalizable version of the frontend bug: metrics dashboards showing you deployed X are not the same as users observing X. The DB said my custom titles were live. The frontend said it was rendering the landing object. Every internal check was green. But the browser served fallback.

There is a class of bug where deployed state and observed state diverge silently for months because both look internally consistent. Search Console showed my pages ranking, so I never thought to view-source them. The tests I had covered the DB path and the fallback path but not the "field name got copy-pasted from a differently-shaped schema" path.

Question I am sitting with: what other systems in this codebase have the same shape? Where does deployed state and observed state diverge silently? The audit I now need to run isn't just this pattern. It is every place where two schemas evolved separately and I am reading from one assuming it matches the other.

Metrics honesty

Two checkpoints locked, both later this month:

  • Day 21 after /resize-image deploy: target impressions greater than or equal to 2500 (baseline 2294), position less than or equal to 55 (baseline 69.4).
  • Day 28 after /blogs/why-wont-heic rewrite: target Bing, position less than or equal to 6.0, three-fixes / free-converter query cluster emergence.

I will not declare victory or defeat before those dates. Reading GSC daily and reacting to noise is an anti-pattern I have caught myself doing before and don't want to repeat.

Current honest numbers: total organic clicks are stuck around 15 to 20 per week. Google impressions are volatile week-over-week (sandbox evaluation phase for a 6-month domain, expected). The AI channel is the strongest active signal: 364 Bing Copilot citations in Q3, up from 234 in Q2, compounding through entity work from earlier in the summer (Wikidata item, centralized JSON-LD graph, IndexNow integration).

Early signal on the frontend fix itself: /heif-to-jpg shows CTR 40% on hif typo queries in this week's Google Search Console window. Sample is small, but the mechanism the fix was supposed to unlock is showing signs of unlocking.

What's next

This week: measurement window opens on the three deploys, a research batch for six WebP/AVIF page titles, and the first outreach batch to tool listicles (linkbuilding has been queued for two weeks and needs to ship).

If you found this useful, the Convertify tool pages are the ones with the newly-visible custom titles.

Top comments (0)