initial commit

This commit is contained in:
Gabriel Franco 2026-09-02 20:08:41 -03:00
commit e4ef0abe41
47 changed files with 6953 additions and 0 deletions

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/

4
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

22
AGENTS.md Normal file
View file

@ -0,0 +1,22 @@
## Development
When starting the dev server, use background mode:
```
astro dev --background
```
Manage the background server with `astro dev stop`, `astro dev status`, and `astro dev logs`.
## Documentation
Full documentation: https://docs.astro.build
Consult these guides before working on related tasks:
- [Adding pages, dynamic routes, or middleware](https://docs.astro.build/en/guides/routing/)
- [Working with Astro components](https://docs.astro.build/en/basics/astro-components/)
- [Using React, Vue, Svelte, or other framework components](https://docs.astro.build/en/guides/framework-components/)
- [Adding or managing content](https://docs.astro.build/en/guides/content-collections/)
- [Adding styles or using Tailwind](https://docs.astro.build/en/guides/styling/)
- [Supporting multiple languages](https://docs.astro.build/en/guides/internationalization/)

1
CLAUDE.md Symbolic link
View file

@ -0,0 +1 @@
AGENTS.md

30
README.md Normal file
View file

@ -0,0 +1,30 @@
# gabeefran.co
Personal blog of Gabriel Franco (gabeefranco). Built with Astro, TypeScript, MDX, React, and Tailwind CSS. Bilingual (English / Portuguese) via Astro's built-in i18n routing.
## Structure
```text
/
├── public/ favicon, logo, static assets
├── src/
│ ├── components/ Astro + React components (Header, PostCard, ObservationCard, ...)
│ ├── content/posts/en/*.mdx English posts
│ ├── content/posts/pt/*.mdx Portuguese posts (same filename pairs a translation)
│ ├── layouts/ BaseLayout, PostLayout
│ ├── lib/ i18n dictionary + post helpers
│ └── pages/ routes: "/" is English (default locale), "/pt/*" is Portuguese
└── astro.config.mjs
```
Every post exists as two files with the same slug — `src/content/posts/en/<slug>.mdx` and `src/content/posts/pt/<slug>.mdx` — so the language switcher can always land on the matching translation.
## Commands
| Command | Action |
| :------------------- | :------------------------------------------ |
| `npm install` | Install dependencies |
| `npm run dev` | Start the dev server at `localhost:4321` |
| `npm run build` | Build the production site to `./dist/` |
| `npm run preview` | Preview the production build locally |
| `npm run astro check` | Type-check the project |

25
astro.config.mjs Normal file
View file

@ -0,0 +1,25 @@
// @ts-check
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import mdx from '@astrojs/mdx';
import tailwindcss from '@tailwindcss/vite';
// https://astro.build/config
export default defineConfig({
site: 'https://gabeefran.co',
i18n: {
defaultLocale: 'en',
locales: ['en', 'pt'],
routing: {
prefixDefaultLocale: false,
},
},
integrations: [react(), mdx()],
vite: {
plugins: [tailwindcss()],
},
});

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

5490
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

32
package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "gabeefran-co-blog",
"type": "module",
"version": "0.0.1",
"engines": {
"node": ">=22.12.0"
},
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/markdown-satteri": "^0.4.0",
"@astrojs/mdx": "^8.0.0",
"@astrojs/react": "^6.0.5",
"@tailwindcss/vite": "^4.3.3",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.5",
"astro": "^7.2.10",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"tailwindcss": "^4.3.3"
},
"allowScripts": {
"esbuild": true
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.20"
}
}

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

38
public/favicon.svg Normal file
View file

@ -0,0 +1,38 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
<defs>
<linearGradient id="bowlGrad" x1="0%" y1="0%" x2="100%" y2="60%">
<stop offset="0%" stop-color="#F2600D"/>
<stop offset="55%" stop-color="#C7350B"/>
<stop offset="100%" stop-color="#7A1204"/>
</linearGradient>
<linearGradient id="fryGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#FFD24C"/>
<stop offset="100%" stop-color="#E08A00"/>
</linearGradient>
<linearGradient id="fryShade" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#E8B23A"/>
<stop offset="100%" stop-color="#A65E00"/>
</linearGradient>
</defs>
<path d="M 13.1,122 L 14.5,97.4 L 19.5,100.4 L 18.9,122 Z" fill="url(#fryShade)"/>
<path d="M 22.4,122 L 21.0,73.6 L 27.7,76.6 L 28.2,122 Z" fill="url(#fryGrad)"/>
<path d="M 31.8,122 L 33.2,91.2 L 38.1,94.2 L 37.6,122 Z" fill="url(#fryGrad)"/>
<path d="M 41.1,122 L 39.7,67.4 L 46.3,70.4 L 46.9,122 Z" fill="url(#fryShade)"/>
<path d="M 50.4,122 L 51.8,85.0 L 56.8,88.0 L 56.2,122 Z" fill="url(#fryGrad)"/>
<path d="M 59.8,122 L 58.4,78.0 L 65.0,81.0 L 65.6,122 Z" fill="url(#fryGrad)"/>
<path d="M 69.1,122 L 70.5,58.6 L 75.5,61.6 L 74.9,122 Z" fill="url(#fryShade)"/>
<path d="M 78.4,122 L 77.0,71.0 L 83.7,74.0 L 84.2,122 Z" fill="url(#fryGrad)"/>
<path d="M 87.8,122 L 89.2,44.6 L 94.1,47.6 L 93.6,122 Z" fill="url(#fryGrad)"/>
<path d="M 97.1,122 L 95.7,64.8 L 102.3,67.8 L 102.9,122 Z" fill="url(#fryShade)"/>
<path d="M 106.4,122 L 107.8,38.4 L 112.8,41.4 L 112.2,122 Z" fill="url(#fryGrad)"/>
<path d="M 115.8,122 L 114.4,60.4 L 121.0,63.4 L 121.6,122 Z" fill="url(#fryGrad)"/>
<path d="M 125.1,122 L 126.5,69.2 L 131.5,72.2 L 130.9,122 Z" fill="url(#fryShade)"/>
<path d="M 134.4,122 L 133.0,51.6 L 139.7,54.6 L 140.2,122 Z" fill="url(#fryGrad)"/>
<path d="M 143.8,122 L 145.2,78.0 L 150.1,81.0 L 149.6,122 Z" fill="url(#fryGrad)"/>
<path d="M 153.1,122 L 151.7,63.9 L 158.3,66.9 L 158.9,122 Z" fill="url(#fryShade)"/>
<path d="M 162.4,122 L 163.8,86.8 L 168.8,89.8 L 168.2,122 Z" fill="url(#fryGrad)"/>
<path d="M 171.8,122 L 170.4,76.2 L 177.0,79.2 L 177.6,122 Z" fill="url(#fryGrad)"/>
<path d="M 181.1,122 L 182.5,95.6 L 187.5,98.6 L 186.9,122 Z" fill="url(#fryShade)"/>
<path d="M 6,122 Q 100,172 194,122 Q 178,110 100,110 Q 22,110 6,122 Z" fill="url(#bowlGrad)"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

View file

@ -0,0 +1,38 @@
---
import { useTranslations, type Lang } from '../lib/i18n';
interface Props {
lang: Lang;
}
const { lang } = Astro.props;
const t = useTranslations(lang);
const year = new Date().getFullYear();
---
<footer class="border-t border-border/80">
<div class="mx-auto flex max-w-3xl flex-col gap-2 px-4 py-8 text-sm text-text-muted sm:flex-row sm:items-center sm:justify-between sm:px-6">
<p>
&copy; {year} <span class="font-mono text-text">gabeefranco</span> &middot; {t('footer.rights')}
</p>
<div class="flex items-center gap-4">
<span class="hidden sm:inline">{t('footer.tagline')}</span>
<a
href="https://github.com/gabeefranco"
target="_blank"
rel="noopener noreferrer me"
class="transition-colors hover:text-primary"
>
GitHub
</a>
<a
href="https://github.com/gabeefranco/gabeefran.co-blog"
target="_blank"
rel="noopener noreferrer"
class="transition-colors hover:text-primary"
>
{t('footer.source')}
</a>
</div>
</div>
</footer>

View file

@ -0,0 +1,99 @@
---
import ThemeToggle from './ThemeToggle.astro';
import { getLocalizedPath, LANG_LABELS, useTranslations, type Lang } from '../lib/i18n';
interface Props {
lang: Lang;
}
const { lang } = Astro.props;
const t = useTranslations(lang);
const otherLang: Lang = lang === 'en' ? 'pt' : 'en';
const pathname = Astro.url.pathname;
const homeHref = getLocalizedPath('/', lang);
const postsHref = getLocalizedPath('/posts', lang);
const aboutHref = getLocalizedPath('/about', lang);
const langSwitchHref = getLocalizedPath(pathname, otherLang);
const navLinks = [
{ href: homeHref, label: t('nav.home') },
{ href: postsHref, label: t('nav.posts') },
{ href: aboutHref, label: t('nav.about') },
];
function isActive(href: string): boolean {
if (href === homeHref) return pathname === href;
return pathname === href || pathname.startsWith(`${href}/`);
}
---
<header class="sticky top-0 z-40 border-b border-border/80 bg-bg/85 backdrop-blur">
<div class="mx-auto flex max-w-3xl items-center justify-between gap-4 px-4 py-3 sm:px-6">
<a href={homeHref} class="flex shrink-0 items-center gap-2.5" aria-label={t('site.name')}>
<img src="/favicon.svg" alt="" width="32" height="32" class="size-8" />
<span class="font-mono text-[15px] font-semibold tracking-tight text-text">{t('site.name')}</span>
</a>
<div class="flex items-center gap-2">
<nav class="hidden items-center gap-1 sm:flex" aria-label={t('nav.menu')}>
{navLinks.map((link) => (
<a
href={link.href}
aria-current={isActive(link.href) ? 'page' : undefined}
class:list={[
'rounded-full px-3 py-1.5 text-sm font-medium transition-colors',
isActive(link.href)
? 'bg-primary/10 text-primary'
: 'text-text-muted hover:bg-surface-alt hover:text-text',
]}
>
{link.label}
</a>
))}
</nav>
<a
href={langSwitchHref}
hreflang={otherLang}
title={t('lang.switch')}
aria-label={`${t('lang.switch')}: ${LANG_LABELS[otherLang]}`}
class="grid size-9 shrink-0 place-items-center rounded-full border border-border text-xs font-bold uppercase text-text-muted transition-colors hover:border-primary hover:text-primary"
>
{otherLang}
</a>
<ThemeToggle lang={lang} />
<details class="relative sm:hidden">
<summary
class="grid size-9 cursor-pointer list-none place-items-center rounded-full border border-border text-text-muted transition-colors hover:border-primary hover:text-primary"
aria-label={t('nav.menu')}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="size-[18px]">
<path d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</summary>
<nav
class="absolute right-0 top-11 flex w-40 flex-col gap-1 rounded-xl border border-border bg-surface p-2 shadow-lg"
aria-label={t('nav.menu')}
>
{navLinks.map((link) => (
<a
href={link.href}
aria-current={isActive(link.href) ? 'page' : undefined}
class:list={[
'rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive(link.href)
? 'bg-primary/10 text-primary'
: 'text-text-muted hover:bg-surface-alt hover:text-text',
]}
>
{link.label}
</a>
))}
</nav>
</details>
</div>
</div>
</header>

View file

@ -0,0 +1,67 @@
---
import PostCard from './PostCard.astro';
import { getPostsByLang } from '../lib/posts';
import { getLocalizedPath, useTranslations, type Lang } from '../lib/i18n';
interface Props {
lang: Lang;
}
const { lang } = Astro.props;
const t = useTranslations(lang);
const posts = await getPostsByLang(lang);
const recentPosts = posts.slice(0, 3);
const postsHref = getLocalizedPath('/posts', lang);
const aboutHref = getLocalizedPath('/about', lang);
---
<section class="mx-auto max-w-3xl px-4 pb-4 pt-14 sm:px-6 sm:pt-20">
<div class="flex flex-col items-start gap-8 sm:flex-row sm:items-center">
<img
src="/logo.png"
alt=""
width="867"
height="458"
class="w-40 shrink-0 drop-shadow-sm sm:w-52"
/>
<div>
<p class="text-sm font-semibold uppercase tracking-wide text-primary">{t('hero.kicker')}</p>
<h1 class="mt-2 text-3xl font-bold tracking-tight text-text sm:text-4xl">{t('hero.title')}</h1>
<p class="mt-4 max-w-xl text-lg leading-relaxed text-text-muted">{t('hero.subtitle')}</p>
<div class="mt-6 flex flex-wrap gap-3">
<a
href={postsHref}
class="rounded-full bg-primary px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-primary-hover"
>
{t('hero.cta.posts')}
</a>
<a
href={aboutHref}
class="rounded-full border border-border px-5 py-2.5 text-sm font-semibold text-text transition-colors hover:border-primary hover:text-primary"
>
{t('hero.cta.about')}
</a>
</div>
</div>
</div>
</section>
<section class="mx-auto max-w-3xl px-4 py-12 sm:px-6">
<div class="flex items-baseline justify-between gap-4">
<div>
<h2 class="text-xl font-semibold text-text">{t('home.recent')}</h2>
<p class="mt-1 text-sm text-text-muted">{t('home.recent.subtitle')}</p>
</div>
<a href={postsHref} class="shrink-0 text-sm font-medium text-primary hover:underline">
{t('home.viewAll')} &rarr;
</a>
</div>
{recentPosts.length > 0 ? (
<div class="mt-6">
{recentPosts.map((post) => <PostCard post={post} lang={lang} />)}
</div>
) : (
<p class="mt-6 text-text-muted">{t('posts.empty')}</p>
)}
</section>

View file

@ -0,0 +1,41 @@
import type { ReactNode } from 'react';
interface ObservationCardProps {
/** Defaults to "Observation" — pass a translated title in localized posts. */
title?: string;
children: ReactNode;
}
/**
* A callout for a side note or personal observation inside a post.
* Colors come from Tailwind's amber scale with dark: variants, so it
* follows whatever theme (light/dark) the site is currently in.
*/
export default function ObservationCard({ title = 'Observation', children }: ObservationCardProps) {
return (
<div className="not-prose my-6 flex gap-3 rounded-xl border border-amber-300/50 bg-amber-50 p-4 dark:border-amber-400/25 dark:bg-amber-400/10">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mt-0.5 size-5 shrink-0 text-amber-600 dark:text-amber-400"
aria-hidden="true"
>
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" />
<circle cx="12" cy="12" r="3" />
</svg>
<div className="min-w-0">
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-amber-800 dark:text-amber-300">
{title}
</p>
<div className="text-sm leading-relaxed text-stone-700 [&_a]:text-primary [&_a]:underline [&_p+p]:mt-2 dark:text-stone-200">
{children}
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,38 @@
---
import type { PostEntry } from '../lib/posts';
import { slugOf, formatDate } from '../lib/posts';
import { getLocalizedPath, type Lang } from '../lib/i18n';
interface Props {
post: PostEntry;
lang: Lang;
}
const { post, lang } = Astro.props;
const href = getLocalizedPath(`/posts/${slugOf(post)}`, lang);
const tagsHref = (tag: string) => getLocalizedPath(`/tags/${tag}`, lang);
---
<article class="group border-b border-border py-6 first:pt-0 last:border-b-0">
<a href={href} class="block">
<time datetime={post.data.pubDate.toISOString()} class="text-xs font-medium uppercase tracking-wide text-text-muted">
{formatDate(post.data.pubDate, lang)}
</time>
<h3 class="mt-1.5 text-xl font-semibold text-text transition-colors group-hover:text-primary">
{post.data.title}
</h3>
<p class="mt-1.5 text-[15px] leading-relaxed text-text-muted">{post.data.description}</p>
</a>
{post.data.tags.length > 0 && (
<div class="mt-3 flex flex-wrap gap-2">
{post.data.tags.map((tag) => (
<a
href={tagsHref(tag)}
class="rounded-full bg-surface-alt px-2.5 py-1 text-xs font-medium text-text-muted transition-colors hover:bg-accent/15 hover:text-accent"
>
#{tag}
</a>
))}
</div>
)}
</article>

View file

@ -0,0 +1,41 @@
---
import PostCard from './PostCard.astro';
import { getPostsByLang } from '../lib/posts';
import { getLocalizedPath, useTranslations, type Lang } from '../lib/i18n';
interface Props {
lang: Lang;
tag?: string;
}
const { lang, tag } = Astro.props;
const t = useTranslations(lang);
const allPosts = await getPostsByLang(lang);
const posts = tag ? allPosts.filter((p) => p.data.tags.includes(tag)) : allPosts;
const postsHref = getLocalizedPath('/posts', lang);
---
<section class="mx-auto max-w-3xl px-4 py-12 sm:px-6">
{tag ? (
<>
<p class="text-sm font-medium text-text-muted">{t('posts.filteredBy')}</p>
<h1 class="mt-1 text-3xl font-bold tracking-tight text-text">#{tag}</h1>
<a href={postsHref} class="mt-2 inline-block text-sm font-medium text-primary hover:underline">
{t('posts.clearFilter')}
</a>
</>
) : (
<>
<h1 class="text-3xl font-bold tracking-tight text-text">{t('posts.title')}</h1>
<p class="mt-2 text-text-muted">{t('posts.subtitle')}</p>
</>
)}
{posts.length > 0 ? (
<div class="mt-8">
{posts.map((post) => <PostCard post={post} lang={lang} />)}
</div>
) : (
<p class="mt-8 text-text-muted">{t('posts.empty')}</p>
)}
</section>

View file

@ -0,0 +1,71 @@
---
import type { Lang } from '../lib/i18n';
import { useTranslations } from '../lib/i18n';
interface Props {
lang: Lang;
}
const { lang } = Astro.props;
const t = useTranslations(lang);
---
<button
id="theme-toggle"
type="button"
aria-label={t('theme.switch')}
title={t('theme.switch')}
class="grid size-9 shrink-0 place-items-center rounded-full border border-border text-text-muted transition-colors hover:border-primary hover:text-primary"
>
<svg
id="theme-toggle-sun"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="hidden size-[18px]"
>
<circle cx="12" cy="12" r="4"></circle>
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"
></path>
</svg>
<svg
id="theme-toggle-moon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="hidden size-[18px]"
>
<path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5Z"></path>
</svg>
</button>
<script is:inline>
(function () {
const btn = document.getElementById('theme-toggle');
const sun = document.getElementById('theme-toggle-sun');
const moon = document.getElementById('theme-toggle-moon');
if (!btn || !sun || !moon) return;
function sync() {
const isDark = document.documentElement.classList.contains('dark');
sun.classList.toggle('hidden', !isDark);
moon.classList.toggle('hidden', isDark);
}
btn.addEventListener('click', function () {
const isDark = document.documentElement.classList.toggle('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
sync();
});
sync();
})();
</script>

19
src/content.config.ts Normal file
View file

@ -0,0 +1,19 @@
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
// Each post lives twice: src/content/posts/en/<slug>.mdx and
// src/content/posts/pt/<slug>.mdx. The loader id is "<lang>/<slug>",
// which is how we pair up translations and derive routes.
const posts = defineCollection({
loader: glob({ pattern: '**/*.mdx', base: './src/content/posts' }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
export const collections = { posts };

View file

@ -0,0 +1,30 @@
---
title: "GNU, Linux, and the fight that built modern software"
description: "A short history of the GNU project, Linus Torvalds' kernel, and the free/open source split that still shapes software today."
pubDate: 2026-05-30
tags: ["open-source", "history"]
---
Most of the software you rely on daily — the languages, the servers, half the tools in a typical terminal — traces back to a decision Richard Stallman made in 1983: to walk away from a proprietary printer driver dispute at MIT and start building a completely free replacement for Unix, from scratch. He called it GNU — "GNU's Not Unix," a recursive acronym, because that's the kind of joke that programmer culture loves.
## The GNU project
The GNU project's goal was total: a free operating system, kernel included, with every tool a Unix user would expect — a compiler, an editor, a shell, coreutils — released under licenses that guaranteed the freedom to run, study, modify, and share the software. That last part became the Free Software Foundation and the GPL, a license explicitly designed to keep derivative works free too — what Stallman called "copyleft."
By the early 1990s, GNU had built almost everything: GCC, Emacs, Bash, binutils, glibc. Almost everything except a kernel. The GNU Hurd, its intended kernel, was ambitious in design and slow to mature.
## Enter Linux
In 1991, a Finnish student named Linus Torvalds posted a now-famous, deliberately modest message to a Usenet newsgroup: he was writing a "free operating system (just a hobby, won't be big and professional like gnu)" for 386(486) AT clones. That hobby project was a kernel — Linux.
Linux filled exactly the gap GNU had left open. Combined with the GNU userland, you got a complete, free operating system: technically "GNU/Linux," though most people just say "Linux." The naming debate itself became a small piece of open source folklore — a genuine disagreement about credit and framing that's still occasionally relitigated on mailing lists today.
Torvalds' contribution didn't stop at the kernel. In 2005, frustrated with existing tools for the scale and pace of kernel development, he wrote Git in about ten days. A distributed version control system built specifically to handle a project with thousands of contributors and no single point of trust. It's hard to overstate how much of today's software workflow — pull requests, forks, the entire GitHub/GitLab ecosystem — sits on top of a tool built to solve one very specific, very large-scale problem.
## Why the history matters
It's easy to use `git commit`, install a package with `apt`, or compile something with `gcc` without ever thinking about where they came from. But the free software movement wasn't inevitable — it was a specific, sometimes contentious set of choices about licensing, collaboration, and what "ownership" of software should even mean.
The split between "free software" (Stallman's ethical framing, about user freedom) and "open source" (a later, more pragmatic framing coined partly to be palatable to business) is still a live philosophical distinction, not just semantics. Understanding it explains a lot about why different projects choose the licenses they do, and why some communities care so much about it.
I find this history genuinely useful, not just interesting — it's a reminder that the tools we treat as neutral infrastructure were built by people making deliberate, sometimes idealistic bets. Worth knowing before you take them for granted.

View file

@ -0,0 +1,22 @@
---
title: "Hello, world"
description: "A short note on why this blog exists and what I plan to write about here."
pubDate: 2026-01-12
tags: ["meta", "blogging"]
---
I finally sat down and built this thing. Not because the world needed another blog, but because I kept having half-formed thoughts about code, systems, and the history of the tools I use every day — and nowhere to put them.
So: hello. I'm Gabe. This is going to be a small, mostly technical corner of the internet where I write things down so I understand them better, and maybe so future-me (or you) can come back to them later.
## What to expect
A few recurring themes, roughly in order of how often I think about them:
- **How things work under the hood** — CPUs, compilers, operating systems, the kind of stuff that's easy to use and hard to explain.
- **Open source history** — the people and decisions that shaped the software we take for granted.
- **Whatever I'm learning at PUCRS** that seems worth writing about properly instead of just leaving in a notebook.
I write in English and Portuguese, since both are useful to me and hopefully to whoever reads this. Every post has both versions — use the language switch in the header to move between them.
No comment section, no newsletter, no tracking scripts I didn't write myself. Just posts. Let's see where this goes.

View file

@ -0,0 +1,41 @@
---
title: "How a CPU actually executes an instruction"
description: "Fetch, decode, execute: a plain-language walk through what your processor does billions of times per second."
pubDate: 2026-03-18
tags: ["computer-science", "systems"]
---
import ObservationCard from '../../../components/ObservationCard';
When people say a CPU "runs code," what actually happens is a lot more mechanical — and a lot more satisfying to understand — than that phrase suggests. There's no magic. Just a very fast, very disciplined loop.
## The loop: fetch, decode, execute
At the bottom of almost everything your computer does is a cycle that repeats, conceptually, forever:
1. **Fetch** — the CPU reads the next instruction from memory, at the address stored in the program counter (PC).
2. **Decode** — control logic figures out what the instruction actually means: which operation, which registers, which addressing mode.
3. **Execute** — the ALU (or another functional unit) performs the operation — an addition, a memory load, a branch.
4. **Write back** — the result, if any, is stored somewhere: a register, or memory.
5. The program counter updates, and the loop starts again.
<ObservationCard title="Observation">
This is the textbook five-stage model (sometimes just three: fetch, decode, execute). Real
processors pipeline these stages so multiple instructions are in flight at once, and modern
chips go much further with out-of-order execution and speculation — but the mental model above
still holds for reasoning about correctness.
</ObservationCard>
## Why this matters even if you never write assembly
You don't need to write machine code to benefit from understanding this loop. It explains, among other things:
- Why branches (`if`, loops) are relatively expensive — the CPU has to figure out where to fetch from next, and a wrong guess (misprediction) means throwing away speculative work.
- Why cache misses hurt so much — the fetch and memory-access stages stall waiting on DRAM, which is glacially slow compared to a clock cycle.
- Why "the compiler optimized it away" is a real thing — the compiler is generating the instruction stream this loop consumes, and it's free to pick a completely different sequence as long as observable behavior matches.
## Under the hood is not a black box
The instruction set architecture (ISA) — x86-64, ARM, RISC-V — is the contract between software and hardware: a fixed vocabulary of operations the fetch/decode/execute loop knows how to handle. Everything above that — your language, your framework, your favorite abstraction — eventually compiles or interprets down into that vocabulary.
Knowing this doesn't make you write faster code by itself. But it makes the rest of the stack stop feeling like magic, and that's usually where I start when I want to actually understand a system instead of just using it.

View file

@ -0,0 +1,28 @@
---
title: "Why I insist on knowing how things work"
description: "On the habit of digging past the abstraction, and why it's worth the extra time."
pubDate: 2026-08-25
tags: ["learning", "philosophy"]
---
import ObservationCard from '../../../components/ObservationCard';
There's a moment that happens fairly often when I'm learning something new: a tool, a library, a framework works, I get the result I wanted, and I still don't feel done. Something in me needs to know *why* it worked before I can move on.
This is slower. I'll admit that up front. You can ship things faster by treating a library as a black box, calling the documented function, and trusting it. Most of the time that's the correct engineering tradeoff, and I do it too when a deadline demands it.
But left to my own devices, I go the other way. If a build tool does something unexpected, I'd rather read its source for ten minutes than paste the error into a search bar. If a language feature feels like magic, I want to know what it actually compiles or desugars to.
<ObservationCard title="Observation">
This isn't really about distrust of abstractions — abstractions are how any of this scales at
all. It's that an abstraction I understand is one I can safely break when I need to, and one I
understand *badly* is where the weirdest, hardest-to-debug problems tend to live.
</ObservationCard>
## Where this comes from
I'm studying computer science at PUCRS, and if there's one thing the coursework reinforces, it's that almost every "advanced" tool is built from a small number of ideas repeated and combined: state machines, trees, graphs, the fetch-decode-execute loop, layers of indirection stacked on layers of indirection. Once you've actually implemented a toy version of something — a shell, a tiny compiler, a basic scheduler — the "real" version stops looking like magic and starts looking like an engineering decision you can evaluate.
That's part of why I find open source history so interesting too: it's the same instinct applied to systems instead of code. Knowing *why* GNU exists, or why Git was built the way it was, changes how I read a commit history or a mailing list thread. The tool stops being a given and becomes a decision someone made, for reasons, under constraints — which is exactly the kind of thing I want to understand before I trust it completely.
I don't think this is a virtue in some abstract sense. It's just a very specific, very particular way of working that happens to suit me, and this blog is mostly a record of the digging.

View file

@ -0,0 +1,30 @@
---
title: "GNU, Linux e a disputa que construiu o software moderno"
description: "Uma breve história do projeto GNU, do kernel do Linus Torvalds e da divisão entre software livre e open source que ainda molda o software hoje."
pubDate: 2026-05-30
tags: ["open-source", "history"]
---
Boa parte do software que usamos todo dia — as linguagens, os servidores, metade das ferramentas de um terminal comum — remonta a uma decisão que Richard Stallman tomou em 1983: abandonar uma disputa sobre um driver de impressora proprietário no MIT e começar a construir, do zero, uma substituta completamente livre para o Unix. Ele chamou isso de GNU — "GNU's Not Unix", um acrônimo recursivo, porque esse é exatamente o tipo de piada que a cultura da programação adora.
## O projeto GNU
O objetivo do projeto GNU era total: um sistema operacional livre, kernel incluído, com todas as ferramentas que um usuário Unix esperaria — compilador, editor, shell, coreutils — lançadas sob licenças que garantissem a liberdade de rodar, estudar, modificar e compartilhar o software. Essa última parte deu origem à Free Software Foundation e à GPL, uma licença desenhada explicitamente para manter livres também os trabalhos derivados — o que Stallman chamou de "copyleft".
No início dos anos 1990, o GNU já tinha construído quase tudo: GCC, Emacs, Bash, binutils, glibc. Quase tudo, menos um kernel. O GNU Hurd, o kernel planejado, era ambicioso em design e lento para amadurecer.
## Entra o Linux
Em 1991, um estudante finlandês chamado Linus Torvalds postou uma mensagem hoje famosa, deliberadamente modesta, em um newsgroup da Usenet: ele estava escrevendo um "sistema operacional livre (só um hobby, não vai ser grande e profissional como o gnu)" para clones AT 386(486). Esse projeto de hobby era um kernel — o Linux.
O Linux preencheu exatamente a lacuna que o GNU tinha deixado aberta. Combinado com o userland do GNU, você tinha um sistema operacional livre e completo: tecnicamente "GNU/Linux", embora a maioria das pessoas diga só "Linux". O próprio debate sobre o nome virou um pequeno pedaço do folclore do open source — uma discordância genuína sobre crédito e enquadramento que volta a ser discutida em listas de e-mail até hoje.
A contribuição de Torvalds não parou no kernel. Em 2005, frustrado com as ferramentas existentes para a escala e o ritmo do desenvolvimento do kernel, ele escreveu o Git em cerca de dez dias. Um sistema de controle de versão distribuído, construído especificamente para lidar com um projeto com milhares de colaboradores e nenhum ponto único de confiança. É difícil superestimar o quanto do fluxo de trabalho de software de hoje — pull requests, forks, todo o ecossistema GitHub/GitLab — está construído em cima de uma ferramenta feita para resolver um problema bem específico e de grande escala.
## Por que essa história importa
É fácil usar `git commit`, instalar um pacote com `apt` ou compilar algo com `gcc` sem nunca pensar de onde essas coisas vieram. Mas o movimento do software livre não era inevitável — foi um conjunto específico, às vezes controverso, de escolhas sobre licenciamento, colaboração e o que "propriedade" de software deveria sequer significar.
A divisão entre "software livre" (o enquadramento ético de Stallman, sobre a liberdade do usuário) e "open source" (um enquadramento posterior, mais pragmático, cunhado em parte para ser palatável ao mundo dos negócios) ainda é uma distinção filosófica viva, não só semântica. Entender isso explica bastante sobre por que projetos diferentes escolhem as licenças que escolhem, e por que algumas comunidades se importam tanto com isso.
Acho essa história genuinamente útil, não só interessante — ela é um lembrete de que as ferramentas que tratamos como infraestrutura neutra foram construídas por pessoas fazendo apostas deliberadas, às vezes idealistas. Vale a pena conhecer antes de tomar isso como garantido.

View file

@ -0,0 +1,22 @@
---
title: "Olá, mundo"
description: "Uma nota curta sobre por que este blog existe e sobre o que pretendo escrever aqui."
pubDate: 2026-01-12
tags: ["meta", "blogging"]
---
Finalmente me sentei e construí isso. Não porque o mundo precisasse de mais um blog, mas porque eu vivia tendo pensamentos pela metade sobre código, sistemas e a história das ferramentas que uso todo dia — e não tinha onde colocá-los.
Então: olá. Eu sou o Gabe. Este vai ser um cantinho pequeno e majoritariamente técnico da internet, onde escrevo as coisas para entendê-las melhor, e talvez para que o meu eu do futuro (ou você) possa voltar a elas depois.
## O que esperar
Alguns temas recorrentes, mais ou menos na ordem em que penso sobre eles:
- **Como as coisas funcionam por baixo dos panos** — CPUs, compiladores, sistemas operacionais, esse tipo de coisa que é fácil de usar e difícil de explicar.
- **História do open source** — as pessoas e decisões que moldaram o software que hoje consideramos garantido.
- **O que eu estiver aprendendo na PUCRS** que pareça valer a pena escrever com calma, em vez de deixar só num caderno.
Escrevo em português e em inglês, já que os dois são úteis pra mim e, espero, para quem estiver lendo. Todo post tem as duas versões — use o botão de idioma no cabeçalho para alternar entre elas.
Sem seção de comentários, sem newsletter, sem script de rastreamento que eu mesmo não tenha escrito. Só posts. Vamos ver até onde isso vai.

View file

@ -0,0 +1,41 @@
---
title: "Como uma CPU de fato executa uma instrução"
description: "Busca, decodificação, execução: um passeio em linguagem simples pelo que o seu processador faz bilhões de vezes por segundo."
pubDate: 2026-03-18
tags: ["computer-science", "systems"]
---
import ObservationCard from '../../../components/ObservationCard';
Quando dizem que uma CPU "executa código", o que de fato acontece é bem mais mecânico — e bem mais satisfatório de entender — do que essa frase sugere. Não há mágica nenhuma. Só um loop muito rápido e muito disciplinado.
## O loop: busca, decodificação, execução
Na base de quase tudo que o seu computador faz existe um ciclo que se repete, conceitualmente, para sempre:
1. **Busca (fetch)** — a CPU lê a próxima instrução da memória, no endereço guardado no contador de programa (PC).
2. **Decodificação (decode)** — a lógica de controle descobre o que a instrução realmente significa: qual operação, quais registradores, qual modo de endereçamento.
3. **Execução (execute)** — a ALU (ou outra unidade funcional) realiza a operação — uma soma, uma leitura de memória, um desvio.
4. **Write back** — o resultado, se houver, é armazenado em algum lugar: um registrador ou a memória.
5. O contador de programa é atualizado, e o loop recomeça.
<ObservationCard title="Observação">
Esse é o modelo clássico de cinco estágios (às vezes reduzido a três: busca, decodificação,
execução). Processadores reais fazem pipeline desses estágios, de forma que várias instruções
estejam "em voo" ao mesmo tempo, e chips modernos vão muito além disso com execução fora de
ordem e especulação — mas o modelo mental acima ainda serve para raciocinar sobre corretude.
</ObservationCard>
## Por que isso importa mesmo que você nunca escreva assembly
Você não precisa escrever código de máquina para se beneficiar de entender esse loop. Ele explica, entre outras coisas:
- Por que desvios (`if`, laços) são relativamente caros — a CPU precisa descobrir de onde buscar a próxima instrução, e um chute errado (misprediction) significa descartar trabalho especulativo.
- Por que cache miss dói tanto — os estágios de busca e acesso à memória ficam parados esperando a DRAM, que é absurdamente lenta comparada a um ciclo de clock.
- Por que "o compilador otimizou isso e sumiu" é uma coisa real — o compilador está gerando o fluxo de instruções que esse loop consome, e é livre para escolher uma sequência completamente diferente desde que o comportamento observável seja o mesmo.
## Por baixo dos panos não é uma caixa-preta
A arquitetura do conjunto de instruções (ISA) — x86-64, ARM, RISC-V — é o contrato entre software e hardware: um vocabulário fixo de operações que o loop de busca/decodificação/execução sabe tratar. Tudo o que vem acima disso — sua linguagem, seu framework, sua abstração favorita — eventualmente compila ou é interpretado até chegar nesse vocabulário.
Saber disso não faz você escrever código mais rápido por si só. Mas faz o resto da pilha parar de parecer mágica, e é geralmente por aí que eu começo quando quero de fato entender um sistema em vez de apenas usá-lo.

View file

@ -0,0 +1,29 @@
---
title: "Por que eu insisto em entender como as coisas funcionam"
description: "Sobre o hábito de cavar além da abstração, e por que vale a pena o tempo extra."
pubDate: 2026-08-25
tags: ["learning", "philosophy"]
---
import ObservationCard from '../../../components/ObservationCard';
Há um momento que acontece com bastante frequência quando estou aprendendo algo novo: uma ferramenta, uma biblioteca, um framework funciona, eu consigo o resultado que queria, e mesmo assim não me sinto satisfeito. Alguma coisa em mim precisa entender *por que* aquilo funcionou antes de eu conseguir seguir em frente.
Isso é mais lento. Admito isso de cara. Dá pra entregar coisas mais rápido tratando uma biblioteca como uma caixa-preta, chamando a função documentada e confiando nela. Na maior parte do tempo essa é a decisão de engenharia correta, e eu também faço isso quando um prazo exige.
Mas, por conta própria, eu vou pelo caminho contrário. Se uma ferramenta de build faz algo inesperado, prefiro ler o código-fonte dela por dez minutos a colar o erro numa busca. Se um recurso de uma linguagem parece mágica, eu quero saber para o que ele realmente compila ou se desdobra.
<ObservationCard title="Observação">
Isso não é bem sobre desconfiar de abstrações — abstrações são o motivo de qualquer coisa
escalar. É que uma abstração que eu entendo é uma que eu posso quebrar com segurança quando
preciso, e uma que eu entendo *mal* costuma ser onde moram os problemas mais estranhos e mais
difíceis de debugar.
</ObservationCard>
## De onde isso vem
Estou cursando ciência da computação na PUCRS, e se tem uma coisa que as disciplinas reforçam, é que quase toda ferramenta "avançada" é construída a partir de um pequeno número de ideias repetidas e combinadas: máquinas de estado, árvores, grafos, o loop de busca-decodificação-execução, camadas de indireção empilhadas sobre camadas de indireção. Depois que você de fato implementa uma versão de brinquedo de alguma coisa — um shell, um compilador minúsculo, um escalonador básico — a versão "de verdade" para de parecer mágica e passa a parecer uma decisão de engenharia que você consegue avaliar.
Essa é parte do motivo pelo qual também acho a história do open source tão interessante: é o mesmo instinto aplicado a sistemas em vez de código. Saber *por que* o GNU existe, ou por que o Git foi construído do jeito que foi, muda como eu leio um histórico de commits ou uma thread de lista de e-mail. A ferramenta deixa de ser um dado e passa a ser uma decisão que alguém tomou, por motivos, sob restrições — que é exatamente o tipo de coisa que eu quero entender antes de confiar completamente nela.
Não acho que isso seja uma virtude em algum sentido abstrato. É só um jeito bem específico e bem particular de trabalhar que combina comigo, e este blog é basicamente um registro dessa cavada.

View file

@ -0,0 +1,64 @@
---
import '../styles/global.css';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import type { Lang } from '../lib/i18n';
interface Props {
title: string;
description: string;
lang: Lang;
}
const { title, description, lang } = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---
<!doctype html>
<html lang={lang}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="alternate icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="canonical" href={canonicalURL} />
<link rel="alternate" hreflang="en" href={new URL(Astro.url.pathname.replace(/^\/pt/, '') || '/', Astro.site)} />
<link rel="alternate" hreflang="pt-BR" href={new URL(`/pt${Astro.url.pathname.replace(/^\/pt/, '')}`, Astro.site)} />
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalURL} />
<meta property="og:image" content={new URL('/logo.png', Astro.site)} />
<meta name="twitter:card" content="summary_large_image" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400..700;1,400..700&family=JetBrains+Mono:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<script is:inline>
// Runs before paint to avoid a flash of the wrong theme.
(function () {
const stored = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const dark = stored ? stored === 'dark' : prefersDark;
document.documentElement.classList.toggle('dark', dark);
})();
</script>
</head>
<body class="flex min-h-screen flex-col bg-bg font-sans text-text antialiased selection:bg-primary selection:text-white">
<Header lang={lang} />
<main class="flex-1">
<slot />
</main>
<Footer lang={lang} />
</body>
</html>

View file

@ -0,0 +1,57 @@
---
import type { PostEntry } from '../lib/posts';
import { formatDate } from '../lib/posts';
import { getLocalizedPath, useTranslations, type Lang } from '../lib/i18n';
interface Props {
post: PostEntry;
lang: Lang;
readingMinutes: number;
}
const { post, lang, readingMinutes } = Astro.props;
const t = useTranslations(lang);
const postsHref = getLocalizedPath('/posts', lang);
const tagsHref = (tag: string) => getLocalizedPath(`/tags/${tag}`, lang);
---
<article class="mx-auto max-w-3xl px-4 py-12 sm:px-6">
<a href={postsHref} class="inline-flex items-center gap-1.5 text-sm font-medium text-text-muted transition-colors hover:text-primary">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4">
<path d="M15 18l-6-6 6-6"></path>
</svg>
{t('post.back')}
</a>
<header class="mt-6 border-b border-border pb-6">
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-text-muted">
<time datetime={post.data.pubDate.toISOString()}>{formatDate(post.data.pubDate, lang)}</time>
<span aria-hidden="true">&middot;</span>
<span>{readingMinutes} {t('post.readingTime')}</span>
{post.data.updatedDate && (
<>
<span aria-hidden="true">&middot;</span>
<span>{t('post.updated')} {formatDate(post.data.updatedDate, lang)}</span>
</>
)}
</div>
<h1 class="mt-3 text-3xl font-bold tracking-tight text-text sm:text-4xl">{post.data.title}</h1>
<p class="mt-3 text-lg text-text-muted">{post.data.description}</p>
{post.data.tags.length > 0 && (
<div class="mt-4 flex flex-wrap gap-2">
{post.data.tags.map((tag) => (
<a
href={tagsHref(tag)}
class="rounded-full bg-surface-alt px-2.5 py-1 text-xs font-medium text-text-muted transition-colors hover:bg-accent/15 hover:text-accent"
>
#{tag}
</a>
))}
</div>
)}
</header>
<div class="prose prose-neutral mt-8 max-w-none prose-headings:font-semibold prose-headings:tracking-tight prose-a:text-primary prose-a:no-underline hover:prose-a:underline prose-code:before:content-none prose-code:after:content-none prose-img:rounded-xl dark:prose-invert">
<slot />
</div>
</article>

111
src/lib/i18n.ts Normal file
View file

@ -0,0 +1,111 @@
export const LANGS = ['en', 'pt'] as const;
export type Lang = (typeof LANGS)[number];
export const DEFAULT_LANG: Lang = 'en';
export const LANG_LABELS: Record<Lang, string> = {
en: 'English',
pt: 'Português',
};
export const ui = {
en: {
'site.name': 'gabeefranco',
'nav.home': 'Home',
'nav.posts': 'Posts',
'nav.about': 'About',
'nav.menu': 'Menu',
'lang.switch': 'Switch language',
'theme.switch': 'Toggle color theme',
'hero.kicker': "Gabriel Franco's notes",
'hero.title': 'Building things, and figuring out how they work.',
'hero.subtitle':
"Notes on computer science, systems, and the open source projects that shaped how I think about software. I'm gabeefranco — welcome.",
'hero.cta.posts': 'Read the posts',
'hero.cta.about': 'About me',
'home.recent': 'Recent posts',
'home.recent.subtitle': 'The latest things I’ve written down.',
'home.viewAll': 'View all posts',
'posts.title': 'All posts',
'posts.subtitle': 'Everything I’ve written, oldest to newest below, newest on top.',
'posts.empty': 'No posts yet. Check back soon.',
'posts.filteredBy': 'Posts tagged',
'posts.clearFilter': 'Clear filter',
'post.back': 'Back to posts',
'post.tags': 'Tags',
'post.readingTime': 'min read',
'post.updated': 'Updated',
'tag.label': 'Tag',
'about.title': 'About',
'footer.tagline': 'Written by hand in Porto Alegre.',
'footer.rights': 'All rights reserved.',
'footer.source': 'Source',
'404.title': 'Page not found',
'404.body': 'There’s nothing here. Let’s get you back home.',
'404.cta': 'Back to home',
},
pt: {
'site.name': 'gabeefranco',
'nav.home': 'Início',
'nav.posts': 'Posts',
'nav.about': 'Sobre',
'nav.menu': 'Menu',
'lang.switch': 'Mudar idioma',
'theme.switch': 'Alternar tema',
'hero.kicker': 'Anotações do Gabriel Franco',
'hero.title': 'Construindo coisas e entendendo como elas funcionam.',
'hero.subtitle':
'Notas sobre ciência da computação, sistemas e os projetos open source que moldaram a forma como penso sobre software. Eu sou o gabeefranco — seja bem-vindo.',
'hero.cta.posts': 'Ler os posts',
'hero.cta.about': 'Sobre mim',
'home.recent': 'Posts recentes',
'home.recent.subtitle': 'As últimas coisas que escrevi.',
'home.viewAll': 'Ver todos os posts',
'posts.title': 'Todos os posts',
'posts.subtitle': 'Tudo que já escrevi, do mais recente para o mais antigo.',
'posts.empty': 'Ainda não há posts. Volte em breve.',
'posts.filteredBy': 'Posts com a tag',
'posts.clearFilter': 'Limpar filtro',
'post.back': 'Voltar para os posts',
'post.tags': 'Tags',
'post.readingTime': 'min de leitura',
'post.updated': 'Atualizado em',
'tag.label': 'Tag',
'about.title': 'Sobre',
'footer.tagline': 'Escrito à mão em Porto Alegre.',
'footer.rights': 'Todos os direitos reservados.',
'footer.source': 'Código-fonte',
'404.title': 'Página não encontrada',
'404.body': 'Não há nada aqui. Vamos te levar de volta para o início.',
'404.cta': 'Voltar para o início',
},
} as const;
export type UiKey = keyof (typeof ui)['en'];
export function getLangFromUrl(url: URL): Lang {
const [, first] = url.pathname.split('/');
return first === 'pt' ? 'pt' : DEFAULT_LANG;
}
export function useTranslations(lang: Lang) {
return function t(key: UiKey): string {
return ui[lang][key] ?? ui[DEFAULT_LANG][key];
};
}
/** Prefix-strip the given pathname down to its language-agnostic form. */
export function stripLangPrefix(pathname: string): string {
return pathname.replace(/^\/pt(?=\/|$)/, '') || '/';
}
/** Given any pathname, build the equivalent path in the target language. */
export function getLocalizedPath(pathname: string, lang: Lang): string {
const stripped = stripLangPrefix(pathname);
if (lang === DEFAULT_LANG) return stripped;
return stripped === '/' ? '/pt' : `/pt${stripped}`;
}
export function localizeHref(path: string, lang: Lang): string {
if (lang === DEFAULT_LANG) return path;
return path === '/' ? '/pt' : `/pt${path}`;
}

54
src/lib/posts.ts Normal file
View file

@ -0,0 +1,54 @@
import { getCollection, type CollectionEntry } from 'astro:content';
import type { Lang } from './i18n';
export type PostEntry = CollectionEntry<'posts'>;
/** The id is "<lang>/<slug>" (see src/content.config.ts). */
export function langOf(entry: PostEntry): Lang {
return entry.id.split('/')[0] as Lang;
}
export function slugOf(entry: PostEntry): string {
return entry.id.split('/').slice(1).join('/');
}
export async function getPostsByLang(lang: Lang): Promise<PostEntry[]> {
const posts = await getCollection('posts', ({ id, data }) => {
return id.startsWith(`${lang}/`) && !data.draft;
});
return posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
}
export async function getPostBySlug(lang: Lang, slug: string): Promise<PostEntry | undefined> {
const posts = await getCollection('posts');
return posts.find((p) => p.id === `${lang}/${slug}`);
}
export async function getAllTags(lang: Lang): Promise<string[]> {
const posts = await getPostsByLang(lang);
const tags = new Set<string>();
for (const post of posts) {
for (const tag of post.data.tags) tags.add(tag);
}
return [...tags].sort((a, b) => a.localeCompare(b));
}
const WORDS_PER_MINUTE = 200;
export function estimateReadingMinutes(body: string): number {
const words = body.trim().split(/\s+/).filter(Boolean).length;
return Math.max(1, Math.round(words / WORDS_PER_MINUTE));
}
const DATE_LOCALES: Record<Lang, string> = {
en: 'en-US',
pt: 'pt-BR',
};
export function formatDate(date: Date, lang: Lang): string {
return new Intl.DateTimeFormat(DATE_LOCALES[lang], {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);
}

38
src/pages/404.astro Normal file
View file

@ -0,0 +1,38 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { getLocalizedPath, ui, type Lang } from '../lib/i18n';
// This file is prerendered once as a static /404.html and served by the host
// for any unmatched path, so we can't know at build time whether the visitor
// was under /pt/*. Render both languages and pick one at runtime with a tiny
// inline script based on the actual requested path.
const langs: Lang[] = ['en', 'pt'];
---
<BaseLayout title={`${ui.en['404.title']} — gabeefranco`} description={ui.en['404.body']} lang="en">
<section class="mx-auto flex max-w-3xl flex-col items-start px-4 py-24 sm:px-6">
<p class="font-mono text-sm text-primary">404</p>
{langs.map((lang) => (
<div data-notfound-lang={lang} class={lang === 'en' ? '' : 'hidden'}>
<h1 class="mt-2 text-3xl font-bold tracking-tight text-text">{ui[lang]['404.title']}</h1>
<p class="mt-3 text-text-muted">{ui[lang]['404.body']}</p>
<a
href={getLocalizedPath('/', lang)}
class="mt-6 inline-block rounded-full bg-primary px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-primary-hover"
>
{ui[lang]['404.cta']}
</a>
</div>
))}
</section>
</BaseLayout>
<script is:inline>
(function () {
if (location.pathname.replace(/\/$/, '').startsWith('/pt') === false) return;
document.documentElement.lang = 'pt';
document.querySelectorAll('[data-notfound-lang]').forEach(function (el) {
el.classList.toggle('hidden', el.getAttribute('data-notfound-lang') !== 'pt');
});
})();
</script>

41
src/pages/about.astro Normal file
View file

@ -0,0 +1,41 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { useTranslations } from '../lib/i18n';
const t = useTranslations('en');
const description =
"Gabriel Franco (gabeefranco): computer science student at PUCRS, interested in how software actually works and in the history of the open source movement.";
---
<BaseLayout title={`${t('about.title')} — ${t('site.name')}`} description={description} lang="en">
<section class="mx-auto max-w-3xl px-4 py-14 sm:px-6">
<h1 class="text-3xl font-bold tracking-tight text-text sm:text-4xl">{t('about.title')}</h1>
<div class="prose prose-neutral mt-6 max-w-none prose-headings:font-semibold prose-a:text-primary prose-a:no-underline hover:prose-a:underline dark:prose-invert">
<p>
Hi, I'm Gabe — Gabriel Franco. I'm a computer science student at PUCRS (Pontifícia
Universidade Católica do Rio Grande do Sul), and this blog is where I write down the
things I'm learning and thinking about along the way.
</p>
<p>
I care a lot about computer science as a field, not just as a set of tools to ship
products with. I have a fairly specific way of doing things: I'd rather understand a
system properly than memorize the steps to make it work, and I tend to slow down on
purpose to figure out what's actually happening under the hood — in a compiler, an
operating system, a language runtime, whatever it is. That habit shapes most of what ends
up on this blog.
</p>
<p>
I'm also genuinely interested in the history of the open source movement: the people,
the arguments, and the decisions that shaped the software we all rely on. Figures like
Linus Torvalds and projects like GNU come up often in what I read and write about, not
as trivia, but because understanding where a tool came from usually explains a lot about
how and why it works the way it does today.
</p>
<p>
This site is bilingual, in English and in Portuguese, and every post exists in both
languages. Thanks for stopping by.
</p>
</div>
</section>
</BaseLayout>

11
src/pages/index.astro Normal file
View file

@ -0,0 +1,11 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import HomeContent from '../components/HomeContent.astro';
import { useTranslations } from '../lib/i18n';
const t = useTranslations('en');
---
<BaseLayout title={`${t('site.name')} — ${t('hero.kicker')}`} description={t('hero.subtitle')} lang="en">
<HomeContent lang="en" />
</BaseLayout>

View file

@ -0,0 +1,22 @@
---
import { render } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import PostLayout from '../../layouts/PostLayout.astro';
import ObservationCard from '../../components/ObservationCard';
import { getPostsByLang, slugOf, estimateReadingMinutes } from '../../lib/posts';
export async function getStaticPaths() {
const posts = await getPostsByLang('en');
return posts.map((post) => ({ params: { slug: slugOf(post) }, props: { post } }));
}
const { post } = Astro.props;
const { Content } = await render(post);
const readingMinutes = estimateReadingMinutes(post.body ?? '');
---
<BaseLayout title={`${post.data.title} — gabeefranco`} description={post.data.description} lang="en">
<PostLayout post={post} lang="en" readingMinutes={readingMinutes}>
<Content components={{ ObservationCard }} />
</PostLayout>
</BaseLayout>

View file

@ -0,0 +1,11 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import PostsContent from '../../components/PostsContent.astro';
import { useTranslations } from '../../lib/i18n';
const t = useTranslations('en');
---
<BaseLayout title={`${t('posts.title')} — ${t('site.name')}`} description={t('posts.subtitle')} lang="en">
<PostsContent lang="en" />
</BaseLayout>

42
src/pages/pt/about.astro Normal file
View file

@ -0,0 +1,42 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { useTranslations } from '../../lib/i18n';
const t = useTranslations('pt');
const description =
'Gabriel Franco (gabeefranco): estudante de ciência da computação na PUCRS, interessado em como o software realmente funciona e na história do movimento open source.';
---
<BaseLayout title={`${t('about.title')} — ${t('site.name')}`} description={description} lang="pt">
<section class="mx-auto max-w-3xl px-4 py-14 sm:px-6">
<h1 class="text-3xl font-bold tracking-tight text-text sm:text-4xl">{t('about.title')}</h1>
<div class="prose prose-neutral mt-6 max-w-none prose-headings:font-semibold prose-a:text-primary prose-a:no-underline hover:prose-a:underline dark:prose-invert">
<p>
Olá, eu sou o Gabe — Gabriel Franco. Sou estudante de ciência da computação na PUCRS
(Pontifícia Universidade Católica do Rio Grande do Sul), e este blog é onde registro as
coisas que vou aprendendo e pensando pelo caminho.
</p>
<p>
Eu me importo bastante com ciência da computação como área, não só como um conjunto de
ferramentas para lançar produtos. Tenho um jeito bem específico de fazer as coisas:
prefiro entender um sistema de verdade a decorar os passos para fazê-lo funcionar, e
costumo desacelerar de propósito para descobrir o que realmente está acontecendo por
baixo dos panos — em um compilador, em um sistema operacional, no runtime de uma
linguagem, seja lá o que for. Esse hábito molda a maior parte do que acaba virando post
aqui.
</p>
<p>
Também tenho um interesse genuíno pela história do movimento open source: as pessoas, os
debates e as decisões que moldaram o software do qual todos dependemos. Figuras como
Linus Torvalds e projetos como o GNU aparecem com frequência no que leio e escrevo, não
como curiosidade, mas porque entender de onde uma ferramenta veio costuma explicar
bastante sobre como e por que ela funciona do jeito que funciona hoje.
</p>
<p>
Este site é bilíngue, em inglês e em português, e todo post existe nos dois idiomas.
Obrigado pela visita.
</p>
</div>
</section>
</BaseLayout>

11
src/pages/pt/index.astro Normal file
View file

@ -0,0 +1,11 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import HomeContent from '../../components/HomeContent.astro';
import { useTranslations } from '../../lib/i18n';
const t = useTranslations('pt');
---
<BaseLayout title={`${t('site.name')} — ${t('hero.kicker')}`} description={t('hero.subtitle')} lang="pt">
<HomeContent lang="pt" />
</BaseLayout>

View file

@ -0,0 +1,22 @@
---
import { render } from 'astro:content';
import BaseLayout from '../../../layouts/BaseLayout.astro';
import PostLayout from '../../../layouts/PostLayout.astro';
import ObservationCard from '../../../components/ObservationCard';
import { getPostsByLang, slugOf, estimateReadingMinutes } from '../../../lib/posts';
export async function getStaticPaths() {
const posts = await getPostsByLang('pt');
return posts.map((post) => ({ params: { slug: slugOf(post) }, props: { post } }));
}
const { post } = Astro.props;
const { Content } = await render(post);
const readingMinutes = estimateReadingMinutes(post.body ?? '');
---
<BaseLayout title={`${post.data.title} — gabeefranco`} description={post.data.description} lang="pt">
<PostLayout post={post} lang="pt" readingMinutes={readingMinutes}>
<Content components={{ ObservationCard }} />
</PostLayout>
</BaseLayout>

View file

@ -0,0 +1,11 @@
---
import BaseLayout from '../../../layouts/BaseLayout.astro';
import PostsContent from '../../../components/PostsContent.astro';
import { useTranslations } from '../../../lib/i18n';
const t = useTranslations('pt');
---
<BaseLayout title={`${t('posts.title')} — ${t('site.name')}`} description={t('posts.subtitle')} lang="pt">
<PostsContent lang="pt" />
</BaseLayout>

View file

@ -0,0 +1,23 @@
---
import BaseLayout from '../../../../layouts/BaseLayout.astro';
import PostsContent from '../../../../components/PostsContent.astro';
import { getAllTags, getPostsByLang } from '../../../../lib/posts';
import { useTranslations } from '../../../../lib/i18n';
export async function getStaticPaths() {
const tags = await getAllTags('pt');
return tags.map((tag) => ({ params: { tag } }));
}
const { tag } = Astro.params;
const t = useTranslations('pt');
const posts = (await getPostsByLang('pt')).filter((p) => p.data.tags.includes(tag!));
---
<BaseLayout
title={`#${tag} — ${t('site.name')}`}
description={`${t('posts.filteredBy')} #${tag}: ${posts.map((p) => p.data.title).join(', ')}`}
lang="pt"
>
<PostsContent lang="pt" tag={tag} />
</BaseLayout>

View file

@ -0,0 +1,23 @@
---
import BaseLayout from '../../../layouts/BaseLayout.astro';
import PostsContent from '../../../components/PostsContent.astro';
import { getAllTags, getPostsByLang } from '../../../lib/posts';
import { useTranslations } from '../../../lib/i18n';
export async function getStaticPaths() {
const tags = await getAllTags('en');
return tags.map((tag) => ({ params: { tag } }));
}
const { tag } = Astro.params;
const t = useTranslations('en');
const posts = (await getPostsByLang('en')).filter((p) => p.data.tags.includes(tag!));
---
<BaseLayout
title={`#${tag} — ${t('site.name')}`}
description={`${t('posts.filteredBy')} #${tag}: ${posts.map((p) => p.data.title).join(', ')}`}
lang="en"
>
<PostsContent lang="en" tag={tag} />
</BaseLayout>

64
src/styles/global.css Normal file
View file

@ -0,0 +1,64 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
/* Class-based dark mode: <html class="dark"> is toggled by ThemeToggle.tsx */
@custom-variant dark (&:where(.dark, .dark *));
@theme inline {
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
--color-bg: var(--bg);
--color-surface: var(--surface);
--color-surface-alt: var(--surface-alt);
--color-border: var(--border);
--color-text: var(--text);
--color-text-muted: var(--text-muted);
--color-primary: var(--primary);
--color-primary-hover: var(--primary-hover);
--color-accent: var(--accent);
--color-accent-deep: var(--accent-deep);
}
:root {
--bg: #fbf9f6;
--surface: #ffffff;
--surface-alt: #f3ede3;
--border: #e7dfd0;
--text: #201a14;
--text-muted: #6b5f52;
--primary: #e8590c;
--primary-hover: #c2410c;
--accent: #c77d00;
--accent-deep: #7a1204;
}
.dark {
--bg: #15110d;
--surface: #1d1712;
--surface-alt: #241c15;
--border: #392c20;
--text: #f3ece2;
--text-muted: #b6a996;
--primary: #ff7a3d;
--primary-hover: #ff9a63;
--accent: #ffc24c;
--accent-deep: #ff8a63;
}
html {
color-scheme: light;
}
html.dark {
color-scheme: dark;
}
body {
background-color: var(--color-bg);
color: var(--color-text);
}
::selection {
background-color: var(--color-primary);
color: #fff;
}

14
tsconfig.json Normal file
View file

@ -0,0 +1,14 @@
{
"extends": "astro/tsconfigs/strict",
"include": [
".astro/types.d.ts",
"**/*"
],
"exclude": [
"dist"
],
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}