javascriptroutesastrojsdecap-cms

How can I create multilingual routing for collections using Astro Js with Decap CMS and astro-i18next?


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!


Solution

  • 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).

    Utility Snippet

    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.

    Post index page

    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>
       ))
    }
    

    Single Post pages (with dynamic routes)

    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
        }));
    }
    

    Nav Bar

    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