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 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()],
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Lang, PageKey[]> = {
|
||||
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<string, string> = { 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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
|
||||
<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-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">
|
||||
<img src="/logo.png" class="h-4"/>
|
||||
<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({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
language: z.enum(['en', 'pt']),
|
||||
pubDate: z.coerce.date(),
|
||||
slug: z.string(),
|
||||
updatedDate: z.coerce.date().optional(),
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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);
|
|||
<link rel="alternate icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="canonical" href={canonicalURL} />
|
||||
<link rel="alternate" hreflang="en" href={new URL(Astro.url.pathname.replace(/^\/pt/, '') || '/', Astro.site)} />
|
||||
<link rel="alternate" hreflang="pt-BR" href={new URL(`/pt${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(getLocalizedPath(Astro.url.pathname, 'pt'), Astro.site)} />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<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 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, string> = {}): 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<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. */
|
||||
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}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
return entry.id.split('/').slice(1).join('/');
|
||||
return entry.id.replace('pt-', '').replace('en-', '');
|
||||
}
|
||||
|
||||
export async function getPostsByLang(lang: Lang): Promise<PostEntry[]> {
|
||||
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());
|
||||
}
|
||||
|
|
|
|||
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 { 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');
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={`${PAGES.en['404'].title} — gabeefranco`}
|
||||
description={PAGES.en['404'].description}
|
||||
title={`${page.title} — ${ui.en['site.name']}`}
|
||||
description={page.description}
|
||||
lang="en"
|
||||
image={pageCardPath('en', '404')}
|
||||
>
|
||||
<section class="mx-10 flex flex-col items-start px-4 py-24 sm:px-6 font-mono">
|
||||
<p class="font-mono text-sm text-primary">404</p>
|
||||
{langs.map((lang) => (
|
||||
{LANGS.map((lang) => (
|
||||
<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>
|
||||
<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