Host your personal site for free with Nuxt.js, GitLab, and Cloudflare.

Tim ✌
8 min readJun 7, 2018


This is a brief overview of how to get a new Nuxt.js app hosted with SSL for free (domain isn’t free, tho). It’ll only take a few hours and that includes customizing the site, adding a Twitter card, and waiting on the DNS.


TBH, I needed a new personal site. I’ve been paying $5/month to DigitalOcean to host an old Ghost blog that I didn’t use anymore. I always wanted to create a custom template for the blog but I never found the time nor the real motivation. So, today I decided to scrap the blog and build a simple personal site instead.


Nuxt.js (Vue.js)

If you don’t know about Nuxt.js yet, then read about it here. This won’t be a full tutorial about Nuxt.js but I’ll try to point out some important things.


This isn’t a protest against GitHub, I like that Microsoft acquired them. I’ve just kind of fell for GitLab in the recent months. It’s free, the CI/CD is amazing, and the pages feature is easy to use. The UI/UX is pretty great too and the whole organization is very open with just about everything.


I use Google Domains as my domain service and I decided to try out Cloudflare for this project since they have free SSL certs and security. So, I’m still testing the waters with their DNS and other services.

Total cost for this project: $0.00

To get started, just follow the installation tutorial for Nuxt.js.

I recommend review the link but here’s the quick breakdown straight from the site:
$ vue init nuxt-community/starter-template <project-name>

If vue-cli is not installed, please install it with npm install -g vue-cli

then install the dependencies:

$ cd <project-name>
$ npm install

and launch the project with:

$ npm run dev

Next, customize the page. In my example, I added a custom graphic, title, links, and footer. The source is open for anyone to see. I didn’t change too much from the original template.

Ok, setup your GitLab repo and link it in to the new Nuxt project directory. You should see the steps on the new repo page:

$ git init
$ git remote add origin
$ git add .
$ git commit -m "Initial commit"
$ git push -u origin master
** USERNAME and PROJECT is for you to fill-out from your own repo

Let’s add a file to the root of the project. This will automatically trigger the GitLab CI to run a build on your next git push origin master.

image: nodebefore_script:
- npm install
- node_modules/
- npm run generate
- public
- master

The image for the CI is a generic Node.js. It then runs npm install and caches the node_modules/before it runs the npm run generate command. The path created is public/ where the pages will run the site. The only: — master is for GitLab’s CI to know to only run this on the master branch. So it you’re working off another branch, it won’t run the CI until the code is merged into master.

We need to update the nuxt.config.js file and add the generated directory and new root (pre root directory before added a URL):

