Developer blog | Ondřej Polách

NuxtJS + Strapi = my web
06.04.2021 strapi nuxt firebase app engine mongodb

Content

  1. Primary target
  2. Brief description of Strapi and NuxtJS
  3. Architecture
  4. Development
  5. Continuos integration
  6. Conclusion and tips

Primary target

I have two primary targets. Build website with informations about me and with blog's articles for other developers. And run this web for the lowest possible price.

Brief description of Strapi and NuxtJS

Strapi is headless CMS. It's not something as Wordpress. Strapi is different. You can design fast API and manage content easily using web admin portal. Strapi has not frontend for web. You can build frontend in your popular frontend framework. Simply said, create your content model in development environment and then deploy to production. Support many databases solutions - classical SQL (mysql, postgres) or NoSQL (mongodb).

NuxtJS is frontend framework based on popular Vue with support for Server side rendering and fully static generator for super fast, CDN based, web applications.

Together they create strong solution for web development.

Architecture

With my price target in mind, I was finding architecture for less price. There is my configuration:

  • MongoDb in free plan (512MB - it's enough for start)
  • Strapi hosted in Google Cloud App Engine on Standard Environment (instance B2 with 9 hours per day on free tier)
  • NuxtJS statically generated and hosted on Firebase hosting CDN (small size, free tier)
  • Images storage on Firebase Storage using strapi-provider-upload-google-cloud-storage

Because I use statically generated frontend, I have generate new content after each change in blog. For this, I use GitHub webhooks.
When I publish new article or update early created article, Strapi automatically call GitHub webhook witch run configured workflow.

Strapi's webhooks technically use POST with hard coded payload (content in JSON format). This payload GitHub rejected and return error.
There is one solution - extend Strapi API for custom endpoint controller and call GitHub from endpoint's handler. In Strapi admin set webhook to call my controller endpoint, and done.

Development

Step 1 - create project for backend and frontend

Ensure that you have installed Node and npm or yarn.
Then create Strapi project using great Strapi's documentation.
And create NuxtJs project using great NuxtJs's documentation.

cd /myweb
yarn create strapi-app backend --quickstart
yarn create nuxt-app frontend

Step 2 - Define your backend content types

Run strapi server using cd backend; npm run develop. Navigate to http://localhost:1337/admin and create your admin account.
Then create and publish your content types. For example, create Post and Tag. Content Types can be created only in development. In production you only use these models for creating content of you website.

Step 3 (Optional) - add GraphQL to backend

Stapi supports REST or GraphQL API. In my website I use GraphQL. You can add this plugin to your backend simply as run yarn strapi install graphql.

Step 4 - configure database for Strapi

In development you can use SqlLite integration. It's default database for Strapi. In production you can select from many options (PostgreSQL, MySQL, MongoDb). In my website I use SqlLite in development, and MongoDb in production.
Connection to database is about configuration /config folder. There is database.js which contains default configuration with SqlLite (for me, this is development version). For production, do following steps:

  1. To /config folder add /env/production folder.
  2. To this folder add database.js file
module.exports = ({ env }) => ({
defaultConnection: 'default',
connections: {
default: {
connector: 'mongoose',
settings: {
uri: env('DATABASE_URI'),
},
options: {
ssl: true,
},
},
},
});
view rawdatabase.jshosted with ❤ by GitHub
DATABASE_URI define in your environment variables and use this format:

mongodb://USER:PASSWORD@cluster0-shard-00-00.evobf.mongodb.net:27017,cluster0-shard-00-01.evobf.mongodb.net:27017,cluster0-shard-00-02.evobf.mongodb.net:27017/strapi?ssl=true&replicaSet=atlas-10ucet-shard-0&authSource=admin&retryWrites=true&w=majority

Step 5 (Optional) - configure storage for Strapi media

In my website I use Firebase Storage (aka Google Cloud Storage bucket). There is plugin for this purpose strapi-provider-upload-google-cloud-storage. For install do following steps:

  1. install plugin using cd backend; yarn add strapi-provider-upload-google-cloud-storage;.
  2. add file plugins.js to /config/env/production (or also to /config/env/development).
  3. add following content to this file
module.exports = ({ env }) => ({
upload: {
provider: 'google-cloud-storage',
providerOptions: {
bucketName: env(BUCKET_NAME),
publicFiles: true,
uniform: false,
basePath: '',
},
}
});
view rawplugins.jshosted with ❤ by GitHub

BUCKET_NAME define in env vars too. In Firebase you can create new bucket, or you can use default one. In free plan you have 5GB space. Next 1GB is for some little fee - see Firebase pricing.

Step 6 - install dependencies for NuxtJS

  1. yarn add @nuxtjs/apollo (for connection to GraphQL API of Strapi)
  2. yarn add @nuxtjs/markdownit (for parse markdown content from API)
  3. yarn add @nuxtjs/axios (for ajax calling, especially for loading Gists during server side rendering)
  4. yarn add vue-jsonp (for loading gists on client side, where using ajax is not possible for CORS problems)
  5. yarn add moment (optional for working with dates like a charm)

Step 7 - config NuxtJS

With price in mind, I use static generated frontend. How save money by this? Simply I need Strapi running only during building. I hosted Strapi in Google Cloud App Engine - standard environment, instance B2 with 9 hours free for day. Strapi is stopped unless I started regenerate static content of my website after content change. NuxtJS support fully static generation by set property target='static' in nuxt.config.js. Also, in this file set Apollo connection to Strapi and others. Configuration file can be as follow:

//...
target='static',
//...
plugins: [
'~/plugins/vue-jsonp.js'
],
//...
modules: [
'@nuxtjs/markdownit',
'@nuxtjs/apollo',
'@nuxtjs/axios'
],
//...
markdownit: {
preset: 'default',
linkify: true,
breaks: true,
injected: true,
html: true
},
//...
apollo: {
clientConfigs: {
default: {
httpEndpoint: process.env.STRAPI_URL || 'http://localhost:1337/graphql'
}
}
},
//...
view rawnuxt.config.jshosted with ❤ by GitHub

To folder /plugins add file vue-jsonp.js with this content:

import Vue from 'vue'
import { VueJsonp } from 'vue-jsonp'
Vue.use(VueJsonp)
view rawvue-jsonp.jshosted with ❤ by GitHub

Then you can use this.$jsonp(url) in Vue component for JSONP (loading JSON over script tag without CORS policy). For now I need JSONP only in development, where I have not statically generated content - during generation, axios is used on server side. Hovewer, I have plan to not use static site in future (when web grows) and use server side rendering (SSR). Then on client side, when I will need to load Gist, I need JSONP for this purpose, so I added it from now.

For GraphQL support by Typescript, add to root folder file gql.d.ts (if you use TS):

declare module '*.gql' {
import { DocumentNode } from 'graphql'
const content: DocumentNode
export default content
}
declare module '*.graphql' {
import { DocumentNode } from 'graphql'
const content: DocumentNode
export default content
}
view rawgql.d.tshosted with ❤ by GitHub

Then you can add your GraphQL queries in folder /apollo/queries (create it).

query Posts($id: ID!) {
post(id: $id) {
id,
image {
url
},
published_at,
name,
content,
tags {
name
}
}
}
view rawpost.gqlhosted with ❤ by GitHub

Example query for load post by id.

Step 8 - create your website

Now is time to create pages, components and other assets for you dreaming site. Also you can use UI framework, or create everything from green grass. I used Vuetify framework, but NuxtJS support many others.

Continuos integration

Because I need rebuild and regenerate static site after content change, I use GitHub workflow. Strapi support webhooks called after some changes. When I change content, Strapi automatically call GitHub Action. Simple? Yes, but! Strapi's webhook request structur is not supported by GitHub. I need some middleman. So, I create controller in Strapi. Then, when event occurs, Strapi hook this controller and from this controller I call github action. Adding Api endpoint to Strapi is straightforward process:

// /api/ci/config/routes.json
{
"routes": [
{
"method": "POST",
"path": "/ci",
"handler": "ci.invokeGithubWebhook",
"config": {
"policies": []
}
}
]
}
// /api/ci/controllers/ci.js
'use strict';
const axios = require('axios');
module.exports = {
invokeGithubWebhook: async (ctx, next) => {
try {
const { data } = await axios.post(`https://api.github.com/repos/${process.env.GITHUB_WEBHOOK_URL}`, { ref: 'main' }, {
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `Bearer ${process.env.GITHUB_KEY}`
}
});
ctx.status = 200;
return data;
} catch (err) {
ctx.status = 500;
return { error: err.message };
}
}
};
view rawgistfile1.txthosted with ❤ by GitHub

