Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions src/components/BaseHead.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,30 @@ import { siteConfig } from "@/site.config";
import type { SiteMeta } from "@/types";
import "@/styles/global.css";

type Props = SiteMeta & { locale?: Locale };
type Props = SiteMeta & { locale?: Locale; alternateUrls?: Partial<Record<Locale, string>> };

const { articleDate, description, ogImage, title, locale = defaultLocale } = Astro.props;
const {
articleDate,
description,
ogImage,
title,
locale = defaultLocale,
alternateUrls,
} = Astro.props;

const titleSeparator = "•";
const siteTitle = `${title} ${titleSeparator} ${siteConfig.title}`;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const socialImageURL = new URL(ogImage ? ogImage : "/social-card.png", Astro.url).href;

const alternates = getAlternateLocalePaths(Astro.url.pathname).map(({ locale: l, path }) => ({
locale: l,
href: new URL(path, Astro.site).href,
}));
const alternates = alternateUrls
? (Object.entries(alternateUrls) as [Locale, string][])
.filter(([, path]) => !!path)
.map(([l, path]) => ({ locale: l, href: new URL(path, Astro.site).href }))
: getAlternateLocalePaths(Astro.url.pathname).map(({ locale: l, path }) => ({
locale: l,
href: new URL(path, Astro.site).href,
}));
void locales;
---

Expand Down
7 changes: 6 additions & 1 deletion src/components/LangToggle.astro
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,12 @@ const t = useTranslations(locale);
this.close();
return;
}
const nextPath = switchLocaleInPath(window.location.pathname, target);
const hreflangLink = document.querySelector<HTMLLinkElement>(
`link[rel="alternate"][hreflang="${target}"]`,
);
const nextPath = hreflangLink
? new URL(hreflangLink.href).pathname
: switchLocaleInPath(window.location.pathname, target);
window.location.assign(nextPath);
});
});
Expand Down
9 changes: 6 additions & 3 deletions src/components/blog/TOC.astro
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
---
import type { MarkdownHeading } from "astro";
import { defaultLocale, type Locale } from "@/i18n/config";
import { useTranslations } from "@/i18n/utils";
import { generateToc } from "@/utils/generateToc";
import TOCHeading from "./TOCHeading.astro";

interface Props {
headings: MarkdownHeading[];
locale?: Locale;
}

const { headings } = Astro.props;

const { headings, locale = defaultLocale } = Astro.props;
const t = useTranslations(locale);
const toc = generateToc(headings);
---

