initial commit
This commit is contained in:
commit
e4ef0abe41
47 changed files with 6953 additions and 0 deletions
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
4
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal 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
22
AGENTS.md
Normal 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
1
CLAUDE.md
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
AGENTS.md
|
||||
30
README.md
Normal file
30
README.md
Normal 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
25
astro.config.mjs
Normal 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
BIN
logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
5490
package-lock.json
generated
Normal file
5490
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
32
package.json
Normal file
32
package.json
Normal 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
BIN
public/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
38
public/favicon.svg
Normal file
38
public/favicon.svg
Normal 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
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
38
src/components/Footer.astro
Normal file
38
src/components/Footer.astro
Normal 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>
|
||||
© {year} <span class="font-mono text-text">gabeefranco</span> · {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>
|
||||
99
src/components/Header.astro
Normal file
99
src/components/Header.astro
Normal 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>
|
||||
67
src/components/HomeContent.astro
Normal file
67
src/components/HomeContent.astro
Normal 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')} →
|
||||
</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>
|
||||
41
src/components/ObservationCard.tsx
Normal file
41
src/components/ObservationCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
38
src/components/PostCard.astro
Normal file
38
src/components/PostCard.astro
Normal 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>
|
||||
41
src/components/PostsContent.astro
Normal file
41
src/components/PostsContent.astro
Normal 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>
|
||||
71
src/components/ThemeToggle.astro
Normal file
71
src/components/ThemeToggle.astro
Normal 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
19
src/content.config.ts
Normal 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 };
|
||||
30
src/content/posts/en/gnu-and-the-free-software-story.mdx
Normal file
30
src/content/posts/en/gnu-and-the-free-software-story.mdx
Normal 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.
|
||||
22
src/content/posts/en/hello-world.mdx
Normal file
22
src/content/posts/en/hello-world.mdx
Normal 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.
|
||||
41
src/content/posts/en/how-cpus-execute-instructions.mdx
Normal file
41
src/content/posts/en/how-cpus-execute-instructions.mdx
Normal 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.
|
||||
28
src/content/posts/en/why-i-like-knowing-how-things-work.mdx
Normal file
28
src/content/posts/en/why-i-like-knowing-how-things-work.mdx
Normal 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.
|
||||
30
src/content/posts/pt/gnu-and-the-free-software-story.mdx
Normal file
30
src/content/posts/pt/gnu-and-the-free-software-story.mdx
Normal 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.
|
||||
22
src/content/posts/pt/hello-world.mdx
Normal file
22
src/content/posts/pt/hello-world.mdx
Normal 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.
|
||||
41
src/content/posts/pt/how-cpus-execute-instructions.mdx
Normal file
41
src/content/posts/pt/how-cpus-execute-instructions.mdx
Normal 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.
|
||||
29
src/content/posts/pt/why-i-like-knowing-how-things-work.mdx
Normal file
29
src/content/posts/pt/why-i-like-knowing-how-things-work.mdx
Normal 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.
|
||||
64
src/layouts/BaseLayout.astro
Normal file
64
src/layouts/BaseLayout.astro
Normal 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>
|
||||
57
src/layouts/PostLayout.astro
Normal file
57
src/layouts/PostLayout.astro
Normal 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">·</span>
|
||||
<span>{readingMinutes} {t('post.readingTime')}</span>
|
||||
{post.data.updatedDate && (
|
||||
<>
|
||||
<span aria-hidden="true">·</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
111
src/lib/i18n.ts
Normal 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
54
src/lib/posts.ts
Normal 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
38
src/pages/404.astro
Normal 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
41
src/pages/about.astro
Normal 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
11
src/pages/index.astro
Normal 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>
|
||||
22
src/pages/posts/[...slug].astro
Normal file
22
src/pages/posts/[...slug].astro
Normal 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>
|
||||
11
src/pages/posts/index.astro
Normal file
11
src/pages/posts/index.astro
Normal 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
42
src/pages/pt/about.astro
Normal 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
11
src/pages/pt/index.astro
Normal 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>
|
||||
22
src/pages/pt/posts/[...slug].astro
Normal file
22
src/pages/pt/posts/[...slug].astro
Normal 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>
|
||||
11
src/pages/pt/posts/index.astro
Normal file
11
src/pages/pt/posts/index.astro
Normal 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>
|
||||
23
src/pages/pt/tags/[tag]/index.astro
Normal file
23
src/pages/pt/tags/[tag]/index.astro
Normal 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>
|
||||
23
src/pages/tags/[tag]/index.astro
Normal file
23
src/pages/tags/[tag]/index.astro
Normal 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
64
src/styles/global.css
Normal 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
14
tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [
|
||||
".astro/types.d.ts",
|
||||
"**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"dist"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue