Compare commits

..

2 commits

Author SHA1 Message Date
aa1bb53b96 pt about page correction 2026-10-01 10:23:06 -03:00
fa3f12a672 render about page from markdown, served at /pt/sobre in portuguese
The about body moves from i18n.ts to src/content/about/{en,pt}.md; the
slug frontmatter field sets each language's URL. /pt/about redirects to
/pt/sobre, and the language switcher and hreflang links point at the
right slug for the other language.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:12:51 -03:00
11 changed files with 120 additions and 43 deletions

View file

@ -49,7 +49,11 @@ export default defineConfig({
// routing would add middleware that 404s the unprefixed on-demand // routing would add middleware that 404s the unprefixed on-demand
// /page-card and /social-card routes (prefix-always) or /en/* (default). // /page-card and /social-card routes (prefix-always) or /en/* (default).
redirects: Object.fromEntries(LEGACY_EN_PATHS.map((path) => [path, path === '/' ? '/en' : `/en${path}`])), redirects: {
...Object.fromEntries(LEGACY_EN_PATHS.map((path) => [path, path === '/' ? '/en' : `/en${path}`])),
// The Portuguese about page moved to /pt/sobre (slug from src/content/about/pt.md).
'/pt/about': '/pt/sobre',
},
integrations: [react(), mdx(), sitemap({ filter: (page) => !DRAFT_PATHS.some((path) => page.endsWith(path)) })], integrations: [react(), mdx(), sitemap({ filter: (page) => !DRAFT_PATHS.some((path) => page.endsWith(path)) })],

View file

@ -1,9 +1,12 @@
--- ---
import ThemeToggle from './ThemeToggle.astro'; import ThemeToggle from './ThemeToggle.astro';
import { getAboutPath } from '../lib/about';
import { getLocalizedPath, LANG_LABELS, useTranslations, type Lang } from '../lib/i18n'; import { getLocalizedPath, LANG_LABELS, useTranslations, type Lang } from '../lib/i18n';
interface Props { interface Props {
lang: Lang; lang: Lang;
/** Where the language switch goes; defaults to swapping the /<lang> prefix. */
langSwitchHref?: string;
} }
const { lang } = Astro.props; const { lang } = Astro.props;
@ -13,8 +16,8 @@ const otherLang: Lang = lang === 'en' ? 'pt' : 'en';
const pathname = Astro.url.pathname; const pathname = Astro.url.pathname;
const homeHref = getLocalizedPath('/', lang); const homeHref = getLocalizedPath('/', lang);
const postsHref = getLocalizedPath('/posts', lang); const postsHref = getLocalizedPath('/posts', lang);
const aboutHref = getLocalizedPath('/about', lang); const aboutHref = await getAboutPath(lang);
const langSwitchHref = getLocalizedPath(pathname, otherLang); const langSwitchHref = Astro.props.langSwitchHref ?? getLocalizedPath(pathname, otherLang);
const navLinks = [ const navLinks = [
{ href: homeHref, label: t('nav.home'), id: 'home' }, { href: homeHref, label: t('nav.home'), id: 'home' },

View file

@ -1,6 +1,7 @@
--- ---
import PostCard from './PostCard.astro'; import PostCard from './PostCard.astro';
import { getPostsByLang } from '../lib/posts'; import { getPostsByLang } from '../lib/posts';
import { getAboutPath } from '../lib/about';
import { getLocalizedPath, useTranslations, type Lang } from '../lib/i18n'; import { getLocalizedPath, useTranslations, type Lang } from '../lib/i18n';
interface Props { interface Props {
@ -12,7 +13,7 @@ const t = useTranslations(lang);
const posts = await getPostsByLang(lang); const posts = await getPostsByLang(lang);
const recentPosts = posts.slice(0, 3); const recentPosts = posts.slice(0, 3);
const postsHref = getLocalizedPath('/posts', lang); const postsHref = getLocalizedPath('/posts', lang);
const aboutHref = getLocalizedPath('/about', lang); const aboutHref = await getAboutPath(lang);
--- ---
<section class="mx-10 my-10 flex flex-col gap-5 font-mono"> <section class="mx-10 my-10 flex flex-col gap-5 font-mono">

View file

@ -18,4 +18,14 @@ const posts = defineCollection({
}), }),
}); });
export const collections = { posts }; // The about page, one file per language: src/content/about/<lang>.md.
// `slug` is the URL segment for that language (about, sobre).
const about = defineCollection({
loader: glob({ pattern: '*.md', base: './src/content/about' }),
schema: z.object({
language: z.enum(['en', 'pt']),
slug: z.string(),
}),
});
export const collections = { posts, about };

