close

DEV Community

Rafael Cavalcanti da Silva
Rafael Cavalcanti da Silva

Posted on

I Ranked on Google's First Page in 6 Weeks — Here's Every SEO Tactic I Used (Part 2)

This is Part 2 of my SEO case study. Part 1 covered the technical foundation: 9 fixes, PageSpeed 58→87, and the Astro stack setup.

In Part 1, I documented the baseline of rafaelroot.com: zero indexation, zero impressions, zero clicks. Astro SSG, strict technical SEO, mobile PageSpeed from 58 to 87.

Now for the growth phase. This article covers weeks 3 through 6: building authority, deploying to production, and reaching position 3 on Google's first page.


📊 TL;DR — Weeks 3 to 6

Metric Start Week 6
Indexed pages 0/16 16/16 (100%)
Primary query position 100+ #3 (page 1)
Weekly impressions 0 847
CTR (main query) 12.4%
Backlinks 0 7 high-quality
Lighthouse scores 87/99/99/99 100/100/100/100

⚡ 10-Day Indexation Checkpoint

Ten days after submitting the sitemap, Google indexed all 16 URLs. Complete coverage.

🎯 CHECKPOINT — Week 2 (03/14/2026)

  • Indexed Pages: 16/16
  • Impressions: 23 | Clicks: 2 | Position: 47.3

Why so fast?

  1. Clean sitemap — 16 URLs, zero 404s, no redirect chains
  2. Semantic HTML — Hierarchical headings + valid JSON-LD schemas
  3. ~50ms TTFB — Googlebot parses static files instantly

🏗️ Production Deploy: Nginx Tuned for SEO

Brotli + Gzip compression

Brotli compresses ~8-9% better than gzip for HTML. Automatic gzip fallback for older clients:

brotli on;
brotli_comp_level 6;
brotli_static on;
brotli_min_length 256;
brotli_types text/plain text/css application/json application/javascript
           text/xml application/xml text/javascript image/svg+xml font/woff2;

gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 4;
Enter fullscreen mode Exit fullscreen mode

Measured results:

Encoding Size Reduction
Uncompressed 18,408 bytes
Gzip 5,372 bytes -70.8%
Brotli 4,897 bytes -73.4%

💡 475 bytes less than gzip per request. On a 3G connection, that's the difference between passing or failing the LCP Core Web Vital.

HTTPS + HTTP/2 + Security headers

Full server block with Let's Encrypt SSL, HSTS preload, CSP, and all 6 security headers. Two permanent 301 redirects: HTTP→HTTPS and www→non-www (prevents duplicate content).

Layered cache strategy

Type Cache Why
CSS, JS, fonts, images 1 year, immutable Hash in filename = never changes
HTML 1 day, must-revalidate Content may update
XML (sitemap) 1 hour Crawlers need fresh data

Returning visitors download zero assets — only HTML is revalidated.

Production TTFB

Homepage: 94ms | Blog post: 73ms | Protocol: HTTP/2 | Encoding: Brotli


📈 GA4 Dashboard for SEO

Rankings mean nothing if users bounce. I configured GA4 with custom events:

// Scroll depth tracking — zero dependencies
const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      gtag('event', 'scroll_depth', {
        section: entry.target.id,
        percent: entry.target.dataset.depth
      });
    }
  });
}, { threshold: 0.5 });

document.querySelectorAll('section[data-depth]').forEach(s => observer.observe(s));
Enter fullscreen mode Exit fullscreen mode

The SEO dashboard correlates Search Console (impressions, CTR, position) with GA4 (scroll depth, time-on-page, bounce rate).


🔗 Link Building: Authority Footholds

No guest post spam. Instead, I built contextual presence on platforms Google already trusts:

# Platform Type DA Context
1 dev.to Original article 61 Full case study with canonical_url
2 GitHub Profile + README 96 Bio + pinned repo with contextual link
3 Stack Overflow Profile 82 Website field
4 LinkedIn Profile + article 98 Featured section + native post
5 npm Published package 75 Homepage field
6 Twitter/X Profile + thread 94 Bio and tweet with link

🔥 DEV.TO canonical hack: Using canonical_url in dev.to frontmatter transfers DA 61 authority to your blog without triggering duplicate content penalties. This article does exactly that.

The key insight: each link has context — it's not a naked URL in an empty bio.


🌍 Multilingual Content: The i18n Multiplier

5 languages (pt-BR, pt-PT, en, es, ru) — each article natively written, not machine-translated.

Why this matters:

  1. Google detects auto-translated content as low quality
  2. Each version ranks independently for queries in that language
  3. hreflang tags mathematically link all versions
<link rel="alternate" hreflang="en"
      href="https://rafaelroot.com/en/blog/seo-case-study-google-ranking-part-2/" />
<link rel="alternate" hreflang="es"
      href="https://rafaelroot.com/es/blog/caso-estudio-seo-de-cero-a-google-parte-2/" />
<link rel="alternate" hreflang="x-default"
      href="https://rafaelroot.com/blog/case-study-seo-do-zero-ao-google-parte-2/" />
Enter fullscreen mode Exit fullscreen mode

📊 Results: 6 Weeks of Data

Week Impressions Clicks CTR Avg. Position
1 0 0
2 23 2 8.7% 47.3
3 89 7 7.9% 28.1
4 234 21 9.0% 14.7
5 512 48 9.4% 7.2
6 847 105 12.4% 3.8

Top queries

Query Position CTR
rafael cavalcanti da silva #3 14.2%
rafaelroot #1 28.6%
rafael cavalcanti developer #5 8.9%

SERP comparison — "rafael cavalcanti da silva"

