How to setup dynamic open graph meta tags in your Flutter web app

Author

So I'm working on my first Flutter web app. In general, I got into Flutter back in 2018 because I was interested in cross-platform mobile development and embedded or IoT development. I didn't think I would develop a web-based platform in Flutter before Flutter. I was working in ReactJS, which is perfect for web-based apps; however, an opportunity from one of my clients presented itself to create a simple app using Flutter web.

Taking on this new project, I ran into different minor challenges with Flutter web, but one huge one was to update meta tags or open graph tags for sharing dynamically. Today if you're creating any public platform like Reddit, Pinterest, YouTube, when a user shares a link, you want a good preview that shows that specific user-generated content.

Here is an excellent example of me sharing a blog post on my Facebook and slack

Facebook sharing

Slack channel sharing

This is a common issue with single-page apps, but most frameworks like React, Vue, or Angular have built ways to handle this using server-side rendering, packages, or plugins. Flutter right now does not.

In this tutorial, I'm going to cover how I was able to implement this.

I want to give a big shoutout to Tyler Savery @tylersavery for helping us with this solution and even taking the time to send us a fully working example on GitHub.

I'm using Google Firebase to host my client's app. The tutorial will cover how to add dynamic tags using Google Firebase, but of course, you can use the same methods or and backend.

The first thing we need to do is remove the # from the app URL so our Firebase rewrite rules will work. I wrote a quick blog post on how to do this "Flutter Tips & Tricks #2 - Remove The Hashtag From URL."

We already have Firebase set up in our app as were hosting the web app on Firebase, but if you need to set this up, you can follow the following guide.

We will need Firebase Functions to check for the bot and load a different index file. We can run the following command and setup functions in our project.

Firebase init

Now that we Firebase Functions initiated in the project and our functions folder, we will want to copy our index.html from the Flutter build web folder and place it inside our Firebase Functions folder.

![Functions Folder]( "Functions Folder")

As you can see, I also copied my splash image, icons, and every else I need.

Let's go ahead and install the following npm package, which we can use to detect bots.

cd functions
npm install device-detector-js

Let's add our imports and create our Firebase function.

const functions = require("firebase-functions");
const fs = require("fs");
const admin = require("firebase-admin");
const BotDetector = require("device-detector-js/dist/parsers/bot");
const DEBUG_BOT = false;
admin.initializeApp();

// Main firebase function called on firebase.json rewrites rules
exports.dynamicMetaTagsUpdate = functions.https.onRequest(async (request, response) => {
});

Now that we have a function read, we will want to load our index file, check if it's a user or bot, and, based on that, load different HTML files.

// Main firebase function called on firebase.json rewrites rules
exports.dynamicMetaTagsUpdate = functions.https.onRequest(async (request, response) => {

  let html = fs.readFileSync("./index.html", "utf8");
  const {id} = request.query;
  const botDetector = new BotDetector();
  const userAgent = request.headers["user-agent"].toString();
  const bot = botDetector.parse(userAgent);
  const url = "https://viz.wiijii.co/chart/?id="+ id;

  // If bot get chat data from Firebase database
  if (bot || DEBUG_BOT) {
    try {
      return response.send(html);
    } catch (e) {
      console.log(e);
      return response.send(html);
    }
  }
  return response.send(html);
});

Let's get some data from our Firebase database based on URL parameters.

// Main firebase function called on firebase.json rewrites rules
exports.dynamicMetaTagsUpdate = functions.https.onRequest(async (request, response) => {
  // console.log("dynamicMetaTagsUpdate Called");

  let html = fs.readFileSync("./index.html", "utf8");
  const {id} = request.query;
  const botDetector = new BotDetector();
  const userAgent = request.headers["user-agent"].toString();
  const bot = botDetector.parse(userAgent);
  const url = "https://viz.wiijii.co/chart/?id="+ id;

  // If bot get chat data from Firebase database
  if (bot || DEBUG_BOT) {
    try {
      // console.log("running try");
      const ref = admin.database().ref("charts");
      const chartData = ref.child(id).get().then((snapshot) => {
        if (snapshot.exists()) {
          // Gets the chart data stocks for seo tags
          const seoTags = [];
          snapshot.child("chartData").val().forEach((element, index, array) => {
            seoTags.push(element.companyName, element.stockName);
          });
          // Add the y and x vaules from chart data to be used in seo tags
          seoTags.push(snapshot.child("xAxisValue").val(), snapshot.child("yAxisValue").val());
          const object = {chartTitle: snapshot.child("chartTitle").val(), chartDescription: snapshot.child("chartDescription").val(), imageUrl: snapshot.child("imageUrl").val(), seoTags: seoTags, xAxisValue: snapshot.child("imageUrl").val()};
          return object;
        } else {
          console.log("No data available");
        }
      }).catch((error) => {
        console.error(error);
      });
      return response.send(html);
    } catch (e) {
      console.log(e);
      return response.send(html);
    }
  }
  return response.send(html);
});

Let's updated our index with the correct open graph and SEO tags.

 const htmlData = await chartData;

