render about page from markdown, served at /pt/sobre in portuguese
The about body moves from i18n.ts to src/content/about/{en,pt}.md; the
slug frontmatter field sets each language's URL. /pt/about redirects to
/pt/sobre, and the language switcher and hreflang links point at the
right slug for the other language.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5e16c97462
commit
fa3f12a672
11 changed files with 120 additions and 43 deletions
|
|
@ -49,7 +49,11 @@ export default defineConfig({
|
||||||
// routing would add middleware that 404s the unprefixed on-demand
|
// routing would add middleware that 404s the unprefixed on-demand
|
||||||
// /page-card and /social-card routes (prefix-always) or /en/* (default).
|
// /page-card and /social-card routes (prefix-always) or /en/* (default).
|
||||||
|
|
||||||
redirects: Object.fromEntries(LEGACY_EN_PATHS.map((path) => [path, path === '/' ? '/en' : `/en${path}`])),
|
redirects: {
|
||||||
|
...Object.fromEntries(LEGACY_EN_PATHS.map((path) => [path, path === '/' ? '/en' : `/en${path}`])),
|
||||||
|
// The Portuguese about page moved to /pt/sobre (slug from src/content/about/pt.md).
|
||||||
|
'/pt/about': '/pt/sobre',
|
||||||
|
},
|
||||||
|
|
||||||
integrations: [react(), mdx(), sitemap({ filter: (page) => !DRAFT_PATHS.some((path) => page.endsWith(path)) })],
|
integrations: [react(), mdx(), sitemap({ filter: (page) => !DRAFT_PATHS.some((path) => page.endsWith(path)) })],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
---
|
---
|
||||||
import ThemeToggle from './ThemeToggle.astro';
|
import ThemeToggle from './ThemeToggle.astro';
|
||||||
|
import { getAboutPath } from '../lib/about';
|
||||||
import { getLocalizedPath, LANG_LABELS, useTranslations, type Lang } from '../lib/i18n';
|
import { getLocalizedPath, LANG_LABELS, useTranslations, type Lang } from '../lib/i18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
lang: Lang;
|
lang: Lang;
|
||||||
|
/** Where the language switch goes; defaults to swapping the /<lang> prefix. */
|
||||||
|
langSwitchHref?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { lang } = Astro.props;
|
const { lang } = Astro.props;
|
||||||
|
|
@ -13,8 +16,8 @@ const otherLang: Lang = lang === 'en' ? 'pt' : 'en';
|
||||||
const pathname = Astro.url.pathname;
|
const pathname = Astro.url.pathname;
|
||||||
const homeHref = getLocalizedPath('/', lang);
|
const homeHref = getLocalizedPath('/', lang);
|
||||||
const postsHref = getLocalizedPath('/posts', lang);
|
const postsHref = getLocalizedPath('/posts', lang);
|
||||||
const aboutHref = getLocalizedPath('/about', lang);
|
const aboutHref = await getAboutPath(lang);
|
||||||
const langSwitchHref = getLocalizedPath(pathname, otherLang);
|
const langSwitchHref = Astro.props.langSwitchHref ?? getLocalizedPath(pathname, otherLang);
|
||||||
|
|
||||||
const navLinks = [
|
const navLinks = [
|
||||||
{ href: homeHref, label: t('nav.home'), id: 'home' },
|
{ href: homeHref, label: t('nav.home'), id: 'home' },
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
---
|
---
|
||||||
import PostCard from './PostCard.astro';
|
import PostCard from './PostCard.astro';
|
||||||
import { getPostsByLang } from '../lib/posts';
|
import { getPostsByLang } from '../lib/posts';
|
||||||
|
import { getAboutPath } from '../lib/about';
|
||||||
import { getLocalizedPath, useTranslations, type Lang } from '../lib/i18n';
|
import { getLocalizedPath, useTranslations, type Lang } from '../lib/i18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -12,7 +13,7 @@ const t = useTranslations(lang);
|
||||||
const posts = await getPostsByLang(lang);
|
const posts = await getPostsByLang(lang);
|
||||||
const recentPosts = posts.slice(0, 3);
|
const recentPosts = posts.slice(0, 3);
|
||||||
const postsHref = getLocalizedPath('/posts', lang);
|
const postsHref = getLocalizedPath('/posts', lang);
|
||||||
const aboutHref = getLocalizedPath('/about', lang);
|
const aboutHref = await getAboutPath(lang);
|
||||||
---
|
---
|
||||||
|
|
||||||
<section class="mx-10 my-10 flex flex-col gap-5 font-mono">
|
<section class="mx-10 my-10 flex flex-col gap-5 font-mono">
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,14 @@ const posts = defineCollection({
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const collections = { posts };
|
// The about page, one file per language: src/content/about/<lang>.md.
|
||||||
|
// `slug` is the URL segment for that language (about, sobre).
|
||||||
|
const about = defineCollection({
|
||||||
|
loader: glob({ pattern: '*.md', base: './src/content/about' }),
|
||||||
|
schema: z.object({
|
||||||
|
language: z.enum(['en', 'pt']),
|
||||||
|
slug: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { posts, about };
|
||||||
|
|
|
||||||
12
src/content/about/en.md
Normal file
12
src/content/about/en.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
language: en
|
||||||
|
slug: about
|
||||||
|
---
|
||||||
|
|
||||||
|
Hi, I'm Gabe — Gabriel Franco. I'm a computer science student at PUCRS (Pontifícia Universidade Católica do Rio Grande do Sul), and this blog is where I write down the things I'm learning and thinking about along the way.
|
||||||
|
|
||||||
|
I care a lot about computer science as a field, not just as a set of tools to ship products with. I have a fairly specific way of doing things: I'd rather understand a system properly than memorize the steps to make it work, and I tend to slow down on purpose to figure out what's actually happening under the hood — in a compiler, an operating system, a language runtime, whatever it is. That habit shapes most of what ends up on this blog.
|
||||||
|
|
||||||
|
I'm also genuinely interested in the history of the open source movement: the people, the arguments, and the decisions that shaped the software we all rely on. Figures like Linus Torvalds and projects like GNU come up often in what I read and write about, not as trivia, but because understanding where a tool came from usually explains a lot about how and why it works the way it does today.
|
||||||
|
|
||||||
|
This site is bilingual, in English and in Portuguese, and every post exists in both languages. Thanks for stopping by.
|
||||||
12
src/content/about/pt.md
Normal file
12
src/content/about/pt.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
language: pt
|
||||||
|
slug: sobre
|
||||||
|
---
|
||||||
|
|
||||||
|
Olá, eu sou o Gabe — Gabriel Franco. Sou estudante de ciência da computação na PUCRS (Pontifícia Universidade Católica do Rio Grande do Sul), e este blog é onde registro as coisas que vou aprendendo e pensando pelo caminho.
|
||||||
|
|
||||||
|
Eu me importo bastante com ciência da computação como área, não só como um conjunto de ferramentas para lançar produtos. Tenho um jeito bem específico de fazer as coisas: prefiro entender um sistema de verdade 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.
|
||||||
|
|
@ -18,9 +18,15 @@ interface Props {
|
||||||
image?: string;
|
image?: string;
|
||||||
/** Keep the page out of search engines (used for draft posts). */
|
/** Keep the page out of search engines (used for draft posts). */
|
||||||
noindex?: boolean;
|
noindex?: boolean;
|
||||||
|
/**
|
||||||
|
* This page's path in each language, for pages whose path isn't the same
|
||||||
|
* across languages (the about page). Defaults to swapping the /<lang> prefix.
|
||||||
|
*/
|
||||||
|
alternates?: Partial<Record<Lang, string>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, description, lang, noindex } = Astro.props;
|
const { title, description, lang, noindex, alternates = {} } = Astro.props;
|
||||||
|
const alternatePath = (target: Lang) => alternates[target] ?? getLocalizedPath(Astro.url.pathname, target);
|
||||||
const image = Astro.props.image && existsSync(`public${Astro.props.image}`) ? Astro.props.image : '/logo.png';
|
const image = Astro.props.image && existsSync(`public${Astro.props.image}`) ? Astro.props.image : '/logo.png';
|
||||||
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||||
---
|
---
|
||||||
|
|
@ -34,8 +40,8 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||||
<link rel="alternate icon" href="/favicon.ico" />
|
<link rel="alternate icon" href="/favicon.ico" />
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
<link rel="canonical" href={canonicalURL} />
|
<link rel="canonical" href={canonicalURL} />
|
||||||
<link rel="alternate" hreflang="en" href={new URL(getLocalizedPath(Astro.url.pathname, 'en'), Astro.site)} />
|
<link rel="alternate" hreflang="en" href={new URL(alternatePath('en'), Astro.site)} />
|
||||||
<link rel="alternate" hreflang="pt-BR" href={new URL(getLocalizedPath(Astro.url.pathname, 'pt'), Astro.site)} />
|
<link rel="alternate" hreflang="pt-BR" href={new URL(alternatePath('pt'), Astro.site)} />
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||||
|
|
||||||
|
|
@ -82,7 +88,7 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="flex min-h-screen flex-col bg-bg font-sans text-text antialiased selection:bg-primary selection:text-white">
|
<body class="flex min-h-screen flex-col bg-bg font-sans text-text antialiased selection:bg-primary selection:text-white">
|
||||||
<Header lang={lang} />
|
<Header lang={lang} langSwitchHref={alternates[lang === 'en' ? 'pt' : 'en']} />
|
||||||
<main class="flex-1">
|
<main class="flex-1">
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
25
src/lib/about.ts
Normal file
25
src/lib/about.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { getCollection, type CollectionEntry } from 'astro:content';
|
||||||
|
import type { Lang } from './i18n';
|
||||||
|
|
||||||
|
// The about page lives at src/content/about/<lang>.md; its URL segment comes
|
||||||
|
// from the `slug` frontmatter field, so each language can have its own
|
||||||
|
// (/en/about, /pt/sobre).
|
||||||
|
export type AboutPage = CollectionEntry<'about'>;
|
||||||
|
|
||||||
|
export async function getAboutPages(): Promise<AboutPage[]> {
|
||||||
|
return getCollection('about');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAboutPage(lang: Lang): Promise<AboutPage> {
|
||||||
|
const page = (await getAboutPages()).find((entry) => entry.data.language === lang);
|
||||||
|
if (!page) throw new Error(`Missing src/content/about/${lang}.md`);
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function aboutPathOf(page: AboutPage): string {
|
||||||
|
return `/${page.data.language}/${page.data.slug}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAboutPath(lang: Lang): Promise<string> {
|
||||||
|
return aboutPathOf(await getAboutPage(lang));
|
||||||
|
}
|
||||||
|
|
@ -45,13 +45,6 @@ export const ui = {
|
||||||
'about.title': 'About',
|
'about.title': 'About',
|
||||||
'about.description':
|
'about.description':
|
||||||
'Gabriel Franco: computer science student at PUCRS, interested in how software actually works and in the history of open source.',
|
'Gabriel Franco: computer science student at PUCRS, interested in how software actually works and in the history of open source.',
|
||||||
// Paragraphs are separated by a blank line.
|
|
||||||
'about.body': [
|
|
||||||
"Hi, I'm Gabe — Gabriel Franco. I'm a computer science student at PUCRS (Pontifícia Universidade Católica do Rio Grande do Sul), and this blog is where I write down the things I'm learning and thinking about along the way.",
|
|
||||||
"I care a lot about computer science as a field, not just as a set of tools to ship products with. I have a fairly specific way of doing things: I'd rather understand a system properly than memorize the steps to make it work, and I tend to slow down on purpose to figure out what's actually happening under the hood — in a compiler, an operating system, a language runtime, whatever it is. That habit shapes most of what ends up on this blog.",
|
|
||||||
"I'm also genuinely interested in the history of the open source movement: the people, the arguments, and the decisions that shaped the software we all rely on. Figures like Linus Torvalds and projects like GNU come up often in what I read and write about, not as trivia, but because understanding where a tool came from usually explains a lot about how and why it works the way it does today.",
|
|
||||||
'This site is bilingual, in English and in Portuguese, and every post exists in both languages. Thanks for stopping by.',
|
|
||||||
].join('\n\n'),
|
|
||||||
'footer.tagline': 'Written by hand in Porto Alegre.',
|
'footer.tagline': 'Written by hand in Porto Alegre.',
|
||||||
'footer.rights': 'All rights reserved.',
|
'footer.rights': 'All rights reserved.',
|
||||||
'footer.source': 'Source',
|
'footer.source': 'Source',
|
||||||
|
|
@ -96,12 +89,6 @@ export const ui = {
|
||||||
'about.title': 'Sobre',
|
'about.title': 'Sobre',
|
||||||
'about.description':
|
'about.description':
|
||||||
'Gabriel Franco: estudante de ciência da computação na PUCRS, interessado em como o software realmente funciona e na história do open source.',
|
'Gabriel Franco: estudante de ciência da computação na PUCRS, interessado em como o software realmente funciona e na história do open source.',
|
||||||
'about.body': [
|
|
||||||
'Olá, eu sou o Gabe — Gabriel Franco. Sou estudante de ciência da computação na PUCRS (Pontifícia Universidade Católica do Rio Grande do Sul), e este blog é onde registro as coisas que vou aprendendo e pensando pelo caminho.',
|
|
||||||
'Eu me importo bastante com ciência da computação como área, não só como um conjunto de ferramentas para lançar produtos. Tenho um jeito bem específico de fazer as coisas: prefiro entender um sistema de verdade a decorar os passos para fazê-lo funcionar, e costumo desacelerar de propósito para descobrir o que realmente está acontecendo por baixo dos panos — em um compilador, em um sistema operacional, no runtime de uma linguagem, seja lá o que for. Esse hábito molda a maior parte do que acaba virando post aqui.',
|
|
||||||
'Também tenho um interesse genuíno pela história do movimento open source: as pessoas, os debates e as decisões que moldaram o software do qual todos dependemos. Figuras como Linus Torvalds e projetos como o GNU aparecem com frequência no que leio e escrevo, não como curiosidade, mas porque entender de onde uma ferramenta veio costuma explicar bastante sobre como e por que ela funciona do jeito que funciona hoje.',
|
|
||||||
'Este site é bilíngue, em inglês e em português, e todo post existe nos dois idiomas. Obrigado pela visita.',
|
|
||||||
].join('\n\n'),
|
|
||||||
'footer.tagline': 'Escrito à mão em Porto Alegre.',
|
'footer.tagline': 'Escrito à mão em Porto Alegre.',
|
||||||
'footer.rights': 'Todos os direitos reservados.',
|
'footer.rights': 'Todos os direitos reservados.',
|
||||||
'footer.source': 'Código-fonte',
|
'footer.source': 'Código-fonte',
|
||||||
|
|
|
||||||
38
src/pages/[lang]/[about].astro
Normal file
38
src/pages/[lang]/[about].astro
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
import { render } from 'astro:content';
|
||||||
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
|
import { aboutPathOf, getAboutPages, type AboutPage } from '../../lib/about';
|
||||||
|
import { pageMeta, useTranslations, type Lang } from '../../lib/i18n';
|
||||||
|
import { pageCardPath } from '../../lib/social-cards';
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const pages = await getAboutPages();
|
||||||
|
// The language switcher and hreflang links need the other language's slug.
|
||||||
|
const alternates = Object.fromEntries(pages.map((page) => [page.data.language, `${aboutPathOf(page)}/`]));
|
||||||
|
return pages.map((page) => ({
|
||||||
|
params: { lang: page.data.language, about: page.data.slug },
|
||||||
|
props: { page, alternates },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
page: AboutPage;
|
||||||
|
alternates: Record<Lang, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { page: entry, alternates } = Astro.props;
|
||||||
|
const lang = entry.data.language;
|
||||||
|
const t = useTranslations(lang);
|
||||||
|
const page = pageMeta(lang, 'about');
|
||||||
|
const { Content } = await render(entry);
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title={`${page.title} — ${t('site.name')}`} description={page.description} lang={lang} image={pageCardPath(lang, 'about')} alternates={alternates}>
|
||||||
|
<section class="mx-10 mt-10 font-mono">
|
||||||
|
<h1 class="text-3xl font-bold tracking-tight text-text sm:text-4xl">{page.title}</h1>
|
||||||
|
|
||||||
|
<div class="prose prose-neutral mt-6 max-w-none prose-headings:font-semibold prose-a:text-primary prose-a:no-underline hover:prose-a:underline dark:prose-invert">
|
||||||
|
<Content />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
---
|
|
||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
||||||
import { langPaths, pageMeta, useTranslations, type Lang } from '../../lib/i18n';
|
|
||||||
import { pageCardPath } from '../../lib/social-cards';
|
|
||||||
|
|
||||||
export const getStaticPaths = langPaths;
|
|
||||||
|
|
||||||
const lang = Astro.params.lang as Lang;
|
|
||||||
const t = useTranslations(lang);
|
|
||||||
const page = pageMeta(lang, 'about');
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout title={`${page.title} — ${t('site.name')}`} description={page.description} lang={lang} image={pageCardPath(lang, 'about')}>
|
|
||||||
<section class="mx-10 mt-10 font-mono">
|
|
||||||
<h1 class="text-3xl font-bold tracking-tight text-text sm:text-4xl">{page.title}</h1>
|
|
||||||
|
|
||||||
<div class="prose prose-neutral mt-6 max-w-none prose-headings:font-semibold prose-a:text-primary prose-a:no-underline hover:prose-a:underline dark:prose-invert">
|
|
||||||
{t('about.body').split('\n\n').map((paragraph) => <p>{paragraph}</p>)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</BaseLayout>
|
|
||||||
Loading…
Reference in a new issue