Post Frequency: from prototype to production with Vue + Node

Hugo Di Francesco - Nov 26 '18 - - Dev Community

If you want to ship, use the tools you know. — A lot of people

Let’s apply that principle: I’m building Post Frequency/Accountable Blogging, I’m familiar with Vue and Node-based backends. Netlify makes frontend deployments trivial and I’ve settled on Dokku for databases/backend applications hosting.

See how I deploy my applications Deployment options: Netlify + Dokku on DigitalOcean vs now.sh, GitHub Pages, Heroku and AWS.

Table of contents:

Context

Accountable Blogging was always going to be a single-ish page application, initially the backend was going to be Netlify lambdas… when I hit some issues with that I just went back to what I know, setting up Dokku to deploy some backend services.

Since my lambdas were already written, I went for micro (see Simple but not too simple: how using Zeit’s micro improves your Node applications) instead of Express. In this other post, I go into more detail about why I did this, in short: it’s lighter and requires less boilerplate/helper packages to get a single POST endpoint up and running.

Frontend

Vue + Vue CLI 3

I’m a big fan of Vue. It’s simple, it has good docs, it’s easy to write.

I had never really tried the Vue CLI. Just like the rest of the official Vue packages and ecosytem, it’s got super nice ergonomics (eg. a plugin system, a local UI, hot module reloading that works).

Vue CLI prerender SPA plugin

To have indexable HTML with our SPA, we can leverage vue-cli-plugin-prerender-spa:

# if you have vue-cli globally installed
vue add prerender-spa
# if you have it only locally installed
npx vue add prerender-spa
Enter fullscreen mode Exit fullscreen mode

Load some scripts only on production

There are situations where you only want to load some scripts on production.

Here’s how to do it, with the example of the crisp.chat JavaScript SDK (it’s slow to load and doesn’t make sense to use locally) public/index.html (using EJS templating I believe):

<script type="text/javascript">
  window.$crisp=[];
</script>
<% if (NODE_ENV === 'production') { %>
    window.CRISP_WEBSITE_ID="SOME_ID";
    (function(){d=document;s=d.createElement("script");s.src="https://client.crisp.chat/l.js";s.async=1;d.getElementsByTagName("head")[0].appendChild(s);})();
  </script>
<% } %>
Enter fullscreen mode Exit fullscreen mode

This <% if (NODE_ENV === 'production') { %> and the matching <% } %> exclude rendering the particular script in dev.

Load scripts only if not pre-rendering injectX using:

Sometimes you want to conditionally load things when not pre-rendering. This can be achieved using window.__PRERENDER_INJECTED (which is a variable set by pre-render-SPA).

It looks like this:

if (!window. __PRERENDER_INJECTED || window.__ PRERENDER_INJECTED !== 'PRERENDERING'){
  // init stuff
}
Enter fullscreen mode Exit fullscreen mode

You need the following in .prerender-spa.json:

{
  "customRendererConfig": {
    "inject": "PRERENDERING"
  }
}
Enter fullscreen mode Exit fullscreen mode

The full example would therefore be:

<script type="text/javascript">
    window.$crisp = [];
    window.CRISP_WEBSITE_ID="SOME_ID";
  </script>
  <% if (NODE_ENV === 'production') { %>
  <script>
    if (!window. __PRERENDER_INJECTED || window.__ PRERENDER_INJECTED !== 'PRERENDERING'){
      (function(){d=document;s=d.createElement("script");s.src="https://client.crisp.chat/l.js";s.async=1;d.getElementsByTagName("head")[0].appendChild(s);})();
    }
  </script>
<% } %>
Enter fullscreen mode Exit fullscreen mode

Pass service URLs using .env.${ENVIRONMENT} files

You probably want to hit a local version of your backend services when developing and obviously the live one when in production, here’s how you do that with .env files.env.development:

VUE_APP_FEED_SERVICE_URL=http://localhost:1234
Enter fullscreen mode Exit fullscreen mode

.env.production:

VUE_APP_FEED_SERVICE_URL=https://my-live-service.accoutableblogging.com
Enter fullscreen mode Exit fullscreen mode

In your application code you can then access it under process.env.VUE_APP_FEED_SERVICE_URL for example:

const FEED_SERVICE_URL = process.env.VUE_APP_FEED_SERVICE_URL;
export const FEED_DATA_URL = `${FEED_SERVICE_URL}`;
Enter fullscreen mode Exit fullscreen mode

Being social and Google-friendly

Be crawlable

As a good internet citizen, we should be readable without JavaScript enabled. Since we’re leveraging Vue/vue-cli/vue-router we should probably pre-render (see Vue CLI prerender SPA plugin section).

What would be even nicer would be a sitemap.xml that you can submit to Google Webmaster tools, I haven’t found a nice solution yet but if/when I do you can be sure I’ll share it so subscribe to my newsletter.

Favicons and manifests

Get your assets from realfavicongenerator.net.

The end of the process on there is a download of a zip folder and some tags that you can copy.

Unzip the favicons and dump them in the public folder and add the tags to the head of the public/index.html file.

Meta description, OpenGraph tags

Create your assets with realfavicongenerator.net/social or metatags.io.

Here’s a fun gotcha: resources (URLs) in og tags need to be an absolute URL.

Without router

ie. you have an app that is a single page 🙂, then just add your tags to public/index.html.

With vue-router

I used this article, and you should subscribe to the newsletter, for next week’s article if you want the breakdown of how I’ve set it up.

Backend service(s)

Have a src/services folder where anything backend will live, say you have a some-service service.

My process starts with a directory src/services/some-service that contains an Express or micro app (see this article about how I set up my micro app).

For each service, we’ll create a git remote to Dokku (or Heroku or whatever other git-based method you’re using):

git remote add dokku-service dokku@${DOKKU_INSTANCE_IP}:some-service`
Enter fullscreen mode Exit fullscreen mode

Deploying to Dokku from the project root (where the top-level package.json is), can be done with:

git subtree push --prefix src/services/some-service dokku-service master
Enter fullscreen mode Exit fullscreen mode

If you would like to deploy using npm run deploy:service, you can put the following into the top-level package.json:

{
  "scripts": {
    "deploy:service": "git subtree push --prefix src/services/some-service dokku-service master"
  }
}
Enter fullscreen mode Exit fullscreen mode

Launch

My launch was moderately successful, it started with an IndieHackers post: Show IH: A GitHub-style graph for your blog and a dev.to post: GitHub-style contribution graph for your blog.

Those two combined got me some traffic (90 referrals from IndieHackers and 60 from dev.to), see:

accountableblogging.com 23/10 to 30/10 page views graph

The ProductHunt launch went relatively well as well, see Post Frequency on ProductHunt. It got onto the front page (I reckon Ryan Hoover had something to do with that 😉) and got 100+ upvotes. This is more of a “check out this thing I made”, rather than a full-blown product so it’s not bad. Around 250 people checked it out from that (direct referrals from ProductHunt). The thing with ProductHunt is that other tools read data from there and syndicate it so you also see a trickle of other traffic, see the following graph:

accountableblogging.com 29/10 to 23/11 page views graph

Outcomes

I’ve currently got 18 email signups, if you’re interested you can sign up too: accountableblogging.com.

I’ve created a little email + survey people can take using Typeform, you’ll receive it if you sign up.

Next steps are to build the thing out. It’s simple: give me your URL + payment details, I track how much you publish, you meet your goal or pay.

unsplash-logo
NeONBRAND

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .