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 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
|
||||
|
||||
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'),
|
||||
published=(metadata['published'].strftime('%b %d, %Y')
|
||||
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([
|
||||
{
|
||||
'path': path,
|
||||
'content': get_page(path) if with_content else '',
|
||||
**get_page_metadata(os.path.basename(path)),
|
||||
}
|
||||
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'])
|
||||
def article_route(article: str):
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: justify;
|
||||
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 {
|
||||
|
|
|
@ -1,74 +1,97 @@
|
|||
html {
|
||||
font-size: calc(1em + 1vw);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) {
|
||||
html {
|
||||
font-size: 20px;
|
||||
}
|
||||
height: 100%;
|
||||
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 {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
a, a:visited {
|
||||
color: #555;
|
||||
border-bottom: 1px dashed #999;
|
||||
text-decoration: none;
|
||||
color: #555;
|
||||
border-bottom: 1px dashed #999;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 3em;
|
||||
padding: 0 .5em;
|
||||
box-shadow: 1px 3px 3px 0 #bbb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 8%;
|
||||
padding: 0 .5em;
|
||||
box-shadow: 1px 3px 3px 0 #bbb;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
header {
|
||||
height: 4em;
|
||||
}
|
||||
header a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
header > a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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 {
|
||||
background: url(/img/icon.png);
|
||||
background-size: 40px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: inline-flex;
|
||||
margin-right: 1em;
|
||||
background-size: 40px !important;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
header .left .icon {
|
||||
background: url(/img/icon.png);
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
header .right .icon {
|
||||
background: url(/img/rss.png);
|
||||
}
|
||||
|
||||
header .title {
|
||||
display: inline-flex;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
main {
|
||||
height: calc(100% - 3em);
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-family: Avenir, Palatino, Georgia, Verdana, Helvetica, Arial, sans-serif;
|
||||
padding: 0 2em;
|
||||
height: 92%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 2em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
line-height: 1.2em;
|
||||
font-size: 2em;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
line-height: 1.1em;
|
||||
font-size: 1.5em;
|
||||
line-height: 1.1em;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
header {
|
||||
height: 6%;
|
||||
}
|
||||
|
||||
main {
|
||||
height: 94%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,11 +4,16 @@ main {
|
|||
|
||||
.articles {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.article {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-height: 80%;
|
||||
box-shadow: 0 1px 3px 1px #ddd;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -17,33 +22,39 @@ main {
|
|||
|
||||
.article:hover {
|
||||
box-shadow: 0 1px 4px 2px #bcbcbc;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
@media screen and (min-width: 767px) {
|
||||
.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 {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 990px) and (max-width: 1023px) {
|
||||
@media screen and (min-width: 980px) and (max-width: 1279px) {
|
||||
.article {
|
||||
width: 33%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1024px) and (max-width: 1279px) {
|
||||
@media screen and (min-width: 1280px) and (max-width: 1599px) {
|
||||
.article {
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1280px) {
|
||||
@media screen and (min-width: 1600px) {
|
||||
.article {
|
||||
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,41 +1,29 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="/css/common.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/blog.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/code.css">
|
||||
<title>{{ title }}</title>
|
||||
</head>
|
||||
{% with title=title or 'Platypush blog', styles=['/css/blog.css', '/css/code.css'] %}
|
||||
{% include 'common-head.html' %}
|
||||
{% endwith %}
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<a href="/">
|
||||
<div class="icon"></div>
|
||||
<div class="title">PlatyBlog</div>
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="title">
|
||||
<h1>{{ title }}</h1>
|
||||
</div>
|
||||
|
||||
{% if description %}
|
||||
<div class="description">
|
||||
<h3>{{ description }}</h3>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if published %}
|
||||
<div class="published-date">
|
||||
Published on {{ published }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="content">
|
||||
{{ content | safe }}
|
||||
</div>
|
||||
<main>
|
||||
<div class="container">
|
||||
<div class="title">
|
||||
<h1>{{ title }}</h1>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
{% if description %}
|
||||
<div class="description">
|
||||
<h3>{{ description }}</h3>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if published %}
|
||||
<div class="published-date">
|
||||
Published on {{ published }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="content">
|
||||
{{ content | safe }}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{% 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,48 +1,37 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="/css/common.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/home.css">
|
||||
<title>Platypush blog</title>
|
||||
</head>
|
||||
{% with title=title or 'Platypush blog', styles=['/css/home.css'] %}
|
||||
{% include 'common-head.html' %}
|
||||
{% endwith %}
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<a href="/">
|
||||
<div class="icon"></div>
|
||||
<div class="title">PlatyBlog</div>
|
||||
</a>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="articles">
|
||||
{% for page in pages %}
|
||||
<a class="article" href="{{ page['uri'] }}">
|
||||
<div class="container">
|
||||
{% if page['image'] %}
|
||||
<div class="image">
|
||||
<img src="{{ page['image'] }}" alt="">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="title">
|
||||
{{ page['title'] }}
|
||||
</div>
|
||||
|
||||
{% if page['published'] %}
|
||||
<div class="published-date">
|
||||
{{ page['published'].strftime('%b %d, %Y') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if page['description'] %}
|
||||
<div class="description">
|
||||
{{ page['description'] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<main>
|
||||
<div class="articles">
|
||||
{% for page in pages %}
|
||||
<a class="article" href="{{ page['uri'] }}">
|
||||
<div class="container">
|
||||
{% if page['image'] %}
|
||||
<div class="image">
|
||||
<img src="{{ page['image'] }}" alt="">
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
{% endif %}
|
||||
|
||||
<div class="title">
|
||||
{{ page['title'] }}
|
||||
</div>
|
||||
|
||||
{% if page['published'] %}
|
||||
<div class="published-date">
|
||||
{{ page['published'].strftime('%b %d, %Y') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if page['description'] %}
|
||||
<div class="description">
|
||||
{{ page['description'] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{% include 'common-tail.html' %}
|
||||
|
|
Loading…
Reference in a new issue