Added RSS feeds and 3rd article [WIP]
This commit is contained in:
parent
d89725f8ab
commit
60ab950093
11 changed files with 406 additions and 141 deletions
|
@ -5,7 +5,7 @@ import re
|
||||||
from glob import glob
|
from glob import glob
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from flask import Flask, abort, send_from_directory, render_template
|
from flask import Flask, Response, abort, send_from_directory, render_template
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
|
|
||||||
basedir = os.path.abspath(os.path.join(os.path.realpath(__file__), '..', '..'))
|
basedir = os.path.abspath(os.path.join(os.path.realpath(__file__), '..', '..'))
|
||||||
|
@ -63,13 +63,14 @@ def get_page(page: str, title: Optional[str] = None):
|
||||||
description=metadata.get('description'),
|
description=metadata.get('description'),
|
||||||
published=(metadata['published'].strftime('%b %d, %Y')
|
published=(metadata['published'].strftime('%b %d, %Y')
|
||||||
if metadata.get('published') else None),
|
if metadata.get('published') else None),
|
||||||
content=markdown(f.read(),extensions=['fenced_code', 'codehilite']))
|
content=markdown(f.read(), extensions=['fenced_code', 'codehilite']))
|
||||||
|
|
||||||
|
|
||||||
def get_pages() -> list:
|
def get_pages(with_content: bool = False) -> list:
|
||||||
return sorted([
|
return sorted([
|
||||||
{
|
{
|
||||||
'path': path,
|
'path': path,
|
||||||
|
'content': get_page(path) if with_content else '',
|
||||||
**get_page_metadata(os.path.basename(path)),
|
**get_page_metadata(os.path.basename(path)),
|
||||||
}
|
}
|
||||||
for path in glob(os.path.join(pages_dir, '*.md'))
|
for path in glob(os.path.join(pages_dir, '*.md'))
|
||||||
|
@ -99,3 +100,50 @@ def css_route(style: str):
|
||||||
@app.route('/article/<article>', methods=['GET'])
|
@app.route('/article/<article>', methods=['GET'])
|
||||||
def article_route(article: str):
|
def article_route(article: str):
|
||||||
return get_page(article)
|
return get_page(article)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/rss', methods=['GET'])
|
||||||
|
def rss_route():
|
||||||
|
pages = get_pages(with_content=True)
|
||||||
|
|
||||||
|
return Response('''<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<rss version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>Platypush blog feeds</title>
|
||||||
|
<link>http://blog.platypush.tech</link>
|
||||||
|
<description>Insights and inspirational projects using Platypush as an automation platform</description>
|
||||||
|
<category>Programming, automation, Python, machine learning, IoT, smart home</category>
|
||||||
|
<image>
|
||||||
|
<url>https://git.platypush.tech/uploads/-/system/appearance/header_logo/1/icon-256.png</url>
|
||||||
|
<title>Platypush</title>
|
||||||
|
<link>https://git.platypush.tech</link>
|
||||||
|
</image>
|
||||||
|
<pubDate>{last_pub_date}</pubDate>
|
||||||
|
<language>en-us</language>
|
||||||
|
|
||||||
|
{items}
|
||||||
|
</channel>
|
||||||
|
</rss>'''.format(
|
||||||
|
last_pub_date=pages[0]['published'].strftime('%a, %d %b %Y %H:%M:%S GMT'),
|
||||||
|
items='\n\n'.join([
|
||||||
|
'''
|
||||||
|
<item>
|
||||||
|
<title>{title}</title>
|
||||||
|
<link>https://blog.platypush.tech{link}</link>
|
||||||
|
<pubDate>{published}</pubDate>
|
||||||
|
<description><![CDATA[{content}]]></description>
|
||||||
|
<image>
|
||||||
|
<url>https://blog.platypush.tech/img{image}</url>
|
||||||
|
</image>
|
||||||
|
</item>
|
||||||
|
'''.format(
|
||||||
|
title=page.get('title', '[No Title]'),
|
||||||
|
link=page.get('uri', ''),
|
||||||
|
published=page['published'].strftime('%a, %d %b %Y %H:%M:%S GMT') if 'published' in page else '',
|
||||||
|
# description=page.get('description', ''),
|
||||||
|
content=page.get('content', ''),
|
||||||
|
image=page.get('image', ''),
|
||||||
|
)
|
||||||
|
for page in pages
|
||||||
|
]),
|
||||||
|
), mimetype='application/rss+xml')
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
main .content {
|
main .content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
text-align: justify;
|
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
letter-spacing: .04em;
|
}
|
||||||
|
|
||||||
|
main .content p,
|
||||||
|
main .content ul {
|
||||||
|
font-family: Avenir, Palatino, charter, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||||
|
text-align: justify;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
main .content code, .codehilite {
|
main .content code, .codehilite {
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
html {
|
html {
|
||||||
font-size: calc(1em + 1vw);
|
height: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 1024px) {
|
|
||||||
html {
|
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
font-family: BlinkMacSystemFont, -apple-system, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@ -23,43 +21,58 @@ a, a:visited {
|
||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 3em;
|
height: 8%;
|
||||||
padding: 0 .5em;
|
padding: 0 .5em;
|
||||||
box-shadow: 1px 3px 3px 0 #bbb;
|
box-shadow: 1px 3px 3px 0 #bbb;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 767px) {
|
header a {
|
||||||
header {
|
|
||||||
height: 4em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
header > a {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header .left,
|
||||||
|
header .left a,
|
||||||
|
header .right {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .right {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: right;
|
||||||
|
text-align: right;
|
||||||
|
opacity: .8;
|
||||||
|
}
|
||||||
|
|
||||||
header .icon {
|
header .icon {
|
||||||
background: url(/img/icon.png);
|
background-size: 40px !important;
|
||||||
background-size: 40px;
|
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
header .left .icon {
|
||||||
|
background: url(/img/icon.png);
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header .right .icon {
|
||||||
|
background: url(/img/rss.png);
|
||||||
|
}
|
||||||
|
|
||||||
header .title {
|
header .title {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
height: calc(100% - 3em);
|
height: 92%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-family: Avenir, Palatino, Georgia, Verdana, Helvetica, Arial, sans-serif;
|
|
||||||
padding: 0 2em;
|
padding: 0 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,3 +85,13 @@ h2 {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
line-height: 1.1em;
|
line-height: 1.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
header {
|
||||||
|
height: 6%;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
height: 94%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,11 +4,16 @@ main {
|
||||||
|
|
||||||
.articles {
|
.articles {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article {
|
.article {
|
||||||
display: block;
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 80%;
|
||||||
box-shadow: 0 1px 3px 1px #ddd;
|
box-shadow: 0 1px 3px 1px #ddd;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
@ -17,33 +22,39 @@ main {
|
||||||
|
|
||||||
.article:hover {
|
.article:hover {
|
||||||
box-shadow: 0 1px 4px 2px #bcbcbc;
|
box-shadow: 0 1px 4px 2px #bcbcbc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 767px) {
|
@media screen and (min-width: 767px) {
|
||||||
.article {
|
.article {
|
||||||
width: 100%;
|
max-height: 65%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) and (max-width: 990px) {
|
@media screen and (min-width: 640px) and (max-width: 767px) {
|
||||||
|
.article {
|
||||||
|
padding: 0 calc(1em + 7vw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) and (max-width: 979px) {
|
||||||
.article {
|
.article {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 990px) and (max-width: 1023px) {
|
@media screen and (min-width: 980px) and (max-width: 1279px) {
|
||||||
.article {
|
.article {
|
||||||
width: 33%;
|
width: 33%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1024px) and (max-width: 1279px) {
|
@media screen and (min-width: 1280px) and (max-width: 1599px) {
|
||||||
.article {
|
.article {
|
||||||
width: 25%;
|
width: 25%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 1280px) {
|
@media screen and (min-width: 1600px) {
|
||||||
.article {
|
.article {
|
||||||
width: 20%;
|
width: 20%;
|
||||||
}
|
}
|
||||||
|
|
BIN
static/img/people-detect-1.png
Normal file
BIN
static/img/people-detect-1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 110 KiB |
BIN
static/img/rss.png
Normal file
BIN
static/img/rss.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
|
@ -0,0 +1,168 @@
|
||||||
|
[//]: # (title: Detect people with a RaspberryPi, a thermal camera, Platypush and Tensorflow)
|
||||||
|
[//]: # (description: Use cheap components and open-source software to build a robust presence detector.)
|
||||||
|
[//]: # (image: /img/people-detect-1.png)
|
||||||
|
[//]: # (published: 2019-09-27)
|
||||||
|
|
||||||
|
Triggering events based on the presence of people has been the dream of many geeks and DIY automation junkies for a
|
||||||
|
while. Having your house to turn the lights on or off when you enter or exit your living room is an interesting
|
||||||
|
application, for instance. Most of the solutions out there to solve these kinds of problems, even more high-end
|
||||||
|
solutions like the [Philips Hue sensors](https://www2.meethue.com/en-us/p/hue-motion-sensor/046677473389), detect
|
||||||
|
motion, not actual people presence — which means that the lights will switch off once you lay on your couch like a
|
||||||
|
sloth. The ability to turn off music and/or tv when you exit the room and head to your bedroom, without the hassle of
|
||||||
|
switching all the buttons off, is also an interesting corollary. Detecting the presence of people in your room while
|
||||||
|
you’re not at home is another interesting application.
|
||||||
|
|
||||||
|
Thermal cameras coupled with deep neural networks are a much more robust strategy to actually detect the presence of
|
||||||
|
people. Unlike motion sensors, they will detect the presence of people even when they aren’t moving. And unlike optical
|
||||||
|
cameras, they detect bodies by measuring the heat that they emit in the form of infrared radiation, and are therefore
|
||||||
|
much more robust — their sensitivity doesn’t depend on lighting conditions, on the position of the target, or the
|
||||||
|
colour. Before exploring the thermal camera solution, I tried for a while to build a model that instead relied on
|
||||||
|
optical images from a traditional webcam. The differences are staggering: I trained the optical model on more than ten
|
||||||
|
thousands 640x480 images taken all through a week in different lighting conditions, while I trained the thermal camera
|
||||||
|
model on a dataset of 900 24x32 images taken during a single day. Even with more complex network architectures, the
|
||||||
|
optical model wouldn’t score above a 91% accuracy in detecting the presence of people, while the thermal model would
|
||||||
|
achieve around 99% accuracy within a single training phase of a simpler neural network. Despite the high potential,
|
||||||
|
there’s not much out there in the market — there’s been some research work on the topic (if you google “people detection
|
||||||
|
thermal camera” you’ll mostly find research papers) and a few high-end and expensive products for professional
|
||||||
|
surveillance. In lack of ready-to-go solutions for my house, I decided to take on my duty and build my own solution —
|
||||||
|
making sure that it can easily be replicated by anyone.
|
||||||
|
|
||||||
|
## Prepare the hardware
|
||||||
|
|
||||||
|
For this example we'll use the following hardware:
|
||||||
|
|
||||||
|
- A RaspberryPi (cost: around $35). In theory any model should work, but it’s probably not a good idea to use a
|
||||||
|
single-core RaspberryPi Zero for machine learning tasks — the task itself is not very expensive (we’ll only use the
|
||||||
|
Raspberry for doing predictions on a trained model, not to train the model), but it may still suffer some latency on a
|
||||||
|
Zero. Plus, it may be really painful to install some of the required libraries (like Tensorflow or OpenCV) on
|
||||||
|
the `arm6` architecture used by the RaspberryPi Zero. Any better performing model (from RPi3 onwards) should
|
||||||
|
definitely do the job.
|
||||||
|
|
||||||
|
- A thermal camera. For this project, I’ve used the
|
||||||
|
[MLX90640](https://shop.pimoroni.com/products/mlx90640-thermal-camera-breakout) Pimoroni breakout camera (cost: $55),
|
||||||
|
as it’s relatively cheap, easy to install, and it provides good results. This camera comes in standard (55°) and
|
||||||
|
wide-angle (110°) versions. I’ve used the wide-angle model as the camera monitors a large living room, but take into
|
||||||
|
account that both have the same resolution (32x24 pixels), so the wider angle comes with the cost of a lower spatial
|
||||||
|
resolution. If you want to use a different thermal camera there’s not much you’ll need to change, as long as it comes
|
||||||
|
with a software interface for RaspberryPi and
|
||||||
|
it’s [compatible with Platypush](https://platypush.readthedocs.io/en/latest/platypush/plugins/camera.ir.mlx90640.html).
|
||||||
|
|
||||||
|
Setting up the MLX90640 on your RaspberryPi if you have a Breakout Garden it’s easy as a pie. Fit the Breakout Garden on
|
||||||
|
top of your RaspberryPi. Fit the camera breakout into an I2C slot. Boot the RaspberryPi. Done. Otherwise, you can also
|
||||||
|
connect the device directly to the [RaspberryPi I2C interface](https://radiostud.io/howto-i2c-communication-rpi/),
|
||||||
|
either using the right hardware PINs or the software emulation layer.
|
||||||
|
|
||||||
|
## Prepare the software
|
||||||
|
|
||||||
|
I tested my code on Raspbian, but with a few minor modifications it should be easily adaptable to any distribution
|
||||||
|
installed on the RaspberryPi.
|
||||||
|
|
||||||
|
The software support for the thermal camera requires a bit of work. The MLX90640 doesn’t come (yet) with a Python
|
||||||
|
ready-to-use interface, but a [C++ open-source driver is provided](https://github.com/pimoroni/mlx90640-library) -
|
||||||
|
and that's the driver that is wrapped by the Platypush integration. Instructions to install it:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Install the dependencies
|
||||||
|
[sudo] apt-get install libi2c-dev
|
||||||
|
|
||||||
|
# Enable the I2C interface
|
||||||
|
echo dtparam=i2c_arm=on | sudo tee -a /boot/config.txt
|
||||||
|
|
||||||
|
# It's advised to configure the SPI bus baud rate to
|
||||||
|
# 400kHz to support the higher throughput of the sensor
|
||||||
|
echo dtparam=i2c1_baudrate=400000 | sudo tee -a /boot/config.txt
|
||||||
|
|
||||||
|
# A reboot is required here if you didn't have the
|
||||||
|
# options above enabled in your /boot/config.txt
|
||||||
|
[sudo] reboot
|
||||||
|
|
||||||
|
# Clone the driver's codebase
|
||||||
|
git clone https://github.com/pimoroni/mlx90640-library
|
||||||
|
cd mlx90640-library
|
||||||
|
|
||||||
|
# Compile the rawrgb example
|
||||||
|
make clean
|
||||||
|
make bcm2835
|
||||||
|
make I2C_MODE=LINUX examples/rawrgb
|
||||||
|
```
|
||||||
|
|
||||||
|
If it all went well you should see an executable named `rawrgb` under the `examples` directory. If you run it you should
|
||||||
|
see a bunch of binary data — that’s the raw binary representation of the frames captured by the camera. Remember where
|
||||||
|
it is located or move it to a custom bin folder, as it’s the executable that platypush will use to interact with the
|
||||||
|
camera module.
|
||||||
|
|
||||||
|
This post assumes that you have already installed and configured Platypush on your system. If not, head to my post on
|
||||||
|
[getting started with Platypush](https://blog.platypush.tech/article/Ultimate-self-hosted-automation-with-Platypush),
|
||||||
|
the [readthedocs page](https://platypush.readthedocs.io/en/latest/), the
|
||||||
|
[Gitlab page](https://git.platypush.tech/platypush/platypush) or
|
||||||
|
[the wiki](https://git.platypush.tech/platypush/platypush/-/wikis/home).
|
||||||
|
|
||||||
|
Install also the Python dependencies for the HTTP server, the MLX90640 plugin and Tensorflow:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
[sudo] pip install 'platypush[http,tensorflow,mlx90640]'
|
||||||
|
```
|
||||||
|
|
||||||
|
Heading to your computer (we'll be using it for building the model that will be used on the RaspberryPi), install
|
||||||
|
OpenCV, Tensorflow and Jupyter and my utilities for handling images:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# For image manipulation
|
||||||
|
[sudo] pip install opencv
|
||||||
|
|
||||||
|
# Install Jupyter notebook to run the training code
|
||||||
|
[sudo] pip install jupyterlab
|
||||||
|
# Then follow the instructions at https://jupyter.org/install
|
||||||
|
|
||||||
|
# Tensorflow framework for machine learning and utilities
|
||||||
|
[sudo] pip install tensorflow numpy matplotlib
|
||||||
|
|
||||||
|
# Clone my repository with the image and training utilities
|
||||||
|
# and the Jupyter notebooks that we'll use for training.
|
||||||
|
git clone https://github.com/BlackLight/imgdetect-utils
|
||||||
|
```
|
||||||
|
|
||||||
|
## Capturing phase
|
||||||
|
|
||||||
|
Now that you’ve got all the hardware and software in place, it’s time to start capturing frames with your camera and use
|
||||||
|
them to train your model. First, configure
|
||||||
|
the [MLX90640 plugin](https://platypush.readthedocs.io/en/latest/platypush/plugins/camera.ir.mlx90640.html) in your
|
||||||
|
Platypush configuration file (by default, `~/.config/platypush/config.yaml`):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Enable the webserver
|
||||||
|
backend.http:
|
||||||
|
enabled: True
|
||||||
|
|
||||||
|
camera.ir.mlx90640:
|
||||||
|
fps: 16 # Frames per second
|
||||||
|
rotate: 270 # Can be 0, 90, 180, 270
|
||||||
|
rawrgb_path: /path/to/your/rawrgb
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart the service, and if you haven't already create a user from the web interface at `http://your-rpi:8008`. You
|
||||||
|
should now be able to take pictures through the API:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
curl -XPOST -H 'Content-Type: application/json' -d '
|
||||||
|
{
|
||||||
|
"type":"request",
|
||||||
|
"action":"camera.ir.mlx90640.capture",
|
||||||
|
"args": {
|
||||||
|
"output_file":"~/snap.png",
|
||||||
|
"scale_factor":20
|
||||||
|
}
|
||||||
|
}' -a 'username:password' http://localhost:8008/execute
|
||||||
|
```
|
||||||
|
|
||||||
|
If everything went well, the thermal picture should be stored under `~/snap.png`. In my case it looks like this while
|
||||||
|
I’m in standing front of the sensor:
|
||||||
|
|
||||||
|
![Thermal camera snapshot](../img/people-detect-1.png)
|
||||||
|
|
||||||
|
Notice the glow at the bottom-right corner — that’s actually the heat from my RaspberryPi 4 CPU. It’s there in all the
|
||||||
|
images I take, and you may probably see similar results if you mounted your camera on top of the Raspberry itself, but
|
||||||
|
it shouldn’t be an issue for your model training purposes.
|
||||||
|
|
||||||
|
If you open the web panel (`http://your-host:8008`) you’ll also notice a new tab, represented by the sun icon, that you
|
||||||
|
can use to monitor your camera from a web interface.
|
|
@ -1,20 +1,8 @@
|
||||||
<html lang="en">
|
{% with title=title or 'Platypush blog', styles=['/css/blog.css', '/css/code.css'] %}
|
||||||
<head>
|
{% include 'common-head.html' %}
|
||||||
<link rel="stylesheet" type="text/css" href="/css/common.css">
|
{% endwith %}
|
||||||
<link rel="stylesheet" type="text/css" href="/css/blog.css">
|
|
||||||
<link rel="stylesheet" type="text/css" href="/css/code.css">
|
|
||||||
<title>{{ title }}</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
<main>
|
||||||
<header>
|
|
||||||
<a href="/">
|
|
||||||
<div class="icon"></div>
|
|
||||||
<div class="title">PlatyBlog</div>
|
|
||||||
</a>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<h1>{{ title }}</h1>
|
<h1>{{ title }}</h1>
|
||||||
|
@ -36,6 +24,6 @@
|
||||||
{{ content | safe }}
|
{{ content | safe }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
|
||||||
</html>
|
{% include 'common-tail.html' %}
|
||||||
|
|
30
templates/common-head.html
Normal file
30
templates/common-head.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||||
|
<link rel="alternate" type="application/rss+xml" title="RSS Feed for the Platypush blog" href="/rss" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/common.css">
|
||||||
|
|
||||||
|
{% if styles %}
|
||||||
|
{% for style in styles %}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ style }}">
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div class="left">
|
||||||
|
<a href="/">
|
||||||
|
<div class="icon"></div>
|
||||||
|
<div class="title">PlatyBlog</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right">
|
||||||
|
<a href="/rss" target="_blank">
|
||||||
|
<div class="icon"></div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
2
templates/common-tail.html
Normal file
2
templates/common-tail.html
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,19 +1,8 @@
|
||||||
<html lang="en">
|
{% with title=title or 'Platypush blog', styles=['/css/home.css'] %}
|
||||||
<head>
|
{% include 'common-head.html' %}
|
||||||
<link rel="stylesheet" type="text/css" href="/css/common.css">
|
{% endwith %}
|
||||||
<link rel="stylesheet" type="text/css" href="/css/home.css">
|
|
||||||
<title>Platypush blog</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
<main>
|
||||||
<header>
|
|
||||||
<a href="/">
|
|
||||||
<div class="icon"></div>
|
|
||||||
<div class="title">PlatyBlog</div>
|
|
||||||
</a>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<div class="articles">
|
<div class="articles">
|
||||||
{% for page in pages %}
|
{% for page in pages %}
|
||||||
<a class="article" href="{{ page['uri'] }}">
|
<a class="article" href="{{ page['uri'] }}">
|
||||||
|
@ -43,6 +32,6 @@
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
|
||||||
</html>
|
{% include 'common-tail.html' %}
|
||||||
|
|
Loading…
Reference in a new issue