<details open class="lg:sticky lg:top-12 lg:order-2 lg:-me-32 lg:basis-64">
<summary class="title hover:marker:text-accent cursor-pointer text-lg">Table of Contents</summary>
<summary class="title hover:marker:text-accent cursor-pointer text-lg">{t("toc.title")}</summary>
<nav class="ms-4 lg:w-full">
<ol class="mt-4">
{toc.map((heading) => <TOCHeading heading={heading} />)}
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export const ui = {
"posts.backToTop": "Back to top",
"posts.viewMoreWithTag": "View more blogs with the tag ",

"toc.title": "Table of Contents",

"tags.title": "All Tags",
"tags.pageDescription": "A list of all the topics I've written about in my posts",
"tags.viewPostsWithTag": "View posts with the tag:",
Expand Down Expand Up @@ -132,6 +134,8 @@ export const ui = {
"posts.backToTop": "Volver arriba",
"posts.viewMoreWithTag": "Ver más posts con la etiqueta ",

"toc.title": "Tabla de contenidos",

"tags.title": "Todas las etiquetas",
"tags.pageDescription": "Una lista de todos los temas sobre los que he escrito en mis posts",
"tags.viewPostsWithTag": "Ver posts con la etiqueta:",
Expand Down
4 changes: 3 additions & 1 deletion src/layouts/Base.astro
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import type { SiteMeta } from "@/types";
interface Props {
meta: SiteMeta;
locale?: Locale;
alternateUrls?: Partial<Record<Locale, string>>;
}

const { meta, locale: localeProp } = Astro.props;
const { meta, locale: localeProp, alternateUrls } = Astro.props;
const locale = localeProp ?? getLocaleFromUrl(Astro.url);
const description = meta.description ?? ui[locale]["site.description"];
const htmlLang = locale === "es" ? "es-ES" : "en-US";
Expand All @@ -28,6 +29,7 @@ const htmlLang = locale === "es" ? "es-ES" : "en-US";
ogImage={meta.ogImage}
title={meta.title}
locale={locale}
{...(alternateUrls && { alternateUrls })}
/>
</head>
<body
Expand Down
6 changes: 4 additions & 2 deletions src/layouts/BlogPost.astro
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import BaseLayout from "./Base.astro";
interface Props {
post: CollectionEntry<"post">;
locale?: Locale;
alternateUrls?: Partial<Record<Locale, string>>;
}

const { post, locale = defaultLocale } = Astro.props;
const { post, locale = defaultLocale, alternateUrls } = Astro.props;
const t = useTranslations(locale);
const { ogImage, title, description, updatedDate, publishDate } = post.data;
const socialImage = ogImage ?? `/og-image/${post.id}.png`;
Expand All @@ -31,13 +32,14 @@ const readingTime: string = remarkPluginFrontmatter.readingTime;
title,
}}
locale={locale}
{...(alternateUrls && { alternateUrls })}
>
<article class="grow break-words" data-pagefind-body>
<div id="blog-hero" class="mb-12">
<Masthead content={post} readingTime={readingTime} locale={locale} />
</div>
<div class="flex flex-col gap-10 lg:flex-row lg:items-start lg:justify-between">
{!!headings.length && <TOC headings={headings} />}
{!!headings.length && <TOC headings={headings} locale={locale} />}
<div
class="prose prose-sm prose-headings:font-semibold prose-headings:text-accent-2 prose-headings:before:absolute prose-headings:before:-ms-4 prose-headings:before:text-gray-600 prose-headings:hover:before:text-accent sm:prose-headings:before:content-['#'] sm:prose-th:before:content-none"
>
Expand Down
14 changes: 14 additions & 0 deletions src/pages/404.astro
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,17 @@ const meta = {
<a class="cactus-link" href="/es/">{ui.es["404.home"]}</a>
</div>
</PageLayout>

<script>
// Redirect to the locale-specific 404 based on URL prefix, cookie, or browser language
const lang = (() => {
const segment = window.location.pathname.split("/").filter(Boolean)[0];
if (segment === "es") return "es";
if (segment === "en") return "en";
const match = document.cookie.match(/(?:^|;\s*)lang=([^;]+)/);
if (match && (match[1] === "en" || match[1] === "es")) return match[1];
const nav = (navigator.languages?.[0] ?? navigator.language ?? "en").toLowerCase();
return nav.startsWith("es") ? "es" : "en";
})();
window.location.replace(`/${lang}/404`);
</script>
36 changes: 29 additions & 7 deletions src/pages/[lang]/posts/[...slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,47 @@
import { render } from "astro:content";
import type { GetStaticPaths, InferGetStaticPropsType } from "astro";
import { getAllPosts } from "@/data/post";
import { isLocale } from "@/i18n/config";
import { isLocale, type Locale, locales } from "@/i18n/config";
import PostLayout from "@/layouts/BlogPost.astro";

export const getStaticPaths = (async () => {
const blogEntries = await getAllPosts();
return blogEntries.map((post) => ({
params: { lang: post.data.lang, slug: post.id },
props: { post },
}));
return blogEntries.map((post) => {
const otherLocale: Locale = post.data.lang === "en" ? "es" : "en";
const translatedId =
post.data.lang === "en"
? `${post.id}-es`
: post.id.endsWith("-es")
? post.id.slice(0, -3)
: null;
const translatedPost = translatedId
? blogEntries.find((p) => p.id === translatedId && p.data.lang === otherLocale)
: undefined;
return {
params: { lang: post.data.lang, slug: post.id },
props: { post, translatedPost },
};
});
}) satisfies GetStaticPaths;

type Props = InferGetStaticPropsType<typeof getStaticPaths>;

const { post } = Astro.props;
const { post, translatedPost } = Astro.props;
const { lang } = Astro.params;
if (!isLocale(lang)) throw new Error(`Invalid locale: ${lang}`);

const alternateUrls: Partial<Record<Locale, string>> = {
[lang]: `/${lang}/posts/${post.id}/`,
};
if (translatedPost) {
const otherLocale: Locale = lang === "en" ? "es" : "en";
alternateUrls[otherLocale] = `/${otherLocale}/posts/${translatedPost.id}/`;
}
void locales;

const { Content } = await render(post);
---

<PostLayout post={post} locale={lang}>
<PostLayout post={post} locale={lang} alternateUrls={alternateUrls}>
<Content />
</PostLayout>