How to generate RSS feed with ROME and Spring Boot
The modern internet is highly centralized. While independent websites still exist, we rarely go beyond the firsrt few search results and get news from social media. These platforms are the internet for many people because they not only help old friends stay in touch, but also act as content aggregators and providers, enabling algorithms to control what we see.
Recent changes on now X and Reddit pushed me to take a look at decentralized solutions. I joined Lemmy and Mastodon. Both are significantly smaller than their commercial counterparts and don't have as much content, which makes mindless scrolling for hours impossible. This change alone freed up a lot of time and helped me realize how addictive these services are.
On the other hand, the absence of a 'smart feed' puts responsibility of finding new content on the user. As I discovered, people continue to post on personal blogs, and some websites still generate RSS feeds. I believe it's important to have full control over our feeds. The cheap pleasure that comes with scrolling social media harms our lives and distracts us from our long-term goals. Instead of doing something useful, we spend endless hours arguing with strangers and watching memes.
Algorithms' job is to keep us engaged, they don't care whether we get useful information or engage in benificial activities in the process. In fact, platforms like Facebook profit off hate speech and misinformation. See also: How Facebook Became a Tool for Genocide.
To be part of the solution and not the problem, I decided to launch this blog a couple of months ago. Now, I want to introduce an RSS feed that should make following me easier. Let's see how I implemented it with ROME, Spring Boot, Docker and Nginx!
Idea
I had the following algorithm in my head:
- Generate an RSS feed.
- Write the feed to a file.
- Put the file to a directory that Nginx exposes to the web.
- Add a link to the file in the webapp.
One might argue that the backend service could handle steps 2 and 3 since it already has numerous controllers. Indeed, this is a viable option. However, generating the feed for each request is a resource-consuming operation, although it isn't really heavy or abnormal for a Spring Boot app. I could also add caching. After all, a bunch of RSS readers pulling the feed 24/7 might create some load. But let's be honest, my blog is not operating at that scale.
The simple fact that there's absolutely no need to generate the feed dynamically convinced me to go with the initial plan.
Generate feed
To generate the feed, I chose the ROME library. According to Maven, it's the most popular library for this job, the development is still in progress, plus it comes with good documentation. I used this article as a reference.
First, get posts by calling the PostService
:
List<SyndEntry> entries = postService.getPosts(1, rssFeedItems)
.getItems()
.stream()
.map(this::mapPostToEntry)
.toList();
The service already had had the getPosts(page, pageSize)
method implemented for the respective API controller. Here, I had to add a mapper to convert my posts to feed entires.
private SyndEntry mapPostToEntry(PostPreviewDto post) {
SyndContentImpl description = new SyndContentImpl();
description.setValue(post.getSummary());
SyndEntry entry = new SyndEntryImpl();
entry.setUri(post.getId());
entry.setTitle(post.getTitle());
entry.setDescription(description);
entry.setLink(format(rssFeedLinkFormat, post.getId()));
return entry;
}
Another option would be to implement the SyndEntry
interface for my PostPreviewDto
to avoid this conversion. I didn't do this because the interface defines 40 methods, it'd be confusing to have all these extra methods in the PostPreviewDto
class. Imagine how code suggestions would look like!
It'd also be redundant, as I'm using only four fields:
- Uri or guid - a unique identifier of the post, i.e. permanent url or id from the database.
- Title - a title of the post.
- Description - a short description of the post. I decided to put the post's summary there, that's the text that you see on the front page.
- Link - a link to the full post.
This is how one of the posts looks in Miniflux:
Yes, I've intentionally picked up a post without markdown in its description for the screenshot. RSS readers don't convert markdown to HTML. I have to address this issue in the future.
Now that entries have been retrieved and mapped, the feed object can be created:
SyndFeedImpl feed = new SyndFeedImpl();
feed.setFeedType(rssFeedType);
feed.setTitle(rssFeedTitle);
feed.setDescription(rssFeedDescription);
feed.setLink(rssFeedWebsiteLink);
feed.setEntries(entries);
ROME supports various kinds of RSS and Atom, it's up to the user to decide which to use. I chose rss_2.0
. Discrepancies between different versions don't matter to me, at least at this point, so let it be the latest edition of the RSS specification. Other fields, I suppose, are self-explanatory.
The feed can finally be saved to a file:
try (FileWriter writer = new FileWriter(rssFeedOutputFile)) {
new SyndFeedOutput().output(feed, writer);
}
Schedule task
Spring Boot comes with a built-in scheduler. A couple of annotations enable it and mark the method that should be executed under the specified condition.
@Configuration
@EnableScheduling
public class JobScheduler {
@Scheduled(cron = "${rss.feed.cronExpression}")
public void generateRssFeed() {
// code that handles the feed generation
// we've discussed it above
}
}
I went with a cron expression because it's a familiar tool that allows fine-tuning. Here are the values that I used locally for testing:
# every hour
rss.feed.cronExpression=0 0 * * * *
# every minute
rss.feed.cronExpression=0 * * * * *
# every second
rss.feed.cronExpression=* * * * * *
You can see in my repository how the feed generator and job scheduler work together.
Serve output
The code above generates a file on the disk, which is not available for the end user yet. Thanks to my poor knowledge of Docker and Docker Compose, this part of the task turned out to be the hardest! Well, now I know a little bit more, and that's one of the goals behind this blog.
As I said at the beginning, Nginx is tasked to serve the feed. The following configuration exposes /path/to/directory/with/static/files
, where feed.xml
is stored, to the outer world:
location /static {
alias /path/to/directory/with/static/files;
}
Only the file is not in this directory yet. The backend service is running in a Docker container with its own filesystem, and we have to map it to a file on the host system that Nginx is working with.
To achieve that, the container had to be reconfigured:
services:
backend:
# Long story short...
volumes:
- /path/to/directory/with/static/files/feed.xml:/backend/feed.xml
The single volumes
entry says:
- Okay, there's a
/path/to/directory/with/static/files/feed.xml
file on the host system. - I want to map it to a
/backend/feed.xml
file in the container. - This way, when
/backend/feed.xml
changes, the updatedfeed.xml
will be served to users pulling it viahttp://website.com/static/feed.xml
.
Here's one caveat I wasn't aware of: /path/to/directory/with/static/files/feed.xml
should exist before building the image. Otherwise, Docker will create a directory called feed.xml
, which is not what we need here.
Conclusion
The Java ecosystem is fantastic, it provides powerful and established libraries. There's plenty of documentation and examples online, you can always find some help. I especially appreciate this after working with Rust, Actix and Diesel. Don't get me wrong, these are great tools. It's just that the community is not that big yet.
The feature is completed and deployed: now, if one wishes to follow my blog, he or she can add my RSS feed to their reader. It's also possible to map it to some other medium, as there are various RSS <--> Something Else
bridges. I also had to update the webapp, and with that, I made little improvements, mostly aimed at users browsing with their mobile phones. Great success!