Compare commits

..

3 commits

Author SHA1 Message Date
1d8b7d58a4 new hello-world post 2026-09-22 18:04:06 -03:00
21cb00d377 theme toggle error fixed 2026-09-20 00:29:38 -03:00
ce5912caaf partial font swap 2026-09-19 11:13:42 -03:00
14 changed files with 156 additions and 159 deletions

2
mise.toml Normal file
View file

@ -0,0 +1,2 @@
[tools]
claude = "latest"

View file

@ -10,7 +10,7 @@ const t = useTranslations(lang);
const year = new Date().getFullYear();
---
<footer class="border-t border-border/80">
<footer class="border-t border-border/80 font-mono">
<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')}

View file

@ -28,72 +28,79 @@ function isActive(href: string): boolean {
}
---
<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" />
<div class="flex flex-col justify-center gap-4 px-4 py-3 sm:px-6">
<div class="flex flex-row justify-between items-end">
<a href={homeHref} class="flex hover:underline shrink-0 items-center gap-2.5" aria-label={t('site.name')}>
<img src="/logo.png" class="h-4"/>
<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>
<div class="ml-10 flex sm:hidden justify-end gap-3">
<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"
class="text-accent max-w-fit font-mono text-xs sm:text-sm underline hover:cursor-pointer hover:text-accent-deep"
>
{otherLang}
{`${t('lang.switch')}: ${LANG_LABELS[otherLang]}`}
</a>
<ThemeToggle lang={lang} />
</div>
<div class="hidden sm:flex items-center gap-2">
<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')}
>
<nav class="flex items-center gap-1 justify-right" 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',
'px-2 py-1 text-sm font-medium font-mono underline',
isActive(link.href)
? 'bg-primary/10 text-primary'
: 'text-text-muted hover:bg-surface-alt hover:text-text',
? 'text-primary hover:text-primary-hover'
: 'text-text-muted hover:text-text',
]}
>
{link.label}
</a>
))}
</nav>
</details>
<a
href={langSwitchHref}
hreflang={otherLang}
title={t('lang.switch')}
aria-label={`${t('lang.switch')}: ${LANG_LABELS[otherLang]}`}
class="text-accent max-w-fit py-1 font-mono text-xs sm:text-sm underline hover:cursor-pointer hover:text-accent-deep "
>
{`${t('lang.switch')}: ${LANG_LABELS[otherLang]}`}
</a>
<ThemeToggle lang={lang} />
</div>
</div>
<div class="flex justify-end sm:hidden items-center gap-2">
<nav class="flex items-center gap-1 justify-right" aria-label={t('nav.menu')}>
{navLinks.map((link) => (
<a
href={link.href}
aria-current={isActive(link.href) ? 'page' : undefined}
class:list={[
'px-2 py-1 text-sm font-medium font-mono underline',
isActive(link.href)
? 'text-primary hover:text-primary-hover'
: 'text-text-muted hover:text-text',
]}
>
{link.label}
</a>
))}
</nav>
</div>
</div>
</header>

View file

