Transform HTML output of webapp to AMP

We are looking to create a parallel AMP version of the dynamic public pages of our app. We will be using amperizer to transform the tags (especially the <img> tag) to the compatible amp tags. Everything is good until that point since we only need to hook it into our SSR processing.

The last part of the process is to transform the entire output html using the amp optimizer (updates the html boilerplate to become a valid AMP output which even updates the <html> tag with necessary amp-related attributes). When I look into the internals of Meteor, seems we need to fork the webapp package and catch the html right at the point before it streams the html output.

Question: is there a cleaner way to do this without changing the webapp package?

When a client 1st connects to your app/server, Meteor gives us the Meteor.onConnection() method to use on the server only.

I just use the main.js file in my server folder because that is how I set up my project, but I use something like this to detect new incoming connections and to parse the user agent to determine if they are on a specific device. From this point, you can trigger anything you’d like to happen via SSR.

You could load anything you like, specific to each client from this.

import { Meteor } from "meteor/meteor";

Meteor.onConnection((connectionObj) => {

  console.log(
    "A new connection was detected...",
    connectionObj.httpHeaders['user-agent']
  );

});

The very 1st place a new client connection can be detected is on the server, that’s why this doesn’t work on the client.

If you log the full connectionObj, you’ll get useful data including:

clientAddress (ip address string) and httpHeaders (obj including user-agent string)… try it yourself, and check on the Meteor Docs on Meteor.onConnection().

This would be the points where you would want to handle things differently for your clients, like a country with low bandwidth perhaps (using IP address to detect) or a mobile device (using the user-agent data to detect).

Based on the console.log example I pasted above my ‘user-agent’ string on my Mac looks like this:

Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36

You of course can just add a cool UserAgent parsing library from npm like this one, and you’ll be able to reliably load the right experience for your users based on their device.

Thanks for this detailed answer, @mullojo

Currently detecting the access to the server is not an issue. Identifying the connection / request is also ok.

The challenge is the final html output. After doing the connection and requests filtering and identification, how can you catch the entire html output, transform it using custom logic, then send the transformed output to the client (ideally without forking the webapp package)

For me, my Vue + Meteor app starts up with the Meteor.startup function on each client, so if I was doing something like you are looking for, I’d test the best way to pass the data from the Meteor.onConnection on the Server to the my clients

The most obvious is to just store the data on your User’s respective record on the User collection. Then on the client I could load that data before my app launches with Meteor.startup, I could then load any version of the app I like for a specific client.

let userDeviceType = Meteor.user().userAgentData; // or something like this

if(userDeviceType == 'mobile'){

   Meteor.startup(() => {
     // AMP version of my app
   })

} else {

   Meteor.startup(() => {
     // Regular version of my app
   })

}

If you are using SSR like you mention, it should be even easier to pass the userAgentData to the needed place in your code, before you build your page. Everything you want to render can then just be shipped to the client.

Are you using React, Vue, etc? What’s your SSR setup / code look like and I’m sure we could figure out where to inject the data / decision point. I’m not regularly using SSR, so it’s tricky to answer generally.

SSR setups are a bit different across the various view layers, but in general all you have to do is control the logic and store the state you want to use in a variable accessible to your SSR code.

Here is the Amp Optimizer for reference: https://github.com/ampproject/amp-toolbox/tree/master/packages/optimizer

Minimal usage:

const AmpOptimizer = require('@ampproject/toolbox-optimizer');

const ampOptimizer = AmpOptimizer.create();

const originalHtml = `
<!doctype html>
<html ⚡>
  ...
</html>`;

ampOptimizer.transformHtml(originalHtml).then((optimizedHtml) => {
  console.log(optimizedHtml);
});

The input is the entire html output including the <html> tags.

In server-render package being used in SSR, we can only manipulate the content of <head> and <body> but not the entire <html> content

Can you show how can you get the entire html output and then transform the output before sending to the client in Meteor + Vue?

Or maybe if you are to use the Amp Optimizer package above, how are you going to use it?

Currently, we forked the webapp package and added the handling here

return getBoilerplateAsync(request, arch)
        .then(async ({ stream, statusCode, headers: newHeaders }) => {
          if (!statusCode) {
            statusCode = res.statusCode ? res.statusCode : 200;
          }

          if (newHeaders) {
            Object.assign(headers, newHeaders);
          }

          res.writeHead(statusCode, headers);

          /* ===== Start of fork changes ===== */
          if (request?.path?.startsWith('/amp/')) {
            const [error, html] = await to(streamToString(stream));
            if (error) {
              debug.error(error);
            }

            const ampOptimizer = AmpOptimizer.create();
            const [err, amp] = await to(ampOptimizer.transformHtml(html));
            if (err) {
              debug.error(err);
            }

            res.write(amp);
            res.end();
            stream.destroy();
          } else {
          /* ===== End of fork changes ===== */

            stream.pipe(res, {
              // End the response when the stream ends.
              end: true
            });
          }
        })
        .catch(error => {
          Log.error('Error running template: ' + error.stack);
          res.writeHead(500, headers);
          res.end();
        });

Hopefully there is a cleaner way to do this without forking webapp