Skip to content
← All writing
Lab · 5 min read

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

When I took on this new project, I encountered various minor challenges with Flutter Web, but one huge one was updating meta tags or open graph tags for dynamic sharing.

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, or 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

A Facebook post sharing an almog.io article with a full link preview card
A Facebook post sharing an almog.io article with a full link preview card
A Slack message where a YouTube link unfurls into a rich preview
A Slack message where a YouTube link unfurls into a rich preview

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 will 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 we are 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.

Shell
Firebase init

Now that we have 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.

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

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

Shell
cd functions
npm install device-detector-js

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

JavaScript
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.

JavaScript
// 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.

JavaScript
// 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 update our index with the correct open graph and SEO tags.

JavaScript
 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 everything set, we must set up our Firebase rewrites. This should be for the chart page and not the main page.\

firebase.json

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

And that is it; you can see it working

A Facebook post showing a Flutter web page’s Open Graph preview after the fix
A Facebook post showing a Flutter web page’s Open Graph preview after the fix

Here is the full firebase function

JavaScript
/* 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.

#flutter#seo#how-to

Building something with AI?

Fractional CTO, AI agents, or a rescue for an AI-built app.