@ -15,47 +15,54 @@ 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>
<section class="mx-10 my-10 flex flex-col gap-5 font-mono">
<div class="flex flex-col gap-2">
<h1 class="text-text text-2xl">{t('hero.title')}</h1>
<h2 class="text-text text-lg">{t('hero.subtitle')}</h2>
</div>
</section>
<section class="mx-auto max-w-3xl px-4 py-12 sm:px-6">
<!-- <section class="mx-auto max-w-3xl px-4 pb-4 pt-14 sm:px-6 sm:pt-20 font-mono"> -->
<!-- <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-5"> -->
<!-- <a -->
<!-- href={postsHref} -->
<!-- class="text-primary hover:underline font-bold px-2 py-1 bg-primary/10" -->
<!-- > -->
<!-- {t('hero.cta.posts')} -->
<!-- </a> -->
<!-- <a -->
<!-- href={aboutHref} -->
<!-- class="text-text hover:underline font-bold px-2 py-1 bg-text/5" -->
<!-- > -->
<!-- {t('hero.cta.about')} -->
<!-- </a> -->
<!-- </div> -->
<!-- </div> -->
<!-- </div> -->
<!-- </section> -->
<section class="mx-10 font-mono">
<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>
<p class="mt-1 text-sm text-text-muted mb-2">{t('home.recent.subtitle')}</p>
<a href={postsHref} class="shrink-0 text-sm font-medium text-primary hover:underline">
{t('home.viewAll')} &rarr;
</a>
</div>
</div>
{recentPosts.length > 0 ? (
<div class="mt-6">

View file

@ -13,12 +13,12 @@ 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">
<article class="group border-b border-border py-6 first:pt-0 last:border-b-0 font-mono">
<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">
<h3 class="mt-1.5 text-xl font-semibold text-text group-hover:text-primary group-hover:underline">
{post.data.title}
</h3>
<p class="mt-1.5 text-[15px] leading-relaxed text-text-muted">{post.data.description}</p>
@ -28,7 +28,7 @@ const tagsHref = (tag: string) => getLocalizedPath(`/tags/${tag}`, lang);
{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"
class="text-text-muted px-1 py-0.5 text-sm hover:text-accent underline"
>
#{tag}
</a>

View file

@ -15,19 +15,19 @@ 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">
<section class="mx-10 mt-10">
{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">
<p class="text-sm font-mono font-medium text-text-muted">{t('posts.filteredBy')}</p>
<h1 class="mt-1 font-mono text-3xl font-bold tracking-tight text-text">#{tag}</h1>
<a href={postsHref} class="font-mono 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>
<h1 class="font-mono text-3xl font-bold tracking-tight text-text">{t('posts.title')}</h1>
<p class="font-mono mt-2 text-text-muted">{t('posts.subtitle')}</p>
</>
)}

View file

@ -11,61 +11,26 @@ const t = useTranslations(lang);
---
<button
id="theme-toggle"
type="button"
class="theme-toggle text-accent sm:ml-2 font-mono text-xs sm:text-sm underline hover:cursor-pointer hover:text-accent-deep"
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>
{t('theme.switch')}
</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);
}
<script>
function init() {
const buttons = document.querySelectorAll('.theme-toggle');
buttons.forEach((btn) => {
btn.addEventListener('click', function () {
const isDark = document.documentElement.classList.toggle('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
sync();
});
});
}
sync();
})();
init();
document.addEventListener('astro:after-swap', init);
</script>

View file