module.exports = {
** Headers of the page
head: {
title: 'Tim Skaggs - %s',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: 'Code + Outside' }
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
** Customize the progress bar color
loading: { color: '#3B8070' },
/** ADD THIS BLOCK **/ /*
** Customize the generated output folder
generate: {
dir: 'public'

** Customize the base url
router: {
base: '/personal-nuxtjs/' //this is whatever the project is named
/** END BLOCK **//*
** Build configuration
build: {
** Run ESLint on save
extend (config, { isDev, isClient }) {
if (isDev && isClient) {
enforce: 'pre',
test: /\.(js|vue)$/,
loader: 'eslint-loader',
exclude: /(node_modules)/

Now, let’s push the changes to master and now go to GitLab’s app and open CI/CD — Pipelines view from the left side-bar.

Let’s watch the CI process the request. Cool, right?

Running with gitlab-runner 10.8.0-rc3 (5470b911)
on docker-auto-scale...
Using Docker executor with image node ...
Pulling docker image node ...
Using docker image sha... for node ...
Running on runner-...
Cloning repository...
Cloning into '/builds/tskaggs/personal-nuxtjs'...
Checking out 53de2361 as master...
Skipping Git submodules setup
Checking cache for default...
FATAL: file does not exist
Failed to extract cache
$ npm install

> uglifyjs-webpack-plugin@0.4.6 postinstall /builds/tskaggs/personal-nuxtjs/node_modules/webpack/node_modules/uglifyjs-webpack-plugin
> node lib/post_install.js

> nuxt@1.4.1 postinstall /builds/tskaggs/personal-nuxtjs/node_modules/nuxt
> opencollective postinstall || exit 0

*** Thank you for using nuxt! ***

Please consider donating to our open collective
to help us maintain this package.


npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.4 (node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.4: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})

added 1177 packages from 670 contributors and audited 7994 packages in 28.781s
found 0 vulnerabilities

$ npm run generate

> timskaggs@1.0.0 generate /builds/tskaggs/personal-nuxtjs
> nuxt generate

2018-06-07T01:12:14.549Z nuxt:generate Generating...
2018-06-07T01:12:14.567Z nuxt:build App root: /builds/tskaggs/personal-nuxtjs
2018-06-07T01:12:14.567Z nuxt:build Generating /builds/tskaggs/personal-nuxtjs/.nuxt files...
2018-06-07T01:12:14.601Z nuxt:build Generating files...
2018-06-07T01:12:14.617Z nuxt:build Generating routes...
2018-06-07T01:12:14.639Z nuxt:build Building files...
DONE Compiled successfully in 12811ms1:12:28 AM

Hash: 69fe87ecf32a105e82d5
Version: webpack 3.12.0
Time: 12811ms
Asset Size Chunks Chunk Names
pages/index.b81a58af917d1ef6bd22.js 4.04 kB 0 [emitted] pages/index
img/angellist.fae94c7.png 22.8 kB [emitted]
img/github.ad04c38.svg 2.36 kB [emitted]
img/bison-shadow.5bc621d.svg 4.24 kB [emitted]
img/twitter.5c86ba6.svg 1.69 kB [emitted]
img/linkedin.7e75842.svg 1.48 kB [emitted]
img/gitlab.33f0376.png 34.9 kB [emitted]
layouts/default.4961876a4222f514cffe.js 1.59 kB 1 [emitted] layouts/default
vendor.c310e83bd40709a41d0b.js 145 kB 2 [emitted] vendor
app.2f27bbef6ee11570e054.js 27.8 kB 3 [emitted] app
manifest.69fe87ecf32a105e82d5.js 1.47 kB 4 [emitted] manifest
LICENSES 584 bytes [emitted]
+ 3 hidden assets
Version: webpack 3.12.0
Time: 826ms
Asset Size Chunks Chunk Names
server-bundle.json 125 kB [emitted]
2018-06-07T01:12:29.186Z nuxt: Call generate:distRemoved hooks (1)
2018-06-07T01:12:29.186Z nuxt:generate Destination folder cleaned
2018-06-07T01:12:29.196Z nuxt: Call generate:distCopied hooks (1)
2018-06-07T01:12:29.196Z nuxt:generate Static & build files copied
2018-06-07T01:12:29.199Z nuxt:render Rendering url /
2018-06-07T01:12:29.350Z nuxt: Call generate:page hooks (1)
2018-06-07T01:12:29.351Z nuxt:generate Generate file: /index.html
2018-06-07T01:12:29.352Z nuxt:render Rendering url /
2018-06-07T01:12:29.355Z nuxt: Call generate:done hooks (1)
2018-06-07T01:12:29.355Z nuxt:generate HTML Files generated in 14.8s
2018-06-07T01:12:29.355Z nuxt:generate Generate done
Creating cache default...
node_modules/: found 20506 matching files
Uploading to http://runners...
Created cache
Uploading artifacts...
public: found 22 matching files
Uploading artifacts to coordinator... ok id=73027255 responseStatus=201 Created
Job succeeded

This built the site and now let’s check it out.

Open the Pages view in the left side-bar.

We should see the site’s public URL from GitLab here:

We can now see the page! If not, you may have to rename your repo to be something like your GitLab repo. Mine is and your username will take place of the tskaggs in the url.

Next, buy your domain. I suggest Google Domains since their UI is great and DNS changes are quick. We’ll need to reroute the DNS to Cloudflare anyways but it seems faster with Google Domains.

I won’t write these steps since this tutorial is awesome. Pay close attention to Step 3 and 4:

The DNS, SSL, and Domain stuff may take a few hours to complete. I don’t mean you’ll work on it for a few hours. The DNS and Cloudflare just need to do their thing.

We’ll need to do one update to the nuxt.config.js. The base for the project will now just be /.

** Customize the base url
router: {
base: '/'

After that’s all done and your site now renders with https, I’ll move on to the Twitter card.

In the nuxt.config.js we’ll add %s to the title like so:

head: {
title: 'Tim Skaggs - %s',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: 'Code + Outside' }
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }

This adds a variable to the title.

Next, let’s open up the page pages/index.vue. Within the <script> tags we’ll need to add a few lines for this page. We want to change the title and meta tags for the page like so:

export default {
components: {
head () {
return {
title: 'Tim Skaggs - Dev Manager, Senior Developer, Outside stuff',
meta: [
{ hid: 'description', name: 'description', content: 'Code + Outside' },
{ property: 'twitter:card', content: 'summary' },
{ property: 'twitter:site', content: '
@tskaggs' },
{ property: 'twitter:url', content: '' },
{ property: 'og:description', content: 'From being a first employee at a startup in the Bay Area to taking on the responsibility as VP of Engineering. I now work with a few talented developers in Colorado as a full-stack dev team. Our team is interdisciplinary in designing and development.' },
{ property: 'og:title', content: 'Tim Skaggs - Dev Manager, Senior Developer, Outside stuff' },
{ property: 'og:image', content: '' },


The head() function overwrites the title and meta tags for that specific page. The meta: properties for Twitter are specific and you should validate them at the Twitter Card Validator site:

One last tip: You’ll need to add the card image that you’re using for `og:image` as a jpg and place it into the static/ directory of the project. The image URL won’t render properly in the assets directory and it was giving me an error with png.

And that’s it. Let me know in the comments if you have any questions. This was a brief overview of the process. Again, it’s all free (except the domain)!




Tim ✌

A developer, a freelancer, a runner, a partner, an optimist

Recommended from Medium


See more recommendations