Robert
Posted on September 1, 2020
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 installdjango_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 yourINSTALLED_APPS
incms/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 folderRun
docker-compose exec cms bash python manage.py migrate
to run wagtail migrationsOpen
localhost:8000
Check our the working example on github:
Posted on September 1, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.