Fix #578: iframe embed

Agate requested to merge embed into develop

Closes #578 (closed), cf https://mastodon.eliotberriot.com/@eliotberriot/101262842342811449 for knowing how it renders.

FWIW, I kinda implemented a poor man's Server Side Rendering. Let me elaborate a bit about that.

As explained in our architecture documentation, Funkwhale's UI is a Single Page Application (SPA). Basically a bunch of static (that is the important word) html, css and javascript files that are served to the browser, and will in turn do API calls to the Funkwhale API server to load dynamic data (music, etc.) or perform actions (login, signup, importing music.).

This is radically different from how most application out there work. In typical application, you have one server (e.g a PHP process) that receive browser requests and generate on the fly an HTML page with relevant content.

Most of the time, this difference is not an issue, but there are a few scenarios where having a pre-built SPA can make things difficult. One of the most difficult one is when third party application need to crawl your site to extract data from it (e.g. to build a preview card with a title and a description when you toot a link to your site, as Mastodon do). In such situations, the third-party app request some URL on your website, extract content from it and do something with the data, such as providing a preview, as shown below:

image

However, since our app is pre-built, our HTML is completely generic. There is no dynamic data in it, since it's loaded at a later point, in javascript. If you load https://open.audio/library/artists/125 or https://open.audio/library/albums/125, in both case, the HTML is exactly the same:

<!DOCTYPE html>
<html lang=en>
    <head>
        <meta charset=utf-8>
        <meta http-equiv=X-UA-Compatible content="IE=edge">
        <meta name=viewport content="width=device-width,initial-scale=1">
        <link rel=icon href=/favicon.png>
        <title>Funkwhale</title>
        <link href=/css/app.81a55840.css rel=preload as=style>
        <link href=/js/app.9fdab308.js rel=preload as=script>
        <link href=/js/chunk-vendors.4bb32a31.js rel=preload as=script>
        <link href=/css/app.81a55840.css rel=stylesheet>
    </head>
    <body>
        <noscript>
            <strong>We're sorry but front doesn'ed. Please enable it to continue.</strong>
        </noscript>
        <div id=app></div>
        <script src=/js/chunk-vendors.4bb32a31.js></script>
        <script src=/js/app.9fdab308.js></script>
    </body>
</html>

A third party app would not be able to extract content from this (unless they execute javascript, but only a few search engine crawlers actually do that, because it consumes additional resources).

So to support such use cases in Funkwhale (which is needed for Oembed discovery, which is directly linked to this MR), I had to find a way.

The most obvious way was to use Server Side Rendering. Basically, it's a setup where your SPA is partially or completely rendered on the server. This is likely doable and supported in Vue.JS but would also require significant overhaul of the codebase and the deployment, and an additionnal process to run on every installation (to serve the SPA and fill it with dynamic content).

Since our use cases for this are quite specific (inserting a bunch of <meta> and <link> tags in our HTML head), I took another route instead, which require very little tweaks to the way the application currently behave and is thus more practical to implement. I implemented the Server Side Rendering in Python, via the Funkwhale API server.

In this new setup, when you visit a page, let's say https://open.audio/library/albums/125, instead of receiving the generic, purely static SPA HTML (which is served by Nginx and does not hit the API server at all), the request will go to the API server. If the request match one of the backend routes (/api/, /api/admin/, etc.), the API server will answer as usual. Otherwise, it will serve the SPA and inject additionnal tags in the <head> depending on the current route.

So if you're requesting /api/v1/albums, nothing change at all. But if you're requesting /library/tracks/125 (which is a front-end URL), the API server will understand that you are loading a track page. It will then take the generic, SPA html, and inject a bunch of meta tags into this HTML (Open Graph tags, a link to the track album cover, the url to play the track, the title of the track, oembed tags, etc.), and send this to the client instead, thus bringing compatibility with software that parse such tags.

I tried this locally and it works as intended. I'm going to deploy that on other instances I own and fix potential issues, but I'm pretty confident this work. This will also unlock other use cases in the future (such as supporting rel=me on user profiles).

Merge request reports