DEV Community

Cover image for SEO Case Study: From zero to Google in 12 weeks — Part 1
Rafael Cavalcanti da Silva
Rafael Cavalcanti da Silva

Posted on • Originally published at rafaelroot.com

SEO Case Study: From zero to Google in 12 weeks — Part 1

When a recruiter searches your name on Google, what shows up defines the first impression. Controlling that is a technical skill — measurable, reproducible, documentable.

This is the first article in a 3-part series where I do exactly that live: going from zero indexation to ranking rafael cavalcanti da silva and rafaelroot on Google. Real screenshots, real metrics, no theory.

Full original article: rafaelroot.com/en/blog/seo-case-study-google-ranking-part-1/


TL;DR — Week 1 (03/08/2026)

  • Baseline: 0 results for me searching "rafael cavalcanti" or "rafael cavalcanti da silva" in the top 100.
  • Stack: Astro 5.17 (SSG) + Nginx with HTTP/2, Brotli, HSTS and security headers.
  • Google Search Console configured, sitemap accepted: 16 URLs discovered.
  • PageSpeed Mobile: 58 → 87 (+29 points) with 9 documented fixes.
  • 8 fixes applied against Google's official guidelines — no guesswork.

The diagnosis (baseline on 03/08/2026)

Before any optimization, I screenshotted each target SERP. This is the zero point to compare against in the coming months.

"rafael cavalcanti"

SERP for

Position Result Domain
1 Rafael Cavalcanti — Bradesco (LinkedIn) linkedin.com
2 Rafael Cavalcanti (@rafaelcavalcantig) instagram.com
3 Rafael Cavalcanti Garcia de Castro Alves escavador.com
4 Rafael Cavalcanti — Lawsuits jusbrasil.com.br
5 Rafael Cavalcanti de Souza — FAPESP bv.fapesp.br

Diagnosis: SERP dominated by namesakes on high-authority platforms. Zero mention of rafaelroot.com.

"rafael cavalcanti da silva"

SERP for

Position Result Domain
1 Rafael Cavalcanti da Silva — Lawsuits jusbrasil.com.br
2 7 profiles with the name br.linkedin.com
3 Who is Rafael Chocolate... g1.globo.com
4–6 Business registrations and influencer serasa, instagram, istoedinheiro

Diagnosis: Moderate competition, but dominated by negative content (lawsuits, convictions) — which opens a real opportunity.

Formal baseline

╔══════════════════════════════════════════════════════════════╗
║                    BASELINE — 03/08/2026                     ║
╠══════════════════════════════════════════════════════════════╣
║  "rafael cavalcanti da silva" → outside top 100              ║
║  "rafael cavalcanti"          → outside top 100              ║
║  "rafaelroot"                 → nonexistent word (vol. 0)    ║
║                                                              ║
║  Indexed pages: 0                                            ║
║  Search Console impressions: 0                               ║
║  Organic clicks: 0                                           ║
║  Known backlinks: 0                                          ║
╚══════════════════════════════════════════════════════════════╝
Enter fullscreen mode Exit fullscreen mode

The plan: 3 phases, 12 weeks

Phase Period Focus
1 — Foundations Weeks 1-2 Baseline, stack, Search Console, GA4, technical fixes
2 — Content & Authority Weeks 3-6 Optimized articles, profiles, link building, cross-links
3 — Measurement & Tuning Weeks 7-12 A/B title tests, real CWV, Part 3 with results

The stack

Why Astro?

Astro SSG → Static HTML → 0kb JS by default
├── LCP: < 1.0s
├── FID: 0ms (no JS = no blocking)
├── CLS: 0 (no reflow)
└── TTFB: ~50ms (static file via Nginx)
Enter fullscreen mode Exit fullscreen mode
Framework JS Bundle Typical LCP SEO Score
Astro (SSG) 0 KB < 1s 100
Next.js (SSR) ~80 KB 1.5-2.5s 90-95
Create React App ~200 KB 3-5s 50-70
WordPress ~150 KB 2-4s 60-80

Sitemap with priorities and i18n (Astro)

// astro.config.mjs
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';

