I'm in the making of a multilingual (EN, ES) website with a blog in Astro js, with Decap CMS for the blog posts and astro-i18next for its internationalisation features.
I'm having problems to find out how to get this to work right.
This astro site is setup with EN as default language and ES as secondary. Here is the page structure:
pages/ ─── index
├── about-us
└── es/ ─── index
└─ about-us
Thanks to the astro-i18next config I can, for example, route the Spanish about to website.com/es/quien-somos.
Now, I've setup Decap CMS to create multilingual blog posts. But when I create a new post through the admin panel, it creates the md files in folders for each language, also for the default language:
content/blog/─── en/ ─── first-post.md
└─ es/ ─── first-post.md
That's my first problem, because, the url for the blog page in English is website.com/blog, but the url for the English blog post is website/blog/en/first-post. Same goes for the Spanish version: the url for the blog page is website.com/es/blog, but the url for the Spanish blog post is website/blog/es/first-post. With on the top of that, the slug being in English and not in Spanish.
Can someone give me some tips on how I could route that the right way? I mean, not hard coded in the i18next config file, with through some dynamic routing magic. That is that:
Thx!
At the end, I could make a solution through routing. The main idea is to have post routes based on the post titles (in each language).
First, in utils.ts, I copy a small snippet from https://www.30secondsofcode.org/js/s/string-to-slug/
export const slugify = (str: string) =>
str
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
I will be using this extendly.
Next, for pages/blog/index.astro
, I filter for all posts in needed language and create the post urls based on the slugified post title. Because Decap CMS saves each post with the same file name but in folders by language, it gives me a way to easily filter them.
---
// get posts and filter them for targeted language and sort them
const lang = i18next.language;
const posts = (
await getCollection('blog', ({ id }) => {
return id.startsWith(lang);
})
).sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
---
<h1>Blog Index</h1>
{
posts.map(post => (
<a href={localizePath(`/blog/${slugify(post.data.title)}/`)}>{post.data.title}</a>
))
}
Then, I create the file for the single posts pages/blog/[...title].astro
. This file is also controling the dynamic routes of these single posts.
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({
params: { title: slugify(post.data.title) },
props: post
}));
}
Because I can't use the i18next config file for my post routes, I've to create an extra function to get the paths to the translations based on the titles and slugs.
async function getTranslatedPostUrl() {
// Split pathname
// While filtereing out the empty entries
// https://stackoverflow.com/a/39184134/2812386
const pathParts = pathname.split('/').filter(i => i);
// Find the index of 'blog' in the pathParts array
const blogIndex = pathParts.indexOf('blog');
// Determine the target language
const targetLang = lang === 'es' ? 'en' : 'es';
// Check if 'blog' exists in the URL and if there's something after it
if (blogIndex !== -1 && pathParts.length > blogIndex + 1) {
// Get all blog posts
const allPosts = await getCollection('blog');
// Extract the slugified title form the pathname
const currentSlugifiedTitle = pathParts[blogIndex + 1];
// Find the current post to get its title in the original language
const currentPost = allPosts.find((post) => {
return slugify(post.data.title) === currentSlugifiedTitle;
});
if (currentPost) {
// Extract the slug without the language prefix from the current post
const baseSlug = currentPost.slug.split('/').slice(1).join('/'); // Removes 'en/' or 'es/'
// Find the translated post based on the base slug
const translatedPost = allPosts.find((post) => {
return post.slug === `${targetLang}/${baseSlug}`;
});
if (translatedPost) {
// Generate the URL for the translated post
const slugifiedTranslatedTitle = slugify(translatedPost.data.title);
return targetLang === 'es' ? `/es/blog/${slugifiedTranslatedTitle}/` : `/blog/${slugifiedTranslatedTitle}/`;
}
}
} else {
// Handle other pages with i18next
return localizePath(pathname, targetLang);
}
}
const translatedPostUrl = await getTranslatedPostUrl();
Now I can just use the value of translatedPostUrl
to link to the translated content.
^_^
EDIT: After deploying this solution on Netlify, I noticed the NavBar logic wasn't working 100%. That was because the pathname.split('/')
was creating some empty entries. So I had to filter it. More about that on https://stackoverflow.com/a/39184134/2812386