From 6cd13476dc4e5c275c05368985432f8882173f90 Mon Sep 17 00:00:00 2001 From: Gabriel Franco Date: Tue, 29 Sep 2026 16:50:21 -0300 Subject: [PATCH] i18n refactor --- astro.config.mjs | 34 ++++++-- scripts/generate-social-cards.ts | 14 +-- src/components/Header.astro | 4 +- src/content.config.ts | 1 + src/content/posts/en/hello-world.mdx | 3 +- src/content/posts/en/spotify-complaining.mdx | 3 +- ...lugin4shell-git-trick-ai-coding-agents.mdx | 3 +- src/content/posts/pt/hello-world.mdx | 3 +- src/content/posts/pt/spotify-complaining.mdx | 3 +- ...lugin4shell-git-trick-ai-coding-agents.mdx | 3 +- src/layouts/BaseLayout.astro | 6 +- src/lib/i18n.ts | 86 +++++++++++++++---- src/lib/pages.ts | 69 --------------- src/lib/posts.ts | 4 +- src/lib/social-cards.ts | 16 ++++ src/pages/404.astro | 12 +-- src/pages/[lang]/about.astro | 21 +++++ src/pages/[lang]/feed.xml.ts | 24 ++++++ src/pages/[lang]/index.astro | 20 +++++ src/pages/[lang]/posts/[...slug].astro | 35 ++++++++ src/pages/[lang]/posts/index.astro | 16 ++++ src/pages/[lang]/tags/[tag]/index.astro | 23 +++++ src/pages/about.astro | 41 --------- src/pages/feed.xml.ts | 19 ---- src/pages/index.astro | 18 ---- src/pages/posts/[...slug].astro | 22 ----- src/pages/posts/index.astro | 13 --- src/pages/pt/about.astro | 42 --------- src/pages/pt/index.astro | 18 ---- src/pages/pt/posts/[...slug].astro | 22 ----- src/pages/pt/posts/index.astro | 13 --- src/pages/pt/tags/[tag]/index.astro | 20 ----- src/pages/tags/[tag]/index.astro | 20 ----- 33 files changed, 285 insertions(+), 366 deletions(-) delete mode 100644 src/lib/pages.ts create mode 100644 src/lib/social-cards.ts create mode 100644 src/pages/[lang]/about.astro create mode 100644 src/pages/[lang]/feed.xml.ts create mode 100644 src/pages/[lang]/index.astro create mode 100644 src/pages/[lang]/posts/[...slug].astro create mode 100644 src/pages/[lang]/posts/index.astro create mode 100644 src/pages/[lang]/tags/[tag]/index.astro delete mode 100644 src/pages/about.astro delete mode 100644 src/pages/feed.xml.ts delete mode 100644 src/pages/index.astro delete mode 100644 src/pages/posts/[...slug].astro delete mode 100644 src/pages/posts/index.astro delete mode 100644 src/pages/pt/about.astro delete mode 100644 src/pages/pt/index.astro delete mode 100644 src/pages/pt/posts/[...slug].astro delete mode 100644 src/pages/pt/posts/index.astro delete mode 100644 src/pages/pt/tags/[tag]/index.astro delete mode 100644 src/pages/tags/[tag]/index.astro diff --git a/astro.config.mjs b/astro.config.mjs index a39ffd9..49440e2 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -7,17 +7,37 @@ import sitemap from '@astrojs/sitemap' import tailwindcss from '@tailwindcss/vite'; import vercel from '@astrojs/vercel'; +// English used to be served without a /en prefix. Every URL published before +// the move keeps working through a 301; new pages only ever existed under /en. +// (Astro can't express this as a dynamic redirect in a static build: the +// destination must be an existing route with the same params.) +const LEGACY_EN_PATHS = [ + '/', + '/about', + '/posts', + '/posts/hello-world', + '/posts/spotify-complaining', + '/posts/understanding-plugin4shell-git-trick-ai-coding-agents', + '/tags/ai', + '/tags/blogging', + '/tags/cybersecurity', + '/tags/git', + '/tags/life', + '/tags/meta', + '/tags/software', + '/feed.xml', +]; + // https://astro.build/config export default defineConfig({ site: 'https://gabeefran.co', - i18n: { - defaultLocale: 'en', - locales: ['en', 'pt'], - routing: { - prefixDefaultLocale: false, - }, - }, + // No `i18n` block on purpose: locales come from src/lib/i18n.ts and every + // locale, English included, lives under src/pages/[lang]/. Astro's i18n + // 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}`])), integrations: [react(), mdx(), sitemap()], diff --git a/scripts/generate-social-cards.ts b/scripts/generate-social-cards.ts index eeb46d9..ff8a9b7 100644 --- a/scripts/generate-social-cards.ts +++ b/scripts/generate-social-cards.ts @@ -9,20 +9,20 @@ // /social-card and /page-card routes must be deployed at BASE_URL. import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; -import { PAGES, pageCardPath, tagCardPath, tagPage, type PageKey } from '../src/lib/pages.ts'; +import { LANGS, pageMeta, tagPageMeta, type Lang, type PageKey } from '../src/lib/i18n.ts'; +import { pageCardPath, postCardPath, tagCardPath } from '../src/lib/social-cards.ts'; const BASE_URL = process.env.SOCIAL_CARD_BASE_URL ?? 'https://gabeefran.co'; const CF_API = 'https://api.cloudflare.com/client/v4/accounts'; const POSTS_DIR = 'src/content/posts'; -const LANGS = ['en', 'pt'] as const; // 404.html is prerendered once, in English (see src/pages/404.astro). -const LANG_PAGES: Record<(typeof LANGS)[number], PageKey[]> = { +const LANG_PAGES: Record = { en: ['home', 'posts', 'about', '404'], pt: ['home', 'posts', 'about'], }; interface Post { - lang: (typeof LANGS)[number]; + lang: Lang; slug: string; title: string; description?: string; @@ -72,13 +72,13 @@ function listCards(posts: Post[]): Card[] { const postCards: Card[] = posts.map((post) => { const params: Record = { title: post.title, author: 'Gabriel Franco' }; if (post.description) params.description = post.description; - return { path: `/social-cards/${post.lang}/${post.slug}.png`, route: '/social-card', params }; + return { path: postCardPath(post.lang, post.slug), route: '/social-card', params }; }); const pageCards: Card[] = LANGS.flatMap((lang) => { - const pages = LANG_PAGES[lang].map((key) => ({ path: pageCardPath(lang, key), meta: PAGES[lang][key] })); + const pages = LANG_PAGES[lang].map((key) => ({ path: pageCardPath(lang, key), meta: pageMeta(lang, key) })); const tags = [...new Set(posts.filter((p) => p.lang === lang).flatMap((p) => p.tags))].sort(); - const tagPages = tags.map((tag) => ({ path: tagCardPath(lang, tag), meta: tagPage(lang, tag) })); + const tagPages = tags.map((tag) => ({ path: tagCardPath(lang, tag), meta: tagPageMeta(lang, tag) })); return [...pages, ...tagPages].map(({ path, meta }) => ({ path, route: '/page-card' as const, diff --git a/src/components/Header.astro b/src/components/Header.astro index f90a1e2..69c03da 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -23,7 +23,7 @@ const navLinks = [ ]; function isActive(href: string): boolean { - if (href === homeHref) return pathname === href; + if (href === homeHref) return pathname.replace(/\/$/, '') === href; return pathname === href || pathname.startsWith(`${href}/`); } --- @@ -31,7 +31,7 @@ function isActive(href: string): boolean {
-
+
{t('site.name')} diff --git a/src/content.config.ts b/src/content.config.ts index 08b3ad3..75790db 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -9,6 +9,7 @@ const posts = defineCollection({ schema: z.object({ title: z.string(), description: z.string(), + language: z.enum(['en', 'pt']), pubDate: z.coerce.date(), slug: z.string(), updatedDate: z.coerce.date().optional(), diff --git a/src/content/posts/en/hello-world.mdx b/src/content/posts/en/hello-world.mdx index dac9858..d41f0fc 100644 --- a/src/content/posts/en/hello-world.mdx +++ b/src/content/posts/en/hello-world.mdx @@ -1,9 +1,10 @@ --- title: "This blog's Hello World (or whatever you want to call this site)" -slug: "hello-world" +slug: "en-hello-world" description: "A short note on why this exists and what I plan to write about here." pubDate: 2026-09-22 tags: ["meta", "blogging"] +language: "en" --- 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. diff --git a/src/content/posts/en/spotify-complaining.mdx b/src/content/posts/en/spotify-complaining.mdx index e3f905d..78968cd 100644 --- a/src/content/posts/en/spotify-complaining.mdx +++ b/src/content/posts/en/spotify-complaining.mdx @@ -1,9 +1,10 @@ --- title: "Spotify's Time Capsule Change and the Enshittification of Software" -slug: "spotify-complaining" +slug: "en-spotify-complaining" description: "Spotify just removed one of its best features. Beyond a bit of complaining, I want to open a debate about what software is turning into." pubDate: 2026-09-2 tags: ["software", "life"] +language: "en" --- import ObservationCard from '../../../components/ObservationCard.tsx' diff --git a/src/content/posts/en/understanding-plugin4shell-git-trick-ai-coding-agents.mdx b/src/content/posts/en/understanding-plugin4shell-git-trick-ai-coding-agents.mdx index 31cdc24..f496328 100644 --- a/src/content/posts/en/understanding-plugin4shell-git-trick-ai-coding-agents.mdx +++ b/src/content/posts/en/understanding-plugin4shell-git-trick-ai-coding-agents.mdx @@ -1,10 +1,11 @@ --- title: "Understanding Plugin4Shell: An Exploit That Uses a Simple Git Trick to Fool AI Coding Agents" description: "A git branch named after a commit hash was enough to make AI coding agents run unverified plugins. I reproduced the Plugin4Shell trick on GitHub and Forgejo." -slug: "understanding-plugin4shell-git-trick-ai-coding-agents" +slug: "en-understanding-plugin4shell-git-trick-ai-coding-agents" pubDate: 2026-09-28 tags: ["cybersecurity", "git", "ai", "software"] seoKeywords: "Plugin4Shell, git branch named after commit hash, SHA pinning bypass, AI coding agents security, Claude Code plugin vulnerability, supply chain attack, git internals, Forgejo" +language: "en" --- This post is based on [Ana Maria Constantin's post at TheNextWeb](https://thenextweb.com/news/plugin4shell-ai-coding-agents-zero-click-rce-sha-pinning) and [Air Security's report](https://www.air.security/blog-posts/plugin4shell). diff --git a/src/content/posts/pt/hello-world.mdx b/src/content/posts/pt/hello-world.mdx index 86a4659..a116e31 100644 --- a/src/content/posts/pt/hello-world.mdx +++ b/src/content/posts/pt/hello-world.mdx @@ -2,8 +2,9 @@ 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 -slug: "hello-world" +slug: "pt-hello-world" tags: ["meta", "blogging"] +language: "pt" --- 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. diff --git a/src/content/posts/pt/spotify-complaining.mdx b/src/content/posts/pt/spotify-complaining.mdx index 1b9d14f..94b544c 100644 --- a/src/content/posts/pt/spotify-complaining.mdx +++ b/src/content/posts/pt/spotify-complaining.mdx @@ -1,9 +1,10 @@ --- title: "A mudança na cápsula do tempo do Spotify e a bostificação do software" description: "Spotify simplesmente removeu uma das suas melhores funcionalidades. Além de reclamar um pouco, quero abrir um debate sobre o que os softwares estão se tornando." -slug: "spotify-complaining" +slug: "pt-spotify-complaining" pubDate: 2026-09-2 tags: ["software", "life"] +language: "pt" --- import ObservationCard from '../../../components/ObservationCard.tsx' diff --git a/src/content/posts/pt/understanding-plugin4shell-git-trick-ai-coding-agents.mdx b/src/content/posts/pt/understanding-plugin4shell-git-trick-ai-coding-agents.mdx index a57ca08..aba5640 100644 --- a/src/content/posts/pt/understanding-plugin4shell-git-trick-ai-coding-agents.mdx +++ b/src/content/posts/pt/understanding-plugin4shell-git-trick-ai-coding-agents.mdx @@ -1,10 +1,11 @@ --- title: "Entendendo o Plugin4Shell: um exploit que usa um truque simples do git para enganar agentes de IA de programação" description: "Uma branch do git com o nome de um hash de commit bastou para fazer agentes de IA de programação rodarem plugins não verificados. Reproduzi o truque do Plugin4Shell no GitHub e no Forgejo." -slug: "understanding-plugin4shell-git-trick-ai-coding-agents" +slug: "pt-understanding-plugin4shell-git-trick-ai-coding-agents" pubDate: 2026-09-28 tags: ["cybersecurity", "git", "ai", "software"] seoKeywords: "Plugin4Shell, branch do git com nome de hash de commit, bypass de SHA pinning, segurança de agentes de IA de programação, vulnerabilidade de plugin do Claude Code, ataque à cadeia de suprimentos, internals do git, Forgejo" +language: "pt" --- Este post é baseado [no texto da Ana Maria Constantin no TheNextWeb](https://thenextweb.com/news/plugin4shell-ai-coding-agents-zero-click-rce-sha-pinning) e [no relatório da Air Security](https://www.air.security/blog-posts/plugin4shell). diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 1c2d8fd..713e050 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -3,7 +3,7 @@ import { existsSync } from 'node:fs'; import '../styles/global.css'; import Header from '../components/Header.astro'; import Footer from '../components/Footer.astro'; -import type { Lang } from '../lib/i18n'; +import { getLocalizedPath, type Lang } from '../lib/i18n'; interface Props { title: string; @@ -31,8 +31,8 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site); - - + + diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts index 0d80da9..8effcf2 100644 --- a/src/lib/i18n.ts +++ b/src/lib/i18n.ts @@ -1,3 +1,6 @@ +// Every localized string on the site lives here. Keep this file free of +// runtime imports: scripts/generate-social-cards.ts loads it directly with Node. + export const LANGS = ['en', 'pt'] as const; export type Lang = (typeof LANGS)[number]; export const DEFAULT_LANG: Lang = 'en'; @@ -22,11 +25,14 @@ export const ui = { "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.description': + 'Notes on computer science, systems, and the open source projects that shaped how I think about software.', '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.description': 'Everything I’ve written about computer science, systems, and open source, newest first.', 'posts.empty': 'No posts yet. Check back soon.', 'posts.filteredBy': 'Posts tagged', 'posts.clearFilter': 'Clear filter', @@ -35,10 +41,22 @@ export const ui = { 'post.readingTime': 'min read', 'post.updated': 'Updated', 'tag.label': 'Tag', + 'tag.description': 'Everything I’ve written tagged #{tag}.', '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', + 'feed.title': 'gabeefran.co', + 'feed.description': "Gabriel Franco's notes about computer science and other topics", '404.title': 'Page not found', '404.body': 'There’s nothing here. Let’s get you back home.', '404.cta': 'Back to home', @@ -57,11 +75,15 @@ export const ui = { '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.description': + 'Notas sobre ciência da computação, sistemas e os projetos open source que moldaram a forma como penso sobre software.', '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.description': + 'Tudo que já escrevi sobre ciência da computação, sistemas e open source, do mais recente ao mais antigo.', 'posts.empty': 'Ainda não há posts. Volte em breve.', 'posts.filteredBy': 'Posts com a tag', 'posts.clearFilter': 'Limpar filtro', @@ -70,10 +92,21 @@ export const ui = { 'post.readingTime': 'min de leitura', 'post.updated': 'Atualizado em', 'tag.label': 'Tag', + 'tag.description': 'Tudo que já escrevi com a tag #{tag}.', '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', + 'feed.title': 'gabeefran.co', + 'feed.description': 'Anotações do Gabriel Franco sobre ciência da computação e outros assuntos', '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', @@ -82,30 +115,51 @@ export const ui = { 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 isLang(value: string | undefined): value is Lang { + return LANGS.some((lang) => lang === value); } +/** getStaticPaths() result for pages that only vary by language. */ +export function langPaths() { + return LANGS.map((lang) => ({ params: { lang } })); +} + +/** `{name}` placeholders in a string are replaced with `vars[name]`. */ export function useTranslations(lang: Lang) { - return function t(key: UiKey): string { - return ui[lang][key] ?? ui[DEFAULT_LANG][key]; + return function t(key: UiKey, vars: Record = {}): string { + const template: string = ui[lang][key] ?? ui[DEFAULT_LANG][key]; + return template.replace(/\{(\w+)\}/g, (placeholder, name: string) => vars[name] ?? placeholder); }; } -/** Prefix-strip the given pathname down to its language-agnostic form. */ -export function stripLangPrefix(pathname: string): string { - return pathname.replace(/^\/pt(?=\/|$)/, '') || '/'; +/** Pages with their own title/description (tag pages use tagPageMeta). */ +export type PageKey = 'home' | 'posts' | 'about' | '404'; + +export interface PageMeta { + title: string; + description: string; +} + +const PAGE_META_KEYS: Record = { + home: { title: 'hero.title', description: 'home.description' }, + posts: { title: 'posts.title', description: 'posts.description' }, + about: { title: 'about.title', description: 'about.description' }, + '404': { title: '404.title', description: '404.body' }, +}; + +export function pageMeta(lang: Lang, page: PageKey): PageMeta { + const t = useTranslations(lang); + const keys = PAGE_META_KEYS[page]; + return { title: t(keys.title), description: t(keys.description) }; +} + +export function tagPageMeta(lang: Lang, tag: string): PageMeta { + return { title: `#${tag}`, description: useTranslations(lang)('tag.description', { tag }) }; } /** 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}`; + const [, first] = pathname.split('/'); + const stripped = (isLang(first) ? pathname.slice(first.length + 1) : pathname) || '/'; + return stripped === '/' ? `/${lang}` : `/${lang}${stripped}`; } diff --git a/src/lib/pages.ts b/src/lib/pages.ts deleted file mode 100644 index f6987fe..0000000 --- a/src/lib/pages.ts +++ /dev/null @@ -1,69 +0,0 @@ -// Title and description for every non-post page, shared by the pages -// themselves and by scripts/generate-social-cards.ts (so keep this file free -// of runtime imports: the script loads it directly with Node). -import type { Lang } from './i18n'; - -export type PageKey = 'home' | 'posts' | 'about' | '404'; - -export interface PageMeta { - title: string; - description: string; -} - -export const PAGES: Record> = { - en: { - home: { - title: 'Building things, and figuring out how they work.', - description: - 'Notes on computer science, systems, and the open source projects that shaped how I think about software.', - }, - posts: { - title: 'All posts', - description: 'Everything I’ve written about computer science, systems, and open source, newest first.', - }, - about: { - title: 'About', - description: - 'Gabriel Franco: computer science student at PUCRS, interested in how software actually works and in the history of open source.', - }, - '404': { - title: 'Page not found', - description: 'There’s nothing here. Let’s get you back home.', - }, - }, - pt: { - home: { - title: 'Construindo coisas e entendendo como elas funcionam.', - description: - 'Notas sobre ciência da computação, sistemas e os projetos open source que moldaram a forma como penso sobre software.', - }, - posts: { - title: 'Todos os posts', - description: 'Tudo que já escrevi sobre ciência da computação, sistemas e open source, do mais recente ao mais antigo.', - }, - about: { - title: 'Sobre', - 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.', - }, - '404': { - title: 'Página não encontrada', - description: 'Não há nada aqui. Vamos te levar de volta para o início.', - }, - }, -}; - -export function tagPage(lang: Lang, tag: string): PageMeta { - return lang === 'pt' - ? { title: `#${tag}`, description: `Tudo que já escrevi com a tag #${tag}.` } - : { title: `#${tag}`, description: `Everything I’ve written tagged #${tag}.` }; -} - -/** Where the generated card for a page lives, relative to public/. */ -export function pageCardPath(lang: Lang, key: PageKey): string { - return `/social-cards/${lang}/pages/${key}.png`; -} - -export function tagCardPath(lang: Lang, tag: string): string { - return `/social-cards/${lang}/tags/${tag}.png`; -} diff --git a/src/lib/posts.ts b/src/lib/posts.ts index 89cb8e9..4d0481c 100644 --- a/src/lib/posts.ts +++ b/src/lib/posts.ts @@ -9,12 +9,12 @@ export function langOf(entry: PostEntry): Lang { } export function slugOf(entry: PostEntry): string { - return entry.id.split('/').slice(1).join('/'); + return entry.id.replace('pt-', '').replace('en-', ''); } export async function getPostsByLang(lang: Lang): Promise { const posts = await getCollection('posts', ({ id, data }) => { - return id.startsWith(`${lang}/`) && !data.draft; + return id.startsWith(`${lang}-`) && !data.draft; }); return posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()); } diff --git a/src/lib/social-cards.ts b/src/lib/social-cards.ts new file mode 100644 index 0000000..b8dcddf --- /dev/null +++ b/src/lib/social-cards.ts @@ -0,0 +1,16 @@ +// Where generated OG cards live, relative to public/. Shared by the pages and +// by scripts/generate-social-cards.ts (so keep this file free of runtime +// imports: the script loads it directly with Node). +import type { Lang, PageKey } from './i18n'; + +export function postCardPath(lang: Lang, slug: string): string { + return `/social-cards/${lang}/${slug}.png`; +} + +export function pageCardPath(lang: Lang, page: PageKey): string { + return `/social-cards/${lang}/pages/${page}.png`; +} + +export function tagCardPath(lang: Lang, tag: string): string { + return `/social-cards/${lang}/tags/${tag}.png`; +} diff --git a/src/pages/404.astro b/src/pages/404.astro index 748064e..3ccee46 100644 --- a/src/pages/404.astro +++ b/src/pages/404.astro @@ -1,24 +1,24 @@ --- import BaseLayout from '../layouts/BaseLayout.astro'; -import { getLocalizedPath, ui, type Lang } from '../lib/i18n'; -import { PAGES, pageCardPath } from '../lib/pages'; +import { getLocalizedPath, LANGS, pageMeta, ui } from '../lib/i18n'; +import { pageCardPath } from '../lib/social-cards'; // 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']; +const page = pageMeta('en', '404'); ---

404

- {langs.map((lang) => ( + {LANGS.map((lang) => (

{ui[lang]['404.title']}

{ui[lang]['404.body']}

diff --git a/src/pages/[lang]/about.astro b/src/pages/[lang]/about.astro new file mode 100644 index 0000000..5164451 --- /dev/null +++ b/src/pages/[lang]/about.astro @@ -0,0 +1,21 @@ +--- +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}

)} +
+
+
diff --git a/src/pages/[lang]/feed.xml.ts b/src/pages/[lang]/feed.xml.ts new file mode 100644 index 0000000..001bad9 --- /dev/null +++ b/src/pages/[lang]/feed.xml.ts @@ -0,0 +1,24 @@ +import rss from '@astrojs/rss'; +import type { APIContext } from 'astro'; +import { getLocalizedPath, langPaths, useTranslations, type Lang } from '../../lib/i18n'; +import { getPostsByLang, slugOf } from '../../lib/posts'; + +export const getStaticPaths = langPaths; + +export async function GET(context: APIContext) { + const lang = context.params.lang as Lang; + const t = useTranslations(lang); + const posts = await getPostsByLang(lang); + return rss({ + title: t('feed.title'), + description: t('feed.description'), + site: new URL(getLocalizedPath('/', lang), context.site), + customData: `${lang}`, + items: posts.map((post) => ({ + title: post.data.title, + pubDate: post.data.pubDate, + description: post.data.description, + link: `${getLocalizedPath(`/posts/${slugOf(post)}`, lang)}/`, + })), + }); +} diff --git a/src/pages/[lang]/index.astro b/src/pages/[lang]/index.astro new file mode 100644 index 0000000..5aa416e --- /dev/null +++ b/src/pages/[lang]/index.astro @@ -0,0 +1,20 @@ +--- +import BaseLayout from '../../layouts/BaseLayout.astro'; +import HomeContent from '../../components/HomeContent.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); +--- + + + + diff --git a/src/pages/[lang]/posts/[...slug].astro b/src/pages/[lang]/posts/[...slug].astro new file mode 100644 index 0000000..1e4fdd5 --- /dev/null +++ b/src/pages/[lang]/posts/[...slug].astro @@ -0,0 +1,35 @@ +--- +import { render } from 'astro:content'; +import BaseLayout from '../../../layouts/BaseLayout.astro'; +import PostLayout from '../../../layouts/PostLayout.astro'; +import ObservationCard from '../../../components/ObservationCard'; +import { LANGS, useTranslations, type Lang } from '../../../lib/i18n'; +import { getPostsByLang, slugOf, estimateReadingMinutes } from '../../../lib/posts'; +import { postCardPath } from '../../../lib/social-cards'; + +export async function getStaticPaths() { + const pathsByLang = await Promise.all( + LANGS.map(async (lang) => + (await getPostsByLang(lang)).map((post) => ({ params: { lang, slug: slugOf(post) }, props: { post } })), + ), + ); + return pathsByLang.flat(); +} + +const lang = Astro.params.lang as Lang; +const t = useTranslations(lang); +const { post } = Astro.props; +const { Content } = await render(post); +const readingMinutes = estimateReadingMinutes(post.body ?? ''); +--- + + + + + + diff --git a/src/pages/[lang]/posts/index.astro b/src/pages/[lang]/posts/index.astro new file mode 100644 index 0000000..356827a --- /dev/null +++ b/src/pages/[lang]/posts/index.astro @@ -0,0 +1,16 @@ +--- +import BaseLayout from '../../../layouts/BaseLayout.astro'; +import PostsContent from '../../../components/PostsContent.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, 'posts'); +--- + + + + diff --git a/src/pages/[lang]/tags/[tag]/index.astro b/src/pages/[lang]/tags/[tag]/index.astro new file mode 100644 index 0000000..c2e3a9c --- /dev/null +++ b/src/pages/[lang]/tags/[tag]/index.astro @@ -0,0 +1,23 @@ +--- +import BaseLayout from '../../../../layouts/BaseLayout.astro'; +import PostsContent from '../../../../components/PostsContent.astro'; +import { LANGS, tagPageMeta, useTranslations, type Lang } from '../../../../lib/i18n'; +import { getAllTags } from '../../../../lib/posts'; +import { tagCardPath } from '../../../../lib/social-cards'; + +export async function getStaticPaths() { + const pathsByLang = await Promise.all( + LANGS.map(async (lang) => (await getAllTags(lang)).map((tag) => ({ params: { lang, tag } }))), + ); + return pathsByLang.flat(); +} + +const lang = Astro.params.lang as Lang; +const tag = Astro.params.tag!; +const t = useTranslations(lang); +const page = tagPageMeta(lang, tag); +--- + + + + diff --git a/src/pages/about.astro b/src/pages/about.astro deleted file mode 100644 index 96d7f20..0000000 --- a/src/pages/about.astro +++ /dev/null @@ -1,41 +0,0 @@ ---- -import BaseLayout from '../layouts/BaseLayout.astro'; -import { useTranslations } from '../lib/i18n'; -import { PAGES, pageCardPath } from '../lib/pages'; - -const t = useTranslations('en'); -const page = PAGES.en.about; ---- - - -
-

{t('about.title')}

- -
-

- 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/pages/feed.xml.ts b/src/pages/feed.xml.ts deleted file mode 100644 index 37a2133..0000000 --- a/src/pages/feed.xml.ts +++ /dev/null @@ -1,19 +0,0 @@ -import rss from '@astrojs/rss'; -import { getCollection } from 'astro:content'; - -export async function GET(context) { - const blog = await getCollection('posts'); - return rss({ - title: 'gabeefran.co', - description: 'Gabriel Franco\'s notes about computer science and other topics', - site: context.site, - items: blog.map((post) => ({ - title: post.data.title, - pubDate: post.data.pubDate, - description: post.data.description, - // Compute RSS link from post `slug` - // This example assumes all posts are rendered as `/blog/[slug]` routes - link: `/posts/${post.data.slug}/`, - })), - }); -} diff --git a/src/pages/index.astro b/src/pages/index.astro deleted file mode 100644 index 4734b9e..0000000 --- a/src/pages/index.astro +++ /dev/null @@ -1,18 +0,0 @@ ---- -import BaseLayout from '../layouts/BaseLayout.astro'; -import HomeContent from '../components/HomeContent.astro'; -import { useTranslations } from '../lib/i18n'; -import { PAGES, pageCardPath } from '../lib/pages'; - -const t = useTranslations('en'); -const page = PAGES.en.home; ---- - - - - diff --git a/src/pages/posts/[...slug].astro b/src/pages/posts/[...slug].astro deleted file mode 100644 index dfc3d7b..0000000 --- a/src/pages/posts/[...slug].astro +++ /dev/null @@ -1,22 +0,0 @@ ---- -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 ?? ''); ---- - - - - - - diff --git a/src/pages/posts/index.astro b/src/pages/posts/index.astro deleted file mode 100644 index dc6ec0c..0000000 --- a/src/pages/posts/index.astro +++ /dev/null @@ -1,13 +0,0 @@ ---- -import BaseLayout from '../../layouts/BaseLayout.astro'; -import PostsContent from '../../components/PostsContent.astro'; -import { useTranslations } from '../../lib/i18n'; -import { PAGES, pageCardPath } from '../../lib/pages'; - -const t = useTranslations('en'); -const page = PAGES.en.posts; ---- - - - - diff --git a/src/pages/pt/about.astro b/src/pages/pt/about.astro deleted file mode 100644 index 16d489d..0000000 --- a/src/pages/pt/about.astro +++ /dev/null @@ -1,42 +0,0 @@ ---- -import BaseLayout from '../../layouts/BaseLayout.astro'; -import { useTranslations } from '../../lib/i18n'; -import { PAGES, pageCardPath } from '../../lib/pages'; - -const t = useTranslations('pt'); -const page = PAGES.pt.about; ---- - - -
-

{t('about.title')}

- -
-

- 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/pages/pt/index.astro b/src/pages/pt/index.astro deleted file mode 100644 index 1435733..0000000 --- a/src/pages/pt/index.astro +++ /dev/null @@ -1,18 +0,0 @@ ---- -import BaseLayout from '../../layouts/BaseLayout.astro'; -import HomeContent from '../../components/HomeContent.astro'; -import { useTranslations } from '../../lib/i18n'; -import { PAGES, pageCardPath } from '../../lib/pages'; - -const t = useTranslations('pt'); -const page = PAGES.pt.home; ---- - - - - diff --git a/src/pages/pt/posts/[...slug].astro b/src/pages/pt/posts/[...slug].astro deleted file mode 100644 index 0aad493..0000000 --- a/src/pages/pt/posts/[...slug].astro +++ /dev/null @@ -1,22 +0,0 @@ ---- -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 ?? ''); ---- - - - - - - diff --git a/src/pages/pt/posts/index.astro b/src/pages/pt/posts/index.astro deleted file mode 100644 index 2028c27..0000000 --- a/src/pages/pt/posts/index.astro +++ /dev/null @@ -1,13 +0,0 @@ ---- -import BaseLayout from '../../../layouts/BaseLayout.astro'; -import PostsContent from '../../../components/PostsContent.astro'; -import { useTranslations } from '../../../lib/i18n'; -import { PAGES, pageCardPath } from '../../../lib/pages'; - -const t = useTranslations('pt'); -const page = PAGES.pt.posts; ---- - - - - diff --git a/src/pages/pt/tags/[tag]/index.astro b/src/pages/pt/tags/[tag]/index.astro deleted file mode 100644 index 59cd988..0000000 --- a/src/pages/pt/tags/[tag]/index.astro +++ /dev/null @@ -1,20 +0,0 @@ ---- -import BaseLayout from '../../../../layouts/BaseLayout.astro'; -import PostsContent from '../../../../components/PostsContent.astro'; -import { getAllTags } from '../../../../lib/posts'; -import { useTranslations } from '../../../../lib/i18n'; -import { tagPage, tagCardPath } from '../../../../lib/pages'; - -export async function getStaticPaths() { - const tags = await getAllTags('pt'); - return tags.map((tag) => ({ params: { tag } })); -} - -const { tag } = Astro.params; -const t = useTranslations('pt'); -const page = tagPage('pt', tag!); ---- - - - - diff --git a/src/pages/tags/[tag]/index.astro b/src/pages/tags/[tag]/index.astro deleted file mode 100644 index c6a742c..0000000 --- a/src/pages/tags/[tag]/index.astro +++ /dev/null @@ -1,20 +0,0 @@ ---- -import BaseLayout from '../../../layouts/BaseLayout.astro'; -import PostsContent from '../../../components/PostsContent.astro'; -import { getAllTags } from '../../../lib/posts'; -import { useTranslations } from '../../../lib/i18n'; -import { tagPage, tagCardPath } from '../../../lib/pages'; - -export async function getStaticPaths() { - const tags = await getAllTags('en'); - return tags.map((tag) => ({ params: { tag } })); -} - -const { tag } = Astro.params; -const t = useTranslations('en'); -const page = tagPage('en', tag!); ---- - - - -