Régiolangues: a collaborative resource portal
Interested in French regional languages, my brother thought it was difficult to find resources on the web.

Introduction
Régiolangues is a collaborative portal for French regional languages related resources.
This website has a bunch of cool features:
- Advanced filtering
- Pagination
- Interactive map
- Contribution forms
- A CMS
For this project, I’m in charge of designing the interface, its database and coding the website. My brother is in charge of the content.
Here are the techs I used:
And probably a lot more I’ve forgotten about!
Check out the website!
Purpose and Goal
My brother enjoys Provençal (a French regional language) but felt that it was difficult to learn it online, that there were not really websites concentrating learning content and resources. So he asked me to build a website for this. The goal was to be able to have many languages so that people can contribute.
Spotlight: interactive map
There are many interesting parts in this project but let’s focus on what was the most challenging part: the interactive map.
Techs
For rendering the map itself, I used React Leaflet which is used a convenient wrapper for Leaflet. I’ve also used plugins such as react-leaflet-cluster that were really useful.
All styling (including the markers) is achieved with Tailwind CSS. Regarding the markers, icons are provided through the Directus CMS (as identifiers) and rendered using Iconify.
Finally, as for all the website, state is managed using Jotai which allowed me to filter things easily.
The custom markers’ nightmare
When using Leaflet it’s possible to use custom markers. They’re usually only custom images but in my case, I needed to render a React component. And it was not that easy…
1st attempt: renderToString
The first way to do this is by using renderToString
from react-dom/server
.
And it actually works pretty well! It just compiles a React component to a
string. For example:
import React from 'react'
import { renderToString } from 'react-dom/server'
function MyComponent({ children }: { children: React.ReactNode }) {
return <div className="some-class">My component</div>
}
const renderedComponent = renderToString(
<MyComponent>Hello there!</MyComponent>
)
Becomes
<div class="some-class">Hello there!</div>
However, you loose any client side interactivity. And that was pretty critical
because I was using the Icon
component from @iconify/react
!
2nd attempt: createRoot().render
So I tried to so some kind of custom rendering. It was really bad, it worked only half of the time. It was an absolute mess. I thrown it away pretty fast.
If you wanna have a look at this monstrosity, check it out here.
3rd attempt: renderToString
… and web components!
The real issue there is that a custom Leaftlet marker expects a Leaftlet
divIcon
, which itself expects an html
prop as a string like so:
function SomeComponent() {
return (
<Marker
icon={L.divIcon({
html: '', // this is what we are talking about
})}
/>
)
}
And I need client-side logic for iconify to run (it fetches icon client-side to avoid downloading all the icons). But there’s actually a way to do this, without React: web components!
So I extracted my custom marker icon to a component:
// MarkerIcon.tsx
import type { IconifyIconAttributes } from 'iconify-icon'
import type { SVGProps } from 'react'
import { Circle, HandsupFlower } from './Markers'
import 'iconify-icon' // imports the web component
// make sure TypeScript doesn't complain because
// iconify-icon is not a valid html tag by default
declare global {
namespace JSX {
interface IntrinsicElements {
'iconify-icon': IconifyIconAttributes &
Omit<SVGProps<SVGElement>, 'className'> &
Partial<{ class: string }>
}
}
}
export default function MarkerIcon({ icon }: { icon: string }) {
return (
<div>
<iconify-icon
icon={icon}
class="z-10 h-5 w-5 text-white"
></iconify-icon>
</div>
)
}
I can then render it as string:
import L from 'leaflet'
import { Marker } from 'react-leaflet'
import { renderToString } from 'react-dom/server'
import MarkerIcon from './MarkerIcon'
function CustomMarker({
icon,
position,
}: {
icon: string
position: [number, number]
}) {
return (
<Marker
icon={L.divIcon({
html: renderToString(<MarkerIcon icon={icon} />),
})}
position={position}
/>
)
}
What about SEO?
The downside with a map like this is that it’s rendered client-side, so I’m loosing all the content associated with each point (viewable on click in a slideover). And here the solution is pretty easy: there is no way to have this content referenced.
So I just created another view: a standard list!
Current status
Most of the development is done right now, there are only a few things like notify CMS authors when contributions are made. Nothing crazy!
Lessons Learned
This project taught me many things:
- Using the right tool for the right job: This is actually the 2nd version of RégioLangues. First one was built like a recursive file explorer view (Supabase, Nuxt 2, prerendering every 20min with cron) and it was not good. What was wrong above all was the mental model: recursive resources and categories VS a list of resources with tags
- Dealing with maps using Leaflet
- A new headless CMS, Directus!