There are times when you want to quickly create a web app that doesn’t necessarily need a backend. We don’t really care about storing data in a central server or having communication between users.

But, you know, we love Meteor, it’s build system, and it’s packages. We want to use Meteor to create a static website that we can host from a simple server, maybe even Github IO pages.

Where this is a will, there is a way. And there is a way to do it!

Getting Started

Before we jump into things, let’s install Meteor and NodeJS. We will need NodeJS and NPM in order to get an offline build that doesn’t need to be hosted on a Node server.

We will then set up our folder structure like so:

root
├──MyMeteorApp
├──build.js
└──package.json

The build.js and package.json files are going to have code in them that will let us run npm run build.  When we run that command, a build folder will be created and populated with static HTML, JS, and CSS assets that we can host anywhere. Awesome.

There is a catch though, and that is that we can’t deploy just ANY Meteor project like this. We need to do certain things in our Meteor code for us to be able to take Meteor fully offline.

Prepping Our Meteor Project for Offline Use

Let’s say you went through the Todo app example located on Meteor’s website (and you have made it to at least step 6).

Great! There are only a few things you need to do to make your Meteor app ready for offline use!

Step 1: Add GroundDB

GroundDB will store a local version of a database on the user’s browser, so when the user revisits our site, it will pull from the cache before trying to load data from the server.

Simply run: meteor add ground:db

Then, anytime you would use Mongo.Collection, just use Ground.Collection instead.

Step 2: Make Your Collections Local

Alright, we added the ground:db project and we changed our Mongo references to Ground references. If you are following along using the Todo App example, you have the following line in your todo.js:

Tasks = new Ground.Collection('todos');

Let’s change that to make our collection local to the browser:

Tasks = new Ground.Collection(null);

This will make it so that our collection is completely local to the browser!

Step 3: Disconnect Your App

When your Meteor app starts up, it will normally connect with the server, listening for data or file changes and responding accordingly.

We don’t want any of that. We want to disconnect from the server as soon as the app starts. Just add the following code to your app:

Meteor.startup(function () {
  Meteor.disconnect();
});

That’s all there is to it!

Building and Deploying

We are going to use a little trick to get the static assets for our app. What we are going to do is launch our Meteor app, and once launched, we are going to scrape the site and save all the assets to disk. The assets will be static at that point and we can easily serve them from just about any server.

Let’s start with our package.json file:

{
  "name": "meteor-local",
  "version": "1.0.0",
  "description": "",
  "main": "build.js",
  "scripts": {
    "build": "node build.js"
  },
  "author": "",
  "license": "MIT",
  "devDependencies": {
    "glob": "^6.0.4",
    "replace": "^0.3.0",
    "website-scraper": "^0.3.0"
  }
}

Once you have that file ready and saved, open a terminal to the root of our project and run:

npm install

Next, let’s write our build.js file, which will do all the hard work for us:

var scraper = require('website-scraper');
var spawn = require('child_process').spawn;
var replace = require("replace");
var glob = require("glob");

function scrapeIt(process) {
  scraper.scrape({
    urls: [
      'http://localhost:8765/',
    ],
    directory: __dirname + '/build',
    subdirectories: [
      {directory: 'img', extensions: ['.jpg', '.png', '.svg']},
      {directory: 'js', extensions: ['.js']},
      {directory: 'css', extensions: ['.css']}
    ],
    sources: [
      {selector: 'img', attr: 'src'},
      {selector: 'link[rel="stylesheet"]', attr: 'href'},
      {selector: 'script', attr: 'src'}
    ],
    request: {
      headers: {
        'User-Agent': 'Mozilla/5.0 (Linux; Android 4.2.1; en-us; Nexus 4 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19'
      }
    }
  }).then(function (result) {
    glob(__dirname + '/build/**/*.js', {}, function (er, files) {
      replace({
        regex: /[\u200B-\u200D\uFEFF ]/g,
        replacement: ' ',
        paths: files,
        recursive: true,
        silent: true,
      });
    });

    if (process) {
      process.kill();
    }
    
  }).catch(function(err){
    console.log(err);
  });
}

if (/^win/.test(process.platform)) {
  console.log('You will need to manually run `meteor run -p 8765`');

  scrapeIt();
} else {
  // launch meteor
  var start = spawn('meteor', ['run', '-p', '8765'], {
    cwd: __dirname + '/LocalApp'
  });
  start.stdout.pipe(process.stdout);
  start.stdout.on('data', function (data) {
    if (data.indexOf('localhost:8765')) {
      // Then crawl the website for all the assets
      scrapeIt(start);
    }
  });
}

Alright, now let’s run:

npm run build

You should now how a build folder with static assets in it!

The last thing you will have to do is deploy the app!

You can see a working example here:

Github Repo

Github IO Pages