Building a Jersey News Aggregator for Blogger Using RSS Feeds
Building a Jersey News Aggregator for Blogger Using RSS Feeds
I recently decided to experiment with building a live Jersey news aggregator directly inside Blogger using nothing more than custom HTML, CSS and JavaScript.
The idea was simple:
Get Jersey news from multiple local sources in one place — without constantly jumping between websites filled with adverts, popups and distractions.
What began as a relatively simple RSS experiment gradually evolved into something much more sophisticated:
- live RSS aggregation
- responsive news cards
- dark mode
- search filtering
- headline ticker
- automatic sorting
- source filtering
- article thumbnails
- auto refresh
- fallback default images
- Today.je source extraction
- duplicate story filtering
- mobile responsive design
- curated feed selection
Important Blogger Limitation
One important thing I discovered during testing is that this works far more reliably inside:
- a dedicated Blogger page
- or a full blog post
rather than inside a standard Blogger HTML gadget/widget.
Blogger sidebar gadgets tend to:
- truncate large scripts
- interfere with CSS
- strip JavaScript
- break modern async functions
- conflict with template styling
Whereas placing the code directly into a Blogger page allows much more freedom and stability.
News Sources Successfully Aggregated
The addition of Today.je significantly improved the aggregator because it already aggregates many Jersey sources server-side.
That meant the Blogger page could indirectly pull in additional Jersey content that did not expose reliable public RSS feeds itself.
Curating Sources Rather Than Aggregating Everything
One interesting realisation during development was that:
more feeds does not necessarily create a better news experience.
At first the aggregator simply imported everything from Today.je.
However, this quickly created several problems:
- duplicate stories
- repeated headlines
- clutter
- lower-quality social media posts
- signal-to-noise issues
The eventual solution was to use Today.je selectively rather than blindly.
Direct feeds were retained for:
- Jersey Evening Post
- BBC Jersey
- Bailiwick Express
while Today.je was used only for selected supplemental sources such as:
- Channel ITV
- Channel 103
This transformed Today.je from:
a primary news source
into:
a curated backend enrichment layer.
The result was a cleaner and more focused aggregator.
Avoiding Duplicate Stories
One unexpected issue was duplication.
Because Today.je itself aggregates:
- Jersey Evening Post
- Bailiwick Express
- BBC Jersey
the same story could appear twice:
- once directly from the original feed
- once again through Today.je
The eventual solution was to filter out stories containing:
[BBC Jersey] [Bailiwick Express] [JEP News] [Jersey Evening Post]
before the titles were cleaned and reformatted.
Making Today.je Invisible
Another interesting improvement was extracting the original source names from Today.je headlines.
For example:
[The Jersey Gaming Hub fb] Major esports event announced
became:
- Source: The Jersey Gaming Hub
- Headline: Major esports event announced
The aggregator automatically:
- removed square brackets
- removed “fb” and “Facebook” suffixes
- cleaned duplicated wording
- decoded HTML entities
- normalised source names
This made the feed feel much more like:
a unified Jersey news portal
rather than:
a feed of feeds.
The Image Problem
Some RSS feeds provided:
- proper thumbnails
- embedded images
- media tags
while others provided no images at all.
Today.je in particular often lacked thumbnails.
The solution was:
- attempt image extraction from descriptions
- then use a fallback default image
- and finally use an automatic image error handler
The fallback image also doubled as the branding image for the entire aggregator project.
This produced a much cleaner and more visually consistent layout.
The Ticker Problem
The scrolling headline ticker initially caused several strange formatting issues.
Some feeds returned encoded entities such as:
•
which created broken ticker output.
The eventual solution was to:
- strip HTML tags
- slow the ticker speed
- use simple plain text separators
Optimising the Aggregator for Mobile Devices
Although the aggregator initially worked well on desktop and laptop screens, the first mobile tests revealed several usability problems.
The original layout was essentially:
- desktop-first
- multi-column
- mouse-oriented
which meant that on smaller screens:
- buttons became cramped
- the search bar became awkward
- filters wrapped unpredictably
- text became difficult to read
- the ticker moved too quickly
- cards felt compressed
The solution was to introduce a responsive CSS media query specifically for mobile devices.
The mobile optimisation automatically:
- switches the layout into a single-column design
- stacks controls vertically
- creates larger touch-friendly buttons
- improves spacing
- increases readability
- slows the ticker speed
- reduces visual clutter
This transformed the aggregator from:
a squeezed desktop webpage
into something that felt much more like:
a lightweight mobile news application.
The Final Working Code
After multiple iterations, debugging sessions and refinements, the final Blogger-safe version became surprisingly capable while still remaining lightweight.
The full final code is shown below.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Jersey News Portal</title>
<style>
body {
margin: 0;
background: #f3f3f3;
font-family: Arial, sans-serif;
color: #222;
}
header {
background: linear-gradient(135deg,#8b0000,#cc0000);
color: white;
padding: 30px 20px;
text-align: center;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
header h1 {
margin: 0;
font-size: 42px;
}
header p {
margin-top: 10px;
opacity: 0.9;
}
.topbar {
background: #111;
color: white;
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button,
select,
input {
padding: 10px;
border-radius: 6px;
font-family: Arial;
}
input{
background:#fff;
color:#111;
border:1px solid #ccc;
}
input::placeholder{
color:#666;
}
select{
background:#fff;
color:#111;
border:1px solid #ccc;
}
select option{
background:#fff;
color:#111;
}
button {
cursor: pointer;
background: #cc0000;
color: white;
border: none;
}
button:hover {
background: #990000;
}
#search {
min-width: 250px;
}
main {
max-width: 1400px;
margin: auto;
padding: 20px;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill,minmax(320px,1fr));
gap: 20px;
}
.card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
transition: 0.2s;
display: flex;
flex-direction: column;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
}
.card img {
width: 100%;
height: 200px;
object-fit: cover;
background: #ddd;
}
.card-content {
padding: 15px;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.source {
font-size: 12px;
font-weight: bold;
color: #cc0000;
text-transform: uppercase;
margin-bottom: 10px;
}
.card h2 {
font-size: 20px;
margin: 0 0 10px;
line-height: 1.3;
}
.card h2 a {
color: #111;
text-decoration: none;
}
.card h2 a:hover {
color: #cc0000;
}
.description {
font-size: 14px;
color: #555;
margin-bottom: 15px;
flex-grow: 1;
}
.meta {
font-size: 12px;
color: #888;
margin-top: auto;
}
.ticker {
background: #222;
color: white;
overflow: hidden;
white-space: nowrap;
box-sizing: border-box;
padding: 12px 0;
}
.ticker span {
display: inline-block;
padding-left: 100%;
animation: ticker 100s linear infinite;
}
@keyframes ticker {
0% { transform: translateX(0); }
100% { transform: translateX(-100%); }
}
.dark-mode {
background: #111;
color: #eee;
}
.dark-mode .card {
background: #1c1c1c;
color: #eee;
}
.dark-mode .card h2 a {
color: #fff;
}
.dark-mode .description {
color: #ccc;
}
.dark-mode .topbar {
background: #000;
}
.dark-mode input{
background:#222;
color:#fff;
border:1px solid #555;
}
.dark-mode input::placeholder{
color:#aaa;
}
.dark-mode select{
background:#222;
color:#fff;
border:1px solid #555;
}
.dark-mode select option{
background:#222;
color:#fff;
}
footer {
text-align: center;
padding: 40px 20px;
color: #777;
font-size: 14px;
}
.loading {
text-align: center;
padding: 50px;
font-size: 20px;
}
@media (max-width: 768px) {
header {
padding: 20px 15px;
}
header h1 {
font-size: 28px;
}
header p {
font-size: 14px;
}
.topbar {
flex-direction: column;
align-items: stretch;
gap: 15px;
}
.controls {
flex-direction: column;
width: 100%;
}
.controls input,
.controls select,
.controls button {
width: 100%;
box-sizing: border-box;
font-size: 16px;
}
#search {
min-width: 100%;
}
main {
padding: 10px;
}
.grid {
grid-template-columns: 1fr;
gap: 15px;
}
.card img {
height: 220px;
}
.card h2 {
font-size: 18px;
}
.description {
font-size: 15px;
}
.ticker {
font-size: 14px;
padding: 10px 0;
}
.ticker span {
animation: ticker 140s linear infinite;
}
footer {
padding: 25px 15px;
font-size: 12px;
}
}
</style>
</head>
<body>
<header>
<h1>Jersey News Portal</h1>
<p>All Jersey news in one place — without the clutter.</p>
</header>
<div class="ticker">
<span id="ticker-text">Loading headlines...</span>
</div>
<div class="topbar">
<div class="controls">
<input type="text" id="search" placeholder="Search headlines...">
<select id="sourceFilter">
<option value="all">All Sources</option>
</select>
<button onclick="toggleDarkMode()">Dark Mode</button>
<button onclick="loadNews()">Refresh</button>
</div>
<div>
Updated: <span id="updated">Loading...</span>
</div>
</div>
<main>
<div id="news" class="loading">
Loading Jersey news...
</div>
</main>
<footer>
Jersey News Portal • RSS Aggregator • Built with HTML & JavaScript
</footer>
<script>
const DEFAULT_IMAGE =
"https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgMJRk62IfzdBvAhdWBKLSm6A-zyAcdWrWJwhzV7LOijLuxTDORHQKCeBklXr-PjgL9BOCKGgs8pU6wuYLP4WyJdrQ8Wv_kqTg8VmPzkZHP5Sbe7ofxKUhyyNF_Xvroi-RA2TKL2QdEMiDGi8kh2Uc0IIC7hLu4wMhz4JtmWtq0B4TFWyOHWwEO8Ir-TnA/s1536/ChatGPT%20Image%20May%2025,%202026,%2001_05_27%20AM.png";
const feeds = [
{
name: "Jersey Evening Post",
url: "https://jerseyeveningpost.com/feed/"
},
{
name: "Bailiwick Express",
url: "https://www.bailiwickexpress.com/feed/"
},
{
name: "BBC Jersey",
url: "https://feeds.bbci.co.uk/news/world/europe/jersey/rss.xml"
},
{
name: "Today.je",
url: "https://today.je/rss/"
}
];
const RSS = "https://api.rss2json.com/v1/api.json?rss_url=";
const allowedTodaySources = [
"Channel ITV",
"Channel 103"
];
const blockedTodaySources = [
"JEP News",
"BBC Jersey",
"Bailiwick Express",
"Jersey Evening Post"
];
let allArticles = [];
async function fetchFeed(feed) {
try {
const response = await fetch(
RSS + encodeURIComponent(feed.url)
);
const data = await response.json();
if (!data.items) {
return [];
}
return data.items.map(item => ({
originalTitle: item.title,
source: getSource(feed.name, item.title),
title: cleanTitle(item.title),
link: item.link,
description: stripHtml(item.description || ""),
image:
item.thumbnail ||
extractImage(item.description || "") ||
DEFAULT_IMAGE,
date: new Date(item.pubDate),
}));
} catch(err) {
console.error(feed.name, err);
return [];
}
}
function stripHtml(html) {
const div = document.createElement("div");
div.innerHTML = html;
return div.textContent || div.innerText || "";
}
function extractImage(html){
const match = html.match(/<img[^>]+src=\"([^\"]+)\"/i);
return match ? match[1] : null;
}
function getSource(defaultSource, title){
if(defaultSource !== "Today.je"){
return defaultSource;
}
const match = title.match(/\[(.*?)\]/);
if(!match){
return "Today.je";
}
let source = match[1];
source = source
.replace(/\bfb\b/gi,"")
.replace(/\bfacebook\b/gi,"")
.replace(/\(\s*\)/g,"")
.replace(/&/gi,"&")
.replace(/\s+/g," ")
.trim();
if(source === "Channel ITV"){
return "Channel ITV";
}
if(source.includes("Jersey Fire")){
return "Jersey Fire & Rescue";
}
return source;
}
function cleanTitle(title){
return title
.replace(/\[.*?\]/,"")
.replace(/\s+/g," ")
.trim();
}
async function loadNews() {
document.getElementById("news").innerHTML =
"<div class='loading'>Loading Jersey news...</div>";
const results = await Promise.all(
feeds.map(feed => fetchFeed(feed))
);
allArticles = results
.flat()
.filter(article => {
const originalTitle =
article.originalTitle || "";
const isTodayArticle =
originalTitle.includes("[") &&
originalTitle.includes("]");
if(isTodayArticle){
const source =
getSource("Today.je", originalTitle);
if(
blockedTodaySources.includes(source)
){
return false;
}
return allowedTodaySources.includes(source);
}
return true;
});
allArticles.sort((a,b)=>b.date-a.date);
populateFilter();
renderArticles(allArticles);
updateTicker();
document.getElementById("updated").innerText =
new Date().toLocaleString();
}
function renderArticles(articles) {
const container = document.getElementById("news");
if (!articles.length) {
container.innerHTML =
"<div class='loading'>No articles found.</div>";
return;
}
container.className = "grid";
container.innerHTML = articles.slice(0,18).map(article => `
<div class="card">
<img
src="${article.image}"
alt="News image"
onerror="this.onerror=null;this.src='${DEFAULT_IMAGE}';"
>
<div class="card-content">
<div class="source">${article.source}</div>
<h2>
<a href="${article.link}" target="_blank" rel="noopener noreferrer">
${article.title}
</a>
</h2>
<div class="description">
${article.description.substring(0,180)}...
</div>
<div class="meta">
${article.date.toLocaleString()}
</div>
</div>
</div>
`).join("");
}
function populateFilter() {
const filter = document.getElementById("sourceFilter");
filter.innerHTML =
'<option value="all">All Sources</option>';
[...new Set(allArticles.map(a=>a.source))]
.sort()
.forEach(source => {
const option = document.createElement("option");
option.value = source;
option.textContent = source;
filter.appendChild(option);
});
}
function updateTicker() {
const headlines = allArticles
.slice(0,15)
.map(a=>a.title.replace(/<[^>]*>/g,""))
.join(" | ");
document.getElementById("ticker-text").textContent = headlines;
}
function toggleDarkMode() {
document.body.classList.toggle("dark-mode");
}
function applyFilters() {
const search =
document.getElementById("search").value.toLowerCase();
const source =
document.getElementById("sourceFilter").value;
let filtered = allArticles.filter(article => {
const matchesSearch =
article.title.toLowerCase().includes(search)
|| article.description.toLowerCase().includes(search);
const matchesSource =
source === "all"
|| article.source === source;
return matchesSearch && matchesSource;
});
renderArticles(filtered);
}
setInterval(loadNews, 300000);
loadNews();
document.getElementById("search")
.addEventListener("input", applyFilters);
document.getElementById("sourceFilter")
.addEventListener("change", applyFilters);
</script>
</body>
</html>
How to Install It in Blogger
- Create a new Blogger page or blog post
- Switch the editor into HTML mode
- Paste the code directly into the page
- Publish the page
This approach proved far more reliable than attempting to run the aggregator inside a sidebar HTML gadget.
Blogger vs Full Aggregation Platforms
This experiment also highlighted the difference between:
- a lightweight frontend RSS reader
- and a full backend aggregation platform
Projects like Today.je use:
- custom scrapers
- server-side ingestion
- databases
- scheduled crawlers
- source-specific parsers
- search indexing
- historical archiving
Whereas this Blogger project was intentionally:
- client-side only
- lightweight
- HTML/JavaScript based
- easy to deploy
- easy to customise
The Appeal of RSS in 2026
One interesting thing this project reinforced is that RSS still has enormous value.
In an era dominated by:
- algorithmic feeds
- social media manipulation
- endless advertising
- engagement optimisation
- platform censorship
there is something refreshing about:
simply reading a chronological list of headlines from multiple sources.
RSS may be old technology, but it still solves a real problem.
Future Possibilities
The next logical step would probably be moving beyond Blogger into:
- server-side aggregation
- custom scrapers
- database storage
- archived news indexing
- automatic categorisation
- searchable Jersey news archives
But even as a simple Blogger experiment, this project proved that it is still possible to build useful independent web tools using relatively lightweight technology.
Interesting article on building a news aggregator and the importance of creating a smooth user experience with reliable functionality. Projects like these benefit from thoughtful design and development. Businesses looking for a trusted Website Designing Company in Delhi can also explore Art Attackk for creative and high performing website solutions.
ReplyDelete