Flutter Web: SPA: Open Graph: Dynamically assign og:image meta tags

Issue

Trying to create dynamic og:image tags for crawlers to catch appropriate thumbnails. I’ve got a JS script generating the appropriate og:image url, but the crawlers don’t appear to run any JS when searching. Is there a better way to do this?

Currently:

<head>
  <script>
    const queryString = window.location.href;
    const urlParams = new URLSearchParams(queryString);
    const uid = urlParams.get('uid')
    const pid = urlParams.get('pid')
    if (uid != null && pid != null)
      document.getElementById('urlThumb').content = `https://my.app/posts%2F${uid}%2F${pid}%2Furl_thumb.jpg?alt=media`;
  </script>
  <meta property="og:image" id='urlThumb' content="https://my.app/default%default.png?alt=media"/>

...

</head>

Solution

ok, so I was able to achieve this in a semi-hack way. I modified firebase.json to route ‘/post’ route to a firebase cloud function. You simple add the route "source" you want to reroute and add the name of the firebase cloud function you want to trigger.

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

I had to add the ‘express’ package to handle the https request. in your functions folder run ‘npm i express’. Then I made these two functions (It looks a little bizarre):

const express = require('express');
const app = express();

app.get('/post', (req, res) => {
    console.log(req.query);
    const uid = req.query.uid;
    const pid = req.query.pid;
    console.log(`uid[${uid}], pid[${pid}]`);
    if (uid == null || pid == null)
        res.status(404).send("Post doesn't exist");
    res.send(`<!DOCTYPE html>
    <html>
    <head>
      <meta property="og:image" id='urlThumb' content="${`https://my.app/posts%2F${uid}%2F${pid}%2Furl_thumb.jpg?alt=media`}"/>
      <meta property="og:image:width" content="800">
      <meta property="og:image:height" content="600">
      
      //Rest is the same as index.js head.

    </head>
    <body id="app-container">
      //Same as index.js body
    </body>
    </html>
    `);
});

exports.prebuildPostPage = functions.https.onRequest(app);

This works great for getting the right thumb to the crawlers, unfortunately it sends people to the homepage. no bueno.

This is because Flutter Web uses a ‘#’ to manage routing and history of pages. Everything after the hashtag is ignored in the url forwarded to my cloud function.

SO… the hack part… I’n my flutter web app main.dart file I had to check if the given URL is in fact a "my.app/post?uid=xxx&pid=xxx" format. In which case instead of loading my default MyApp which starts at the homepage, I created a second option called MyAppPost which defaults to the post screen with the provided uid and pid data. This works, but it screws up my navigator system.

Will continue to try and improve this setup.

void main() {
  //Provider.debugCheckInvalidValueType = null;
  setupLocator();
  String url = window.location.href;
  String _uid;
  String _pid;
  bool isPost = false;
  print(url);
  if (url.contains('/post')) {
    _uid = getParam(url, 'uid', 28);
    _pid = getParam(url, 'pid', 20);
    if (_uid != null && _pid != null) isPost = true;
  }
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (_) => PageManager(),
        ),
      ],
      child: isPost
          ? MyAppPost(
              uid: _uid,
              pid: _pid,
            )
          : MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'VESTIQ',
      navigatorKey: locator<NavigationService>().navigatorKey,
      onGenerateRoute: (rs) => generateRoute(rs, context),
      initialRoute: HomeRoute,
    );
  }
}

class MyAppPost extends StatelessWidget {
  final String uid;
  final String pid;

  const MyAppPost({Key key, this.uid, this.pid}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'VESTIQ',
      navigatorKey: locator<NavigationService>().navigatorKey,
      //onGenerateRoute: (rs) => generateRoute(rs, context),
      home: PostView(
        oid: uid,
        pid: pid,
      ),
    );
  }
}

EDIT: Working navigator

void main() {
  setupLocator();
  String url = window.location.href;
  String _uid;
  String _pid;
  bool launchWebApp = false;
  if (url.contains('/post')) {
    _uid = getParam(url, 'uid', 28);
    _pid = getParam(url, 'pid', 20);
  }
  if (url.contains('/app')) launchWebApp = true;
  runApp(
    MyApp(
      uid: _uid,
      pid: _pid,
      launchWebApp: launchWebApp,
    ),
  );
}

class MyApp extends StatelessWidget {
  final String uid;
  final String pid;
  final bool launchWebApp;

  const MyApp({Key key, this.uid, this.pid, this.launchWebApp})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    bool isPostLink = (uid != null && pid != null);
    if (isPostLink) {
      urlPost = PostCard.fromPost(Post()
        ..updatePost(
          uid: uid,
          pid: pid,
        ));
    }
    Provider.of<Post>(context, listen: false).updatePost(uid: uid, pid: pid);
    return MaterialApp(
      title: 'VESTIQ',
      navigatorKey: locator<NavigationService>().navigatorKey,
      onGenerateRoute: (rs) => generateRoute(rs, context),
      initialRoute: launchWebApp
          ? AppRoute
          : isPostLink
              ? PostRoute
              : HomeRoute,
    );
  }
}

Answered By – Maksym Moros

Answer Checked By – David Marino (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *