diff --git a/content/post/django-rss/django_rss.png b/content/post/django-rss/django_rss.png new file mode 100644 index 0000000..93aac9f Binary files /dev/null and b/content/post/django-rss/django_rss.png differ diff --git a/content/post/django-rss/index.md b/content/post/django-rss/index.md new file mode 100644 index 0000000..ca51d75 --- /dev/null +++ b/content/post/django-rss/index.md @@ -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 + 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 + +``` + + +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'') +``` + +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'') + 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 + +``` + +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" >}} diff --git a/content/post/django-rss/rss-styles.css b/content/post/django-rss/rss-styles.css new file mode 100644 index 0000000..d0e38c5 --- /dev/null +++ b/content/post/django-rss/rss-styles.css @@ -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; + } + \ No newline at end of file diff --git a/content/post/django-rss/rss.xsl b/content/post/django-rss/rss.xsl new file mode 100644 index 0000000..ef5afb9 --- /dev/null +++ b/content/post/django-rss/rss.xsl @@ -0,0 +1,221 @@ + + + + + + + + RSS Feed | + <xsl:value-of select="/atom:feed/atom:title"/> + + + + + + + +
+ + This is an RSS feed. Subscribe by copying + the URL from the address bar into your newsreader. Visit About Feeds + to learn more and get started. It’s free. + +
+

+ RSS Feed Preview + + + + + + + + + + + + + + + + + + + + + + + + + + +

+

+

+ +

+ + + + + Visit Website → + + +

Adoption Notices

+ +
+

+ + + + + + +

+ +
+ +
+
+
+
+
+ + +
+
diff --git a/content/post/django-rss/screenshot1.jpeg b/content/post/django-rss/screenshot1.jpeg new file mode 100644 index 0000000..53d6917 Binary files /dev/null and b/content/post/django-rss/screenshot1.jpeg differ