Then in Strapi backoffice, I can set webhook url to https://<backend_url>/ci

After regenerated frontend, script deploy static site to CDN hosting of Firebase.

name: rebuild_frontend
on:
workflow_dispatch:
branches:
- main
defaults:
run:
working-directory: ./nuxt
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎
uses: actions/checkout@master
- name: Setup node env 🏗
uses: actions/setup-node@v2.1.2
with:
node-version: ${{ matrix.node }}
check-latest: true
- name: Get yarn cache directory path 🛠
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache node_modules 📦
uses: actions/cache@v2
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies 👨🏻‍💻
run: yarn
- name: Run linter 👀
run: yarn lint
- name: Run generate 👀
run: yarn generate:prod
- name: Deploy to Firebase hosting live
uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_ONDREJPOLACHCZ }}'
channelId: live
projectId: ondrejpolachcz
env:
FIREBASE_CLI_PREVIEWS: hostingchannels

Conclusion and tips

This is my first article in my blogging carrier. I hope you found some informations for your development. There is one tip before end - integration Gists to markdown.

Integration gists

I want to show codes using Gists. Gist use script element which is not naturally supported by Markdown. I can use Axios only during generation on server side. Hovewer, on client I need used solution with JSONP, because Github use CORS policy which blocks Ajax call. JSONP is allowed. There is my vue's component code for prepare markdown with gist code. Support server using axios and client using vue-jsonp \

Note: Gist URL is typed in markdown using following syntax [embed-gist](embed_gist_url.json)
IMPORTANT: change extension in url from .js to .json!

<template>
<div v-html="markdownContent"/>
</template>
<script>
export default {
data() {
return {
markdownContent: null,
};
},
/* Content Fetching in async fetch() */
methods: {
async prepareContentForGistEmbed (content) {
let c = content
const r = /\[embed-gist\]\(.*\)/g
const m = content.match(r)
if (m) {
for (let i = 0; i < m.length; i++) {
const url = m[i]
.replace('[embed-gist](', '')
.replace(')', '')
let res
if (process.client) {
res = await this.$jsonp(url)
} else {
res = await this.$axios.$get(url)
}
const gist = this.removeNewLinesAndSpacesAfter(res.div)
c = c.replace('[embed-gist](' + url + ')', gist)
}
}
return c
}
removeNewLinesAndSpacesAfter(str) {
return str.replace(/\n\s*/g, "");
}
}
}
</script>
<style>
@import url("https://github.githubassets.com/assets/gist-embed-128c4378b91120802003e5c0b2ac3a41.css");
</style>

Code available on GitHub

Github code

Have a nice day, or night.