a little push
This commit is contained in:
commit
266d5fc1cd
9
Makefile
Normal file
9
Makefile
Normal file
@ -0,0 +1,9 @@
|
||||
default: run
|
||||
|
||||
setup:
|
||||
@python3 -m venv .venv
|
||||
@.venv/bin/pip install -r requirements.txt
|
||||
|
||||
run:
|
||||
@.venv/bin/python feedmode.py
|
||||
|
12
README.md
Normal file
12
README.md
Normal file
@ -0,0 +1,12 @@
|
||||
# feedmode
|
||||
|
||||
Multifeeder/RSS feeds → selection + CSS + template → PDF (very beta)
|
||||
|
||||
## Use feedmode locally
|
||||
|
||||
`make setup` (sets up a virtual environment and install the requirements)
|
||||
|
||||
`make run` (runs the Flask application)
|
||||
|
||||
Open the application at <http://localhost:5001>.
|
||||
|
4
config.py
Normal file
4
config.py
Normal file
@ -0,0 +1,4 @@
|
||||
class Config(object):
|
||||
PORTNUMBER = 5001
|
||||
PAD_API_URL = 'https://pad.vvvvvvaria.org/api/1.2.15/'
|
||||
PAD_API_KEY = '<insert API key here>'
|
138
feedmode.py
Executable file
138
feedmode.py
Executable file
@ -0,0 +1,138 @@
|
||||
import flask
|
||||
from flask import request, redirect
|
||||
from urllib.request import urlopen
|
||||
from urllib.parse import urlencode
|
||||
import json
|
||||
import os
|
||||
import pypandoc
|
||||
from jinja2 import Template
|
||||
|
||||
APP = flask.Flask(__name__)
|
||||
APP.config.from_object("config.Config")
|
||||
|
||||
# ---
|
||||
|
||||
def get_pad_content(pad):
|
||||
arguments = {
|
||||
'padID' : pad,
|
||||
'apikey' : APP.config['PAD_API_KEY']
|
||||
}
|
||||
api_call = 'getText'
|
||||
response = json.load(urlopen(f"{ APP.config['PAD_API_URL'] }{ api_call }", data=urlencode(arguments).encode()))
|
||||
content = response['data']['text']
|
||||
|
||||
return content
|
||||
|
||||
def update_pad_contents():
|
||||
# download the stylesheet + template pad
|
||||
css = get_pad_content('feedmode.css')
|
||||
template = get_pad_content('feedmode.template')
|
||||
# !!! this breaks the whole idea that this application can be shared by multiple projects at the same time
|
||||
# !!! but py_pandoc needs to run with files........ hmmm
|
||||
with open('templates/pandoc-template.html', 'w') as f:
|
||||
f.write(template)
|
||||
|
||||
return css, template
|
||||
|
||||
def get_multifeeder_feed(query):
|
||||
feed_json = json.load(urlopen(query))
|
||||
multifeeder_template = open('templates/multifeeder-template.html').read()
|
||||
jinja_template = Template(multifeeder_template)
|
||||
feed_html = jinja_template.render(response=feed_json)
|
||||
|
||||
return feed_html, feed_json
|
||||
|
||||
def load_query():
|
||||
query = open('static/query.txt').read().strip() # !!! needs fixing
|
||||
|
||||
return query
|
||||
|
||||
# ---
|
||||
|
||||
@APP.route('/', methods=['GET'])
|
||||
def index():
|
||||
return redirect("/preview/", code=302)
|
||||
|
||||
@APP.route('/update/', methods=['POST'])
|
||||
def update():
|
||||
query = request.form['query']
|
||||
with open('static/query.txt', 'w') as out:
|
||||
out.write(query)
|
||||
|
||||
return redirect("/select/", code=302)
|
||||
|
||||
@APP.route('/select/', methods=['GET'])
|
||||
def select():
|
||||
# get feed contents
|
||||
# render selection template with checkboxes
|
||||
query = load_query()
|
||||
x, feed_json = get_multifeeder_feed(query)
|
||||
|
||||
return flask.render_template('selection-template.html', feed=feed_json, query=query)
|
||||
|
||||
@APP.route('/preview/', methods=['GET'])
|
||||
def preview():
|
||||
# update pad contents
|
||||
x, template_content = update_pad_contents()
|
||||
# get feed contents
|
||||
query = load_query()
|
||||
feed_html, x = get_multifeeder_feed(query)
|
||||
# render multifeeder feed in template
|
||||
template = Template(template_content)
|
||||
html = template.render(feed=feed_html, mode="screen")
|
||||
|
||||
return flask.render_template('html.html', html=html, query=query)
|
||||
|
||||
@APP.route('/stylesheet/', methods=['GET'])
|
||||
def stylesheet():
|
||||
ext = '.css'
|
||||
query = load_query()
|
||||
|
||||
return flask.render_template('pad.html', name="feedmode", ext=ext, query=query)
|
||||
|
||||
@APP.route('/template/', methods=['GET'])
|
||||
def template():
|
||||
ext = '.template'
|
||||
query = load_query()
|
||||
|
||||
return flask.render_template('pad.html', name="feedmode", ext=ext, query=query)
|
||||
|
||||
@APP.route('/pdf/')
|
||||
def pdf():
|
||||
query = load_query()
|
||||
|
||||
return flask.render_template('pdf.html', query=query)
|
||||
|
||||
# //////////////
|
||||
# rendered resources (not saved as a file on the server)
|
||||
|
||||
@APP.route('/print.css')
|
||||
def css():
|
||||
css, x = update_pad_contents()
|
||||
|
||||
return css, 200, {'Content-Type': 'text/css; charset=utf-8'}
|
||||
|
||||
@APP.route('/pandoc-template.html')
|
||||
def pandoc_template():
|
||||
x, template = update_pad_contents()
|
||||
|
||||
return template, 200, {'Content-Type': 'text/html; charset=utf-8'}
|
||||
|
||||
@APP.route('/pagedjs.html')
|
||||
def pagedjs():
|
||||
# update pad contents
|
||||
x, template = update_pad_contents()
|
||||
# get feed contents
|
||||
query = "https://multi.vvvvvvaria.org/API/latest/50" # !!! needs fixing
|
||||
feed_html, feed_json = get_multifeeder_feed(query)
|
||||
# render multifeeder feed in template
|
||||
jinja_template = Template(template)
|
||||
html = jinja_template.render(feed=feed_html, mode="print")
|
||||
|
||||
return html, 200, {'Content-Type': 'text/html; charset=utf-8'}
|
||||
|
||||
# //////////////
|
||||
|
||||
if __name__ == '__main__':
|
||||
APP.debug=True
|
||||
APP.run(host="0.0.0.0", port=f'{ APP.config["PORTNUMBER"] }', threaded=True)
|
17
requirements.txt
Normal file
17
requirements.txt
Normal file
@ -0,0 +1,17 @@
|
||||
click==8.0.3
|
||||
Flask==2.0.2
|
||||
importlib-metadata==4.10.1
|
||||
itsdangerous==2.0.1
|
||||
Jinja2==3.0.3
|
||||
Markdown==3.3.6
|
||||
MarkupSafe==2.0.1
|
||||
pandoc==2.0.1
|
||||
pkg-resources==0.0.0
|
||||
plumbum==1.7.2
|
||||
ply==3.11
|
||||
pypandoc==1.7.2
|
||||
typing-extensions==4.0.1
|
||||
urllib3==1.26.8
|
||||
Werkzeug==2.0.2
|
||||
zipp==3.7.0
|
||||
|
76
static/main.css
Normal file
76
static/main.css
Normal file
@ -0,0 +1,76 @@
|
||||
body{
|
||||
min-width: 900px;
|
||||
}
|
||||
|
||||
/* GENERAL RULES */
|
||||
|
||||
/* main title element that says "in feedmode" */
|
||||
h1 em.feedmode{
|
||||
color: darkorchid;
|
||||
}
|
||||
|
||||
/* navigation */
|
||||
div#nav{
|
||||
position: fixed;
|
||||
width: calc(100% - 1em);
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
div#nav h1{
|
||||
position: absolute;
|
||||
width: auto;
|
||||
line-height: 0;
|
||||
margin: 0.75em 15px;
|
||||
float: left;
|
||||
font-size: 24px;
|
||||
}
|
||||
div#nav div#buttons{
|
||||
margin: 0.5em 15px;
|
||||
float: right;
|
||||
}
|
||||
div#nav input{
|
||||
min-width: 300px;
|
||||
}
|
||||
div#nav input#html{
|
||||
min-width: unset;
|
||||
}
|
||||
div#nav form#update{
|
||||
float: left;
|
||||
margin: 0.5em 15px 0 300px;
|
||||
}
|
||||
|
||||
/* iframe rules */
|
||||
iframe{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* main content area */
|
||||
div#wrapper{
|
||||
/* pages with an iframe are on fixed mode */
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
left: 25px;
|
||||
width: calc(100vw - 25px - 25px);
|
||||
height: calc(100vh - 50px - 25px);
|
||||
}
|
||||
div#wrapper.scroll{
|
||||
/* the HTML page is on scroll mode */
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: 5em 0 0 0;
|
||||
}
|
||||
|
||||
/* Z-INDEX */
|
||||
|
||||
div#wrapper,
|
||||
div.pagedjs_pages{
|
||||
z-index: 1;
|
||||
}
|
||||
div#nav{
|
||||
z-index: 11;
|
||||
}
|
31061
static/paged.js
Normal file
31061
static/paged.js
Normal file
File diff suppressed because it is too large
Load Diff
31107
static/paged.polyfill.js
Normal file
31107
static/paged.polyfill.js
Normal file
File diff suppressed because it is too large
Load Diff
180
static/pagedjs.css
Normal file
180
static/pagedjs.css
Normal file
@ -0,0 +1,180 @@
|
||||
/* CSS for Paged.js interface – v0.2 */
|
||||
|
||||
/* Change the look */
|
||||
:root {
|
||||
--color-background: whitesmoke;
|
||||
--color-pageSheet: #cfcfcf;
|
||||
--color-pageBox: violet;
|
||||
--color-paper: white;
|
||||
--color-marginBox: transparent;
|
||||
--pagedjs-crop-color: black;
|
||||
--pagedjs-crop-shadow: white;
|
||||
--pagedjs-crop-stroke: 1px;
|
||||
}
|
||||
|
||||
/* To define how the book look on the screen: */
|
||||
@media screen {
|
||||
body {
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
|
||||
.pagedjs_pages {
|
||||
display: flex;
|
||||
width: calc(var(--pagedjs-width) * 2);
|
||||
flex: 0;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.pagedjs_page {
|
||||
background-color: var(--color-paper);
|
||||
box-shadow: 0 0 0 1px var(--color-pageSheet);
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
margin-top: 10mm;
|
||||
}
|
||||
|
||||
.pagedjs_first_page {
|
||||
margin-left: var(--pagedjs-width);
|
||||
}
|
||||
|
||||
.pagedjs_page:last-of-type {
|
||||
margin-bottom: 10mm;
|
||||
}
|
||||
|
||||
.pagedjs_pagebox{
|
||||
box-shadow: 0 0 0 1px var(--color-pageBox);
|
||||
}
|
||||
|
||||
.pagedjs_left_page{
|
||||
z-index: 20;
|
||||
width: calc(var(--pagedjs-bleed-left) + var(--pagedjs-pagebox-width))!important;
|
||||
}
|
||||
|
||||
.pagedjs_left_page .pagedjs_bleed-right .pagedjs_marks-crop {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.pagedjs_left_page .pagedjs_bleed-right .pagedjs_marks-middle{
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.pagedjs_right_page{
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
left: calc(var(--pagedjs-bleed-left)*-1);
|
||||
}
|
||||
|
||||
/* show the margin-box */
|
||||
|
||||
.pagedjs_margin-top-left-corner-holder,
|
||||
.pagedjs_margin-top,
|
||||
.pagedjs_margin-top-left,
|
||||
.pagedjs_margin-top-center,
|
||||
.pagedjs_margin-top-right,
|
||||
.pagedjs_margin-top-right-corner-holder,
|
||||
.pagedjs_margin-bottom-left-corner-holder,
|
||||
.pagedjs_margin-bottom,
|
||||
.pagedjs_margin-bottom-left,
|
||||
.pagedjs_margin-bottom-center,
|
||||
.pagedjs_margin-bottom-right,
|
||||
.pagedjs_margin-bottom-right-corner-holder,
|
||||
.pagedjs_margin-right,
|
||||
.pagedjs_margin-right-top,
|
||||
.pagedjs_margin-right-middle,
|
||||
.pagedjs_margin-right-bottom,
|
||||
.pagedjs_margin-left,
|
||||
.pagedjs_margin-left-top,
|
||||
.pagedjs_margin-left-middle,
|
||||
.pagedjs_margin-left-bottom {
|
||||
box-shadow: 0 0 0 1px inset var(--color-marginBox);
|
||||
}
|
||||
|
||||
/* uncomment this part for recto/verso book : ------------------------------------ */
|
||||
/*
|
||||
|
||||
.pagedjs_pages {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pagedjs_first_page {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.pagedjs_page {
|
||||
margin: 0 auto;
|
||||
margin-top: 10mm;
|
||||
}
|
||||
|
||||
.pagedjs_left_page{
|
||||
width: calc(var(--pagedjs-bleed-left) + var(--pagedjs-pagebox-width) + var(--pagedjs-bleed-left))!important;
|
||||
}
|
||||
|
||||
.pagedjs_left_page .pagedjs_bleed-right .pagedjs_marks-crop{
|
||||
border-color: var(--pagedjs-crop-color);
|
||||
}
|
||||
|
||||
.pagedjs_left_page .pagedjs_bleed-right .pagedjs_marks-middle{
|
||||
width: var(--pagedjs-cross-size)!important;
|
||||
}
|
||||
|
||||
.pagedjs_right_page{
|
||||
left: 0;
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/*--------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
|
||||
/* uncomment this par to see the baseline : -------------------------------------------*/
|
||||
|
||||
/*
|
||||
.pagedjs_pagebox {
|
||||
--pagedjs-baseline: 22px;
|
||||
--pagedjs-baseline-position: 5px;
|
||||
--pagedjs-baseline-color: cyan;
|
||||
background: linear-gradient(transparent 0%, transparent calc(var(--pagedjs-baseline) - 1px), var(--pagedjs-baseline-color) calc(var(--pagedjs-baseline) - 1px), var(--pagedjs-baseline-color) var(--pagedjs-baseline)), transparent;
|
||||
background-size: 100% var(--pagedjs-baseline);
|
||||
background-repeat: repeat-y;
|
||||
background-position-y: var(--pagedjs-baseline-position);
|
||||
} */
|
||||
|
||||
|
||||
/*--------------------------------------------------------------------------------------*/
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* Marks (to delete when merge in paged.js) */
|
||||
|
||||
.pagedjs_marks-crop{
|
||||
z-index: 999999999999;
|
||||
|
||||
}
|
||||
|
||||
.pagedjs_bleed-top .pagedjs_marks-crop,
|
||||
.pagedjs_bleed-bottom .pagedjs_marks-crop{
|
||||
box-shadow: 1px 0px 0px 0px var(--pagedjs-crop-shadow);
|
||||
}
|
||||
|
||||
.pagedjs_bleed-top .pagedjs_marks-crop:last-child,
|
||||
.pagedjs_bleed-bottom .pagedjs_marks-crop:last-child{
|
||||
box-shadow: -1px 0px 0px 0px var(--pagedjs-crop-shadow);
|
||||
}
|
||||
|
||||
.pagedjs_bleed-left .pagedjs_marks-crop,
|
||||
.pagedjs_bleed-right .pagedjs_marks-crop{
|
||||
box-shadow: 0px 1px 0px 0px var(--pagedjs-crop-shadow);
|
||||
}
|
||||
|
||||
.pagedjs_bleed-left .pagedjs_marks-crop:last-child,
|
||||
.pagedjs_bleed-right .pagedjs_marks-crop:last-child{
|
||||
box-shadow: 0px -1px 0px 0px var(--pagedjs-crop-shadow);
|
||||
}
|
13
static/select.css
Normal file
13
static/select.css
Normal file
@ -0,0 +1,13 @@
|
||||
div.post{
|
||||
width: 250px;
|
||||
border: 1px solid magenta;
|
||||
padding: 1em;
|
||||
margin: 0.5em;
|
||||
max-height: 400px;
|
||||
float: left;
|
||||
overflow-y: scroll;
|
||||
font-size: 12px;
|
||||
}
|
||||
div.post img{
|
||||
max-width: 100%;
|
||||
}
|
44
templates/base.html
Normal file
44
templates/base.html
Normal file
@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>feedmode</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}">
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper" class="{% block pagetype %}{% endblock %}">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
window.addEventListener('load', function () {
|
||||
|
||||
// Insert the nav buttons, after the page is loaded
|
||||
const nav = document.createElement('div');
|
||||
nav.id = 'nav';
|
||||
|
||||
nav.innerHTML = `
|
||||
<h1><em class="feedmode">feedmode</em><sup>(very beta)</sup></h1>
|
||||
<form id="update" action="/update/" method="post">
|
||||
<input type="text" name="query" value="{% if query %}{{ query }}{% else %}https://multi.vvvvvvaria.org/API/latest/50{% endif %}">
|
||||
<input id="html" type="submit" value="update">
|
||||
</form>
|
||||
<div id="buttons">
|
||||
<a href="/select/"><button>select</button></a>
|
||||
<a href="/preview/"><button>preview</button></a>
|
||||
<a href="/pdf/"><button>pdf</button></a>
|
||||
<a href="/stylesheet/"><button>stylesheet</button></a>: <input type="text" name="pad" value="https://pad.vvvvvvaria.org/feedmode.css">
|
||||
<a href="/template/"><button>template</button></a>: <input type="text" name="pad" value="https://pad.vvvvvvaria.org/feedmode.template">
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.insertBefore(nav, document.body.firstChild);
|
||||
|
||||
})
|
||||
</script>
|
||||
{% block footer %}
|
||||
{% endblock %}
|
||||
</html>
|
9
templates/html.html
Normal file
9
templates/html.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block pagetype %}
|
||||
scroll
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ html | safe }}
|
||||
{% endblock %}
|
5
templates/iframe.html
Normal file
5
templates/iframe.html
Normal file
@ -0,0 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<iframe src="{{ url }}"></iframe>
|
||||
{% endblock %}
|
10
templates/multifeeder-template.html
Normal file
10
templates/multifeeder-template.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% for post in response %}
|
||||
<div class="post">
|
||||
<!-- <input type="checkbox" name="check" value="{{ loop.index }}"><br><br> -->
|
||||
<h1>{{ post.title }}</h1>
|
||||
<!-- <div><small>{{ post.author }}</small></div> -->
|
||||
<div><small>{{ post.published }}</small></div>
|
||||
<div><small><a href="{{ post.link }}">{{ post.link }}</a></small></div>
|
||||
<div>{{ post.summary }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
5
templates/pad.html
Normal file
5
templates/pad.html
Normal file
@ -0,0 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<iframe src="https://pad.vvvvvvaria.org/{{ name }}{{ ext }}"></iframe>
|
||||
{% endblock %}
|
36
templates/pandoc-template.html
Normal file
36
templates/pandoc-template.html
Normal file
@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{% if mode == "print" %}
|
||||
<script src="/static/paged.js" type="text/javascript"></script>
|
||||
<script src="/static/paged.polyfill.js" type="text/javascript"></script>
|
||||
<link href="/static/pagedjs.css" rel="stylesheet" type="text/css" media="screen">
|
||||
{% endif %}
|
||||
<link href="/print.css" rel="stylesheet" type="text/css" media="{{ mode }}">
|
||||
<title>The Everydaily</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<section id="cover">
|
||||
<h1 id="title">The Everydaily</h1>
|
||||
</section>
|
||||
|
||||
<section id="header">
|
||||
<div>This is the header...</div>
|
||||
</section>
|
||||
|
||||
<section id="feed">
|
||||
{{ feed }}
|
||||
</section>
|
||||
|
||||
<section id="footer">
|
||||
<div>Anything in the footer here...</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
30
templates/pdf.html
Normal file
30
templates/pdf.html
Normal file
@ -0,0 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<iframe id="pdf" name="pdf" src="/pagedjs.html"></iframe>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
<script>
|
||||
function printPage(){
|
||||
window.frames["pdf"].focus();
|
||||
window.frames["pdf"].print();
|
||||
}
|
||||
|
||||
window.addEventListener('load', function () {
|
||||
|
||||
// Load the main.css again, to load the stylesheet for the nav
|
||||
var cssLink = document.createElement('link');
|
||||
cssLink.rel = 'stylesheet';
|
||||
cssLink.href = '/static/main.css';
|
||||
var head = document.getElementsByTagName('head')[0];
|
||||
head.insertBefore(cssLink, head.firstChild);
|
||||
|
||||
// Insert the SAVE button
|
||||
const nav = document.getElementById('buttons');
|
||||
const save = '<a href="#"><button id="save" onClick="printPage()">save</button></a>';
|
||||
nav.innerHTML = nav.innerHTML + save;
|
||||
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
21
templates/selection-template.html
Normal file
21
templates/selection-template.html
Normal file
@ -0,0 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" type="text/css" href="/static/select.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block pagetype %}
|
||||
scroll
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% for post in feed %}
|
||||
<div class="post">
|
||||
<input type="checkbox" name="check" value="{{ loop.index }}"><br><br>
|
||||
<h1>{{ post.title }}</h1>
|
||||
<div><small>{{ post.published }}</small></div>
|
||||
<div><small><a href="{{ post.link }}">{{ post.link }}</a></small></div>
|
||||
<div>{{ post.summary | safe }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
Loading…
Reference in New Issue
Block a user