Position Baseline Week 6
1 jusbrasil.com.br jusbrasil.com.br
2 br.linkedin.com br.linkedin.com
3 g1.globo.com rafaelroot.com

Separating technical vs. content impact

Technical fixes (weeks 1-2): complete indexation in 10 days, PageSpeed 87, zero Search Console errors.

Content + authority (weeks 3-6): impressions 0→847, ranking 100+→#3, CTR above market average.

The technical foundation let Google index quickly. But it was content and backlinks that moved the ranking.


💡 Lesson Learned: Don't Shorten Your Brand Name

I tested using the shorter "Rafael Cavalcanti" in titles. The logic: it's a substring, so coverage should be additive.

It wasn't. Within days, I lost ranking for the full name query. Google re-evaluated the page's primary entity and the diluted signal hurt more than the broader match helped.

Fix: Reverted all <title>, meta descriptions, og:site_name back to "Rafael Cavalcanti da Silva". The short form stays only in alternateName in structured data.

Takeaway: For personal brand SEO, consistency > coverage. If you rank for "firstname lastname suffix," don't dilute it by removing the suffix.


🚀 Post-Launch Optimizations (Updates 2-4)

These updates happened after the initial 6-week period and are fully documented in the canonical article.

Lighthouse: 100/100/100/100

Three targeted fixes to reach perfect scores:

Issue Before After Fix
LCP render delay 920ms 0 Replaced JS font injection with CSS media="print" onload pattern
Missing og:image:alt SEO 99 100 Added dynamic og:image:alt + twitter:image:alt on all pages
CSS critical chain 922ms 0 build.inlineStylesheets: 'always' — zero external CSS
Forced reflow 39ms 0 Batched getBoundingClientRect() reads before DOM writes

Sitemap: from basic to fully optimized

Feature Before After
<lastmod> ✅ All 35 URLs
<changefreq> ✅ Per-page-type
<priority> ✅ Hierarchy (1.0→0.5)
Blog hreflang ✅ Cross-language links (5 locales)
Redirect URLs Leaking ❌ Filtered ✅
<image:image> 15 image entries

The trickiest part: @astrojs/sitemap doesn't support blog posts with different slugs per locale. Solution: a blogTranslations map in serialize() that links all 5 versions:

const blogTranslations = {
  'seo-case-study-part-1': {
    'pt-BR': '/blog/case-study-seo-do-zero-ao-google-parte-1/',
    'en': '/en/blog/seo-case-study-google-ranking-part-1/',
    'es': '/es/blog/caso-estudio-seo-de-cero-a-google-parte-1/',
  }
};
Enter fullscreen mode Exit fullscreen mode

Image SEO + sitemap indexing

  • Renamed image.pngrafael-cavalcanti-da-silva-fullstack-developer.png
  • Created a custom Astro integration (sitemapImageInjector) that injects <image:image> tags into sitemap XML post-build
  • 15 image entries enabling Google Image Search discovery

Custom 404 page + i18n coverage

Multilingual 404 page with contextual navigation. Added 55+ translation keys and the missing Russian "Trajetória" page.


📋 Full Checklist — Weeks 3-6

  • ✅ 16/16 URLs indexed (100% coverage)
  • ✅ GA4 with scroll depth + time-on-page tracking
  • ✅ 7 high-quality backlinks built
  • ✅ dev.to article with canonical link
  • ✅ 5 languages, natively written
  • ✅ hreflang validated across all versions
  • ✅ Nginx: Brotli + HTTP/2 + HSTS + 6 security headers
  • ✅ TTFB: 73-94ms in production
  • ✅ Lighthouse: 100/100/100/100
  • ✅ Sitemap: 35 URLs + priorities + image indexing
  • ✅ "rafael cavalcanti da silva" → position 3
  • ✅ "rafaelroot" → position 1
  • ✅ CTR 12.4% (above market average)

What's Next (Part 3)

  • A/B titles and meta descriptions for CTR optimization
  • Real Core Web Vitals field data (CrUX) vs. lab (Lighthouse)
  • Cannibalization analysis across multilingual versions
  • Final goal: position 1

Community Feedback

The Part 1 article on dev.to generated valuable feedback:

@apogeewatcher suggested separating results into categories (indexation, ranking, CTR, on-page performance) instead of attributing everything to a single change. That suggestion directly influenced the "Separating technical vs. content impact" section above.

@apex_stack shared experience running a 100k+ page Astro site across 12 languages, validating the translationKey pattern and warning about crawl budget as a "silent variable."

@kritika_barod confirmed the pattern: Google crawled thousands of pages but didn't index them until authority signals improved.


Full technical details with Nginx configs, schema code, and all 4 updates: rafaelroot.com/en/blog/seo-case-study-google-ranking-part-2/

Found this useful? Drop a 🦄 and follow for Part 3 — or check out rafaelroot.com to see everything in action.

Top comments (1)

Collapse
 
soniarotglam profile image
Sonia

Great update, Rafael! Seeing that jump from 'outside top 100' to the first page in just 6 weeks is the best proof that a clean, Astro-based architecture isn't just a 'nice to have,' but a competitive advantage.

I found your point about 'Authority Footholds' (dev.to, GitHub, LinkedIn) particularly interesting. Many people focus on backlinks as a numbers game, but using high-authority platforms to signal identity to Google is a much smarter move for a personal brand.

At The Good Shell, we often see that Google indexes pages 3x faster when there’s a consistent 'Entity' signal across these hubs before the main domain even scales.

Quick question on the LCP fix you mentioned: Did you notice a significant difference in the 'Field Data' vs 'Lab Data' after switching the font loading to the media-swap pattern? Sometimes Lighthouse gives us that 100/100, but real-world users on slow 4G still experience that annoying layout shift.