How to create amazing SSR website with Wagtail 2 + Vue 3

robert197

Robert

Posted on September 1, 2020

How to create amazing SSR website with Wagtail 2 + Vue 3

Why do I do that?

I want to share some tools combined with best practices.
This setup is flexible enough to create huge web projects. Indeed this is just a general idea and proof of concept, how we can combine Vue3 with Django based CMS.

In case you are to lazy or to excited to see it in action, you can go directly to the github repo.

If you like django and python you will love wagtail CMS. Check it out at: https://wagtail.io/

You heard a lot about the new version of vue? It's amazing. Let's try to combine it with the best Python CMS and make sure that we still have SSR (Server Side Rendering) and make use of the popular package django_webpack_loader

Let's dive in.

  • First we create a docker-compose.yaml file:
version: "3"

services:
    cms:
        restart: always
        image: cms/wagtail
        build:
            context: ./cms
        volumes:
            - ./cms:/code/cms
            - ./frontend:/code/cms/frontend
        ports:
            - 8000:8000
        links:
            - frontend

    frontend:
        restart: always
        image: frontend/node
        build:
            context: ./frontend
        command: yarn serve
        ports:
            - 8080:8080
        volumes:
            - ./frontend:/code/cms/frontend
  • We generate wagtail project inside the project folder and name it cms:
pip install wagtail
wagtail start cms
  • For the frontend part create a frontend folder

mkdir frontend

  • After this you can create package.json file inside this folder.
{
  "name": "frontend",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "serve": "webpack-dev-server --host 0.0.0.0"
  },
  "dependencies": {
    "vue": "3.0.0-rc.9",
    "webpack-bundle-tracker": "0.4.3"
  },
  "devDependencies": {
    "@vue/compiler-sfc": "3.0.0-rc.9",
    "ts-loader": "8.0.3",
    "typescript": "4.0.2",
    "vue-loader": "16.0.0-beta.5",
    "webpack": "4.44.1",
    "webpack-cli": "3.3.12",
    "webpack-dev-server": "3.11.0",
    "yarn": "1.22.5"
  }
}
  • tsconfig.json:
{
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "declaration": false,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "module": "es2015",
    "moduleResolution": "node",
    "noImplicitAny": false,
    "noLib": false,
    "sourceMap": true,
    "strict": true,
    "strictPropertyInitialization": false,
    "suppressImplicitAnyIndexErrors": true,
    "target": "es2015",
    "baseUrl": "."
  },
  "exclude": [
    "./node_modules"
  ],
  "include": [
    "./src/**/*.ts",
    "./src/**/*.vue"
  ]
}
  • webpack.config.js
const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')
const BundleTracker = require('webpack-bundle-tracker');

module.exports = (env = {}) => {
  return {
    mode: env.prod ? 'production' : 'development',
    devtool: env.prod ? 'source-map' : 'cheap-module-eval-source-map',
    entry: path.resolve(__dirname, './src/main.ts'),
    output: {
      path: path.resolve(__dirname, './dist'),
      publicPath: "http://0.0.0.0:8080/"
    },
    module: {
      rules: [
        {
          test: /\.vue$/,
          use: 'vue-loader'
        },
        {
          test: /\.ts$/,
          loader: 'ts-loader',
          options: {
            appendTsSuffixTo: [/\.vue$/],
          }
        },
      ]
    },
    resolve: {
      extensions: ['.ts', '.js', '.vue', '.json'],
      alias: {
        'vue': '@vue/runtime-dom'
      }
    },
    plugins: [
      new VueLoaderPlugin(),
      new BundleTracker({ filename: './webpack-stats.json' })
    ],
    devServer: {
      headers: {
        "Access-Control-Allow-Origin": "\*"
      },
      public: 'http://0.0.0.0:8080',
      inline: true,
      hot: true,
      stats: "minimal",
      contentBase: __dirname,
      overlay: true
    }
  };
}
  • Dockerfile
FROM node:12.15.0 as base

WORKDIR /code/cms/frontend
COPY ./package*.json ./
RUN yarn install
COPY . .
  • Than you can create frontend/src folder with following files inside.

  • main.ts

import { createApp } from 'vue';
import CountButton from './components/count_button.vue';

createApp(CountButton).mount('#vue-count-button'); // This selector name will be used in wagtail / django template.

The idea is to create each vue instance for each component which has to be binded to the django template.

  • shims-vue.d.ts
declare module "*.vue" {
  import { defineComponent } from "vue";
  const Component: ReturnType<typeof defineComponent>;
  export default Component;
}
  • Create a frontend/src/utils/ folder
  • and following django-variables.js file. This get method will help us to get properties directly from django templating to our vue instance.
function _getSingle(id) {
  if (!document.querySelector(`#${id}`)) {
    console.error(`Selector #${id} could not be found. Please check your django templates.`);
    console.error(`
      You are probably missing something like {{ value|json_script:'${id}' }}
      in your django template.`
    );
    return "";
  }
  return document.querySelector(`#${id}`).textContent.replace("\"", "");
}

