mirror of
https://github.com/offen/website.git
synced 2024-11-22 09:00:28 +01:00
Merge pull request #96 from offen/asset-pipeline
Add asset pipeline to pelican setup
This commit is contained in:
commit
b493f1c8f8
@ -377,7 +377,7 @@ jobs:
|
|||||||
|
|
||||||
deploy_homepage:
|
deploy_homepage:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/python:3.6
|
- image: circleci/python:3.6-node
|
||||||
working_directory: ~/offen/homepage
|
working_directory: ~/offen/homepage
|
||||||
environment:
|
environment:
|
||||||
- SOURCE_BRANCH: master
|
- SOURCE_BRANCH: master
|
||||||
@ -397,6 +397,11 @@ jobs:
|
|||||||
paths:
|
paths:
|
||||||
- ~/offen/homepage/venv
|
- ~/offen/homepage/venv
|
||||||
key: offen-homepage-{{ checksum "requirements.txt" }}
|
key: offen-homepage-{{ checksum "requirements.txt" }}
|
||||||
|
- run:
|
||||||
|
name: Install image optimization deps
|
||||||
|
command: |
|
||||||
|
npm install svgo -g
|
||||||
|
sudo apt-get install libjpeg-progs optipng
|
||||||
- run:
|
- run:
|
||||||
name: Deploy
|
name: Deploy
|
||||||
command: |
|
command: |
|
||||||
|
@ -12,6 +12,8 @@ services:
|
|||||||
command: make devserver
|
command: make devserver
|
||||||
ports:
|
ports:
|
||||||
- 8000:8000
|
- 8000:8000
|
||||||
|
environment:
|
||||||
|
DEBUG: '1'
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
homepagedeps:
|
homepagedeps:
|
||||||
|
@ -4,12 +4,10 @@ from __future__ import unicode_literals
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
# If your site is available via HTTPS, make sure SITEURL begins with https://
|
# If your site is available via HTTPS, make sure SITEURL begins with https://
|
||||||
#SITEURL = 'https://www.offen.dev'
|
|
||||||
RELATIVE_URLS = False
|
RELATIVE_URLS = False
|
||||||
|
|
||||||
AUTHOR = 'offen'
|
AUTHOR = 'offen'
|
||||||
SITENAME = 'offen'
|
SITENAME = 'offen'
|
||||||
SITEURL = 'https://www.offen.dev'
|
|
||||||
PATH = 'content'
|
PATH = 'content'
|
||||||
TIMEZONE = 'Europe/Berlin'
|
TIMEZONE = 'Europe/Berlin'
|
||||||
DEFAULT_LANG = 'en'
|
DEFAULT_LANG = 'en'
|
||||||
@ -30,6 +28,9 @@ THEME = './theme'
|
|||||||
# Delete the output directory before generating new files.
|
# Delete the output directory before generating new files.
|
||||||
DELETE_OUTPUT_DIRECTORY = True
|
DELETE_OUTPUT_DIRECTORY = True
|
||||||
|
|
||||||
|
PLUGIN_PATHS = ['./plugins']
|
||||||
|
PLUGINS = ['assets']
|
||||||
|
|
||||||
# dont create following standard pages
|
# dont create following standard pages
|
||||||
AUTHORS_SAVE_AS = None
|
AUTHORS_SAVE_AS = None
|
||||||
ARCHIVES_SAVE_AS = None
|
ARCHIVES_SAVE_AS = None
|
||||||
|
106
homepage/plugins/assets/Readme.rst
Normal file
106
homepage/plugins/assets/Readme.rst
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
Asset management
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This plugin allows you to use the `Webassets`_ module to manage assets such as
|
||||||
|
CSS and JS files. The module must first be installed::
|
||||||
|
|
||||||
|
pip install webassets
|
||||||
|
|
||||||
|
The Webassets module allows you to perform a number of useful asset management
|
||||||
|
functions, including:
|
||||||
|
|
||||||
|
* CSS minifier (``cssmin``, ``yui_css``, ...)
|
||||||
|
* CSS compiler (``less``, ``sass``, ...)
|
||||||
|
* JS minifier (``uglifyjs``, ``yui_js``, ``closure``, ...)
|
||||||
|
|
||||||
|
Others filters include CSS URL rewriting, integration of images in CSS via data
|
||||||
|
URIs, and more. Webassets can also append a version identifier to your asset
|
||||||
|
URL to convince browsers to download new versions of your assets when you use
|
||||||
|
far-future expires headers. Please refer to the `Webassets documentation`_ for
|
||||||
|
more information.
|
||||||
|
|
||||||
|
When used with Pelican, Webassets is configured to process assets in the
|
||||||
|
``OUTPUT_PATH/theme`` directory. You can use Webassets in your templates by
|
||||||
|
including one or more template tags. The Jinja variable ``{{ ASSET_URL }}`` can
|
||||||
|
be used in templates and is relative to the ``theme/`` url. The
|
||||||
|
``{{ ASSET_URL }}`` variable should be used in conjunction with the
|
||||||
|
``{{ SITEURL }}`` variable in order to generate URLs properly. For example:
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
{% assets filters="cssmin", output="css/style.min.css", "css/inuit.css", "css/pygment-monokai.css", "css/main.css" %}
|
||||||
|
<link rel="stylesheet" href="{{ SITEURL }}/{{ ASSET_URL }}">
|
||||||
|
{% endassets %}
|
||||||
|
|
||||||
|
... will produce a minified css file with a version identifier that looks like:
|
||||||
|
|
||||||
|
.. code-block:: html
|
||||||
|
|
||||||
|
<link href="http://{SITEURL}/theme/css/style.min.css?b3a7c807" rel="stylesheet">
|
||||||
|
|
||||||
|
These filters can be combined. Here is an example that uses the SASS compiler
|
||||||
|
and minifies the output:
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
{% assets filters="sass,cssmin", output="css/style.min.css", "css/style.scss" %}
|
||||||
|
<link rel="stylesheet" href="{{ SITEURL }}/{{ ASSET_URL }}">
|
||||||
|
{% endassets %}
|
||||||
|
|
||||||
|
Another example for Javascript:
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
{% assets filters="uglifyjs", output="js/packed.js", "js/jquery.js", "js/base.js", "js/widgets.js" %}
|
||||||
|
<script src="{{ SITEURL }}/{{ ASSET_URL }}"></script>
|
||||||
|
{% endassets %}
|
||||||
|
|
||||||
|
The above will produce a minified JS file:
|
||||||
|
|
||||||
|
.. code-block:: html
|
||||||
|
|
||||||
|
<script src="http://{SITEURL}/theme/js/packed.js?00703b9d"></script>
|
||||||
|
|
||||||
|
Pelican's debug mode is propagated to Webassets to disable asset packaging
|
||||||
|
and instead work with the uncompressed assets.
|
||||||
|
|
||||||
|
If you need to create named bundles (for example, if you need to compile SASS
|
||||||
|
files before minifying with other CSS files), you can use the ``ASSET_BUNDLES``
|
||||||
|
variable in your settings file. This is an ordered sequence of 3-tuples, where
|
||||||
|
the 3-tuple is defined as ``(name, args, kwargs)``. This tuple is passed to the
|
||||||
|
`environment's register() method`_. The following will compile two SCSS files
|
||||||
|
into a named bundle, using the ``pyscss`` filter:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
ASSET_BUNDLES = (
|
||||||
|
('scss', ['colors.scss', 'main.scss'], {'filters': 'pyscss'}),
|
||||||
|
)
|
||||||
|
|
||||||
|
Many of Webasset's available compilers have additional configuration options
|
||||||
|
(i.e. 'Less', 'Sass', 'Stylus', 'Closure_js'). You can pass these options to
|
||||||
|
Webassets using the ``ASSET_CONFIG`` in your settings file.
|
||||||
|
|
||||||
|
The following will handle Google Closure's compilation level and locate
|
||||||
|
LessCSS's binary:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
ASSET_CONFIG = (('closure_compressor_optimization', 'WHITESPACE_ONLY'),
|
||||||
|
('less_bin', 'lessc.cmd'), )
|
||||||
|
|
||||||
|
If you wish to place your assets in locations other than the theme output
|
||||||
|
directory, you can use ``ASSET_SOURCE_PATHS`` in your settings file to provide
|
||||||
|
webassets with a list of additional directories to search, relative to the
|
||||||
|
theme's top-level directory:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
ASSET_SOURCE_PATHS = [
|
||||||
|
'vendor/css',
|
||||||
|
'scss',
|
||||||
|
]
|
||||||
|
|
||||||
|
.. _Webassets: https://github.com/miracle2k/webassets
|
||||||
|
.. _Webassets documentation: http://webassets.readthedocs.org/en/latest/builtin_filters.html
|
||||||
|
.. _environment's register() method: http://webassets.readthedocs.org/en/latest/environment.html#registering-bundles
|
1
homepage/plugins/assets/__init__.py
Normal file
1
homepage/plugins/assets/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .assets import *
|
75
homepage/plugins/assets/assets.py
Normal file
75
homepage/plugins/assets/assets.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Asset management plugin for Pelican
|
||||||
|
===================================
|
||||||
|
|
||||||
|
This plugin allows you to use the `webassets`_ module to manage assets such as
|
||||||
|
CSS and JS files.
|
||||||
|
|
||||||
|
The ASSET_URL is set to a relative url to honor Pelican's RELATIVE_URLS
|
||||||
|
setting. This requires the use of SITEURL in the templates::
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="{{ SITEURL }}/{{ ASSET_URL }}">
|
||||||
|
|
||||||
|
.. _webassets: https://webassets.readthedocs.org/
|
||||||
|
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pelican import signals
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import webassets
|
||||||
|
from webassets import Environment
|
||||||
|
from webassets.ext.jinja2 import AssetsExtension
|
||||||
|
except ImportError:
|
||||||
|
webassets = None
|
||||||
|
|
||||||
|
def add_jinja2_ext(pelican):
|
||||||
|
"""Add Webassets to Jinja2 extensions in Pelican settings."""
|
||||||
|
|
||||||
|
if 'JINJA_ENVIRONMENT' in pelican.settings: # pelican 3.7+
|
||||||
|
pelican.settings['JINJA_ENVIRONMENT']['extensions'].append(AssetsExtension)
|
||||||
|
else:
|
||||||
|
pelican.settings['JINJA_EXTENSIONS'].append(AssetsExtension)
|
||||||
|
|
||||||
|
|
||||||
|
def create_assets_env(generator):
|
||||||
|
"""Define the assets environment and pass it to the generator."""
|
||||||
|
|
||||||
|
theme_static_dir = generator.settings['THEME_STATIC_DIR']
|
||||||
|
assets_destination = os.path.join(generator.output_path, theme_static_dir)
|
||||||
|
generator.env.assets_environment = Environment(
|
||||||
|
assets_destination, theme_static_dir)
|
||||||
|
|
||||||
|
if 'ASSET_CONFIG' in generator.settings:
|
||||||
|
for item in generator.settings['ASSET_CONFIG']:
|
||||||
|
generator.env.assets_environment.config[item[0]] = item[1]
|
||||||
|
|
||||||
|
if 'ASSET_BUNDLES' in generator.settings:
|
||||||
|
for name, args, kwargs in generator.settings['ASSET_BUNDLES']:
|
||||||
|
generator.env.assets_environment.register(name, *args, **kwargs)
|
||||||
|
|
||||||
|
if 'ASSET_DEBUG' in generator.settings:
|
||||||
|
generator.env.assets_environment.debug = generator.settings['ASSET_DEBUG']
|
||||||
|
elif logging.getLevelName(logger.getEffectiveLevel()) == "DEBUG":
|
||||||
|
generator.env.assets_environment.debug = True
|
||||||
|
|
||||||
|
for path in (generator.settings['THEME_STATIC_PATHS'] +
|
||||||
|
generator.settings.get('ASSET_SOURCE_PATHS', [])):
|
||||||
|
full_path = os.path.join(generator.theme, path)
|
||||||
|
generator.env.assets_environment.append_path(full_path)
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
"""Plugin registration."""
|
||||||
|
if webassets:
|
||||||
|
signals.initialized.connect(add_jinja2_ext)
|
||||||
|
signals.generator_init.connect(create_assets_env)
|
||||||
|
else:
|
||||||
|
logger.warning('`assets` failed to load dependency `webassets`.'
|
||||||
|
'`assets` plugin not loaded.')
|
28
homepage/plugins/optimize_images/Readme.md
Normal file
28
homepage/plugins/optimize_images/Readme.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
Optimize Images Plugin For Pelican
|
||||||
|
==================================
|
||||||
|
|
||||||
|
This plugin applies lossless compression on JPEG, PNG and SVG images, with no
|
||||||
|
effect on image quality via [jpegtran][], [OptiPNG][] and [svgo][] respectively.
|
||||||
|
The plugin assumes that all of these tools are installed, with associated
|
||||||
|
executables available on the system path.
|
||||||
|
|
||||||
|
[jpegtran]: http://jpegclub.org/jpegtran/
|
||||||
|
[OptiPNG]: http://optipng.sourceforge.net/
|
||||||
|
[SVGO]: https://github.com/svg/svgo
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
To enable, ensure that `optimize_images.py` is put somewhere that is accessible.
|
||||||
|
Then use as follows by adding the following to your settings.py:
|
||||||
|
|
||||||
|
PLUGIN_PATH = 'path/to/pelican-plugins'
|
||||||
|
PLUGINS = ["optimize_images"]
|
||||||
|
|
||||||
|
`PLUGIN_PATH` can be a path relative to your settings file or an absolute path.
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
The plugin will activate and optimize images upon `finalized` signal of
|
||||||
|
Pelican.
|
1
homepage/plugins/optimize_images/__init__.py
Normal file
1
homepage/plugins/optimize_images/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .optimize_images import *
|
61
homepage/plugins/optimize_images/optimize_images.py
Normal file
61
homepage/plugins/optimize_images/optimize_images.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Optimized images (jpg and png)
|
||||||
|
Assumes that jpegtran and optipng are isntalled on path.
|
||||||
|
http://jpegclub.org/jpegtran/
|
||||||
|
http://optipng.sourceforge.net/
|
||||||
|
Copyright (c) 2012 Irfan Ahmad (http://i.com.pk)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from subprocess import call
|
||||||
|
|
||||||
|
from pelican import signals
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Display command output on DEBUG and TRACE
|
||||||
|
SHOW_OUTPUT = logger.getEffectiveLevel() <= logging.DEBUG
|
||||||
|
|
||||||
|
# A list of file types with their respective commands
|
||||||
|
COMMANDS = {
|
||||||
|
# '.ext': ('command {flags} {filename', 'silent_flag', 'verbose_flag')
|
||||||
|
'.svg': ('svgo {flags} --input="{filename}" --output="{filename}"', '--quiet', ''),
|
||||||
|
'.jpg': ('jpegtran {flags} -copy none -optimize -outfile "{filename}" "{filename}"', '', '-v'),
|
||||||
|
'.png': ('optipng {flags} "{filename}"', '--quiet', ''),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def optimize_images(pelican):
|
||||||
|
"""
|
||||||
|
Optimized jpg and png images
|
||||||
|
|
||||||
|
:param pelican: The Pelican instance
|
||||||
|
"""
|
||||||
|
for dirpath, _, filenames in os.walk(pelican.settings['OUTPUT_PATH']):
|
||||||
|
for name in filenames:
|
||||||
|
if os.path.splitext(name)[1] in COMMANDS.keys():
|
||||||
|
optimize(dirpath, name)
|
||||||
|
|
||||||
|
def optimize(dirpath, filename):
|
||||||
|
"""
|
||||||
|
Check if the name is a type of file that should be optimized.
|
||||||
|
And optimizes it if required.
|
||||||
|
|
||||||
|
:param dirpath: Path of the file to be optimzed
|
||||||
|
:param name: A file name to be optimized
|
||||||
|
"""
|
||||||
|
filepath = os.path.join(dirpath, filename)
|
||||||
|
logger.info('optimizing %s', filepath)
|
||||||
|
|
||||||
|
ext = os.path.splitext(filename)[1]
|
||||||
|
command, silent, verbose = COMMANDS[ext]
|
||||||
|
flags = verbose if SHOW_OUTPUT else silent
|
||||||
|
command = command.format(filename=filepath, flags=flags)
|
||||||
|
call(command, shell=True)
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
signals.finalized.connect(optimize_images)
|
@ -19,9 +19,6 @@ CATEGORY_FEED_ATOM = 'feeds/{slug}.atom.xml'
|
|||||||
|
|
||||||
DELETE_OUTPUT_DIRECTORY = True
|
DELETE_OUTPUT_DIRECTORY = True
|
||||||
|
|
||||||
# Following items are often useful when publishing
|
PLUGINS += ['optimize_images']
|
||||||
|
|
||||||
#DISQUS_SITENAME = ""
|
|
||||||
#GOOGLE_ANALYTICS = ""
|
|
||||||
|
|
||||||
OFFEN_ACCOUNT_ID = "5ec8345a-2a45-4eb9-92e5-8d9e5684db58"
|
OFFEN_ACCOUNT_ID = "5ec8345a-2a45-4eb9-92e5-8d9e5684db58"
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
pelican==4.0.1
|
pelican==4.0.1
|
||||||
markdown==3.1.1
|
markdown==3.1.1
|
||||||
|
webassets==0.12.1
|
||||||
|
cssmin==0.2.0
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
$(window).scroll(function(){
|
;(function ($) {
|
||||||
$(".brand-index").css("opacity", 0 + $(window).scrollTop() / 100);
|
$(document).ready(function () {
|
||||||
});
|
$(window).scroll(function () {
|
||||||
|
$('.brand-index').css('opacity', 0 + $(window).scrollTop() / 100)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})(window.jQuery)
|
||||||
|
@ -1,18 +1,16 @@
|
|||||||
(function($) {
|
;(function ($) {
|
||||||
$(function() {
|
$(function () {
|
||||||
$('nav ul li a:not(:only-child)').click(function(e) {
|
$('nav ul li a:not(:only-child)').click(function (e) {
|
||||||
$(this).siblings('.nav-dropdown').toggle();
|
$(this).siblings('.nav-dropdown').toggle()
|
||||||
$('.dropdown').not($(this).siblings()).hide();
|
$('.dropdown').not($(this).siblings()).hide()
|
||||||
e.stopPropagation();
|
e.stopPropagation()
|
||||||
});
|
})
|
||||||
$('html').click(function() {
|
$('html').click(function () {
|
||||||
$('.nav-dropdown').hide();
|
$('.nav-dropdown').hide()
|
||||||
});
|
})
|
||||||
$('#nav-toggle').click(function() {
|
$('#nav-toggle').click(function () {
|
||||||
$('nav ul').slideToggle();
|
$(this).closest('nav').find('ul').slideToggle()
|
||||||
});
|
$(this).toggleClass('active')
|
||||||
$('#nav-toggle').on('click', function() {
|
})
|
||||||
this.classList.toggle('active');
|
})
|
||||||
});
|
})(window.jQuery)
|
||||||
});
|
|
||||||
})(jQuery);
|
|
||||||
|
@ -17,12 +17,9 @@
|
|||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<link rel="canonical" href="{{ SITEURL }}/{{ page.save_as }}">
|
<link rel="canonical" href="{{ SITEURL }}/{{ page.save_as }}">
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="/theme/images/favicon.ico">
|
<link rel="shortcut icon" type="image/x-icon" href="/theme/images/favicon.ico">
|
||||||
<link rel="stylesheet" type="text/css" href="/theme/css/normalize.css">
|
{% assets filters="cssmin", output="css/style.min.css", "css/normalize.css", "css/fonts.css", "css/style.css" %}
|
||||||
<link rel="stylesheet" type="text/css" href="/theme/css/fonts.css">
|
<link rel="stylesheet" href="{{ SITEURL }}/{{ ASSET_URL }}">
|
||||||
<!--
|
{% endassets %}
|
||||||
<link rel="stylesheet" type="text/css" href="/theme/styles/index.css">
|
|
||||||
-->
|
|
||||||
<link rel="stylesheet" type="text/css" href="/theme/css/style.css">
|
|
||||||
{% if OFFEN_ACCOUNT_ID %}
|
{% if OFFEN_ACCOUNT_ID %}
|
||||||
<script async src="https://script-alpha.offen.dev/script.js" data-account-id="{{ OFFEN_ACCOUNT_ID }}"></script>
|
<script async src="https://script-alpha.offen.dev/script.js" data-account-id="{{ OFFEN_ACCOUNT_ID }}"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -40,7 +37,7 @@
|
|||||||
<a href="/"><img src="/theme/images/offen-brand-white.svg" alt="offen logo" width="42" height="46" class="logo"></a>
|
<a href="/"><img src="/theme/images/offen-brand-white.svg" alt="offen logo" width="42" height="46" class="logo"></a>
|
||||||
</div>
|
</div>
|
||||||
<nav>
|
<nav>
|
||||||
<div class="nav-mobile"><a id="nav-toggle" href="#!"><span></span></a></div>
|
<div class="nav-mobile"><span id="nav-toggle"><span></span></span></div>
|
||||||
<ul class="nav-list">
|
<ul class="nav-list">
|
||||||
<li>
|
<li>
|
||||||
<a href="/">Summary</a>
|
<a href="/">Summary</a>
|
||||||
@ -240,9 +237,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/theme/scripts/jquery-3.4.1.min.js"></script>
|
{% assets filters="rjsmin", output="scripts/packed.js", "scripts/jquery-3.4.1.min.js", "scripts/menu.js", "scripts/fade.js" %}
|
||||||
<script src="/theme/scripts/menu.js"></script>
|
<script src="{{ SITEURL }}/{{ ASSET_URL }}"></script>
|
||||||
<script src="/theme/scripts/fade.js"></script>
|
{% endassets %}
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
Reference in New Issue
Block a user