12
src/content/about/en.md Normal file
View file

@ -0,0 +1,12 @@
---
language: en
slug: about
---
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.
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.
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.
This site is bilingual, in English and in Portuguese, and every post exists in both languages. Thanks for stopping by.

12
src/content/about/pt.md Normal file
View file

@ -0,0 +1,12 @@
---
language: pt
slug: sobre
---
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.
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 do que 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.
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.
Este site é bilíngue, em inglês e em português, e todo post existe nos dois idiomas. Obrigado pela visita.

View file

@ -18,9 +18,15 @@ interface Props {
image?: string; image?: string;
/** Keep the page out of search engines (used for draft posts). */ /** Keep the page out of search engines (used for draft posts). */
noindex?: boolean; noindex?: boolean;
/**
* This page's path in each language, for pages whose path isn't the same
* across languages (the about page). Defaults to swapping the /<lang> prefix.
*/
alternates?: Partial<Record<Lang, string>>;
} }
const { title, description, lang, noindex } = Astro.props; const { title, description, lang, noindex, alternates = {} } = Astro.props;
const alternatePath = (target: Lang) => alternates[target] ?? getLocalizedPath(Astro.url.pathname, target);
const image = Astro.props.image && existsSync(`public${Astro.props.image}`) ? Astro.props.image : '/logo.png'; const image = Astro.props.image && existsSync(`public${Astro.props.image}`) ? Astro.props.image : '/logo.png';
const canonicalURL = new URL(Astro.url.pathname, Astro.site); const canonicalURL = new URL(Astro.url.pathname, Astro.site);
--- ---
@ -34,8 +40,8 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site);
<link rel="alternate icon" href="/favicon.ico" /> <link rel="alternate icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="canonical" href={canonicalURL} /> <link rel="canonical" href={canonicalURL} />
<link rel="alternate" hreflang="en" href={new URL(getLocalizedPath(Astro.url.pathname, 'en'), Astro.site)} /> <link rel="alternate" hreflang="en" href={new URL(alternatePath('en'), Astro.site)} />
<link rel="alternate" hreflang="pt-BR" href={new URL(getLocalizedPath(Astro.url.pathname, 'pt'), Astro.site)} /> <link rel="alternate" hreflang="pt-BR" href={new URL(alternatePath('pt'), Astro.site)} />
<meta name="generator" content={Astro.generator} /> <meta name="generator" content={Astro.generator} />
<link rel="sitemap" href="/sitemap-index.xml" /> <link rel="sitemap" href="/sitemap-index.xml" />
@ -82,7 +88,7 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site);
</script> </script>
</head> </head>
<body class="flex min-h-screen flex-col bg-bg font-sans text-text antialiased selection:bg-primary selection:text-white"> <body class="flex min-h-screen flex-col bg-bg font-sans text-text antialiased selection:bg-primary selection:text-white">
<Header lang={lang} /> <Header lang={lang} langSwitchHref={alternates[lang === 'en' ? 'pt' : 'en']} />
<main class="flex-1"> <main class="flex-1">
<slot /> <slot />
</main> </main>

25
src/lib/about.ts Normal file
View file

@ -0,0 +1,25 @@
import { getCollection, type CollectionEntry } from 'astro:content';
import type { Lang } from './i18n';
// The about page lives at src/content/about/<lang>.md; its URL segment comes
// from the `slug` frontmatter field, so each language can have its own
// (/en/about, /pt/sobre).
export type AboutPage = CollectionEntry<'about'>;
export async function getAboutPages(): Promise<AboutPage[]> {
return getCollection('about');
}
export async function getAboutPage(lang: Lang): Promise<AboutPage> {
const page = (await getAboutPages()).find((entry) => entry.data.language === lang);
if (!page) throw new Error(`Missing src/content/about/${lang}.md`);
return page;
}
export function aboutPathOf(page: AboutPage): string {
return `/${page.data.language}/${page.data.slug}`;
}
export async function getAboutPath(lang: Lang): Promise<string> {
return aboutPathOf(await getAboutPage(lang));
}

