Language-specific URLs for SEO?

What I want to do: create URLs that add a language prefix depending on the session language. There is a ton of endpoints already defined so this needs to be a somewhat plug-and-play solution.

Flask has some helpers which can do this:

As my app has lots of endpoints that are already defined, and there doesn’t seem like there is an easy way to do this, I’ve hacked together a solution that appears to be working — but I’m wondering if this is at all sound.

My solution is to add a class called SBlueprint which inherits Blueprint, and when .add_route() is called, have it also automatically add all the language specific routes by adding them to language specific blueprints. I have tested on some of my existing blueprint definition and simply change the class imports:

  • from sanic import Blueprint
  • from app.utils.blueprint import SBlueprint

It appears to be working but I am wondering if there are better ways of doing this before I go down this rabbit hole to redo the entire app (which contains a ton of endpoints).

Thoughts?


from sanic import Blueprint

language_keys = ['en', 'es', 'fr', 'de', 'pl', 'pt', 'ja', 'zh', 'zh_Hant']

bp_langs = []
for key in language_keys:
    bp_lang = Blueprint(f'lang_{key}', url_prefix=f'/{key}')
    bp_langs.append(bp_lang)


def init_app(app: Sanic):
    for bp_lang in bp_langs:
        app.blueprint(bp_lang)

    @app.middleware('request')
    async def app_language(request):
        for key in language_keys:
            if request.path.startswith(f'/{key}/'):
                request['session']['language'] = key

class SBlueprint(Blueprint):
    def add_route(
            self,
            handler,
            uri,
            methods=frozenset({"GET"}),
            host=None,
            strict_slashes=None,
            version=None,
            name=None,
            stream=False,
    ):
        """Create a blueprint route from a function.

        :param handler: function for handling uri requests. Accepts function,
                        or class instance with a view_class method.
        :param uri: endpoint at which the route will be accessible.
        :param methods: list of acceptable HTTP methods.
        :param host: IP Address of FQDN for the sanic server to use.
        :param strict_slashes: Enforce the API urls are requested with a
            training */*
        :param version: Blueprint Version
        :param name: user defined route name for url_for
        :param stream: boolean specifying if the handler is a stream handler
        :return: function or class instance
        """
        super().add_route(
            handler,
            uri,
            methods=methods,
            host=host,
            strict_slashes=strict_slashes,
            version=version,
            name=name,
            stream=stream
        )
        for bp_lang in bp_langs:
            bp_lang.add_route(
                handler,
                uri,
                methods=methods,
                host=host,
                strict_slashes=strict_slashes,
                version=version,
                name=name,
                stream=stream
            )

Yes, I believe this is the best way for doing this, since Sanic itself is more “barebones” than Flask (IMHO) and doesn’t have all those nice plugins out there.

Another solution (that I even mentioned somewhere) is to simply create a Blueprint.group with a url_prefix that stores a language variable:

from sanic import Blueprint, Sanic
from sanic.response import text

bp_hello = Blueprint("bp_hello", url_prefix='/hello')


@bp_hello.route("/")
async def hello_route(request, lang):
    if lang == "pt":
        msg = "Olá, mundo!"
    elif lang == "es":
        msg = "¡Hola mundo!"
    else:
        msg = "Hello, world!"
    return text(msg)


bp_lang_group = Blueprint.group(bp_hello, url_prefix='/<lang>')

app = Sanic(__name__)
app.blueprint(bp_lang_group)


if __name__ == '__main__':
    app.run(host="0.0.0.0", port=8000, workers=1)

Running this gives you the proper result:

$ curl -H "Content-Type: application/json" http://localhost:8000/pt/hello
Olá, mundo!

$ curl -H "Content-Type: application/json" http://localhost:8000/es/hello
¡Hola mundo!

$ curl -H "Content-Type: application/json" http://localhost:8000/en/hello
Hello, world!                                                                                                                       

$ curl -H "Content-Type: application/json" http://localhost:8000/XX/hello
Hello, world!

It’s kind of a hack to say the least because storing a variable in the url_prefix is kind of a “gray area” - I have no idea if this will work in a future release or not, so I guess I would stick with your own solution since it does look more appropriate.

Your implementation works, except that it will require me to modify all of the handlers to include the lang now that it is present as a variable on the URL. That will require me to modify the entire app and it’s not workable.

I have in fact tried your blueprint group solution like this:

bp_lang_en = Blueprint.group(*bps, url_prefix=f'/en')
app.blueprint(bp_lang_en)

bp_lang_es = Blueprint.group(*bps, url_prefix='/es')
app.blueprint(bp_lang_es)

where bps is my list of imported blueprints, but somehow this fails. Using one group was ok but once I start to put the same blueprints to different groups, it fails completely. (I have previously put it in a loop and wondered why it kept on giving me errors, so I decided to hard code it and see if anything changes. It doesn’t, it failed).

If you’re doing this with 19.2.2, this may be running into the problem with stackable routes which @ahopkins and I are trying to get resolved.

I am on 19.6.0 right now. I didn’t even realize that 19.12.2 is available. Are you suggesting that I should upgrade or not? Are there issues with stackable routes right now? I have used stacked decorators for routing in my app right now and they seem to be ok. (it didn’t work for the blueprint group solution though).

@smlbiobot no - don’t upgrade now; we have to get the fix for the stackable routes merged in. I don’t think the issue existed with 19.6.0 but I’d have to go back and look.

Thanks for letting me know. I have to check these updates more often and read the patch notes more carefully!