export default defineConfig({
  site: 'https://rafaelroot.com',
  integrations: [
    sitemap({
      i18n: {
        defaultLocale: 'pt-br',
        locales: { 'pt-br': 'pt-BR', pt: 'pt-PT', es: 'es', en: 'en', ru: 'ru' },
      },
      serialize(item) {
        if (item.url === 'https://rafaelroot.com/') item.priority = 1.0;
        else if (item.url.match(/\/blog\/.+\//)) {
          item.priority = 0.9;
          item.changefreq = 'monthly';
        }
        return item;
      },
    }),
  ],
});
Enter fullscreen mode Exit fullscreen mode

Nginx: SEO-focused configuration

server {
    listen 443 ssl http2;
    server_name rafaelroot.com www.rafaelroot.com;

    # HSTS — 1 year with preload
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # Security headers
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Brotli + Gzip
    brotli on; brotli_comp_level 6;
    gzip on;

    # Aggressive cache for immutable assets
    location ~* \.(css|js|woff2|png|webp|avif|svg|ico)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Always fresh HTML
    location ~* \.html$ {
        expires 1h;
        add_header Cache-Control "public, must-revalidate";
    }
}
Enter fullscreen mode Exit fullscreen mode

Why each directive matters for SEO:

Directive SEO Impact
http2 Multiplexing → lower TTFB → better LCP
brotli ~20% smaller than gzip → reduced transfer size
expires 1y + immutable Eliminates re-downloads → better FCP
HSTS Google favors HTTPS → ranking signal

Google Search Console ✅

Sitemap accepted on 03/08/2026:

Sitemap indexed with 16 discovered URLs

Metric Value
Sitemap sitemap-index.xmlsitemap-0.xml
Status Success
Discovered URLs 16 (5 homepages + 5 blog indexes + 6 articles)

9 documented technical fixes

🚨 Fix 1 — robots.txt was blocking Googlebot's CSS ✅

The problem: Disallow: /_astro/ prevented Googlebot from accessing Astro's bundled CSS. Google was seeing raw HTML without styles — this can silently hurt your ranking.

Google's own documentation states:

"For this, Google needs to be able to access the same resources as the user's browser. If your site is hiding important components that make up your website (like CSS and JavaScript), Google might not be able to understand your pages."

# robots.txt — BEFORE (blocking CSS)
User-agent: *
Disallow: /_astro/    BLOCKED BUNDLED CSS!

# robots.txt — AFTER (correct)
User-agent: *
Allow: /
# /_astro/ removed — Googlebot needs the CSS
Enter fullscreen mode Exit fullscreen mode

How to verify: Search Console → URL Inspection → "Test Live URL" → Screenshot. If CSS doesn't load, you'll see unstyled HTML.

🔧 Fix 2 — Removed <meta name="keywords">

Google explicitly states:

"Google Search doesn't use the keywords meta tag."

Besides being useless, it exposes your keyword strategy to competitors. Removed from both layouts.

🌐 Fix 3 — hreflang on blog articles ✅

The homepage had correct hreflang for 5 languages. Blog articles had none. I implemented a translationKey system in the frontmatter:

# article frontmatter
translationKey: "reverse-engineering-android-frida"
Enter fullscreen mode Exit fullscreen mode

Articles sharing the same translationKey are automatically cross-linked at build time:

<!-- Generated output for the PT-BR article -->
<link rel="alternate" hreflang="pt-BR" href="https://rafaelroot.com/blog/engenharia-reversa-android-frida/" />
<link rel="alternate" hreflang="en" href="https://rafaelroot.com/en/blog/reverse-engineering-android-frida/" />
<link rel="alternate" hreflang="x-default" href="https://rafaelroot.com/blog/engenharia-reversa-android-frida/" />
Enter fullscreen mode Exit fullscreen mode

Without this, Google may index only one version and treat the other as duplicate content.

⚡ Fix 4 — CSS/Fonts no longer block rendering ✅

The problem: Google Fonts and devicon.min.css loaded synchronously in <head> were blocking First Contentful Paint. On slow mobile, this cost up to 4,570ms.

<!-- BEFORE (render-blocking) -->
<link href="https://fonts.googleapis.com/..." rel="stylesheet" />

<!-- AFTER (non-blocking) -->
<link rel="preload" as="style" href="https://fonts.googleapis.com/..."
      onload="this.onload=null;this.rel='stylesheet'" />
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/..." /></noscript>
Enter fullscreen mode Exit fullscreen mode

The browser paints immediately with system fonts and swaps when custom fonts load — without blocking FCP.

🎨 Fix 5 — AA contrast fixed ✅

The --muted color (#5a7a9a) against background #040a12 had a ratio of 4.45:1 — below the WCAG AA minimum of 4.5:1.

/* BEFORE */
--muted: #5a7a9a;  /* 4.45:1 — FAILS AA */

/* AFTER */
--muted: #7090b0;  /* 5.96:1 — PASSES both: bg and glass nav */
Enter fullscreen mode Exit fullscreen mode

📱 Fix 6 — Mobile performance: content-visibility ✅

Google uses Mobile-First Indexing — the mobile score is what affects your ranking. Our page has 9 sections but the browser was rendering all of them at once.

/* Skip rendering for off-screen sections */
section {
  content-visibility: auto;
  contain-intrinsic-size: auto 600px;
}

/* Disable expensive GPU effects on mobile */
@media (max-width: 768px) {
  .nav-outer { backdrop-filter: blur(8px); }  /* was 16px */
  .btn, .hero-badge, .bio-grid { backdrop-filter: none; }
  .hero-badge { animation: none; }
}
Enter fullscreen mode Exit fullscreen mode

🔤 Fix 7 — CLS: font fallbacks with size-adjust ✅

font-display: swap caused layout shift when Inter and JetBrains Mono replaced system fonts. Fix: declare @font-face fallbacks with matching metrics.

@font-face {
  font-family: 'JetBrains Mono Fallback';
  src: local('Courier New');
  size-adjust: 108%;  /* calibrated metrics for zero reflow */
}
@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  size-adjust: 100%;
}

/* Updated stacks */
--mono: 'JetBrains Mono', 'JetBrains Mono Fallback', monospace;
--sans: 'Inter', 'Inter Fallback', system-ui, sans-serif;
Enter fullscreen mode Exit fullscreen mode

🏷️ Fix 8 — Optimized titles ✅

Problem 1 — Truncation: Titles at ~87 chars. Google truncates at ~55-60 chars.

Problem 2 — Redundant site name: Since we have WebSite schema with name: 'rafaelroot.com', Google already shows the site name separately in SERPs. Including it in <title> wasted 18 chars.

Problem 3 — Mixed delimiters: | and in the same title.

# BEFORE (87 chars, truncated)
Rafael Cavalcanti da Silva | Fullstack Developer & Security Specialist — rafaelroot.com

# AFTER (57 chars, fully visible)
Rafael Cavalcanti da Silva — Fullstack Developer & Security
Enter fullscreen mode Exit fullscreen mode

📄 Fix 9 — Structured data: ProfilePage + WebSite ✅

Homepage upgraded from WebPage to ProfilePage (a subtype of WebPage). The WebSite schema gained alternateName and a trailing slash on url:

// WebSite  AFTER
{
  "@type": "WebSite",
  "name": "rafaelroot.com",
  "alternateName": ["Rafael Cavalcanti", "Rafael Cavalcanti da Silva", "rafaelroot"],
  "url": "https://rafaelroot.com/"
}

// Homepage  AFTER
{
  "@type": "ProfilePage",
  "mainEntity": { "@id": "https://rafaelroot.com/#person" },
  "dateCreated": "2026-02-01T00:00:00-03:00"
}
Enter fullscreen mode Exit fullscreen mode

PageSpeed: before vs. after

Desktop and Mobile scores after optimizations

Metric (Mobile) Before After Delta
Performance 58 87 +29 🚀
Accessibility 96 100 +4
Best Practices 100 100
SEO 100 100
FCP 6.7s 2.8s -58%
LCP 6.7s 2.8s -58%
Speed Index 9.4s 2.9s -69%
TBT 0ms 0ms
CLS 0 0.113* *fixing with size-adjust

Desktop stays at 90 / 96 / 100 / 100.

Going from 58 → 87 on mobile is the highest-impact SEO improvement possible: Google uses Mobile-First Indexing to determine ranking. Mobile performance = ranking.

Render-blocking and cache diagnostics
Passing audits and best practices


What Google says NOT to worry about

Common myths debunked by Google's own guidelines:

Myth What Google says
Meta keywords "Google Search doesn't use the keywords meta tag."
Keyword stuffing Against spam policies. Write naturally.
Keywords in domain "...have hardly any effect beyond appearing in breadcrumbs."
Word count "The length of the content alone doesn't matter for ranking."
E-E-A-T as ranking factor It's not. It's a quality evaluation framework.
Duplicate content penalty "It's inefficient, but it's not something that will cause a manual action."

Week 1 checklist

  • [x] SERP baseline with screenshots and tables (03/08/2026)
  • [x] Search Console configured + sitemap accepted (16 URLs)
  • [x] robots.txt fixed (no longer blocks /_astro/)
  • [x] <meta name="keywords"> removed from both layouts
  • [x] hreflang on articles via translationKey system
  • [x] CSS/Fonts non-blocking (async preload + noscript fallback)
  • [x] AA contrast: --muted#7090b0 (5.96:1)
  • [x] Mobile: content-visibility: auto + reduced backdrop-filter
  • [x] CLS: @font-face fallbacks with calibrated size-adjust
  • [x] Titles shortened (<60 chars) + single delimiter
  • [x] ProfilePage schema on homepage + WebSite schema fixed
  • [x] Real link to /blog in footer (sitelink-friendly)
  • [x] PageSpeed Mobile: 58 → 87 | Desktop: 90/96/100/100
  • [ ] Create GA4 property + set PUBLIC_GA_ID
  • [ ] Deploy Nginx with optimized cache headers
  • [ ] Re-run PageSpeed after deploy (confirm CLS < 0.1)

Next steps (Part 2)

  • Measure real indexation: checkpoint 03/14
  • GA4 with real ID + SEO dashboard (CTR, scroll depth, time on page)
  • Link building: GitHub, Stack Overflow, dev.to
  • First article targeting "rafael cavalcanti developer" with internal cross-links

I'm documenting everything in real time. Follow the series and let me know in the comments: which metric do you want to track in Part 2?

Last updated: March 8, 2026 | Full article with more technical details

Top comments (0)