Server-render with blaze

I know this is an old topic, and most people might not even care anymore because they moved on to React or VueJS …

But I am dogged and like to tinker, so here is an approach I got to work and want to share with the community.

On the server I have this

import { onPageLoad } from 'meteor/server-render'
const Hydrated = new Meteor.Collection('hydrated')

onPageLoad(sink => {
  const pathname = sink.request.url.pathname
  const hydrated = Hydrated.findOne({url: pathname})
  if(hydrated) {
    sink.appendToBody(`<div class='meteor-blaze-hydrated'>${hydrated.html}</div>`)
  }
});

and on the client in the onRendered callback of my layout Template I have

    const hydrated = document.querySelectorAll(`.meteor-blaze-hydrated`)
    if (hydrated) {
      hydrated.forEach((elem) => {
        // check if we have video and pause it
        elem.querySelectorAll('video').forEach((video) => {
          video.pause()
        })
        elem.remove()
      })
    }

Now you might wonder how is the collection Hydrated is getting populated. Together with FlowRouter I came up with this

export const getHydrated = function() {
  const body = document.getElementsByTagName('body')[0]
  const clone = body.cloneNode(true)
  clone.querySelectorAll('script').forEach((s) => { s.remove() })
  return clone.innerHTML.replace(/\s{2,}/g,' ').replace(/autoplay/g,'')
}

Tracker.autorun(function serverRenderPathChange(){
  const userId = Meteor.userId()
  FlowRouter.watchPathChange()
  const current = FlowRouter.current()
  //
  // only save page as anon user
  //
  if (!userId) {
    // is the route defined
    if (current.route != undefined) {
      // get the path
      const path = current.route.path
      // unless the route explicitly prevents server_render
      if(! current.route.options.server_render === false){
        //
        // allow time for the page to be rendered
        //
        Meteor.setTimeout(function () {
          const html = getHydrated()
          Meteor.call('saveHydrated', path, html, current.queryParams, (err, res) => {
            if (err) console.error(err); else if (config.debug && res.saved) console.log(`saveHydrated ${res.id} ${res.created}`)
          })
        }, 1000)
      } else {
        if(current.queryParams && current.queryParams['server-render-delete'] == "true") {
          Meteor.call('saveHydrated', path, null, current.queryParams, (err, res) => {
            if (err) console.error(err); else if (config.debug && res.saved) console.log(`saveHydrated ${res.id} ${res.created}`)
          })
        } 
      }
    }
  }
})

Clearly this attempt has severe limitations (security for one), second some flickering (because I can’t get the timing right of when to remove the hydrated div), not to mention some really weird behavior with autoplaying video components (ghost playback).

Still, I wonder what you guys think?

5 Likes

Wow, that’s a hell of a workaround!

My biggest concern is that the hydrated collection is updated on every pageview from every user. Ideally, you send a timestamp to the client and it only updates once every t minutes/hours.

Also, should have a check for authenticated users to make sure you don’t save private info

Yeah, performance is a concern. Currently I only save when not logged in, but every anon user would submit the page they’re looking at and the server would only save it when it doesn’t exist.

A much better approach would be to trigger a render of all routes programmatically or manually under certain circumstances …

As I said, not optimal by any means. I just wanted to play around with the possibilities of

onPageLoad(sink => {
   sink.appendToBody( somePrerenderedHtml );
})

which took me WAY to long to understand, frankly :wink:

1 Like

Maybe the server could do a HTTP.get( ) to prerender.io and cache the result?

I am just re-inventing prerender.io, aren’t I?

Also, I just realized that for SEO purposes all I need to do is inject the cached page inside of <noscript></noscript> and not worry about removing the hydrated page …