Using Prember for a static site with Ember Data

Gaurav Munjal
Jul 8 · 5 min read

Following this post, I thought FastBoot and prember would make a great way to create a static site. I happened to have one for my resume and portfolio sitting around, and it had not been updated in a while. It didn’t strictly need to make requests, but it did because I wanted to practice my ember.js skills.

My existing static site, made with Ember.js, had used Mirage in production to fulfill the Ember Data requests. However, Mirage isn’t compatible with FastBoot yet, and therefore cannot be used with prember.

No problem, I thought. I would use http mocks instead of Mirage. This is an older technology built into ember-cli that uses an express server to mock responses. You can generate a mock using:

ember g http-mock <name>

You will need to do this for every request you need to mock in your application. By default, the host is http://localhost:4200 and the namespace is api, so a quick edit of the application adapter is required:

// app/adapters/application.jsimport DS from 'ember-data';
import { inject as service } from '@ember/service';
export default DS.JSONAPIAdapter.extend({
host: 'http://localhost:4200',
namespace: 'api'
});

This will also set everything up for you and start an express server automatically when running ember serve.

Everything is great when running outside FastBoot, so I go ahead and install FastBoot and prember.

I go ahead and make a bunch of usual edits to get my app working in FastBoot. A bunch of things like jQuery, window, document don’t exist, so I work around them. Another thing I need to get ember-data working is the ember-fetch polyfill, which substitutes jQuery ajax with implementations of fetch on both server and client.

After a few hours of fixing these sorts of issues, my app is finally working in FastBoot. It is unfortunate how difficult it still is to make an Ember app work with FastBoot, but it is generally doable unless you need some jQuery plugin. I’m sure with more practice, I’ll be able to do it more quickly. I feel the SEO benefits will be worth it.

Of course, I’m not looking to run FastBoot in production, but to prerender a static site with prember. To test my site with prember, I now make my first attempt to use it:

PREMBER=true ember serve

The index page loads! But unfortunately, no other page does. It turns out prember needs to be told what urls to use. No fear, Edward Faulkner, the author of prember and Ember core team member, has created prember-crawl, which allows us to crawl our website and list out the urls. Installing it was a breeze, just a small edit to ember-cli-build.js:

// ember-cli-build.js

'use strict';
const EmberApp = require('ember-cli/lib/broccoli/ember-app');const crawl = require('prember-crawler');module.exports = function(defaults) {
let app = new EmberApp(defaults, {
prember: {
urls: crawl
}
});
return app.toTree();
};

At this point, I actually believed I was done. So I went ahead and generated the static site:

PREMBER=true ember build --prod

I turned off my ember server and tried to load my dist directory as a static site. Unfortunately, every request was still made to the nonexistent mock server and failed. I realized then that prember wasn’t really designed for my use case, and really still expected a server in production for any AJAX requests.

Then I remembered the FastBoot shoebox. I could use that, perhaps, to fulfill all my ember-data requests. I installed the most widely used addon for interacting with the shoebox, ember-cached-shoe. Then I went ahead and rebuilt and tried to load my dist directory as a static site again. My index page no longer made AJAX requests, but every other page still did. Also, when I loaded any page, including the main one, the data appeared but disappeared as soon as rehydration occurred.

Long story short, there were a number of issues. First, I had to turn off Ember Data’s not so smart default of background reloading. This was done by editing my adapter some more:

// app/adapters/application.js
import DS from 'ember-data';
import AdapterFetch from 'ember-fetch/mixins/adapter-fetch';
import CachedShoe from 'ember-cached-shoe';
import { inject as service } from '@ember/service';
export default DS.JSONAPIAdapter.extend(AdapterFetch, CachedShoe, {
host: 'http://localhost:4200',
namespace: 'api',
shouldBackgroundReloadRecord() { return false; },
shouldBackgroundReloadAll() { return false; },
shouldReloadRecord() { return true; },
shouldReloadAll() { return true; }
});

The next problem was that FastBoot only called the model hook of the route(s) loaded, so whenever I changed to a different route, it wasn’t in the shoebox. This required some rearchitecting of my application to load all ember data requests in the application route, so they would be in the shoebox for all routes.

// app/routes/application.jsimport Route from '@ember/routing/route';
import { hash } from 'rsvp';
export default class ApplicationRoute extends Route {
model() {
return hash({
projects: this.store.findAll('project')
jobs: this.store.findAll('job')
// ...
});
}
}

Then each individual route that needed some data would just grab it from the application model:

// apps/routes/projects.jsimport Route from '@ember/routing/route';export default class ProjectsRoute extends Route {
model() {
return this.modelFor('application').projects;
}
}

This isn’t practical for any real application, but for my resume / portfolio site it was ok to block on the results of every AJAX request for each page prerendered in FastBoot.

However, even after this, the data was disappearing in rehydration. It took some debugging, but I eventually realized the problem was that ember-cached-shoe used a token that was per url. This could be fixed, by directly overriding the cached-shoe service. While I was there, I prevented erasing of any data in the shoebox.

// app/services/cached-shoe.jsimport CachedShoeService from 'ember-cached-shoe/service';export default CachedShoeService.extend({
eraseResponse() {
// do not erase
},
tokenizeAjaxRequest(url, type, options = {}) {
return this._super('', type, options);
}
});

Finally, all my issues were fixed, and I was able to load the dist directory as a static site and everything worked. I deployed the site using the linux cp command, and you can now see it at https://gaurav0.github.io

Unfortunately, prerendering an ambitious ember application with lots of routes and pages following this workflow is just going to be problematically slow, as every route needs to refetch all the data for the entire application. I hope that, at the minimum, an addon can be created to automate some of this, and perhaps even mitigate the need to rearchitect the application to load all data in the application route.

By the way, I’m still looking for a job. Reach out if you’d like an experienced Ember.js engineer.