View file

@ -45,13 +45,6 @@ export const ui = {
'about.title': 'About', 'about.title': 'About',
'about.description': 'about.description':
'Gabriel Franco: computer science student at PUCRS, interested in how software actually works and in the history of open source.', 'Gabriel Franco: computer science student at PUCRS, interested in how software actually works and in the history of open source.',
// Paragraphs are separated by a blank line.
'about.body': [
"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.",
"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.",
"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.",
'This site is bilingual, in English and in Portuguese, and every post exists in both languages. Thanks for stopping by.',
].join('\n\n'),
'footer.tagline': 'Written by hand in Porto Alegre.', 'footer.tagline': 'Written by hand in Porto Alegre.',
'footer.rights': 'All rights reserved.', 'footer.rights': 'All rights reserved.',
'footer.source': 'Source', 'footer.source': 'Source',
@ -96,12 +89,6 @@ export const ui = {
'about.title': 'Sobre', 'about.title': 'Sobre',
'about.description': 'about.description':
'Gabriel Franco: estudante de ciência da computação na PUCRS, interessado em como o software realmente funciona e na história do open source.', 'Gabriel Franco: estudante de ciência da computação na PUCRS, interessado em como o software realmente funciona e na história do open source.',
'about.body': [
'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.',
'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.',
'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.',
'Este site é bilíngue, em inglês e em português, e todo post existe nos dois idiomas. Obrigado pela visita.',
].join('\n\n'),
'footer.tagline': 'Escrito à mão em Porto Alegre.', 'footer.tagline': 'Escrito à mão em Porto Alegre.',
'footer.rights': 'Todos os direitos reservados.', 'footer.rights': 'Todos os direitos reservados.',
'footer.source': 'Código-fonte', 'footer.source': 'Código-fonte',

View file

@ -0,0 +1,38 @@
---
import { render } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { aboutPathOf, getAboutPages, type AboutPage } from '../../lib/about';
import { pageMeta, useTranslations, type Lang } from '../../lib/i18n';
import { pageCardPath } from '../../lib/social-cards';
export async function getStaticPaths() {
const pages = await getAboutPages();
// The language switcher and hreflang links need the other language's slug.
const alternates = Object.fromEntries(pages.map((page) => [page.data.language, `${aboutPathOf(page)}/`]));
return pages.map((page) => ({
params: { lang: page.data.language, about: page.data.slug },
props: { page, alternates },
}));
}
interface Props {
page: AboutPage;
alternates: Record<Lang, string>;
}
const { page: entry, alternates } = Astro.props;
const lang = entry.data.language;
const t = useTranslations(lang);
const page = pageMeta(lang, 'about');
const { Content } = await render(entry);
---
<BaseLayout title={`${page.title} — ${t('site.name')}`} description={page.description} lang={lang} image={pageCardPath(lang, 'about')} alternates={alternates}>
<section class="mx-10 mt-10 font-mono">
<h1 class="text-3xl font-bold tracking-tight text-text sm:text-4xl">{page.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">
<Content />
</div>
</section>
</BaseLayout>

View file

@ -1,21 +0,0 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { langPaths, pageMeta, useTranslations, type Lang } from '../../lib/i18n';
import { pageCardPath } from '../../lib/social-cards';
export const getStaticPaths = langPaths;
const lang = Astro.params.lang as Lang;
const t = useTranslations(lang);
const page = pageMeta(lang, 'about');
---
<BaseLayout title={`${page.title} — ${t('site.name')}`} description={page.description} lang={lang} image={pageCardPath(lang, 'about')}>
<section class="mx-10 mt-10 font-mono">
<h1 class="text-3xl font-bold tracking-tight text-text sm:text-4xl">{page.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">
{t('about.body').split('\n\n').map((paragraph) => <p>{paragraph}</p>)}
</div>
</section>
</BaseLayout>