@ -1,8 +1,15 @@
---
title: "Hello, world"
description: "A short note on why this blog exists and what I plan to write about here."
pubDate: 2026-01-12
title: "This blog's Hello World (or whatever you want to call this site)"
description: "A short note on why this exists and what I plan to write about here."
pubDate: 2026-09-22
tags: ["meta", "blogging"]
---
Hi!
I've always had a creative side, as anyone who knows me can tell you. Sometimes it gets tucked away somewhere obscure, in the back of a drawer, waiting years to be used. And sometimes I make really good use of it.
I've always loved writing, and I've wanted a place of my own to publish my thoughts for a long time. Only the ones I'd want to be public, of course. Nobody wants to hand over the entire inner workings of their brain just like that, for free. Still, I think some of my intellectual adventures deserve a bit of exposure, especially since, without someone to judge them, I'll never know if they were worth it.
One of my inspirations for creating a site/blog was [Fábio Akita](https://instagram.com/akitaonrails). I've been following him for a long time, and it was through the videos on [his YouTube channel](https://youtube.com/akitaonrails) that I started studying computer science, before I even got into college. Even though the videos are genuinely great, teaching things that are hard to learn any other way, after a while of binge-watching them I found his blog, [akitaonrails.com](https://akitaonrails.com). I thought it was super cool, and I liked the idea of having a personal place to share your opinions and the things you've learned.
Akita is a seasoned guy who's been in the industry for years. Me? Just a student slightly ahead of the material in my CS program. To people my age, this whole writing thing, having a blog, sounds like something old people do. But, like I said in my [last post](https://gabeefran.co/posts/spotify-complaining), I like the idea of doing things the old-fashioned way every now and then. The word _"Blog"_ sounds so 2010, but I don't know what else I'd call this crappy site you're reading. I guess _"blog"_ it is. Good thing I've been practicing caring less about what other people think, and look at that: it's working!
Anyway, call it whatever you want, I think it's worth posting something here every now and then. Even if nobody's interested, whatever! Then let it just be my public folder of notes. Just the fact that I opened the drawer and pulled my creativity out from the back of it already makes it all worth it.

View file

@ -1,8 +1,17 @@
---
title: "Olá, mundo"
description: "Uma nota curta sobre por que este blog existe e sobre o que pretendo escrever aqui."
pubDate: 2026-01-12
title: "O Hello World desse blog (ou seja lá como você queira chamar esse site)"
description: "Uma nota curta sobre por que isso existe e sobre o que pretendo escrever aqui."
pubDate: 2026-09-22
tags: ["meta", "blogging"]
---
Oie
Eu sempre tive um lado criativo, quem me conhece sabe. Às vezes ele fica guardado em algum lugar obscuro, no fundo da gaveta, esperando anos pra ser utilizado. E às vezes eu me aproveito dele muito bem.
Sempre gostei muito de escrever, e faz tempo que queria ter um lugar só meu pra publicar meus pensamentos. Mas só aqueles que eu gostaria que fossem públicos, claro. Ninguém quer entregar todo o funcionamento interno do seu cérebro assim, de graça. Ainda assim, acho que algumas das minhas aventuras intelectuais merecem um pouco de exposição, até porque, sem alguém pra julgar, nunca saberei se valeram a pena.
Uma das minhas inspirações pra criar um site/blog foi o [Fábio Akita](https://instagram.com/akitaonrails). Eu acompanho ele há muito tempo, e foi por meio dos vídeos do [canal do YouTube dele](https://youtube.com/akitaonrails) que eu comecei a estudar sobre computação, antes de entrar na faculdade. Apesar dos vídeos serem realmente muito bons, fornecendo aprendizados difíceis de obter de outra forma, depois de um tempo maratonando os vídeos eu descobri o blog dele, [akitaonrails.com](https://akitaonrails.com). Eu achei muito massa, e gostei da ideia de se ter um lugar pessoal onde se expõem opiniões e aprendizados.
O Akita é um cara com experiência, que vive há anos na indústria. Eu, um mero estudante levemente adiantado no conteúdo da faculdade de computação. Esse negócio de escrever, ter um blog, parece coisa de gente velha pras pessoas da minha geração. Mas, como falei no [último post](https://gabeefran.co/pt/posts/spotify-complaining), eu gosto da ideia de fazer coisas à moda antiga de vez em quando. O termo _"Blog"_ soa muito 2010, mas não sei que outro nome daria a esta porcaria deste site que você está lendo. Acho que vai ser _"blog"_ mesmo. Bem que eu tenho treinado pra ligar menos pra opinião dos outros, e olha só: tá funcionando!
Enfim, chame como você quiser, eu acho que vale a pena publicar algo de vez em quando por aqui. Mesmo que ninguém esteja interessado, tanto faz! Que seja apenas a minha pasta pública de notas, então. Só de eu ter aberto a gaveta e tirado de lá do fundo a minha criatividade, considero que já valeu a pena.

View file

@ -15,8 +15,8 @@ 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">
<article class="mx-10 my-10 font-mono">
<a href={postsHref} class="underline inline-flex items-center gap-1.5 text-sm font-medium text-text-muted 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>
@ -42,7 +42,7 @@ const tagsHref = (tag: string) => getLocalizedPath(`/tags/${tag}`, lang);
{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"
class=" text-xs font-medium text-text-muted underline hover:text-accent"
>
#{tag}
</a>

View file

@ -19,7 +19,7 @@ export const ui = {
'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.",
"Notes on computer science, systems, and the open source projects that shaped how I think about software. I'm Gabriel Franco — welcome.",
'hero.cta.posts': 'Read the posts',
'hero.cta.about': 'About me',
'home.recent': 'Recent posts',
@ -54,7 +54,7 @@ export const ui = {
'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.',
'Notas sobre ciência da computação, sistemas e os projetos open source que moldaram a forma como penso sobre software. Eu sou o Gabriel Franco — seja bem-vindo.',
'hero.cta.posts': 'Ler os posts',
'hero.cta.about': 'Sobre mim',
'home.recent': 'Posts recentes',

View file

@ -10,7 +10,7 @@ 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">
<section class="mx-10 flex flex-col items-start px-4 py-24 sm:px-6 font-mono">
<p class="font-mono text-sm text-primary">404</p>
{langs.map((lang) => (
<div data-notfound-lang={lang} class={lang === 'en' ? '' : 'hidden'}>
@ -18,7 +18,7 @@ const langs: Lang[] = ['en', 'pt'];
<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"
class="mt-4 inline-block underline text-primary hover:text-primary-hover font-bold"
>
{ui[lang]['404.cta']}
</a>

View file

@ -8,7 +8,7 @@ const description =
---
<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">
<section class="mx-10 mt-10 font-mono">
<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">

View file

@ -8,7 +8,7 @@ const description =
---
<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">
<section class="mx-10 mt-10 font-mono">
<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">