Compare commits

...

10 Commits

Author SHA1 Message Date
0be5ad8b28 feat: Add post on django+rss
Some checks failed
ci/woodpecker/manual/woodpecker Pipeline failed
2024-04-16 21:58:24 +02:00
cbd90cdfb1 chore: .gitignore extension 2023-11-10 12:56:22 +01:00
0b9ee1bae3 refactor: Move to folder 2023-11-10 12:55:08 +01:00
f0c8dda93e fix: Oxitraffic screenshot 2023-11-10 12:51:04 +01:00
767549318f feat: Add oxitraffic header 2023-11-10 12:47:06 +01:00
b6f302f822 feat: Add oxitraffic post 2023-11-10 12:46:14 +01:00
20a189c9e9 3 2023-11-09 05:08:46 +01:00
f77af5e85f 2 2023-11-09 05:01:55 +01:00
a29ac5481c fix: Typo 2023-11-09 04:51:46 +01:00
238bde7d6f feat: Add oxitraffic 2023-11-09 04:50:00 +01:00
11 changed files with 552 additions and 14 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
# Hugo default output directory
/public
*.lock

View File

@ -19,6 +19,11 @@ paginate = 5 #frontpage pagination
respectDoNotTrack = true
useSessionStorage = true
[tracking]
[tracking.oxitraffic]
enabled = true
hostname = "traffic.hyteck.de"
[params]
# Unmark to use post folder for images. Default is static folder.
#usepostimgfolder = true
@ -34,20 +39,8 @@ paginate = 5 #frontpage pagination
'&copy; 2023 CC-BY Julian-Samuel Gebühr</a> '
]
# Contact page
# Since this template is static, the contact form uses www.formspree.io as a
# proxy. The form makes a POST request to their servers to send the actual
# email. Visitors can send up to a 1000 emails each month for free.
#
# What you need to do for the setup?
#
# - set your email address under 'email' below (it is also used in Gravatar for the bio).
# - upload the generated site to your server
# - send a dummy email yourself to confirm your account
# - click the confirm link in the email from www.formspree.io
# - you're done. Happy mailing!
email = "julian-samuel@gebuehr.net"
oxitraffic_url = "https://traffic.hyteck.de/count.js"
# Nav links in the side bar
[[menu.main]]

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -0,0 +1,160 @@
---
title: "Styling an Django RSS Feed"
date: 2024-04-16T12:10:10+02:00
draft: false
image: "django_rss.png"
categrories: ['English']
tags: ['django', 'rss', 'privacy', 'rss-styling', 'xml', 'xsl', 'atom', 'feed', 'rss-feed']
---
## Introduction
RSS is amazing! While not everyone thinks that, most people that *understand* RSS like it. This presents a problem as most people don't have chance to learn about it. Unless there is a person in the community that doesn't shut up about how great RSS is, they might not even know what it is, let alone use it.
One big reason for this is, that when you click an link to an RSS feed you download a strange file or you browser is nice and renders some XML which is also not meant for human consumption. Wouldn't it be nice if people clicked on the RSS link and were greeted by a text explaining RSS and how to use it? And if the site would still be a valid RSS feed?
Luckily you don't have to imagine that - it's possible! You can even try it on this blog by clicking the RSS link in the menu.
To do this has not been my idea. Darek Kay described this in the blog post [Style your RSS feed](https://darekkay.com/blog/rss-styling/) and I just copied most of their work! This was fairly easy for this Hugo-blog and is [available in my for of the hugo-nederburg-theme](https://github.com/moan0s/hugo-nederburg-theme). However, in a Django project it get's a bit more complicated. Let me explain.
## The Problem
Django has the great [Syndication feed framework](https://docs.djangoproject.com/en/5.0/ref/contrib/syndication/), a high level framework to create RSS and Atom Feeds. This is great as we only need a few lines of code to create a feed. Here is an example from [notfellchen.org](https://notfellchen.org) that list animals that are in search for a new home. People should be able to follow the RSS feed to see new adoption notices. So lets do it
```python
# in src/fellchennasen/feeds.py
from django.contrib.syndication.views import Feed
from .models import AdoptionNotice
class LatestAdoptionNoticesFeed(Feed):
title = "Notfellchen"
link = "/rss/"
description = "Updates zu neuen Vermittlungen."
def items(self):
return AdoptionNotice.objects.order_by("-created_at")[:5]
def item_title(self, item):
return item.name
def item_description(self, item):
return item.description
```
```python
# in src/fellchennasen/urls.py
urlpatterns = [
path("", views.index, name="index"),
path("rss/", LatestAdoptionNoticesFeed(), name="rss"), # <--- Added
...
```
Wait that's it? Yeah! We have a working RSS feed. And it was very convenient, Django allows us to create by just pointing it to the right model and fields we want to display.
But here is the problem: How do we style this? We can't just add a link to a stylesheet here.
## The solution
First we need to add our styling files. I'll not go into detail how they work her, just refer [Darek's blog post](https://darekkay.com/blog/rss-styling/) for that. In Django we add them to our static files
* `static/rss.xsl` will be adjusted based on [this file](rss.xsl). It is responsible for creating a html rendering of your XML file
* for `static/css/rss-styles` you can drop in [this file](rss-styles.css), which is a basic CSS file you can edit to your liking.
After that comes the hard part. How do tweak this wonderfully simple Feed class to include a link to our style sheet? I first thought "that must be easy, just follow the docs on [custom feed generators](https://docs.djangoproject.com/en/5.0/ref/contrib/syndication/#custom-feed-generators) and add a root element. Something like this:
```python
class FormattedFeed(Rss201rev2Feed):
def add_root_elements(self, handler):
super().add_root_elements(handler)
# We want <?xml-stylesheet href="/static/rss.xsl" type="text/xsl"?>
handler.addQuickElement("?xml-stylesheet", f'href="{static("rss.xsl")}"')
class LatestAdoptionNoticesFeed(Feed):
feed_type = FormattedFeed
title = "Notfellchen"
...
```
Looks good. Let's try. Oh no what is this?
```xml
<?xml-stylesheet href="/static/rss.xsl"/>
```
Yes, we can't correctly close this tag. There is (to my knowledge) no easy way to do this. So let's take the hard road an implement a custom write function. In the following the write function will be copied from `django.utils.feedgenerator.RssFeed`. We make two important changes to the class:
1. Changing the content type from `content_type = "application/rss+xml; charset=utf-8"` to `content_type = "text/rss+xml; charset=utf-8`. This will make a browser display the content rather than opening it in a app.
2. Adding our xml-stylsheet information. This is done in the `write()` function with this line
```python
handler._write(f'<?xml-stylesheet href="{static("rss.xsl")}" type="text/xsl"?>')
```
Putting it all together we still have a relativly simple solution with only the necessary adjustments. Here is the full `feeds.py`:
```python
from django.contrib.syndication.views import Feed
from django.utils.feedgenerator import Rss201rev2Feed
from django.templatetags.static import static
from django.utils.xmlutils import SimplerXMLGenerator
from .models import AdoptionNotice
class FormattedFeed(Rss201rev2Feed):
content_type = "text/xml; charset=utf-8"
def write(self, outfile, encoding):
handler = SimplerXMLGenerator(outfile, encoding, short_empty_elements=True)
handler.startDocument()
handler._write(f'<?xml-stylesheet href="{static("rss.xsl")}" type="text/xsl"?>')
handler.startElement("rss", self.rss_attributes())
handler.startElement("channel", self.root_attributes())
self.add_root_elements(handler)
self.write_items(handler)
self.endChannelElement(handler)
handler.endElement("rss")
class LatestAdoptionNoticesFeed(Feed):
feed_type = FormattedFeed
title = "Notfellchen"
link = "/rss/"
description = "Updates zu neuen Vermittlungen."
def items(self):
return AdoptionNotice.objects.order_by("-created_at")[:5]
def item_title(self, item):
return item.name
def item_description(self, item):
return item.description
```
And finally we have what we want! A RSS feed displayed in the browser, with beginner-friendly explanation and still completely spec-compliant.
![Screenshot of a website](screenshot1.jpeg)
## Outlook
Now you may recognize I'm not a frontend person. The style could be prettier and provide a better overview. But I'd argue the improvement is immense and might help a user to get started with RSS.
There are still a couple things to improve:
* Translation: The current text is only displayed in english
* The `rss.xsl` file has a hard-coded link to the css stylesheet in it
```html
<link rel="stylesheet" type="text/css" href="/static/fellchensammlung/css/rss-styles.css"/>
```
Both can be solved by templating the `rss.xsl` instead of serving it as static file.
So have fun playing around! If you have created or found a nice-looking RSS feed let me know. Let's keep RSS alive and thriving!
{{< chat "django-rss" >}}

View File

@ -0,0 +1,57 @@
:root {
--background-color: #727272;
--background-color-dark: #2a2a2a;
--text-color: #000000;
--link-color: rgb(10, 10, 42);
--text-background: #aaaaaa;
}
body {
display: flex;
flex-direction: column;
background-color: var(--background-color-dark);
color: var(--text-color);
}
alert-box[type="info"] {
--alert-border: var(--background-color);
--alert-background: var(--text-background);
}
alert-box {
display: block;
margin: 3rem 0;
padding: 2rem 3rem;
border: 1px solid var(--alert-border);
border-left-width: .5rem;
border-radius: .4rem;
background-color: var(--alert-background);
}
a {
color: inherit;
text-decoration: none;
}
.post-summary {
margin: 1rem;
padding: 5px;
border-radius: .4rem;
background-color: var(--text-background);
}
.rss-summary {
padding: 15px;
border-radius: .4rem;
background-color: var(--background-color);
}
.post-summary h1 {
color: var(--link-color);
font-size: large;
}
.inline-icon {
height: 1.5rem;
width: 1.5rem;
}

View File

@ -0,0 +1,221 @@
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:atom="http://www.w3.org/2005/Atom">
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>
RSS Feed |
<xsl:value-of select="/atom:feed/atom:title"/>
</title>
<meta charset="utf-8"/>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" type="text/css" href="/static/fellchensammlung/css/rss-styles.css"/>
</head>
<body>
<main>
<alert-box type="info">
<strong>This is an RSS feed</strong>. Subscribe by copying
the URL from the address bar into your newsreader. Visit <a
href="https://aboutfeeds.com">About Feeds
</a> to learn more and get started. Its free.
</alert-box>
<div class="rss-summary">
<h1 class="flex items-start">
RSS Feed Preview
<svg
class="inline-icon"
version="1.1"
width="128px"
height="128px"
id="RSSicon"
viewBox="0 0 256 256"
sodipodi:docname="Feed-icon.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview32"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="2.9085291"
inkscape:cx="161.07798"
inkscape:cy="133.22886"
inkscape:window-width="2048"
inkscape:window-height="1252"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="RSSicon" />
<defs
id="defs17">
<linearGradient
x1="0.085"
y1="0.085"
x2="0.915"
y2="0.915"
id="RSSg">
<stop
offset="0.0"
stop-color="#E3702D"
id="stop2" />
<stop
offset="0.1071"
stop-color="#EA7D31"
id="stop4" />
<stop
offset="0.3503"
stop-color="#F69537"
id="stop6" />
<stop
offset="0.5"
stop-color="#FB9E3A"
id="stop8" />
<stop
offset="0.7016"
stop-color="#EA7C31"
id="stop10" />
<stop
offset="0.8866"
stop-color="#DE642B"
id="stop12" />
<stop
offset="1.0"
stop-color="#D95B29"
id="stop14" />
</linearGradient>
</defs>
<rect
width="256"
height="256"
rx="55"
ry="55"
x="0"
y="0"
fill="#CC5D15"
id="rect19"
style="fill:#414141;fill-opacity:1" />
<rect
width="246"
height="246"
rx="50"
ry="50"
x="5"
y="5"
fill="#F49C52"
id="rect21"
style="fill:#414141;fill-opacity:1" />
<rect
width="236"
height="236"
rx="47"
ry="47"
x="10"
y="10"
fill="url(#RSSg)"
id="rect23"
style="fill:#414141;fill-opacity:1" />
<circle
cx="68"
cy="189"
r="24"
fill="#FFF"
id="circle25" />
<path
d="M160 213h-34a82 82 0 0 0 -82 -82v-34a116 116 0 0 1 116 116z"
fill="#FFF"
id="path27" />
<path
d="M184 213A140 140 0 0 0 44 73 V 38a175 175 0 0 1 175 175z"
fill="#FFF"
id="path29" />
<rect
width="256"
height="256"
rx="55"
ry="55"
x="299.70761"
y="188.99872"
fill="#CC5D15"
id="rect19-3"
style="fill:#414141;fill-opacity:1" />
<rect
width="246"
height="246"
rx="50"
ry="50"
x="304.70761"
y="193.99872"
fill="#F49C52"
id="rect21-6"
style="fill:#414141;fill-opacity:1" />
<rect
width="236"
height="236"
rx="47"
ry="47"
x="309.70761"
y="198.99872"
fill="url(#RSSg)"
id="rect23-7"
style="fill:#414141;fill-opacity:1" />
<circle
cx="367.70761"
cy="377.99872"
r="24"
fill="#ffffff"
id="circle25-5" />
<path
d="m 459.7076,401.99872 h -34 a 82,82 0 0 0 -82,-82 v -34 a 116,116 0 0 1 116,116 z"
fill="#ffffff"
id="path27-3" />
<path
d="m 483.7076,401.99872 a 140,140 0 0 0 -140,-140 v -35 a 175,175 0 0 1 175,175 z"
fill="#ffffff"
id="path29-5" />
</svg>
</h1>
<h2><xsl:value-of select="/rss/channel/title"/></h2>
<p>
<xsl:value-of select="/atom:feed/atom:subtitle"/>
</p>
<a>
<xsl:attribute name="href">
<xsl:value-of select="/atom:feed/atom:link/@href"/>
</xsl:attribute>
Visit Website &#x2192;
</a>
<h2>Adoption Notices</h2>
<xsl:for-each select="/rss/channel/item">
<div class="post-summary">
<h1>
<a>
<xsl:attribute name="href">
<xsl:value-of select="atom:link/@href"/>
</xsl:attribute>
<xsl:value-of select="title"/>
</a>
</h1>
<div class="text-2 text-offset">
<xsl:value-of select="description"/>
</div>
</div>
</xsl:for-each>
</div>
</main>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View File

@ -0,0 +1,106 @@
---
title: "Tracking blog readers with OxiTraffic"
date: 2023-11-10T12:10:10+02:00
draft: false
image: "uploads/oxitraffic.png"
categrories: ['English']
tags: ['MASH', 'tracking', 'privacy', 'ansible', 'docker', hugo]
---
I recently stumbled upon [OxiTraffic](https://codeberg.org/mo8it/oxitraffic), a self-hosted, simple and privacy respecting website traffic tracker which is well suited for blogs. What that means is
* No personal data is logged
* one binary or simple docker container
* Readers are only counted if they spend >20s per site
As I currently have no analytics on my blog and I am not inclined to use anything that adds more than 2 sentences to my privacy disclaimer I thought I give it a try. Naturally I wrote an ansible role for this, which can be found under [mother-of-all-self-hosting/ansible-role-oxitraffic](https://github.com/mother-of-all-self-hosting/ansible-role-oxitraffic). I now have this neat graph.
![A screenshot of OxiTraffic that shows low readership on hyteck.de](oxitraffic_screenshot.jpeg)
As the main prupose of a blog is to describe how to host the blog, I'll continue in this tradition and describe my process below.
# The Ansible Role & Playbook Integration
The ansible role is pretty simple so I won't go into detail. It set's up the configuration file based on your environment variables and sensible defaults and adds a labels file for traefik to use later. The systemd service that starts the container ensures it runs read-only and as non-root user (which worked out of the box, kudos to the developer).
The [mash-playbook](https://github.com/mother-of-all-self-hosting/mash-playbook) integration is wiring the OxiTraffic to the Traefik reverse proxy and the Postgres database.
After running `just install-all` everything was set up\*.
\* Actually I [found a bug which was fixed very fast](https://codeberg.org/mo8it/oxitraffic/issues/7)
# Hugo Theme Integration
I maintain a fork of the [hugo-nederburg-theme](https://github.com/moan0s/hugo-nederburg-theme) by Appernetic and naturally wanted to include it there. Adding the following to `themes/hugo-nederburg-theme/layouts/partials/head.html` is all I needed
```html
{{ with .Site.Params.oxitraffic_url }}
<script src="{{ . }}" defer></script>
{{ end }}
```
I could then make us of this by setting the Oxitraffic URL in the theme settings
```toml
[params]
slogan = "Blog of Julian-Samuel Gebühr"
description = "Blog of Julian-Samuel Gebühr" # meta description
[...]
oxitraffic_url = "https://traffic.hyteck.de/count.js"
```
And that was it. You can have a look at the traffic of this blog at [traffic.hyteck.de](https://traffic.hyteck.de).
# Advanced: Setting up multiple sites in on one MASH host
You might have multiple sites that need tracking, but an instance of OxiTraffic can only monitor one site. Setting up multiple instances of OxiTraffic is more complicated in MASH, but can be done. Here is how (always replace `s3` and `other` with you own names):
1. Re-Do your Inventory as described in [running-multiple-instances](https://github.com/mother-of-all-self-hosting/mash-playbook/blob/main/docs/running-multiple-instances.md#re-do-your-inventory-to-add-supplementary-hosts). I'll use `s3` as my "main" host here and `s3.other` as new host.
2. Add the following in `inventory/host_vars/s3.other`
```yaml
# PLAYBOOK STUFF
mash_playbook_generic_secret_key: 'LONGSECRET'
mash_playbook_service_identifier_prefix: 'mash-other-'
mash_playbook_service_base_directory_name_prefix: 'other-'
# OXITRAFFIC configuration
oxitraffic_enabled: true
oxitraffic_hostname: traffic.other-service.de
oxitraffic_tracked_origin: https://other-service.de
oxitraffic_database_hostname: mash-postgres
oxitraffic_database_port: 5432
oxitraffic_database_name: other-oxitraffic
oxitraffic_database_password: VERYSECRET
oxitraffic_database_username: other-oxitraffic
oxitraffic_systemd_required_services_list: |
{{
(['docker.service'])
+
(['mash-postgres.service'])
}}
oxitraffic_container_additional_networks: |
{{
(['traefik'])
+
(['mash-postgres'])
}}
oxitraffic_container_labels_traefik_enabled: "true"
oxitraffic_container_labels_traefik_docker_network: "traefik"
oxitraffic_container_labels_traefik_entrypoints: "web-secure"
oxitraffic_container_labels_traefik_tls_certResolver: "default"
```
3. Create the database
Unlike for other mash services th database will not be created automatically. You therefore need to set it up yourself. Here are the steps that you need to run in the postgres CLI (which cou can access by running `/mash/postgres/bin/cli`)
* Create a user: `CREATE USER "other-oxitraffic" with ENCRYPTED PASSWORD 'PASSWORD_FROM_ABOVE';`
* Create database: `CREATE DATABASE other-oxitraffic;`
* Grant privileges: `GRANT ALL PRIVILEGES ON DATABASE "other-oxitraffic" TO "other-oxitraffic";`
* Grant ownership: `ALTER DATABASE "other-oxitraffic" OWNER TO "other-oxitraffic";`

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

@ -1 +1 @@
Subproject commit 6bdd03b62ead96a8b0b32490a978ac47e909e1c8
Subproject commit 755f52e6fe6ee26d57566c1bcb4d7cc6a5343e82