From fa3f12a672a551ef7901f5e394da42edfbd6ff34 Mon Sep 17 00:00:00 2001 From: Gabriel Franco Date: Thu, 1 Oct 2026 10:12:51 -0300 Subject: [PATCH] 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 --- astro.config.mjs | 6 ++++- src/components/Header.astro | 7 ++++-- src/components/HomeContent.astro | 3 ++- src/content.config.ts | 12 +++++++++- src/content/about/en.md | 12 ++++++++++ src/content/about/pt.md | 12 ++++++++++ src/layouts/BaseLayout.astro | 14 ++++++++---- src/lib/about.ts | 25 +++++++++++++++++++++ src/lib/i18n.ts | 13 ----------- src/pages/[lang]/[about].astro | 38 ++++++++++++++++++++++++++++++++ src/pages/[lang]/about.astro | 21 ------------------ 11 files changed, 120 insertions(+), 43 deletions(-) create mode 100644 src/content/about/en.md create mode 100644 src/content/about/pt.md create mode 100644 src/lib/about.ts create mode 100644 src/pages/[lang]/[about].astro delete mode 100644 src/pages/[lang]/about.astro diff --git a/astro.config.mjs b/astro.config.mjs index d9d9e68..9597494 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -49,7 +49,11 @@ export default defineConfig({ // routing would add middleware that 404s the unprefixed on-demand // /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)) })], diff --git a/src/components/Header.astro b/src/components/Header.astro index 69c03da..b4210f5 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -1,9 +1,12 @@ --- import ThemeToggle from './ThemeToggle.astro'; +import { getAboutPath } from '../lib/about'; import { getLocalizedPath, LANG_LABELS, useTranslations, type Lang } from '../lib/i18n'; interface Props { lang: Lang; + /** Where the language switch goes; defaults to swapping the / prefix. */ + langSwitchHref?: string; } const { lang } = Astro.props; @@ -13,8 +16,8 @@ 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 aboutHref = await getAboutPath(lang); +const langSwitchHref = Astro.props.langSwitchHref ?? getLocalizedPath(pathname, otherLang); const navLinks = [ { href: homeHref, label: t('nav.home'), id: 'home' }, diff --git a/src/components/HomeContent.astro b/src/components/HomeContent.astro index 8a149bf..98e256a 100644 --- a/src/components/HomeContent.astro +++ b/src/components/HomeContent.astro @@ -1,6 +1,7 @@ --- import PostCard from './PostCard.astro'; import { getPostsByLang } from '../lib/posts'; +import { getAboutPath } from '../lib/about'; import { getLocalizedPath, useTranslations, type Lang } from '../lib/i18n'; interface Props { @@ -12,7 +13,7 @@ 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); +const aboutHref = await getAboutPath(lang); ---
diff --git a/src/content.config.ts b/src/content.config.ts index 75790db..9818fb1 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -18,4 +18,14 @@ const posts = defineCollection({ }), }); -export const collections = { posts }; +// The about page, one file per language: src/content/about/.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 }; diff --git a/src/content/about/en.md b/src/content/about/en.md new file mode 100644 index 0000000..03ab3dc --- /dev/null +++ b/src/content/about/en.md @@ -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. diff --git a/src/content/about/pt.md b/src/content/about/pt.md new file mode 100644 index 0000000..34867c7 --- /dev/null +++ b/src/content/about/pt.md @@ -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 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. diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index a86e7f0..9ceafff 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -18,9 +18,15 @@ interface Props { image?: string; /** Keep the page out of search engines (used for draft posts). */ 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 / prefix. + */ + alternates?: Partial>; } -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 canonicalURL = new URL(Astro.url.pathname, Astro.site); --- @@ -34,8 +40,8 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site); - - + + @@ -82,7 +88,7 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site); -
+
diff --git a/src/lib/about.ts b/src/lib/about.ts new file mode 100644 index 0000000..dcb0ff6 --- /dev/null +++ b/src/lib/about.ts @@ -0,0 +1,25 @@ +import { getCollection, type CollectionEntry } from 'astro:content'; +import type { Lang } from './i18n'; + +// The about page lives at src/content/about/.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 { + return getCollection('about'); +} + +export async function getAboutPage(lang: Lang): Promise { + 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 { + return aboutPathOf(await getAboutPage(lang)); +} diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts index 8effcf2..9aece5e 100644 --- a/src/lib/i18n.ts +++ b/src/lib/i18n.ts @@ -45,13 +45,6 @@ export const ui = { 'about.title': 'About', 'about.description': '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.rights': 'All rights reserved.', 'footer.source': 'Source', @@ -96,12 +89,6 @@ export const ui = { 'about.title': 'Sobre', '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.', - '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.rights': 'Todos os direitos reservados.', 'footer.source': 'Código-fonte', diff --git a/src/pages/[lang]/[about].astro b/src/pages/[lang]/[about].astro new file mode 100644 index 0000000..3c7d382 --- /dev/null +++ b/src/pages/[lang]/[about].astro @@ -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; +} + +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); +--- + + +
+

{page.title}

+ +
+ +
+
+
diff --git a/src/pages/[lang]/about.astro b/src/pages/[lang]/about.astro deleted file mode 100644 index 5164451..0000000 --- a/src/pages/[lang]/about.astro +++ /dev/null @@ -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'); ---- - - -
-

{page.title}

- -
- {t('about.body').split('\n\n').map((paragraph) =>

{paragraph}

)} -
-
-