Rebuilding eurocert.com.tr: An End-to-End SEO Case Study
Background
Eurocert is an international certification body operating in Türkiye, issuing accredited management-system, product, and personnel certifications across the standards landscape (ISO 9001, ISO 14001, ISO 45001, ISO 22000, ISO 27001, ISO 13485, helal certification, CE marking, and more). The Turkish-market site at eurocert.com.tr is the primary inbound channel for certification inquiries from manufacturers, service businesses, exporters, and public-sector buyers.
In May 2026 we cut over eurocert.com.tr from its legacy ASP.NET / IIS infrastructure to a modern Next.js + Payload CMS stack hosted on a single VPS behind Cloudflare. This case study walks through what we found, what we shipped, and what we learned.
We don't sell SEO services, this is a candid post-mortem we're publishing because most of the issues we hit are not specific to certification websites. They are general lessons for any service business going through a CMS migration.
Where we started
The legacy ASPX site
eurocert.com.tr had been running for years on an ASP.NET / IIS stack at a separate origin (213.159.6.113). It had accumulated several hundred indexed pages covering the full certification catalog in Turkish, English, and German, a meaningful international audience because many of Eurocert's certifications are export-oriented and inquiries come from buyers and consultants outside Türkiye.
The site had three structural assets worth preserving:
- A deep catalog of certification-specific pages (one per standard, one per scope, one per industry application).
- A trilingual experience, TR / EN / DE, with parallel content for the international audience.
- A long backlink history from sister certification bodies, accreditation registries, sector associations, and ministry pages.
It also had a long list of accumulated technical-SEO problems that no amount of in-place fixing was going to resolve.
The audit: nine structural problems
A pre-migration audit combining Search Console data, a fresh full crawl, server log archives, and a Wayback Machine sweep surfaced nine clusters of issues.
1. Slow, JavaScript-heavy rendering on a legacy IIS stack
Mobile Lighthouse Performance scores were in the high 50s. LCP routinely exceeded 4 seconds on a mid-tier mobile connection. Images were unoptimized (full-resolution JPEGs served with no responsive variants). No HTTP/2, no Brotli, no modern caching.
2. Missing or malformed hreflang graph
EN and DE versions of pages existed but the hreflang annotations between them were inconsistent, many pages declared themselves as the only locale, some declared loops, some declared counterparts that returned 404. Google had effectively given up on the locale graph.
3. Localized URL slugs were not actually localized
The "English" and "German" sections served pages whose URLs still contained Turkish slugs (/en/iso-9001-belgesi rather than /en/iso-9001-certificate). This is one of the most consistent silent SEO drains we see, English-speaking users see a Turkish slug in the SERP and don't click; Google's locale signals are weakened; the URL itself fails to carry any of the intended keyword.
4. Lazy .aspx redirects pointing everything at the homepage
A subset of legacy .aspx URLs, roughly 45 of them in the EN and DE sections, had at some point been "redirected" by lumping them all into a 301 pointing at the locale homepage. From Google's perspective this is functionally equivalent to deleting the page: the destination doesn't match the source intent, so the link equity is discarded.
5. Missing or wrong canonical, OpenGraph, and Twitter Card URLs
Page-level canonicals were inconsistent. og:image and twitter:image URLs pointed at a staging hostname rather than the production domain, breaking every social-share preview that had ever been generated. Some certification pages had no canonical at all.
6. Structured data essentially absent
No Organization schema. No Service schema on certification pages. No BreadcrumbList. The blog had no Article schema. Rich-result eligibility was effectively zero across the site.
7. Sitemap / robots.txt drift
The XML sitemap was hand-maintained, missed roughly a third of live pages, and included a long tail of dead URLs. The robots.txt blocked several AI crawlers (GPTBot, ClaudeBot) by default, cutting the site out of LLM-driven search entirely.
8. Title-tag pollution
A class of certification pages had <title> strings that included two or three appended brand suffixes plus a tagline ("ISO 9001 Belgesi - Eurocert Türkiye | Eurocert | Sertifikasyon"), well over 70 characters and truncated in SERPs in ways that obscured the actual keyword.
9. Editorial workflow friction
Content updates required developer involvement. Editors couldn't safely publish without staging deploys. Content drifted out of date because the cost of editing was high.
Strategy
Rather than incrementally patch the legacy stack, we rebuilt on a platform where each of the nine problems was either structurally impossible or correct by default.
Priority order at launch:
- Preserve every URL Google has ever indexed through a complete 301 redirect map, single-hop, no chains.
- Engineer the multilingual experience properly from day one, per-locale slugs, full hreflang graph, locale-aware canonicalization.
- Get the technical foundation right at launch, performance, structured data, crawl control, canonicalization.
- Preserve the legacy site as a frozen, noindex'd reference rather than deleting it outright.
- Then improve content.
Execution
Technical foundation: Next.js + Payload + Cloudflare
The new stack is Next.js 16 (server-rendered React), Payload v3 (TypeScript-native headless CMS) on PostgreSQL, fronted by Cloudflare for DNS, CDN, and WAF, with the origin running on a single VPS behind Caddy with auto-managed TLS.
SEO-relevant properties of the stack:
- Server-rendered HTML by default, with static and incremental-static generation for content pages. Google crawls the rendered HTML directly, no JavaScript-required indexing risk.
- Native i18n routing with locale-aware metadata, canonical URLs, and hreflang generation at the framework level.
next/imagefor automatic WebP/AVIF conversion, responsive sizing, and lazy loading, LCP improvements come essentially for free.- Full programmatic control over the
<head>, title, description, canonical, OpenGraph, Twitter, hreflang, JSON-LD, all generated from source content with no plugin in between. - Type-safe content model, Payload's collections compile to TypeScript types consumed by the front end. Schema changes that would break a page fail at build time, not in production.
- Single VPS, no plugin sprawl, no CVE treadmill. The attack surface and maintenance overhead shrink dramatically compared with the legacy stack.
The redirect map: from 416 to 1,283 single-hop 301s
This was treated as a launch-blocking deliverable. The redirect inventory grew through three rounds:
Round one captured the obvious moves, old .aspx URLs to new clean slugs, deprecated query-string URLs to canonical paths, www / non-www / http normalization. 416 redirects.
Round two was driven by the per-locale slug migration. When we rewrote 897 English and German slugs from the original Turkish placeholders to actual English/German keywords, every old URL needed a 301 to its new canonical destination. Hundreds more redirects, all programmatically generated from the slug-rewrite manifest.
Round three was the audit phase. We crawled the full production site, cross-referenced every old URL still present in Search Console and external backlinks, and added redirects for the long tail of legacy URLs that hadn't been caught in rounds one or two. We also fixed the 45 lazy .aspx redirects that had been pointing at locale homepages, each one was grounded against the actual migrated page, and where an unambiguous match existed (exact title match or unique slug-prefix match) it was redirected there. A residual ~78 ambiguous legacy URLs were left pointing at locale homepages on purpose, with an internal task to revisit them.
Final state: 1,283 single-hop 301 redirects, verified by a post-launch crawl. No chains, no 302s, no meta refreshes.
Per-locale URL slugs: 897 slugs translated
This was the single most leveraged content-side intervention.
We built an asciiSlugify utility that takes a localized string (TR / EN / DE) and produces a clean, ASCII-safe URL slug appropriate for that locale, handling Turkish diacritics (İ, ş, ğ, ç, ö, ü), German umlauts (ä, ö, ü, ß → ae/oe/ue/ss), and the various edge cases that arise when transliterating between alphabets for URL safety.
We then ran this against every Payload document with localized fields and produced a per-locale slug for each. 897 slugs were rewritten from their previous Turkish-placeholder forms to actual localized URL strings.
Examples:
/en/iso-9001-belgesi→/en/iso-9001-certificate/de/helal-belgesi→/de/halal-zertifikat/en/ce-belgelendirme→/en/ce-certification
This wasn't a cosmetic change. Localized slugs are a substantial click-through-rate signal in non-Turkish SERPs, and they also feed Google's locale-detection heuristics. Every one of those 897 old URLs got a 301 to its new canonical equivalent.
A separate complication: Payload v3's local-API update() method can wipe version-less locale rows under certain conditions. We took an incident on this during the slug migration where three services lost their EN/DE rows entirely, and recovered cleanly from a pre-migration database snapshot. The fix for the actual migration was to use a SQL projection + version write rather than payload.update(). This is now a documented gotcha in our internal playbook and a lesson worth flagging publicly: for bulk locale-aware writes on Payload sites, validate your write path against a single document before running it across the full collection.
Multilingual SEO done correctly
With the per-locale slugs in place, we rebuilt the hreflang graph from scratch:
<link rel="alternate" hreflang="...">annotations on every localized page, declaring TR / EN / DE counterparts plusx-default.xhtml:linkalternates in the XML sitemap, mirroring the on-page annotations.- Per-locale canonical URLs so each language version is canonical to itself, not to the Turkish original.
- A locale-aware language switcher that performs a full navigation rather than a client-side route change, ensuring server-rendered chrome (footer,
<html lang>) refreshes correctly with the new locale. - Locale-aware sitemaps, separate
sitemap-tr.xml,sitemap-en.xml,sitemap-de.xmlfiles, each declared in the mastersitemap-index.xml.
We hit and fixed an interesting Next.js 16 gotcha during this work: a blog route was emitting hreflang from both the framework's metadata.alternates.languages field and a hand-rolled <HrefLangTags> body component, producing contradictory and partially broken hreflang annotations. The fix was to pick one source of truth (the body component, generalized through a queryPostLocaleSlug helper) and drop the metadata-side emitter. Worth noting publicly because the Next.js documentation is ambiguous on this, metadata.alternates.languages does emit to the HTML, and if your custom body component also does so you will silently double-emit.
Title-tag and canonical pollution fix
A separate sweep grounded 15 certification-page titles that had been polluted with double-suffix brand strings, normalizing them to a clean "%s - Eurocert" template with bespoke absolute titles on top commercial pages.
og:image and twitter:image URLs were routed through a single CANONICAL_ORIGIN helper so they always match the production canonical, eliminating the staging-host leakage that had broken social previews on the legacy site.
Performance: from the 60s to the 90s on mobile
Mobile Lighthouse Performance scores on the same certification pages now consistently land in the low- to mid-90s, with desktop scores in the upper 90s. Specific contributors:
- Server-side rendering with HTML streaming, so the LCP element ships in the initial response.
next/imagefor every above-the-fold image, with explicit dimensions to eliminate cumulative layout shift.- Critical CSS inlined by Next.js, with the rest deferred.
- Tailwind CSS ships only the styles actually used on a given page.
- Third-party scripts (chat widget, analytics) deferred and consent-gated through a KVKK-compliant cookie banner.
- Self-hosted webfonts with
font-display: swapand a preload hint on the primary weight. - HTTP/2 over Cloudflare, Brotli compression on every text response, 1-year immutable cache on static assets.
Structured data: complete coverage, generated from source
Every page on the new site ships rich JSON-LD automatically:
Organizationschema site-wide in the root layout, withsameAspointing at verified Eurocert profiles, consistent NAP, andcontactPointentries per office.Serviceschema on every certification page (ISO 9001, ISO 14001, ISO 45001, ISO 22000, ISO 27001, ISO 13485, helal, CE marking, and so on), withprovider,areaServed: "Türkiye", andserviceType.Articleschema on every blog post.BreadcrumbListon every internal page.FAQPageschema on the FAQ sections of certification landing pages.
All generated from the source content at render time. No plugin to drift, no manual maintenance.
Crawl, index, and sitemap control
- Modern
robots.txtexplicitly allowing GPTBot, ClaudeBot, CCBot, Google-Extended, and PerplexityBot, because AI-driven search is now a meaningful inbound channel and blocking it by default is a strategic mistake. - Programmatic XML sitemap generated by Next.js from published-content state, with strict
_status: 'published'filtering so drafts never leak. - Per-locale sitemap entries with
xhtml:linkalternates. noindexheaders on the staging environment (via Caddy'sX-Robots-Tagheader, note that header-level noindex is materially stronger thanrobots.txtfor staging environments, becauserobots.txtis advisory whereas the header is binding).noindexon faceted search results and admin routes, but never on production content pages.
The frozen ASPX mirror, a tactical decision worth explaining
Rather than deleting the legacy ASPX site outright, we preserved a frozen, fully static copy at a dedicated subdomain (old-asp.eurocert.com.tr) for archival reference. The full IIS origin was crawled with wget, converted to offline-static form, and served from the new VPS through Caddy with:
X-Robots-Tag: noindex, nofollowat the server level (header-level, so it cannot be overridden by individual page meta tags).robots.txt: Disallow: /at the document root.Cache-Control: no-storeto keep stale snapshots out of edge caches.- Cloudflare proxying for TLS and DDoS shielding.
Why bother? Two reasons:
- Internal reference value. Editorial and sales teams occasionally need to look up exactly what the legacy site said about a specific certification scope. Having it accessible is cheap.
- Cleaner deprecation path for backlinks we missed. Any external link we failed to catch in the redirect inventory hits the frozen mirror rather than a hard 404, degrading gracefully while we extend the redirect map.
The frozen mirror is explicitly excluded from indexing at both the header and the robots.txt level. It is not a duplicate-content risk; it is a graceful-degradation surface.
A subtle gotcha, unseeded singleton globals on forked Payload sites
Worth flagging publicly because we hit it and lost time on it: when a Payload v3 site is forked from a template, the site can ship with unseeded singleton globals (header, footer, settings). The findGlobal() call returns null or default Turkish placeholders on every locale, and the ?? fallback guards in the template silently mask the issue.
The diagnostic: count rows in the <global>_locales tables per locale. If they're zero, the global was never seeded. The fix is a per-locale Local-API seed run as part of the launch checklist.
Editorial workflow
The new Payload admin is fully Turkish (admin chrome translated, all 15+ collections labeled in Turkish, sidebar grouped into logical sections). Editors can publish certification updates, blog posts, and FAQ entries directly, with version history and draft/publish status on every record. Content updates that previously required developer involvement now happen in minutes.
Cutover and rollback strategy
The cutover was structured for instant rollback:
- Pre-cutover database snapshot taken and stored both on the VPS and in an off-site backup.
- DNS flip of the apex (
eurocert.com.tr) andwwwrecords from the legacy origin (213.159.6.113) to the new VPS (46.62.151.85), proxied through Cloudflare. - Legacy IIS origin left running and accessible at its original IP, not removed, not turned off. Any issue with the new site could have been recovered from in minutes by reverting the Cloudflare DNS records.
- Production Caddy configuration loaded from
/etc/caddy/eurocert-prod.caddy, validated withcaddy validatebefore reload, and gated bysystemctl restart caddyrather thanreloadto ensure clean state. - Post-cutover verification crawl of every critical URL, confirming 200s on the canonical pages, single-hop 301s on every redirected URL, and zero 5xx responses across the site.
A pre-cutover readiness sweep also surfaced two SEO defects worth fixing in flight:
- OpenGraph and Twitter image URLs were still pointing at the staging hostname. Fixed by routing through the shared
CANONICAL_ORIGINhelper. - Blog hreflang annotations were being double-emitted (see the Next.js 16 metadata-vs-body emitter note above). Fixed by consolidating onto a single source of truth.
Both were verified post-fix with a render-time crawl, not just a build-time type check.
Specific bugs and lessons worth documenting
A handful of issues we hit are worth flagging publicly because we see them on other migrations too:
payload.update()can wipe version-less locale rows on Payload v3 sites under specific conditions. Always validate your bulk-write path against a single document before running it across the full collection. Recover from a snapshot if the write blast-radius was unbounded.- Next.js 16 emits
metadata.alternates.languagesto the HTML. If you also have a custom hreflang component, you will double-emit. Pick one source of truth. - A per-site Caddy log block with a non-existent log path will fail Caddy's startup validation and wedge the entire service into
Active: reloading. The old config keeps serving sites in the meantime, there is no immediate outage, which is misleading. Alwayscaddy validatebefore reloading. og:imageURLs need to route through a single canonical helper. Without one, you will eventually emit a staging hostname into production HTML and not notice until a customer shares your page on LinkedIn.- Per-locale slug rewrites are an underrated SEO lever. They cost real translation work but they pay off in non-domestic-locale CTR for years.
Results and what we are tracking
We are deliberately not quoting specific traffic-recovery percentages in this post. The cutover happened in May 2026 and we want to give the new platform a full Google reindexing cycle before publishing numbers we can stand behind.
What we can say now, with confidence:
- Single-hop 301 coverage for every URL Google had ever indexed for the domain, 1,283 redirects, all verified.
- 897 per-locale URL slugs rewritten from Turkish placeholders to actual EN / DE keywords.
- Trilingual coverage complete across the certification catalog and the supporting content surfaces, with full hreflang and per-locale canonicalization.
- Core Web Vitals consistently in the 90s on mobile and upper 90s on desktop.
- Structured-data coverage at 100% across certification, blog, and FAQ templates.
- AI-crawler accessibility full-allow rather than the legacy default of partial-block.
- Editorial autonomy, content updates that previously took days now take minutes.
Lessons we would give to anyone doing a similar rebuild
- A CMS migration without a complete 301 redirect map is malpractice. Treat the redirect inventory as a launch blocker. Pull every URL Google has ever indexed, map each one, verify single-hop 301s with a post-launch crawl.
- Per-locale URL slugs matter more than people realize. If your "English site" serves Turkish slugs (or vice versa), you are leaving CTR and locale-targeting equity on the table every single day.
- The "lazy redirect to homepage" is functionally a delete. Every legacy URL deserves an intent-matched destination. Where none exists, leave the URL as a 404, at least that's an honest signal to Google.
- Preserve the legacy site as a frozen, noindex'd reference rather than deleting it. The cost is near zero and the optionality is real.
- Performance is now a ranking and a conversion factor. A site scoring 95 on mobile Lighthouse measurably outperforms a 65, and Google notices both.
- Allow the AI crawlers. Blocking GPTBot, ClaudeBot, and Google-Extended by default, which is the WordPress/IIS factory default, cuts you out of LLM-driven search entirely. That channel is small today and large in three years.
- Validate your bulk-write path before running it across a localized collection. One bad
update()call can wipe locale rows you didn't realize were at risk. Snapshots are cheap. Snapshots before bulk writes are mandatory. - Audit what you actually publish, not what your CMS says you publish. Render-time crawls catch a category of bugs, locale leaks, doubled hreflang, hardcoded staging URLs, missing canonicals, that no amount of staging-environment testing will surface.
Where the site is now
eurocert.com.tr is live on Next.js + Payload, fully trilingual with per-locale URL slugs, a complete 301 redirect map back to the ASPX era, modern Core Web Vitals, full structured-data coverage, AI-crawler-friendly indexing controls, and an editorial workflow that lets Eurocert's team publish certification updates directly without developer involvement.
If you are planning a similar migration and would like to compare notes, our door is open.