html = `
            <!doctype html>
            <html lang="en">
              <head>
                <title>${htmlData.chartTitle}</title>
                <meta name="description" content="${htmlData.chartDescription}">
                <meta name="title" content="${htmlData.chartTitle}">
                <meta name="keywords" content="${htmlData.seoTags}">
                <meta property="og:locale" content="en_US" />
                <meta property="og:type" content="website" />
                <meta property="og:title" content="${htmlData.chartTitle}" />
                <meta property="og:description" content="${htmlData.chartDescription}" />
                <meta property="og:image" content="${htmlData.imageUrl}" />
                <meta property="og:image:secure" content="${htmlData.imageUrl}" />
                <meta property="og:url" content="${url}" />
                <meta name="twitter:card" content="${htmlData.chartDescription}" />
                <meta name="twitter:creator" content="@WiijiiInvest" />
                <meta name="twitter:title" content="${htmlData.chartTitle}" />
                <meta name="twitter:description" content="${htmlData.chartDescription}" />
              </head>
            <body>
             <h1>Wiijii Financial Visualizations</h1>
              <h2>${htmlData.chartTitle}</h2>
              <p>${htmlData.chartDescription}</p>
              <img src="${htmlData.imageUrl}" />
            </body>
            </html>
            `;

Now that we have all that setup, we need to set up our Firebase rewrites. this should be for the chart page and not the main page.\


firebase.json

"rewrites": [
      {
        "source": "chart/**",
        "function": "dynamicMetaTagsUpdate"
      },
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]

and that is it, you can see it working

Working

Here is the full firebase function

/* eslint-disable max-len */
const functions = require("firebase-functions");
const fs = require("fs");
const admin = require("firebase-admin");
const BotDetector = require("device-detector-js/dist/parsers/bot");
const DEBUG_BOT = false;
admin.initializeApp();

// Main firebase function called on firebase.json rewrites rules
exports.dynamicMetaTagsUpdate = functions.https.onRequest(async (request, response) => {
  // console.log("dynamicMetaTagsUpdate Called");

  let html = fs.readFileSync("./index.html", "utf8");
  const {id} = request.query;
  const botDetector = new BotDetector();
  const userAgent = request.headers["user-agent"].toString();
  const bot = botDetector.parse(userAgent);
  const url = "https://viz.wiijii.co/chart/?id="+ id;

  // If bot get chat data from Firebase database
  if (bot || DEBUG_BOT) {
    try {
      // console.log("running try");
      const ref = admin.database().ref("charts");
      const chartData = ref.child(id).get().then((snapshot) => {
        if (snapshot.exists()) {
          // Gets the chart data stocks for seo tags
          const seoTags = [];
          snapshot.child("chartData").val().forEach((element, index, array) => {
            seoTags.push(element.companyName, element.stockName);
          });
          // Add the y and x vaules from chart data to be used in seo tags
          seoTags.push(snapshot.child("xAxisValue").val(), snapshot.child("yAxisValue").val());
          const object = {chartTitle: snapshot.child("chartTitle").val(), chartDescription: snapshot.child("chartDescription").val(), imageUrl: snapshot.child("imageUrl").val(), seoTags: seoTags, xAxisValue: snapshot.child("imageUrl").val()};
          return object;
        } else {
          console.log("No data available");
        }
      }).catch((error) => {
        console.error(error);
      });

      const htmlData = await chartData;
      // HTML to return with updated open graph meta tags
      html = `
            <!doctype html>
            <html lang="en">
              <head>
                <title>${htmlData.chartTitle}</title>
                <meta name="description" content="${htmlData.chartDescription}">
                <meta name="title" content="${htmlData.chartTitle}">
                <meta name="keywords" content="${htmlData.seoTags}">
                <meta property="og:locale" content="en_US" />
                <meta property="og:type" content="website" />
                <meta property="og:title" content="${htmlData.chartTitle}" />
                <meta property="og:description" content="${htmlData.chartDescription}" />
                <meta property="og:image" content="${htmlData.imageUrl}" />
                <meta property="og:image:secure" content="${htmlData.imageUrl}" />
                <meta property="og:url" content="${url}" />
                <meta name="twitter:card" content="${htmlData.chartDescription}" />
                <meta name="twitter:creator" content="@WiijiiInvest" />
                <meta name="twitter:title" content="${htmlData.chartTitle}" />
                <meta name="twitter:description" content="${htmlData.chartDescription}" />
              </head>
            <body>
             <h1>Wiijii Financial Visualizations</h1>
              <h2>${htmlData.chartTitle}</h2>
              <p>${htmlData.chartDescription}</p>
              <img src="${htmlData.imageUrl}" />
            </body>
            </html>
            `;
      return response.send(html);
    } catch (e) {
      console.log(e);
      return response.send(html);
    }
  }
  return response.send(html);
});

If you have any questions or issues, leave a comment, I will get back to you.




Let’s Work Together

If you have an app idea in mind or you need some advice about development, product UX/UI, contact me. Currently my time books quickly, so the sooner you write, the better it is for both of us.

Reply time: within 1-2 working days