Merge pull request 'New frontent and article grouping' (#1) from new-frontend into main

Reviewed-on: https://git.nolog.cz/mdivecky/headline/pulls/1
This commit is contained in:
mdivecky 2023-08-17 11:32:34 +02:00
commit b46af16644
15 changed files with 696 additions and 166 deletions

15
.editorconfig Normal file
View file

@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.py]
indent_style = tab
indent_size = 4
[*.{html,css}]
indent_style = tab
indent_size = 2

5
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"files.associations": {
"*.html": "jinja-html"
}
}

View file

@ -4,7 +4,7 @@ services:
build: ./view/ build: ./view/
command: python app.py command: python app.py
ports: ports:
- "5000:5000" - "5050:5000"
volumes: volumes:
- ./view:/app - ./view:/app
- ./data:/data - ./data:/data

View file

@ -0,0 +1,44 @@
#!/usr/bin/python3
#
# Create a UID of the article in old articles where we don't have RSS UID and where we can't generate the article_id on the fly.
# It takes a while, but it's a one-shot.
#
import sqlite3
import hashlib
db_con = sqlite3.connect("../data/diffs.db")
db = db_con.cursor()
def create_article_id(uid, feed):
# Create a fake unique ID from RSS unique tag and feed name to reference the article in database
id_string = str(uid) + str(feed)
id_bytes = id_string.encode('utf-8')
article_id = hashlib.sha256(id_bytes).hexdigest()
return(article_id)
def update_diff(diff_id, article_id):
sql = "UPDATE diffs SET article_id = ? WHERE diff_id = ?"
sql_data = (article_id, diff_id)
db.execute(sql, sql_data)
db_con.commit()
db.execute(
"SELECT * FROM diffs WHERE NOT 'article_id' ORDER BY diff_id DESC ",
)
diffs = db.fetchall()
for diff in diffs:
article_id = create_article_id(diff[1], diff[2])
update_diff(diff[0], article_id)
print(article_id)

View file

