i18n refactor
This commit is contained in:
parent
5f114e21e7
commit
6cd13476dc
33 changed files with 285 additions and 366 deletions
|
|
@ -7,17 +7,37 @@ import sitemap from '@astrojs/sitemap'
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
import vercel from '@astrojs/vercel';
|
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
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: 'https://gabeefran.co',
|
site: 'https://gabeefran.co',
|
||||||
|
|
||||||
i18n: {
|
// No `i18n` block on purpose: locales come from src/lib/i18n.ts and every
|
||||||
defaultLocale: 'en',
|
// locale, English included, lives under src/pages/[lang]/. Astro's i18n
|
||||||
locales: ['en', 'pt'],
|
// routing would add middleware that 404s the unprefixed on-demand
|
||||||
routing: {
|
// /page-card and /social-card routes (prefix-always) or /en/* (default).
|
||||||
prefixDefaultLocale: false,
|
|
||||||
},
|
redirects: Object.fromEntries(LEGACY_EN_PATHS.map((path) => [path, path === '/' ? '/en' : `/en${path}`])),
|
||||||
},
|
|
||||||
|
|
||||||
integrations: [react(), mdx(), sitemap()],
|
integrations: [react(), mdx(), sitemap()],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,20 +9,20 @@
|
||||||
// /social-card and /page-card routes must be deployed at BASE_URL.
|
// /social-card and /page-card routes must be deployed at BASE_URL.
|
||||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
import { dirname, join } from 'node:path';
|
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 BASE_URL = process.env.SOCIAL_CARD_BASE_URL ?? 'https://gabeefran.co';
|
||||||
const CF_API = 'https://api.cloudflare.com/client/v4/accounts';
|
const CF_API = 'https://api.cloudflare.com/client/v4/accounts';
|
||||||
const POSTS_DIR = 'src/content/posts';
|
const POSTS_DIR = 'src/content/posts';
|
||||||
const LANGS = ['en', 'pt'] as const;
|
|
||||||
// 404.html is prerendered once, in English (see src/pages/404.astro).
|
// 404.html is prerendered once, in English (see src/pages/404.astro).
|
||||||
const LANG_PAGES: Record<(typeof LANGS)[number], PageKey[]> = {
|
const LANG_PAGES: Record<Lang, PageKey[]> = {
|
||||||
en: ['home', 'posts', 'about', '404'],
|
en: ['home', 'posts', 'about', '404'],
|
||||||
pt: ['home', 'posts', 'about'],
|
pt: ['home', 'posts', 'about'],
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Post {
|
interface Post {
|
||||||
lang: (typeof LANGS)[number];
|
lang: Lang;
|
||||||
slug: string;
|
slug: string;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
@ -72,13 +72,13 @@ function listCards(posts: Post[]): Card[] {
|
||||||
const postCards: Card[] = posts.map((post) => {
|
const postCards: Card[] = posts.map((post) => {
|
||||||
const params: Record<string, string> = { title: post.title, author: 'Gabriel Franco' };
|
const params: Record<string, string> = { title: post.title, author: 'Gabriel Franco' };
|
||||||
if (post.description) params.description = post.description;
|
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 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 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 }) => ({
|
return [...pages, ...tagPages].map(({ path, meta }) => ({
|
||||||
path,
|
path,
|
||||||
route: '/page-card' as const,
|
route: '/page-card' as const,
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ const navLinks = [
|
||||||
];
|
];
|
||||||
|
|
||||||
function isActive(href: string): boolean {
|
function isActive(href: string): boolean {
|
||||||
if (href === homeHref) return pathname === href;
|
if (href === homeHref) return pathname.replace(/\/$/, '') === href;
|
||||||
return pathname === href || pathname.startsWith(`${href}/`);
|
return pathname === href || pathname.startsWith(`${href}/`);
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
@ -31,7 +31,7 @@ function isActive(href: string): boolean {
|
||||||
|
|
||||||
<header class="sticky top-0 z-40 border-b border-border/80 bg-bg/85 backdrop-blur">
|
<header class="sticky top-0 z-40 border-b border-border/80 bg-bg/85 backdrop-blur">
|
||||||
<div class="flex flex-col justify-center gap-4 px-4 py-3 sm:px-6">
|
<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">
|
<div class="flex flex-row justify-between items-center">
|
||||||
<a href={homeHref} class="flex hover:underline shrink-0 items-center gap-2.5" aria-label={t('site.name')} data-umami-event="logo-header-click">
|
<a href={homeHref} class="flex hover:underline shrink-0 items-center gap-2.5" aria-label={t('site.name')} data-umami-event="logo-header-click">
|
||||||
<img src="/logo.png" class="h-4"/>
|
<img src="/logo.png" class="h-4"/>
|
||||||
<span class="font-mono text-[15px] font-semibold tracking-tight text-text">{t('site.name')}</span>
|
<span class="font-mono text-[15px] font-semibold tracking-tight text-text">{t('site.name')}</span>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ const posts = defineCollection({
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
|
language: z.enum(['en', 'pt']),
|
||||||
pubDate: z.coerce.date(),
|
pubDate: z.coerce.date(),
|
||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
updatedDate: z.coerce.date().optional(),
|
updatedDate: z.coerce.date().optional(),
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
---
|
---
|
||||||
title: "This blog's Hello World (or whatever you want to call this site)"
|
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."
|
description: "A short note on why this exists and what I plan to write about here."
|
||||||
pubDate: 2026-09-22
|
pubDate: 2026-09-22
|
||||||
tags: ["meta", "blogging"]
|
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.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
---
|
---
|
||||||
title: "Spotify's Time Capsule Change and the Enshittification of Software"
|
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."
|
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
|
pubDate: 2026-09-2
|
||||||
tags: ["software", "life"]
|
tags: ["software", "life"]
|
||||||
|
language: "en"
|
||||||
---
|
---
|
||||||
import ObservationCard from '../../../components/ObservationCard.tsx'
|
import ObservationCard from '../../../components/ObservationCard.tsx'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
---
|
---
|
||||||
title: "Understanding Plugin4Shell: An Exploit That Uses a Simple Git Trick to Fool AI Coding Agents"
|
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."
|
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
|
pubDate: 2026-09-28
|
||||||
tags: ["cybersecurity", "git", "ai", "software"]
|
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"
|
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).
|
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).
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@
|
||||||
title: "O Hello World desse blog (ou seja lá como você queira chamar esse site)"
|
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."
|
description: "Uma nota curta sobre por que isso existe e sobre o que pretendo escrever aqui."
|
||||||
pubDate: 2026-09-22
|
pubDate: 2026-09-22
|
||||||
slug: "hello-world"
|
slug: "pt-hello-world"
|
||||||
tags: ["meta", "blogging"]
|
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.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
---
|
---
|
||||||
title: "A mudança na cápsula do tempo do Spotify e a bostificação do software"
|
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."
|
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
|
pubDate: 2026-09-2
|
||||||
tags: ["software", "life"]
|
tags: ["software", "life"]
|
||||||
|
language: "pt"
|
||||||
---
|
---
|
||||||
import ObservationCard from '../../../components/ObservationCard.tsx'
|
import ObservationCard from '../../../components/ObservationCard.tsx'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
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."
|
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
|
pubDate: 2026-09-28
|
||||||
tags: ["cybersecurity", "git", "ai", "software"]
|
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"
|
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).
|
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).
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { existsSync } from 'node:fs';
|
||||||
import '../styles/global.css';
|
import '../styles/global.css';
|
||||||
import Header from '../components/Header.astro';
|
import Header from '../components/Header.astro';
|
||||||
import Footer from '../components/Footer.astro';
|
import Footer from '../components/Footer.astro';
|
||||||
import type { Lang } from '../lib/i18n';
|
import { getLocalizedPath, type Lang } from '../lib/i18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -31,8 +31,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(Astro.url.pathname.replace(/^\/pt/, '') || '/', Astro.site)} />
|
<link rel="alternate" hreflang="en" href={new URL(getLocalizedPath(Astro.url.pathname, 'en'), Astro.site)} />
|
||||||
<link rel="alternate" hreflang="pt-BR" href={new URL(`/pt${Astro.url.pathname.replace(/^\/pt/, '')}`, Astro.site)} />
|
<link rel="alternate" hreflang="pt-BR" href={new URL(getLocalizedPath(Astro.url.pathname, '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" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 const LANGS = ['en', 'pt'] as const;
|
||||||
export type Lang = (typeof LANGS)[number];
|
export type Lang = (typeof LANGS)[number];
|
||||||
export const DEFAULT_LANG: Lang = 'en';
|
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.",
|
"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.posts': 'Read the posts',
|
||||||
'hero.cta.about': 'About me',
|
'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': 'Recent posts',
|
||||||
'home.recent.subtitle': 'The latest things I’ve written down.',
|
'home.recent.subtitle': 'The latest things I’ve written down.',
|
||||||
'home.viewAll': 'View all posts',
|
'home.viewAll': 'View all posts',
|
||||||
'posts.title': 'All posts',
|
'posts.title': 'All posts',
|
||||||
'posts.subtitle': 'Everything I’ve written, oldest to newest below, newest on top.',
|
'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.empty': 'No posts yet. Check back soon.',
|
||||||
'posts.filteredBy': 'Posts tagged',
|
'posts.filteredBy': 'Posts tagged',
|
||||||
'posts.clearFilter': 'Clear filter',
|
'posts.clearFilter': 'Clear filter',
|
||||||
|
|
@ -35,10 +41,22 @@ export const ui = {
|
||||||
'post.readingTime': 'min read',
|
'post.readingTime': 'min read',
|
||||||
'post.updated': 'Updated',
|
'post.updated': 'Updated',
|
||||||
'tag.label': 'Tag',
|
'tag.label': 'Tag',
|
||||||
|
'tag.description': 'Everything I’ve written tagged #{tag}.',
|
||||||
'about.title': 'About',
|
'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.tagline': 'Written by hand in Porto Alegre.',
|
||||||
'footer.rights': 'All rights reserved.',
|
'footer.rights': 'All rights reserved.',
|
||||||
'footer.source': 'Source',
|
'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.title': 'Page not found',
|
||||||
'404.body': 'There’s nothing here. Let’s get you back home.',
|
'404.body': 'There’s nothing here. Let’s get you back home.',
|
||||||
'404.cta': 'Back to 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.',
|
'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.posts': 'Ler os posts',
|
||||||
'hero.cta.about': 'Sobre mim',
|
'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': 'Posts recentes',
|
||||||
'home.recent.subtitle': 'As últimas coisas que escrevi.',
|
'home.recent.subtitle': 'As últimas coisas que escrevi.',
|
||||||
'home.viewAll': 'Ver todos os posts',
|
'home.viewAll': 'Ver todos os posts',
|
||||||
'posts.title': 'Todos os posts',
|
'posts.title': 'Todos os posts',
|
||||||
'posts.subtitle': 'Tudo que já escrevi, do mais recente para o mais antigo.',
|
'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.empty': 'Ainda não há posts. Volte em breve.',
|
||||||
'posts.filteredBy': 'Posts com a tag',
|
'posts.filteredBy': 'Posts com a tag',
|
||||||
'posts.clearFilter': 'Limpar filtro',
|
'posts.clearFilter': 'Limpar filtro',
|
||||||
|
|
@ -70,10 +92,21 @@ export const ui = {
|
||||||
'post.readingTime': 'min de leitura',
|
'post.readingTime': 'min de leitura',
|
||||||
'post.updated': 'Atualizado em',
|
'post.updated': 'Atualizado em',
|
||||||
'tag.label': 'Tag',
|
'tag.label': 'Tag',
|
||||||
|
'tag.description': 'Tudo que já escrevi com a tag #{tag}.',
|
||||||
'about.title': 'Sobre',
|
'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.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',
|
||||||
|
'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.title': 'Página não encontrada',
|
||||||
'404.body': 'Não há nada aqui. Vamos te levar de volta para o início.',
|
'404.body': 'Não há nada aqui. Vamos te levar de volta para o início.',
|
||||||
'404.cta': 'Voltar 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 type UiKey = keyof (typeof ui)['en'];
|
||||||
|
|
||||||
export function getLangFromUrl(url: URL): Lang {
|
export function isLang(value: string | undefined): value is Lang {
|
||||||
const [, first] = url.pathname.split('/');
|
return LANGS.some((lang) => lang === value);
|
||||||
return first === 'pt' ? 'pt' : DEFAULT_LANG;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 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) {
|
export function useTranslations(lang: Lang) {
|
||||||
return function t(key: UiKey): string {
|
return function t(key: UiKey, vars: Record<string, string> = {}): string {
|
||||||
return ui[lang][key] ?? ui[DEFAULT_LANG][key];
|
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. */
|
/** Pages with their own title/description (tag pages use tagPageMeta). */
|
||||||
export function stripLangPrefix(pathname: string): string {
|
export type PageKey = 'home' | 'posts' | 'about' | '404';
|
||||||
return pathname.replace(/^\/pt(?=\/|$)/, '') || '/';
|
|
||||||
|
export interface PageMeta {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAGE_META_KEYS: Record<PageKey, { title: UiKey; description: UiKey }> = {
|
||||||
|
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. */
|
/** Given any pathname, build the equivalent path in the target language. */
|
||||||
export function getLocalizedPath(pathname: string, lang: Lang): string {
|
export function getLocalizedPath(pathname: string, lang: Lang): string {
|
||||||
const stripped = stripLangPrefix(pathname);
|
const [, first] = pathname.split('/');
|
||||||
if (lang === DEFAULT_LANG) return stripped;
|
const stripped = (isLang(first) ? pathname.slice(first.length + 1) : pathname) || '/';
|
||||||
return stripped === '/' ? '/pt' : `/pt${stripped}`;
|
return stripped === '/' ? `/${lang}` : `/${lang}${stripped}`;
|
||||||
}
|
|
||||||
|
|
||||||
export function localizeHref(path: string, lang: Lang): string {
|
|
||||||
if (lang === DEFAULT_LANG) return path;
|
|
||||||
return path === '/' ? '/pt' : `/pt${path}`;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<Lang, Record<PageKey, PageMeta>> = {
|
|
||||||
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`;
|
|
||||||
}
|
|
||||||
|
|
@ -9,12 +9,12 @@ export function langOf(entry: PostEntry): Lang {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function slugOf(entry: PostEntry): string {
|
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<PostEntry[]> {
|
export async function getPostsByLang(lang: Lang): Promise<PostEntry[]> {
|
||||||
const posts = await getCollection('posts', ({ id, data }) => {
|
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());
|
return posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
src/lib/social-cards.ts
Normal file
16
src/lib/social-cards.ts
Normal file
|
|
@ -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`;
|
||||||
|
}
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
---
|
---
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
import { getLocalizedPath, ui, type Lang } from '../lib/i18n';
|
import { getLocalizedPath, LANGS, pageMeta, ui } from '../lib/i18n';
|
||||||
import { PAGES, pageCardPath } from '../lib/pages';
|
import { pageCardPath } from '../lib/social-cards';
|
||||||
|
|
||||||
// This file is prerendered once as a static /404.html and served by the host
|
// 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
|
// 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
|
// was under /pt/*. Render both languages and pick one at runtime with a tiny
|
||||||
// inline script based on the actual requested path.
|
// inline script based on the actual requested path.
|
||||||
const langs: Lang[] = ['en', 'pt'];
|
const page = pageMeta('en', '404');
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
title={`${PAGES.en['404'].title} — gabeefranco`}
|
title={`${page.title} — ${ui.en['site.name']}`}
|
||||||
description={PAGES.en['404'].description}
|
description={page.description}
|
||||||
lang="en"
|
lang="en"
|
||||||
image={pageCardPath('en', '404')}
|
image={pageCardPath('en', '404')}
|
||||||
>
|
>
|
||||||
<section class="mx-10 flex flex-col items-start px-4 py-24 sm:px-6 font-mono">
|
<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>
|
<p class="font-mono text-sm text-primary">404</p>
|
||||||
{langs.map((lang) => (
|
{LANGS.map((lang) => (
|
||||||
<div data-notfound-lang={lang} class={lang === 'en' ? '' : 'hidden'}>
|
<div data-notfound-lang={lang} class={lang === 'en' ? '' : 'hidden'}>
|
||||||
<h1 class="mt-2 text-3xl font-bold tracking-tight text-text">{ui[lang]['404.title']}</h1>
|
<h1 class="mt-2 text-3xl font-bold tracking-tight text-text">{ui[lang]['404.title']}</h1>
|
||||||
<p class="mt-3 text-text-muted">{ui[lang]['404.body']}</p>
|
<p class="mt-3 text-text-muted">{ui[lang]['404.body']}</p>
|
||||||
|
|
|
||||||
21
src/pages/[lang]/about.astro
Normal file
21
src/pages/[lang]/about.astro
Normal file
|
|
@ -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');
|
||||||
|
---
|
||||||
|
|
||||||
|
<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>
|
||||||
24
src/pages/[lang]/feed.xml.ts
Normal file
24
src/pages/[lang]/feed.xml.ts
Normal file
|
|
@ -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: `<language>${lang}</language>`,
|
||||||
|
items: posts.map((post) => ({
|
||||||
|
title: post.data.title,
|
||||||
|
pubDate: post.data.pubDate,
|
||||||
|
description: post.data.description,
|
||||||
|
link: `${getLocalizedPath(`/posts/${slugOf(post)}`, lang)}/`,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
20
src/pages/[lang]/index.astro
Normal file
20
src/pages/[lang]/index.astro
Normal file
|
|
@ -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);
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title={`${t('site.name')} — ${t('hero.kicker')}`}
|
||||||
|
description={pageMeta(lang, 'home').description}
|
||||||
|
lang={lang}
|
||||||
|
image={pageCardPath(lang, 'home')}
|
||||||
|
>
|
||||||
|
<HomeContent lang={lang} />
|
||||||
|
</BaseLayout>
|
||||||
35
src/pages/[lang]/posts/[...slug].astro
Normal file
35
src/pages/[lang]/posts/[...slug].astro
Normal file
|
|
@ -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 ?? '');
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title={`${post.data.title} — ${t('site.name')}`}
|
||||||
|
description={post.data.description}
|
||||||
|
lang={lang}
|
||||||
|
image={postCardPath(lang, slugOf(post))}
|
||||||
|
>
|
||||||
|
<PostLayout post={post} lang={lang} readingMinutes={readingMinutes}>
|
||||||
|
<Content components={{ ObservationCard }} />
|
||||||
|
</PostLayout>
|
||||||
|
</BaseLayout>
|
||||||
16
src/pages/[lang]/posts/index.astro
Normal file
16
src/pages/[lang]/posts/index.astro
Normal file
|
|
@ -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');
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title={`${page.title} — ${t('site.name')}`} description={page.description} lang={lang} image={pageCardPath(lang, 'posts')}>
|
||||||
|
<PostsContent lang={lang} />
|
||||||
|
</BaseLayout>
|
||||||
23
src/pages/[lang]/tags/[tag]/index.astro
Normal file
23
src/pages/[lang]/tags/[tag]/index.astro
Normal file
|
|
@ -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);
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title={`${page.title} — ${t('site.name')}`} description={page.description} lang={lang} image={tagCardPath(lang, tag)}>
|
||||||
|
<PostsContent lang={lang} tag={tag} />
|
||||||
|
</BaseLayout>
|
||||||
|
|
@ -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;
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout title={`${page.title} — ${t('site.name')}`} description={page.description} lang="en" image={pageCardPath('en', 'about')}>
|
|
||||||
<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">
|
|
||||||
<p>
|
|
||||||
Hi, I'm Gabe — Gabriel Franco. I'm a computer science student at PUCRS (Pontifícia
|
|
||||||
Universidade Católica do Rio Grande do Sul), and this blog is where I write down the
|
|
||||||
things I'm learning and thinking about along the way.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
I care a lot about computer science as a field, not just as a set of tools to ship
|
|
||||||
products with. I have a fairly specific way of doing things: I'd rather understand a
|
|
||||||
system properly than memorize the steps to make it work, and I tend to slow down on
|
|
||||||
purpose to figure out what's actually happening under the hood — in a compiler, an
|
|
||||||
operating system, a language runtime, whatever it is. That habit shapes most of what ends
|
|
||||||
up on this blog.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
I'm also genuinely interested in the history of the open source movement: the people,
|
|
||||||
the arguments, and the decisions that shaped the software we all rely on. Figures like
|
|
||||||
Linus Torvalds and projects like GNU come up often in what I read and write about, not
|
|
||||||
as trivia, but because understanding where a tool came from usually explains a lot about
|
|
||||||
how and why it works the way it does today.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
This site is bilingual, in English and in Portuguese, and every post exists in both
|
|
||||||
languages. Thanks for stopping by.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</BaseLayout>
|
|
||||||
|
|
@ -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}/`,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout
|
|
||||||
title={`${t('site.name')} — ${t('hero.kicker')}`}
|
|
||||||
description={page.description}
|
|
||||||
lang="en"
|
|
||||||
image={pageCardPath('en', 'home')}
|
|
||||||
>
|
|
||||||
<HomeContent lang="en" />
|
|
||||||
</BaseLayout>
|
|
||||||
|
|
@ -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 ?? '');
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout title={`${post.data.title} — gabeefranco`} description={post.data.description} lang="en" image={`/social-cards/${post.id}.png`}>
|
|
||||||
<PostLayout post={post} lang="en" readingMinutes={readingMinutes}>
|
|
||||||
<Content components={{ ObservationCard }} />
|
|
||||||
</PostLayout>
|
|
||||||
</BaseLayout>
|
|
||||||
|
|
@ -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;
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout title={`${page.title} — ${t('site.name')}`} description={page.description} lang="en" image={pageCardPath('en', 'posts')}>
|
|
||||||
<PostsContent lang="en" />
|
|
||||||
</BaseLayout>
|
|
||||||
|
|
@ -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;
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout title={`${page.title} — ${t('site.name')}`} description={page.description} lang="pt" image={pageCardPath('pt', 'about')}>
|
|
||||||
<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">
|
|
||||||
<p>
|
|
||||||
Olá, eu sou o Gabe — Gabriel Franco. Sou estudante de ciência da computação na PUCRS
|
|
||||||
(Pontifícia Universidade Católica do Rio Grande do Sul), e este blog é onde registro as
|
|
||||||
coisas que vou aprendendo e pensando pelo caminho.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Eu me importo bastante com ciência da computação como área, não só como um conjunto de
|
|
||||||
ferramentas para lançar produtos. Tenho um jeito bem específico de fazer as coisas:
|
|
||||||
prefiro entender um sistema de verdade a decorar os passos para fazê-lo funcionar, e
|
|
||||||
costumo desacelerar de propósito para descobrir o que realmente está acontecendo por
|
|
||||||
baixo dos panos — em um compilador, em um sistema operacional, no runtime de uma
|
|
||||||
linguagem, seja lá o que for. Esse hábito molda a maior parte do que acaba virando post
|
|
||||||
aqui.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Também tenho um interesse genuíno pela história do movimento open source: as pessoas, os
|
|
||||||
debates e as decisões que moldaram o software do qual todos dependemos. Figuras como
|
|
||||||
Linus Torvalds e projetos como o GNU aparecem com frequência no que leio e escrevo, não
|
|
||||||
como curiosidade, mas porque entender de onde uma ferramenta veio costuma explicar
|
|
||||||
bastante sobre como e por que ela funciona do jeito que funciona hoje.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Este site é bilíngue, em inglês e em português, e todo post existe nos dois idiomas.
|
|
||||||
Obrigado pela visita.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</BaseLayout>
|
|
||||||
|
|
@ -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;
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout
|
|
||||||
title={`${t('site.name')} — ${t('hero.kicker')}`}
|
|
||||||
description={page.description}
|
|
||||||
lang="pt"
|
|
||||||
image={pageCardPath('pt', 'home')}
|
|
||||||
>
|
|
||||||
<HomeContent lang="pt" />
|
|
||||||
</BaseLayout>
|
|
||||||
|
|
@ -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 ?? '');
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout title={`${post.data.title} — gabeefranco`} description={post.data.description} lang="pt" image={`/social-cards/${post.id}.png`}>
|
|
||||||
<PostLayout post={post} lang="pt" readingMinutes={readingMinutes}>
|
|
||||||
<Content components={{ ObservationCard }} />
|
|
||||||
</PostLayout>
|
|
||||||
</BaseLayout>
|
|
||||||
|
|
@ -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;
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout title={`${page.title} — ${t('site.name')}`} description={page.description} lang="pt" image={pageCardPath('pt', 'posts')}>
|
|
||||||
<PostsContent lang="pt" />
|
|
||||||
</BaseLayout>
|
|
||||||
|
|
@ -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!);
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout title={`${page.title} — ${t('site.name')}`} description={page.description} lang="pt" image={tagCardPath('pt', tag!)}>
|
|
||||||
<PostsContent lang="pt" tag={tag} />
|
|
||||||
</BaseLayout>
|
|
||||||
|
|
@ -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!);
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout title={`${page.title} — ${t('site.name')}`} description={page.description} lang="en" image={tagCardPath('en', tag!)}>
|
|
||||||
<PostsContent lang="en" tag={tag} />
|
|
||||||
</BaseLayout>
|
|
||||||
Loading…
Reference in a new issue