function get(...args) {
  let obj = {};
  args.forEach((id) => {
    obj[id] = _getSingle(id)
  })
  return obj;
}

export {
  get
}
  • You can create a component in frontend/src/components
  • count-button.vue component
<template>
  <div>
    <h2>This is a Vue 3 component!!</h2>
    <button @click="increase">Clicked {{ count }} times.</button>
  </div>
</template>
<script lang="ts">
import { ref } from 'vue';
import { get } from '../utils/django-variables';
export default {
  setup() {
    const djangoVariables = get('header_title', 'header_title2');
    console.log(djangoVariables);
    const count = ref(0);
    const increase = () => {
      count.value++;
    };
    return {
      count,
      increase
    };
  }
};
</script>
  • For the CMS Part you need to install django_webpack_loader
  • Add following line to requirements.txt in cms folder
...
django-webpack-loader==0.6.0

In order to apply this change you need to build your cms container. Simply run: docker-compose build cms

  • Add 'webpack_loader' to your INSTALLED_APPS in cms/cms/settings/base.py
INSTALLED_APPS = [
    'home',
    'search',

    'wagtail.contrib.forms',
    'wagtail.contrib.redirects',
    'wagtail.embeds',
    'wagtail.sites',
    'wagtail.users',
    'wagtail.snippets',
    'wagtail.documents',
    'wagtail.images',
    'wagtail.search',
    'wagtail.admin',
    'wagtail.core',

    'modelcluster',
    'taggit',

    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'webpack_loader'
]
  • On the bottom of cms/cms/settings/base.py add following dict:
WEBPACK_LOADER = {
    'DEFAULT': {
        'CACHE': True,
        'BUNDLE_DIR_NAME': '/bundles/',  # must end with slash
        'STATS_FILE': '/code/cms/frontend/webpack-stats.json',
    }
}
  • In your base.html template you will need to add following tags:

{% load render_bundle from webpack_loader %}

{% render_bundle 'main' %}

Example cms/cms/templates/base.html:

{% load static wagtailuserbar %}
{% load render_bundle from webpack_loader %}

<!DOCTYPE html>
<html class="no-js" lang="en">
    <head>
        <meta charset="utf-8" />
        <title>
            {% block title %}
                {% if self.seo_title %}{{ self.seo_title }}{% else %}{{ self.title }}{% endif %}
            {% endblock %}
            {% block title_suffix %}
                {% with self.get_site.site_name as site_name %}
                    {% if site_name %}- {{ site_name }}{% endif %}
                {% endwith %}
            {% endblock %}
        </title>
        <meta name="description" content="" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />

        {# Global stylesheets #}
        <link rel="stylesheet" type="text/css" href="{% static 'css/cms.css' %}">

        {% block extra_css %}
            {# Override this in templates to add extra stylesheets #}
        {% endblock %}
    </head>

    <body class="{% block body_class %}{% endblock %}">
        {% wagtailuserbar %}

        {% block content %}{% endblock %}

        {# Global javascript #}
        {% render_bundle 'main' %}
        <script type="text/javascript" src="{% static 'js/cms.js' %}"></script>

        {% block extra_js %}
            {# Override this in templates to add extra javascript #}
        {% endblock %}

    </body>
</html>
  • Now in order to load our count button vue component to a template, we need just to reference the id defined in main.ts
<div>
    <div id="vue-count-button"></div>
    <div id="vue-sidebar"></div>
</div>
  • To pass some variables from template to vue components. Just add:

{{ block.value|json_script:'header_title'}}

Inside the template.
and add:

import { get } from '../utils/django-variables';
...
const djangoVariables = get('header_title');
...
  • Example cms/home/templates/home/home_page.html:
{% extends "base.html" %}
{% load static %}
{% load wagtailcore_tags %}

{% block body_class %}template-homepage{% endblock %}

{% block extra_css %}


{% comment %}
Delete the line below if you're just getting started and want to remove the welcome screen!
{% endcomment %}
<link rel="stylesheet" href="{% static 'css/welcome_page.css' %}">
{% endblock extra_css %}

{% block content %}

<div>
    <div id="vue-count-button"></div>
    <div id="vue-sidebar"></div>
</div>
<article>
    {% for block in page.body %}
        {% if block.block_type == 'heading' %}
            {{ block.value|json_script:'header_title'}}
            {{ block.value|json_script:'header_title2'}}
            <h1>{{ block.value }}</h1>
        {% endif %}
        {% if block.block_type == 'paragraph' %}
            <p>{{ block.value }}</p>
        {% endif %}

    {% endfor %}
</article>


{% endblock content %}

Finally

  • Run yarn install in your frontend folder

  • Run docker-compose exec cms bash python manage.py migrate to run wagtail migrations

  • Open localhost:8000

Check our the working example on github:

💖 💪 🙅 🚩
robert197
Robert

Posted on September 1, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related