@ -6,6 +6,7 @@ import redis
import time import time
import json import json
import sqlite3 import sqlite3
import hashlib
from diff_match_patch import diff_match_patch from diff_match_patch import diff_match_patch
@ -34,6 +35,7 @@ db = db_con.cursor()
db.executescript(""" db.executescript("""
CREATE TABLE IF NOT EXISTS diffs ( CREATE TABLE IF NOT EXISTS diffs (
diff_id INTEGER PRIMARY KEY, diff_id INTEGER PRIMARY KEY,
article_id TEXT,
feed_name TEXT NOT NULL, feed_name TEXT NOT NULL,
article_url TEXT NOT NULL, article_url TEXT NOT NULL,
title_orig TEXT NOT NULL, title_orig TEXT NOT NULL,
@ -84,8 +86,8 @@ def process_diff(old, new, rss_id):
# print(old['link']) # print(old['link'])
# print(diff) # print(diff)
sql = "INSERT INTO diffs(feed_name, article_url, title_orig, title_new, diff_html, diff_time) VALUES (?,?,?,?,?,datetime('now', 'localtime'))" sql = "INSERT INTO diffs(article_id, feed_name, article_url, title_orig, title_new, diff_html, diff_time) VALUES (?,?,?,?,?,datetime('now', 'localtime'))"
sql_data = (old['medium'], old['link'], old['title'], new['title'], html_diff) sql_data = (new['article_id'], old['medium'], old['link'], old['title'], new['title'], html_diff)
db.execute(sql, sql_data) db.execute(sql, sql_data)
db_con.commit() db_con.commit()
@ -108,7 +110,12 @@ def process_item(article, rc):
# Article is new, just create it and exit # Article is new, just create it and exit
write_article(article, rc) write_article(article, rc)
def create_article_id(uid, feed):
# Create a unique ID from RSS unique tag and feed name to reference the article in database
id_string = str(uid) + str(feed)
id_bytes = id_string.encode('utf-8')
article_id = hashlib.sha256(id_bytes).hexdigest()
return(article_id)
for feed in config['feeds']: for feed in config['feeds']:
@ -123,11 +130,13 @@ for feed in config['feeds']:
try: try:
rss_id = item[unique_tag] rss_id = item[unique_tag]
title = item['title'] title = item['title']
article_id = create_article_id(rss_id, name)
#description = item['description'] ## Don't store description for now, as we don't need it and it's big. #description = item['description'] ## Don't store description for now, as we don't need it and it's big.
published = time.strftime('%Y:%m:%d %H:%M:%S %Z %z', item['published_parsed']) published = time.strftime('%Y:%m:%d %H:%M:%S %Z %z', item['published_parsed'])
link = item['link'] link = item['link']
article_data = { article_data = {
'title' : title, 'title' : title,
'article_id': article_id,
#'description': description, #'description': description,
'published' : published, 'published' : published,
'link' : link, 'link' : link,

View file

@ -80,6 +80,16 @@ def index():
) )
@app.route("/article/<path:article_id>")
def article_detail(article_id: str):
db = get_db().cursor()
db.execute("SELECT * FROM diffs WHERE article_id = ?", (article_id,))
result = db.fetchall()
article_url = result[0]['article_url']
# TODO: Handle if nothing is found and return 404 in that case.
return render_template("article_detail.html", article_id=article_id, article_url=article_url, diffs=result )
@app.route('/about') @app.route('/about')
def about(): def about():
return render_template('about.html') return render_template('about.html')

View file

@ -0,0 +1,372 @@
/* Global */
:root {
--border-color: hsl(0 0% 80% / 60%);
--accent-color: hsl(225 90% 50%);
--accent-color-pressed: hsl(225 90% 35%);
--color-muted: hsl(0 0% 50%);
--radius-s: 0.25em;
--radius-m: 0.5em;
--font-size-m: 1rem;
--font-size-s: 0.85rem;
--font-size-xs: 0.75rem;
--font-size-l: 1.25rem;
--box-shadow: 0 2px 0 hsl(0 0% 50% / 20%);
}
html {
box-sizing: border-box;
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
body {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: stretch;
line-height: 1.5;
background-color: hsl(0 0% 98%);
}
*,
*::before,
*::after {
box-sizing: inherit;
}
* {
margin: 0;
}
body {
line-height: 1.5;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
input,
button,
textarea,
select {
font: inherit;
}
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
font-size: inherit;
font-weight: inherit;
}
table,
th,
tr,
td {
text-align: inherit;
border-collapse: collapse;
}
a {
color: var(--accent-color);
font-weight: 500;
text-decoration: none;
}
a:hover {
color: var(--accent-color-pressed);
}
ins {
background-color: hsl(120 100% 95%);
text-decoration-color: hsl(120 50% 75% / 50%);
}
del {
background-color: hsl(0 100% 95%);
text-decoration-color: hsl(0 50% 40% / 50%);
}
code {
font-size: inherit;
}
summary {
cursor: pointer;
list-style: none;
display: flex;
align-items: center;
gap: 0.5em;
}
summary::-webkit-details-marker {
display: none;
}
summary::before {
content: url('data:image/svg+xml,<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13.1714 12.0007L8.22168 7.05093L9.63589 5.63672L15.9999 12.0007L9.63589 18.3646L8.22168 16.9504L13.1714 12.0007Z"></path></svg>');
display: grid;
place-content: center;
transition: transform 120ms;
}
details[open] summary::before {
transform: rotate(90deg);
}
.header {
padding-top: 1rem;
padding-bottom: 1rem;
background-color: white;
}
.header:not(.header-extended) {
border-bottom: 1px solid var(--border-color);
margin-bottom: 2rem;
}
.header .container {
display: flex;
align-items: center;
gap: 2rem;
}
.header nav {
display: flex;
align-items: center;
gap: 1rem;
}
.header-link-home {
color: inherit;
text-decoration: none;
}
.header h1 {
font-size: 1.5rem;
font-weight: 700;
}
.header h1 .del {
text-decoration: line-through;
text-decoration-thickness: 2px;
}
.header h1 .ins {
text-decoration: underline;
text-decoration-thickness: 1px;
text-decoration-style: wavy;
}
.main {
margin-bottom: auto;
}
.footer {
margin-top: 4rem;
padding: 1.5rem 0;
color: hsl(0 0% 60%);
}
.footer a {
color: inherit;
}
.footer-container {
display: flex;
align-items: center;
gap: 0.5rem;
}
.footer-nologo {
display: inline-block;
fill: hsl(0 0% 60%);
transition: fill 120ms;
}
.footer-nologo:hover {
fill: hsl(0 0% 40%);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
}
.input {
border-radius: var(--radius-s);
border: 1px solid var(--border-color);
padding: 0.375rem 0.75rem;
font-size: var(--font-size-m);
transition: border-color 120ms, box-shadow 120ms;
}
.input:focus {
border-color: var(--accent-color);
box-shadow: 0 0 0 2px hsl(225 90% 40% / 50%);
outline: none;
}
.button {
display: inline-flex;
align-items: center;
gap: 0.5em;
border-radius: var(--radius-s);
border: 1px solid var(--border-color);
padding: 0.375rem 0.75rem;
background: hsl(0 0% 95%);
font-size: var(--font-size-m);
transition: background-color 120ms;
color: hsl(0 0% 40%);
font-weight: 500;
font-size: 0.9em;
line-height: 1.5rem;
}
.button:not(:disabled) {
cursor: pointer;
}
.button:hover {
background: hsl(0 0% 90%);
}
.card {
border: 1px solid var(--border-color);
border-radius: var(--radius-m);
background-color: white;
box-shadow: 0 2px 0 hsl(0 0% 50% / 20%);
overflow: hidden;
}
.pagination {
list-style-type: none;
padding: 0;
display: flex;
margin-bottom: 1rem;
}
.page-link {
display: block;
padding: 0.5em 1em;
text-decoration: none;
color: inherit;
}
.page-item {
border: 1px solid var(--border-color);
background: hsl(0 0% 95%);
transition: background-color 120ms;
color: hsl(0 0% 40%);
font-weight: 500;
font-size: 0.85em;
line-height: 1.5rem;
}
.page-item:not(.active):hover {
background: hsl(0 0% 90%);
}
.page-item:first-of-type {
border-radius: var(--radius-s) 0 0 var(--radius-s);
}
.page-item:last-of-type {
border-radius: 0 var(--radius-s) var(--radius-s) 0;
}
.page-item:not(:last-of-type) {
border-right-width: 0px;
}
.page-item.active {
background-color: var(--accent-color);
color: white;
border-color: transparent;
}
.pagination-page-info {
color: var(--color-muted);
}
.prose p:not(:last-of-type) {
margin-bottom: 1rem;
}
/* Index */
.filters {
margin-bottom: 2rem;
position: sticky;
top: 0;
background-color: white;
padding: 0.75rem 0;
border-bottom: 1px solid var(--border-color);
}
.changesets {
margin-bottom: 2rem;
}
.changeset {
padding: 1rem 1.5rem;
margin-bottom: 1rem;
}
.changeset-actions {
display: flex;
gap: 1rem;
margin-bottom: 0.75rem;
}
.changeset-feed-name,
.changeset-time,
.changeset-action {
font-weight: 500;
font-size: var(--font-size-s);
}
.changeset-action {
display: inline-flex;
align-items: center;
gap: 0.5em;
font-weight: 500;
color: var(--color-muted);
text-decoration: none;
transition: color 200ms;
}
.changeset-action:first-of-type {
margin-left: 0.5em;
}
.changeset-title {
font-size: var(--font-size-l);
}
.inline-icon {
display: inline-block;
fill: currentColor;
width: 1.25em;
}
.changeset details[open] summary {
margin-bottom: 1rem;
}
.changeset table th {
padding-right: 1rem;
}

View file

@ -1,42 +1,32 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="en" class="h100">
<head> {% block head %}
{% include 'parts/head.html' %} <style>
<style> .prose {
font-size: var(--font-size-l);
max-width: 800px;
}
</style>
{% endblock head %}
html { {% block body %}
position: relative; <div class="container">
min-height: 100%; <div class="prose">
} <p>
Headliner is monitoring rss feeds of czech news websites for changes in
article headlines. Just because it might be interesting.
</p>
<p>
Check out <a href="https://git.nolog.cz/mdivecky/headline">the source code</a>,
but be aware that it's not too nice. Feel free to improve it or run the tool yourself.
</p>
body { <p>
margin-bottom: 60px; /* Margin bottom by footer height */ If you want to access the raw data collected by this tool, you can
} <a href="https://git.nolog.cz/NoLog.cz/headline-exports"> download the full archive from our git repo.</a>
</p>
.footer { <p><a href="https://ondrej.nyv.lt/">Ondřej</a> and <a href="https://bain.cz">Bain</a> made important contribution to the project. Thank you!</p>
position: absolute; </div>
bottom: 0; </div>
width: 100%; {% endblock body %}
}
</style>
</head>
<body class="d-flex flex-column h-100">
<!-- Begin page content -->
<main class="flex-shrink-0">
<div class="container">
<h1 class="mt-5">Headliner</h1>
<p class="lead">Headliner is monitoring rss feeds of czech news websites for changes in article headlines. Just
because it might be interesting.</p>
<p>See <a href="https://git.nolog.cz/mdivecky/headline">the source code</a>, but be aware that it's not too nice.
Feel free to improve it.</p>
<p>If you want to access the raw data collected by this tool, you can <a href="https://git.nolog.cz/NoLog.cz/headline-exports">download the full archive from our git</a></p>
</div>
</main>
{% include 'parts/footer.html' %}
</body>
</html>

View file

@ -0,0 +1,47 @@
{% extends "base.html" %}
{% block head %}
<style>
.page-heading {
font-size: var(--font-size-l);
margin-bottom: 2rem;
}
.diffs-list {
list-style-type: none;
padding: 0;
}
.diff-table th, .diff-table td {
padding: 0.5rem 0.75rem;
}
.diff-table th {
font-size: var(--font-size-xs);
color: var(--color-muted);
font-weight: 500;
}
.diff-table tr:not(:last-of-type) {
border-bottom: 1px solid var(--border-color);
}
</style>
{% endblock head %}
{% block body %}
<div class="container">
<h1>Diffs for the article at <a href="{{ article_url }}">{{ article_url|truncate(50) }}</a></h1>
<br>
<div class="card">
<table class="diff-table">
{% for diff in diffs %}
<tr>
<th>{{ diff.diff_time }}</th>
<td>{{ diff.diff_html|safe }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endblock body %}

41
view/templates/base.html Normal file
View file

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Headliner</title>
<link
rel="shortcut icon"
href="{{ url_for('static', filename='favicon.ico') }}"
/>
<link
rel="stylesheet"
href="{{ url_for('static', filename='main.css') }}"
/>
<script
defer
data-domain="headline.beta.nolog.cz"
src="https://plausible.nolog.cz/js/plausible.js"
></script>
{% block head %}{% endblock %}
</head>
<body>
<header class="header {% block header_class %}{% endblock %}">
<div class="container">
<a class="header-link-home" href="/">
<h1>
<span class="del"> Head</span><span class="ins">liner</span>
</h1>
</a>
<nav>
<a href="/about">About</a>
<a href="/feeds">Feeds</a>
</nav>
</div>
</header>
<main class="main">
{% block body %}{% endblock %}
</main>
{% include "parts/footer.html" %}
</body>
</html>

View file

@ -1,35 +1,27 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="en">
<head> {% block body %}
{% include 'parts/head.html' %} <div class="container">
</head> <div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>RSS/Atom URL</th>
<th>Unique tag</th>
</tr>
</thead>
<tbody>
{% for feed in feeds %}
<tr>
<td>{{ feed.feed_name }}</td>
<td>{{ feed.rss_source | urlize(target="_blank") }}</td>
<td>{{ feed.unique_tag }}</td>
<body> {% endfor %}
</tr>
<div class="container"> </tbody>
<div class="table-responsive"> </table>
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>RSS/Atom URL</th>
<th>Unique tag</th>
</tr>
</thead>
<tbody>
{% for feed in feeds %}
<tr>
<td>{{ feed.feed_name }}</td>
<td>{{ feed.rss_source | urlize(target="_blank") }}</td>
<td>{{ feed.unique_tag }}</td>
{% endfor %}
</tbody>
</table>
</div>
</div> </div>
{% include 'parts/footer.html' %} </div>
</body> {% endblock body %}
</html>

View file

@ -1,80 +1,68 @@
<!DOCTYPE html> {% extends "base.html" %}
<html lang="en">
<head> {% block header_class %}header-extended{% endblock header_class %}
{% include 'parts/head.html' %}
<style> {% block body %}
/* Longer name hidden by default */ <div class="filters">
span.long { <div class="container">
display: none; <form method="get">
} <div class="d-flex">
<input
class="input"
type="text"
id="search"
name="search"
value="{{ search|e }}"
/>
<button
class="button"
type="submit"
value="Hledat"
>
Hledat
<svg class="inline-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z"></path></svg>
</button>
</div>
</form>
</div>
</div>
/* On hover, hide the short name */ <div class="container changesets">
.expanded:hover span.short { {% for diff in diffs %}
display: none; <article class="card changeset">
} <p class="changeset-actions">
<span class="changeset-feed-name">{{ diff.feed_name }}</span>
<time class="changeset-time">{{ diff.diff_time }}</time>
<a class="changeset-action" href="{{ diff.article_url }}">
<svg class="inline-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 6V8H5V19H16V14H18V20C18 20.5523 17.5523 21 17 21H4C3.44772 21 3 20.5523 3 20V7C3 6.44772 3.44772 6 4 6H10ZM21 3V11H19L18.9999 6.413L11.2071 14.2071L9.79289 12.7929L17.5849 5H13V3H21Z"></path></svg>
Display current article
</a>
<a class="changeset-action" href="/article/{{ diff.article_id }}">
<svg class="inline-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM13 12H17V14H11V7H13V12Z"></path></svg>
Show change history
</a>
</p>
<details>
<summary class="changeset-title">
<h2 class="diff">{{ diff.diff_html|safe }}</h2>
</summary>
<table>
<tr>
<th>Old</th>
<td>{{ diff.title_orig }}</td>
</tr>
<tr>
<th>New</th>
<td>{{ diff.title_new }}</td>
</tr>
</table>
</details>
</article>
{% endfor %}
</div>
/* On hover, display the longer name. */ <div class="container">
.expanded:hover span.long { {{ pagination.links }}
display: block; {{ pagination.info }}
} </div>
{% endblock body %}
</style>
</head>
<body>
<div class="container">
<form>
<div class="d-flex">
<input class="m-2 form-control" type="text" id="search" name="search" value="{{ search|e }}" />
<input class="m-2 btn btn-primary" type="submit" formenctype="application/x-www-form-urlencoded" formmethod="get" value="Hledat" />
</div>
</form>
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Detection time</th>
<th>Source</th>
<th>Diff</th>
<th>Original</th>
<th>Changed</th>
</tr>
</thead>
<tbody>
{% for diff in diffs %}
<tr>
<td>{{ diff.diff_time }}</td>
<td><a href='{{ diff.article_url }}' target="_blank">{{ diff.feed_name }}</a></td>
<td>{{ diff.diff_html|safe }}</td>
<td class="expanded">
<span class="short">{{ diff.title_orig|truncate(15) }} </span>
<span class="long">{{ diff.title_orig }} </span>
</td>
<td class="expanded">
<span class="short">{{ diff.title_new|truncate(15) }} </span>
<span class="long">{{ diff.title_new}} </span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="container text-center">
<div class="row">
<div class="col">
{{ pagination.links }}
</div>
<div class="col">
{{ pagination.info }}
</div>
</div>
</div>
</div>
{% include 'parts/footer.html' %}
</body>
</html>

View file

@ -1,5 +1,16 @@
<footer class="footer mt-auto py-3 bg-light"> <footer class="footer">
<div class="container"> <div class="container footer-container">
<span class="text-muted"> <a href="{{ url_for('index') }}">Headliner</a> | <a href="{{ url_for('feed_list') }}">Feed list</a> | <a href="{{ url_for('about') }}">About</a> | <a href="https://nolog.cz">NoLog.cz</a></span> <span>created by</span>
</div> <a href="https://nolog.cz">
<svg class="footer-nologo" width="120" viewBox="0 0 400 60" fill-rule="evenodd">
<title>NoLog</title>
<path d="M60,0l-60,0l0,60l20,0l0,-41l20,0l-0,41l20,0l-0,-60Z"></path>
<path d="M140,60l50,0l-0,-19l-30,0l0,-41l-20,0l0,60Z"></path>
<path d="M130,0l-60,0l0,60l60,0l0,-60Zm-40,19l0,22l20,0l0,-22l-20,0Z"></path>
<path d="M260,0l-60,0l-0,60l60,0l-0,-60Zm-40,19l-0,22l20,0l-0,-22l-20,0Z"></path>
<path d="M270,60l60,-0l-0,-35l-20,0l-0,16l-20,0l-0,-22l40,-0l-0,-19l-60,-0l-0,60Z"></path>
<path d="M372.164,2.865l6.571,20.729l21.265,0l-17.203,12.812l6.571,20.729l-17.204,-12.811l-17.203,12.811l6.571,-20.729l-17.204,-12.812l21.265,0l6.571,-20.729Z"></path>
</svg>
</a>
</div>
</footer> </footer>

View file

@ -1,7 +0,0 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Headliner</title>
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='main.css') }}">
<script defer data-domain="headline.beta.nolog.cz" src="https://plausible.nolog.cz/js/plausible.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">

View file

@ -0,0 +1,13 @@
<header class="header">
<div class="container">
<a href="/">
<h1>
<span class="del"> Head</span><span class="ins">liner</span>
</h1>
</a>
<nav>
<a href="/about">About</a>
<a href="/feeds">Feeds</a>
</nav>
</div>
</header>