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 (0)