Removed version strings from _globals.njk,
in favor of src/versions.json.
That versions in that file can be bumped with "yarn bump-versions".
v1 is automatically bumped when doing "yarn build" as well.

Added README file in src directory, explaining versions.json file.

Added README file in scripts directory, detailing usage of each scripts.

Version strings will no longer be appended when cacheControl is disabled
in config file.
After all, version strings are only needed when the static assets are
cached indefinitely in users' browsers.

Initial Cloudflare's cache purging will no longer be executed when
cloudflare -> purgeCache is disabled, even if cacheControl is enabled.
Just in case someone wants to use version strings for other use cases.

Actually use custom metaDesc variable on meta description tag.
This commit is contained in:
Bobby Wibowo 2019-09-19 19:10:37 +07:00
parent 386787c6ce
commit 8ab77a6464
No known key found for this signature in database
GPG Key ID: 51C3A1E1E22D26CF
19 changed files with 289 additions and 91 deletions

View File

@ -1,3 +1,4 @@
const { exec } = require('child_process')
const gulp = require('gulp')
const cssnano = require('cssnano')
const del = require('del')
@ -91,7 +92,15 @@ gulp.task('build:js', () => {
gulp.task('build', gulp.parallel('build:css', 'build:js'))
gulp.task('default', gulp.series('lint', 'clean', 'build'))
gulp.task('exec:bump-versions', cb => {
exec('node ./scripts/bump-versions.js 1', (error, stdout, stderr) => {
if (stdout) process.stdout.write(stdout)
if (stderr) process.stderr.write(stderr)
cb(error)
})
})
gulp.task('default', gulp.series('lint', 'clean', 'build', 'exec:bump-versions'))
/** TASKS: WATCH (SKIP LINTER) */
@ -109,7 +118,7 @@ gulp.task('watch:js', () => {
gulp.task('watch:src', gulp.parallel('watch:css', 'watch:js'))
gulp.task('nodemon', done => {
gulp.task('nodemon', cb => {
return nodemon({
script: './lolisafe.js',
env: process.env,
@ -125,7 +134,7 @@ gulp.task('nodemon', done => {
'views/album.njk'
],
ext: 'js',
done
done: cb
})
})

View File

@ -8,6 +8,7 @@ const RateLimit = require('express-rate-limit')
const readline = require('readline')
const config = require('./config')
const logger = require('./logger')
const versions = require('./src/versions')
const safe = express()
process.on('uncaughtException', error => {
@ -112,14 +113,28 @@ safe.use('/api', api)
process.exit(1)
}
// Re-map version strings if cache control is enabled (safe.fiery.me)
utils.versionStrings = {}
if (config.cacheControl)
for (const type in versions)
utils.versionStrings[type] = `?_=${versions[type]}`
// Check for custom pages, otherwise fallback to Nunjucks templates
for (const page of config.pages) {
const customPage = path.join(paths.customPages, `${page}.html`)
if (!await paths.access(customPage).catch(() => true))
safe.get(`/${page === 'home' ? '' : page}`, (req, res, next) => res.sendFile(customPage))
else if (page === 'home')
safe.get('/', (req, res, next) => res.render(page, { config, gitHash: utils.gitHash }))
safe.get('/', (req, res, next) => res.render(page, {
config,
versions: utils.versionStrings,
gitHash: utils.gitHash
}))
else
safe.get(`/${page}`, (req, res, next) => res.render(page, { config }))
safe.get(`/${page}`, (req, res, next) => res.render(page, {
config,
versions: utils.versionStrings
}))
}
// Error pages
@ -176,23 +191,27 @@ safe.use('/api', api)
logger.log(`lolisafe started on port ${config.port}`)
// Cache control (safe.fiery.me)
if (config.cacheControl) {
logger.log('Cache control enabled, purging...')
const routes = config.pages.concat(['api/check'])
const results = await utils.purgeCloudflareCache(routes)
let errored = false
let succeeded = 0
for (const result of results) {
if (result.errors.length) {
if (!errored) errored = true
result.errors.forEach(error => logger.log(`[CF]: ${error}`))
continue
// Also only if explicitly using Cloudflare
if (config.cacheControl)
if (config.cloudflare.purgeCache) {
logger.log('Cache control enabled, purging Cloudflare\'s cache...')
const routes = config.pages.concat(['api/check'])
const results = await utils.purgeCloudflareCache(routes)
let errored = false
let succeeded = 0
for (const result of results) {
if (result.errors.length) {
if (!errored) errored = true
result.errors.forEach(error => logger.log(`[CF]: ${error}`))
continue
}
succeeded += result.files.length
}
succeeded += result.files.length
if (!errored)
logger.log(`Successfully purged ${succeeded} cache`)
} else {
logger.log('Cache control enabled without Cloudflare\'s cache purging')
}
if (!errored)
logger.log(`Purged ${succeeded} Cloudflare's cache`)
}
// Temporary uploads
if (Array.isArray(config.uploads.temporaryUploadAges) && config.uploads.temporaryUploadAges.length) {

View File

@ -20,6 +20,7 @@
"build": "gulp default",
"watch": "gulp watch",
"develop": "env NODE_ENV=development yarn watch",
"bump-versions": "node ./scripts/bump-versions.js",
"cf-purge": "node ./scripts/cf-purge.js",
"clean-up": "node ./scripts/clean-up.js",
"delete-expired": "node ./scripts/delete-expired.js",

View File

@ -82,7 +82,13 @@ routes.get('/a/:identifier', async (req, res, next) => {
album.url = `a/${album.identifier}`
return res.render('album', { config, album, files, nojs }, (error, html) => {
return res.render('album', {
config,
versions: utils.versionStrings,
album,
files,
nojs
}, (error, html) => {
utils.albumsCache[cacheid].cache = error ? null : html
utils.albumsCache[cacheid].generating = false

View File

@ -13,6 +13,7 @@ routes.post('/nojs', (req, res, next) => {
const result = args[0]
return res.render('nojs', {
config,
versions: utils.versionStrings,
gitHash: utils.gitHash,
errorMessage: result.success ? '' : (result.description || 'An unexpected error occurred.'),
files: result.files || [{}]

57
scripts/README.md Normal file
View File

@ -0,0 +1,57 @@
# README
## cf-purge.js
```none
$ yarn cf-purge
$ node ./scripts/cf-purge.js
Purge Cloudflare's cache.
Usage:
node scripts/cf-purge.js ...filename
filename:
Upload names separated by space (will automatically include their thumbs if available).
```
## clean-up.js
```none
$ yarn clean-up -h
$ node ./scripts/clean-up.js -h
Clean up files that are not in the database.
Usage:
node scripts/clean-up.js [mode=0|1|2]
mode:
0 = Only list names of files that are not in the database.
1 = Clean up the files.
```
## delete-expired.js
```none
$ yarn delete-expired -h
$ node ./scripts/delete-expired.js -h
Bulk delete expired files.
Usage:
node scripts/delete-expired.js [mode=0|1|2]
mode:
0 = Only list names of the expired files.
1 = Delete expired files (output file names).
2 = Delete expired files (no output).
```
## thumbs.js
[\[...\]](..#script-for-missing-thumbnails)
## bump-versions.js
[\[...\]](../src#readme)

83
scripts/bump-versions.js Normal file
View File

@ -0,0 +1,83 @@
const { promisify } = require('util')
const fs = require('fs')
const path = require('path')
const utils = require('../controllers/utilsController')
const self = {
access: promisify(fs.access),
readFile: promisify(fs.readFile),
writeFile: promisify(fs.writeFile),
types: null
}
;(async () => {
const location = process.argv[1].replace(process.cwd() + '/', '')
const args = process.argv.slice(2)
self.types = {}
for (const arg of args) {
const lower = arg.toLowerCase()
if (lower === 'a') {
self.types = { 1: '', 2: '', 3: '', 4: '' }
break
}
const parsed = parseInt(lower)
// Only accept 1 to 4
if (!isNaN(parsed) && parsed >= 1 && parsed <= 4)
self.types[parsed] = ''
}
if (args.includes('--help') || args.includes('-h') || !Object.keys(self.types).length)
return console.log(utils.stripIndents(`
Bump version strings for client-side assets.
Usage:
node ${location} <types>
types:
Space separated list of types (accepts 1 to 4).
1: CSS and JS files (lolisafe core assets + fontello.css).
2: Icons, images and config files (manifest.json, browserconfig.xml, etc).
3: CSS and JS files (libs from /public/libs, such as bulma, lazyload, etc).
4: Renders from /public/render/* directories (to be used with /src/js/misc/render.js).
a: Shortcut to update all types.
`))
const file = path.resolve('./src/versions.json')
// Create an empty file if it does not exist
try {
await self.access(file)
} catch (error) {
if (error.code === 'ENOENT')
await self.writeFile(file, '{}')
else
// Re-throw error
throw error
}
// Read & parse existing versions
const old = JSON.parse(await self.readFile(file))
// Bump version of selected types
// We use current timestamp cause it will always increase
const types = Object.keys(self.types)
const bumped = String(Math.floor(Date.now() / 1000)) // 1s precision
for (const type of types)
self.types[type] = bumped
// Overwrite existing versions with new versions
const data = Object.assign(old, self.types)
// Stringify new versions
const stringified = JSON.stringify(data, null, 2)
// Write to file
await self.writeFile(file, stringified)
console.log(`Successfully bumped version string of type ${types.join(', ')} to "${bumped}".`)
})()
.then(() => process.exit(0))
.catch(error => {
console.error(error)
process.exit(1)
})

View File

@ -23,8 +23,6 @@ self.getFiles = async directory => {
const location = process.argv[1].replace(process.cwd() + '/', '')
const args = process.argv.slice(2)
self.mode = parseInt(args[0]) || 0
if (args.includes('--help') || args.includes('-h'))
return console.log(utils.stripIndents(`
Clean up files that are not in the database.
@ -37,6 +35,7 @@ self.getFiles = async directory => {
1 = Clean up the files.
`))
self.mode = parseInt(args[0]) || 0
const dryrun = self.mode === 0
const uploads = await self.getFiles(paths.uploads)

View File

@ -8,8 +8,6 @@ const self = {
const location = process.argv[1].replace(process.cwd() + '/', '')
const args = process.argv.slice(2)
self.mode = parseInt(args[0]) || 0
if (args.includes('--help') || args.includes('-h'))
return console.log(utils.stripIndents(`
Bulk delete expired files.
@ -23,6 +21,7 @@ const self = {
2 = Delete expired files (no output).
`))
self.mode = parseInt(args[0]) || 0
const dryrun = self.mode === 0
const quiet = self.mode === 2

31
src/README.md Normal file
View File

@ -0,0 +1,31 @@
# README
`versions.json` is the file that tells Nunjucks what version strings to append to client-side lolisafe assets.
To bump the version, it's recommended to use use `yarn bump-versions`.
```none
$ yarn bump-versions
$ node ./scripts/bump-versions.js
Bump version strings for client-side assets.
Usage:
node scripts/bump-versions.js <types>
types:
Space separated list of types (accepts 1 to 4).
1: CSS and JS files (lolisafe core assets + fontello.css).
2: Icons, images and config files (manifest.json, browserconfig.xml, etc).
3: CSS and JS files (libs from /public/libs, such as bulma, lazyload, etc).
4: Renders from /public/render/* directories (to be used with /src/js/misc/render.js).
a: Shortcut to update all types.
```
By default, running `yarn build` will also run `node ./scripts/bump-versions.js 1`.
## Cache-Control
Version strings will NOT be used when `cacheControl` in `config.js` is not enabled.
To begin with, version strings are only necessary when the assets are being cached indefinitely in browsers.

6
src/versions.json Normal file
View File

@ -0,0 +1,6 @@
{
"1": "1568894228",
"2": "1568894058",
"3": "1568894058",
"4": "1568894058"
}

View File

@ -4,7 +4,7 @@ Normal priority:
* [x] Improve performance of album public pages, ~~and maybe paginate them~~.
* [x] Use [native lazy-load tag](https://web.dev/native-lazy-loading) on nojs album pages.
* [ ] Use incremental version numbering instead of randomized strings.
* [x] Use incremental version numbering instead of randomized strings.
* [ ] Use versioning in APIs, somehow.
* [ ] Better `df` handling (system disk stats).
* [x] Use loading spinners on dashboard's sidebar menus.

View File

@ -7,19 +7,6 @@
{% set google_site_verification = null %}
{#
This will be appended to the URLs of static files (CSS, JS, images, etc),
and should be changed on every updates to make sure clients load the very latest version of them.
v1: CSS and JS files (lolisafe).
v2: Images and config files (manifest.json, browserconfig.xml, etc).
v3: CSS and JS files (libs such as bulma, lazyload, etc).
v4: Renders in /public/render/* directories (to be used by render.js).
#}
{% set v1 = "sWcNtiNy3c" %}
{% set v2 = "sWcNtiNy3c" %}
{% set v3 = "YFsTqC68Bc" %}
{% set v4 = "S3TAWpPeFS" %}
{#
These will be the links in the homepage and the No-JS uploader.
You may remove icons by changing "home_icons" to false.

View File

@ -17,7 +17,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="description" content="{{ globals.description }}">
<meta name="description" content="{{ metaDesc or globals.description }}">
<meta name="keywords" content="upload,{{ globals.name }},file,images,hosting">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
@ -26,8 +26,8 @@
{% block stylesheets %}
<!-- Stylesheets -->
<link rel="stylesheet" href="libs/bulma/bulma.min.css?v={{ globals.v3 }}">
<link rel="stylesheet" href="css/style.css?v={{ globals.v1 }}">
<link rel="stylesheet" href="libs/bulma/bulma.min.css{{ versions[3] }}">
<link rel="stylesheet" href="css/style.css{{ versions[1] }}">
{% endblock %}
{% block opengraph %}
@ -39,10 +39,10 @@
{%- if metaImage %}
<meta property="og:image" content="{{ metaImage }}" />
{%- endif %}
<meta property="og:image" content="{{ root }}/icons/600px.png?v={{ globals.v2 }}" />
<meta property="og:image" content="{{ root }}/icons/600px.png{{ versions[2] }}" />
<meta property="og:image:width" content="600" />
<meta property="og:image:height" content="600" />
<meta property="og:image" content="{{ root }}/images/fb_share.png?v={{ globals.v2 }}" />
<meta property="og:image" content="{{ root }}/images/fb_share.png{{ versions[2] }}" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:locale" content="en_US" />
@ -54,21 +54,21 @@
{%- if metaImage %}
<meta name="twitter:image" content="{{ metaImage }}">
{% else %}
<meta name="twitter:image" content="{{ root }}/icons/600px.png?v={{ globals.v2 }}">
<meta name="twitter:image" content="{{ root }}/icons/600px.png{{ versions[2] }}">
{%- endif %}
{% endblock %}
<!-- Icons, configs, etcetera -->
<link rel="icon" href="{{ root }}/icons/32pxr.png?v={{ globals.v2 }}" sizes="32x32">
<link rel="icon" href="{{ root }}/icons/96pxr.png?v={{ globals.v2 }}" sizes="96x96">
<link rel="apple-touch-icon" href="{{ root }}/icons/120px.png?v={{ globals.v2 }}" sizes="120x120">
<link rel="apple-touch-icon" href="{{ root }}/icons/152px.png?v={{ globals.v2 }}" sizes="152x152">
<link rel="apple-touch-icon" href="{{ root }}/icons/167px.png?v={{ globals.v2 }}" sizes="167x167">
<link rel="apple-touch-icon" href="{{ root }}/icons/180px.png?v={{ globals.v2 }}" sizes="180x180">
<link rel="manifest" href="{{ root }}/icons/manifest.json?v={{ globals.v2 }}">
<link rel="icon" href="{{ root }}/icons/32pxr.png{{ versions[2] }}" sizes="32x32">
<link rel="icon" href="{{ root }}/icons/96pxr.png{{ versions[2] }}" sizes="96x96">
<link rel="apple-touch-icon" href="{{ root }}/icons/120px.png{{ versions[2] }}" sizes="120x120">
<link rel="apple-touch-icon" href="{{ root }}/icons/152px.png{{ versions[2] }}" sizes="152x152">
<link rel="apple-touch-icon" href="{{ root }}/icons/167px.png{{ versions[2] }}" sizes="167x167">
<link rel="apple-touch-icon" href="{{ root }}/icons/180px.png{{ versions[2] }}" sizes="180x180">
<link rel="manifest" href="{{ root }}/icons/manifest.json{{ versions[2] }}">
<meta name="apple-mobile-web-app-title" content="{{ globals.name }}">
<meta name="application-name" content="{{ globals.name }}">
<meta name="msapplication-config" content="{{ root }}/icons/browserconfig.xml?v={{ globals.v2 }}">
<meta name="msapplication-config" content="{{ root }}/icons/browserconfig.xml{{ versions[2] }}">
<meta name="theme-color" content="#232629">
{% block endmeta %}{% endblock %}
</head>

View File

@ -14,18 +14,18 @@
{% block stylesheets %}
<!-- Stylesheets -->
<link rel="stylesheet" href="../libs/bulma/bulma.min.css?v={{ globals.v3 }}">
<link rel="stylesheet" href="../css/style.css?v={{ globals.v1 }}">
<link rel="stylesheet" href="../css/thumbs.css?v={{ globals.v1 }}">
<link rel="stylesheet" href="../css/album.css?v={{ globals.v1 }}">
<link rel="stylesheet" href="../libs/bulma/bulma.min.css{{ versions[3] }}">
<link rel="stylesheet" href="../css/style.css{{ versions[1] }}">
<link rel="stylesheet" href="../css/thumbs.css{{ versions[1] }}">
<link rel="stylesheet" href="../css/album.css{{ versions[1] }}">
{% endblock %}
{% block scripts %}
{% if not nojs -%}
<!-- Scripts -->
<script src="../libs/lazyload/lazyload.min.js?v={{ globals.v3 }}"></script>
<script src="../js/album.js?v={{ globals.v1 }}"></script>
<script src="../js/misc/utils.js?v={{ globals.v1 }}"></script>
<script src="../libs/lazyload/lazyload.min.js{{ versions[3] }}"></script>
<script src="../js/album.js{{ versions[1] }}"></script>
<script src="../js/misc/utils.js{{ versions[1] }}"></script>
{% endif %}
{% endblock %}

View File

@ -5,15 +5,15 @@
{% block stylesheets %}
{{ super() }}
<link rel="stylesheet" href="libs/fontello/fontello.css?v={{ globals.v3 }}">
<link rel="stylesheet" href="css/sweetalert.css?v={{ globals.v1 }}">
<link rel="stylesheet" href="libs/fontello/fontello.css{{ versions[1] }}">
<link rel="stylesheet" href="css/sweetalert.css{{ versions[1] }}">
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="libs/sweetalert/sweetalert.min.js?v={{ globals.v3 }}"></script>
<script src="libs/axios/axios.min.js?v={{ globals.v3 }}"></script>
<script src="js/auth.js?v={{ globals.v1 }}"></script>
<script src="libs/sweetalert/sweetalert.min.js{{ versions[3] }}"></script>
<script src="libs/axios/axios.min.js{{ versions[3] }}"></script>
<script src="js/auth.js{{ versions[1] }}"></script>
{% endblock %}
{% block content %}

View File

@ -5,22 +5,22 @@
{% block stylesheets %}
{{ super() }}
<link rel="stylesheet" href="libs/fontello/fontello.css?v={{ globals.v3 }}">
<link rel="stylesheet" href="css/sweetalert.css?v={{ globals.v1 }}">
<link rel="stylesheet" href="css/thumbs.css?v={{ globals.v1 }}">
<link rel="stylesheet" href="css/dashboard.css?v={{ globals.v1 }}">
<link rel="stylesheet" href="libs/fontello/fontello.css{{ versions[1] }}">
<link rel="stylesheet" href="css/sweetalert.css{{ versions[1] }}">
<link rel="stylesheet" href="css/thumbs.css{{ versions[1] }}">
<link rel="stylesheet" href="css/dashboard.css{{ versions[1] }}">
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="libs/sweetalert/sweetalert.min.js?v={{ globals.v3 }}"></script>
<script src="libs/axios/axios.min.js?v={{ globals.v3 }}"></script>
<script src="libs/clipboard.js/clipboard.min.js?v={{ globals.v3 }}"></script>
<script src="libs/lazyload/lazyload.min.js?v={{ globals.v3 }}"></script>
<script src="libs/sweetalert/sweetalert.min.js{{ versions[3] }}"></script>
<script src="libs/axios/axios.min.js{{ versions[3] }}"></script>
<script src="libs/clipboard.js/clipboard.min.js{{ versions[3] }}"></script>
<script src="libs/lazyload/lazyload.min.js{{ versions[3] }}"></script>
{# Polyfill smooth scroll for older browsers #}
<script src="libs/smoothscroll/smoothscroll.min.js?v={{ globals.v3 }}"></script>
<script src="js/dashboard.js?v={{ globals.v1 }}"></script>
<script src="js/misc/utils.js?v={{ globals.v1 }}"></script>
<script src="libs/smoothscroll/smoothscroll.min.js{{ versions[3] }}"></script>
<script src="js/dashboard.js{{ versions[1] }}"></script>
<script src="js/misc/utils.js{{ versions[1] }}"></script>
{% endblock %}
{% block content %}
@ -89,7 +89,7 @@
</aside>
</div>
<div id="page" class="column has-text-centered is-third-quarters">
<img alt="logo" src="images/logo.png?v={{ globals.v2 }}">
<img alt="logo" src="images/logo.png{{ versions[2] }}">
</div>
</div>
</div>

View File

@ -17,22 +17,22 @@
{% block stylesheets %}
{{ super() }}
<link rel="stylesheet" href="libs/fontello/fontello.css?v={{ globals.v3 }}">
<link rel="stylesheet" href="css/sweetalert.css?v={{ globals.v1 }}">
<link rel="stylesheet" href="css/home.css?v={{ globals.v1 }}">
<link rel="stylesheet" href="libs/fontello/fontello.css{{ versions[1] }}">
<link rel="stylesheet" href="css/sweetalert.css{{ versions[1] }}">
<link rel="stylesheet" href="css/home.css{{ versions[1] }}">
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="libs/sweetalert/sweetalert.min.js?v={{ globals.v3 }}"></script>
<script src="libs/dropzone/dropzone.min.js?v={{ globals.v3 }}"></script>
<script src="libs/axios/axios.min.js?v={{ globals.v3 }}"></script>
<script src="libs/clipboard.js/clipboard.min.js?v={{ globals.v3 }}"></script>
<script src="libs/lazyload/lazyload.min.js?v={{ globals.v3 }}"></script>
<script src="js/home.js?v={{ globals.v1 }}"></script>
<script src="js/misc/utils.js?v={{ globals.v1 }}"></script>
<script src="libs/sweetalert/sweetalert.min.js{{ versions[3] }}"></script>
<script src="libs/dropzone/dropzone.min.js{{ versions[3] }}"></script>
<script src="libs/axios/axios.min.js{{ versions[3] }}"></script>
<script src="libs/clipboard.js/clipboard.min.js{{ versions[3] }}"></script>
<script src="libs/lazyload/lazyload.min.js{{ versions[3] }}"></script>
<script src="js/home.js{{ versions[1] }}"></script>
<script src="js/misc/utils.js{{ versions[1] }}"></script>
{# We assign an ID for this so that the script can find out version string for render images #}
<script id="renderScript" data-version="{{ globals.v4 }}" src="js/misc/render.js?v={{ globals.v1 }}"></script>
<script id="renderScript" data-version="{{ versions[4] }}" src="js/misc/render.js{{ versions[1] }}"></script>
{% endblock %}
{% block content %}
@ -41,7 +41,7 @@
<div class="hero-body">
<div class="container has-text-centered">
<p id="b" class="is-relative">
<img class="logo" alt="logo" src="images/logo_smol.png?v={{ globals.v2 }}">
<img class="logo" alt="logo" src="images/logo_smol.png{{ versions[2] }}">
</p>
<h1 class="title">{{ globals.name }}</h1>
<h2 class="subtitle">{{ globals.home_subtitle | safe }}</h2>

View File

@ -16,7 +16,7 @@
{% block stylesheets %}
{{ super() }}
<link rel="stylesheet" href="css/home.css?v={{ globals.v1 }}">
<link rel="stylesheet" href="css/home.css{{ versions[1] }}">
{% endblock %}
{% block content %}
@ -25,7 +25,7 @@
<div class="hero-body">
<div class="container has-text-centered">
<p id="b">
<img class="logo" alt="logo" src="images/logo_smol.png?v={{ globals.v2 }}">
<img class="logo" alt="logo" src="images/logo_smol.png{{ versions[2] }}">
</p>
<h1 class="title">{{ globals.name }}</h1>
<h2 class="subtitle">{{ globals.home_subtitle | safe }}</h2>