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.png β†’ rafael-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 (2)

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.

Collapse
Β 
tarzant profile image
Tarzan β€’

β€œ5 languages (pt-BR, pt-PT, en, es, ru) β€” each article natively written” Great job!