Build a News Aggregator Website
Scrape live headlines from multiple sources into one styled webpage.
Introduction
30 Second Summary
Every morning you probably check three or four different news sites, hopping between tabs to piece together what's happening in the world. What if you had one page that pulled the latest headlines from all your favourite sources automatically?
In this project, you will build a personal news aggregator website using Python and Flask. Your site will pull live headlines from BBC News and Hacker News, then display them in a clean, styled page you control.
What You'll Build
When you open your browser at the end of this project, you see a live news dashboard with the freshest headlines from your chosen sources, all in one place, ready to show off or use every day.
By the end of this project, you'll have:
- A running news website where you can open your browser and see live BBC News headlines pulled directly from their RSS feed.
- Headlines scraped from Hacker News using web scraping (a technique for grabbing data from sites that don't offer a feed), displayed right alongside your BBC articles.
- A styled, newspaper-like page that you can refresh to get the latest news from all sources instantly.
- Secret Mission: Add automatic background refresh so your site updates itself every 30 minutes without you lifting a finger.
Are there any prerequisites?
None. This guide walks you through installing every tool you need, including Python, Flask, and the parsing libraries. All you need is a computer with VS Code installed.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Your Project
To build a news aggregator, you need Python libraries that can fetch web pages, parse RSS feeds, and serve a website.
In this step, you will create a project folder, install the required packages, and get a basic Flask server running to prove everything is wired up correctly.
In this step, get ready to:
- Create a project folder with a Python virtual environment.
- Install Flask and the parsing libraries.
- Run a minimal Flask app to confirm everything works.
Create your project folder and virtual environment
A virtual environment keeps your project's packages isolated from the rest of your system. Your news aggregator gets its own clean set of libraries that won't conflict with anything else on your machine.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir news-aggregator
cd news-aggregator
- Create a Python virtual environment inside this folder by running:
python3 -m venv venv
- Activate the virtual environment so your terminal uses the project's own Python and packages:
macOS
source venv/bin/activate
Windows
venv\Scripts\activate
You should see (venv) appear at the beginning of your terminal prompt. This confirms you are now working inside the virtual environment.
Don't see (venv) in your prompt?
Make sure you ran the correct activation command for your operating system. On Windows, if you see an error about running scripts being disabled, run this command first, then try activating again:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
help me activate my Python virtual environment
- Install the four packages your project needs by running:
pip install flask==3.1.3 feedparser==6.0.13 beautifulsoup4==4.15.0 requests==2.34.2
What are these packages?
- flask is a web framework that lets Python serve web pages.
- feedparser reads RSS feeds and turns them into Python objects.
- beautifulsoup4 parses HTML so you can extract data from web pages.
- requests lets Python download web pages from the internet.
- Verify all packages installed correctly by running:
python3 -c "import flask; import feedparser; import requests; from bs4 import BeautifulSoup; print('All good!')"
You should see All good! printed in your terminal.
Seeing an ImportError?
Make sure (venv) is still showing in your terminal prompt. If it disappeared, re-run the activation command from the platform tab above, then try the import check again.
On Windows, if python3 is not recognized, try using python instead.
help me fix my Python package imports
Create the project file structure
Your project needs two files: a main Python file for the server logic, and an HTML template for the web page layout.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the news-aggregator folder, and click Open.
You should see the news-aggregator folder name in VS Code's Explorer sidebar on the left, with the venv folder visible inside it.
- Create the templates folder by opening VS Code's integrated terminal with Ctrl+` (macOS and Windows) and running:
mkdir templates
- In the Explorer sidebar, hover over the NEWS-AGGREGATOR heading and click the New File icon (a page with a plus sign).
- Type app.py and press Enter.
- In the Explorer sidebar, right-click the templates folder and select New File.
- Type index.html and press Enter.
- Confirm your file structure by running this in the integrated terminal:
ls
You should see app.py, templates, and venv listed in the output.
Don't see app.py or templates?
Check that you created app.py at the top level of the news-aggregator folder, not inside venv or templates. If a file ended up in the wrong place, right-click it in the Explorer sidebar and select Delete, then create it again in the correct location.
help me fix my project file structure
Write a minimal Flask app and run it
Before building the full news aggregator, you will write the smallest possible Flask app to confirm the server works. If this tiny version runs, you know the foundation is solid.
- Click on app.py in the Explorer sidebar to open it in the editor.
- Add the following code to app.py:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "Hello, News!"
if __name__ == "__main__":
app.run(debug=True)
What does this code do?
- app = Flask(__name__) creates your web application.
- @app.route("/") tells Flask to run the index() function when someone visits the homepage.
- app.run(debug=True) starts a local web server that automatically reloads when you change code.
- Save app.py with Cmd+S (macOS) or Ctrl+S (Windows).
Before you run the server, what do you think you will see when you visit http://127.0.0.1:5000 in your browser?
- Make sure (venv) is showing in your integrated terminal prompt, then start the Flask server by running:
python3 app.py
You should see output showing the server is running at http://127.0.0.1:5000. The server is now waiting for visitors.
Your terminal is now busy
The Flask server takes over this terminal window while it runs. You will not be able to type other commands here until you stop the server with Ctrl+C. Leave it running for now.
- Open your web browser and visit http://127.0.0.1:5000.
You should see the text Hello, News! displayed in the browser. Your Flask server is alive and responding to requests.
Seeing an error or a blank page?
If the terminal shows a ModuleNotFoundError, your virtual environment is not active. Press Ctrl+C to stop the server, activate the venv again using the platform tab above, then re-run python3 app.py.
If your browser says the site cannot be reached, double-check that the terminal still shows the server running. Make sure you typed the URL exactly as http://127.0.0.1:5000.
help me get my Flask server running
✔️ Awesome, I've got everything!
Great. Double check you have saved app.py and that your browser shows Hello, News! at http://127.0.0.1:5000.
ⓧ I'd like to double check the full code
Here is the complete app.py file. Make sure yours matches exactly:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "Hello, News!"
if __name__ == "__main__":
app.run(debug=True)
Your templates/index.html file should exist but be empty for now.
Your environment is ready and your server is alive. Next up, you will connect to the BBC News RSS feed and display real headlines on your page.
Pull Live News from BBC via RSS
In Step 1, you installed all the tools your news aggregator needs. Now it is time to pull real data from the internet.
RSS feeds are the easiest and most reliable way to pull structured news data from websites. In this step, you will connect to the BBC News RSS feed and display live headlines on your page.
In this step, get ready to:
- Write a Python function that fetches live BBC News headlines from their RSS feed.
- Build an HTML template that displays articles as clickable links with dates.
- Connect everything together so your Flask app serves real news in the browser.
Write the RSS feed function
An RSS feed is a structured data file that websites publish so programs can read their latest content. The BBC publishes one at a public URL, and the feedparser library can turn that feed into Python objects you can work with.
What is an RSS feed?
RSS stands for Really Simple Syndication. It is a standardized format that websites use to publish a list of their latest articles. Each entry in the feed includes the article title, link, and publication date in a structured format that programs can easily parse.
Think of it like a menu at a restaurant. Instead of you having to scan the whole website for new articles, the RSS feed hands you a neatly organized list of what is available.
- Replace the entire contents of app.py with the following code:
from flask import Flask, render_template
import feedparser
from datetime import datetime
app = Flask(__name__)
def fetch_bbc_news():
"""Fetch news articles from BBC News RSS feed."""
feed = feedparser.parse("https://feeds.bbci.co.uk/news/rss.xml")
articles = []
for entry in feed.entries[:10]:
articles.append({
"title": entry.title,
"link": entry.link,
"published": entry.get("published", ""),
"source": "BBC News"
})
return articles
@app.route("/")
def index():
return "Hello, News!"
if __name__ == "__main__":
app.run(debug=True)
What does this code do?
- feedparser.parse() fetches the BBC RSS feed URL and converts the XML data into a Python object.
- feed.entries[:10] grabs the first 10 articles from the feed. The BBC publishes around 30-50 entries, so you take a manageable slice.
- Each article is stored as a dictionary with four keys: title, link, published, and source. This consistent structure makes it easy to display later.
- The route still returns "Hello, News!" for now. You will update it after building the HTML template.
- Save app.py.
- Test that your function can reach the BBC feed by running this command:
python3 -c "from app import fetch_bbc_news; articles = fetch_bbc_news(); print(articles[0]['title'])"
You should see a real BBC News headline printed in your terminal. The exact headline will vary depending on what is in the news right now.
Seeing an import error or no output?
- Make sure your virtual environment is still active. You should see (venv) at the start of your terminal prompt.
- Check that you are running the command from inside the news-aggregator folder where app.py lives.
- If you see a network error, confirm you have an internet connection and try again.
Still stuck? Help me debug why my feedparser import or BBC feed fetch is failing.
Create the HTML template
Your function returns a list of articles, but you need an HTML page to display them. Flask uses Jinja2 templates to combine Python data with HTML structure.
- Open templates/index.html and replace its contents with this code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My News Aggregator</title>
</head>
<body>
<h1>My News Aggregator</h1>
<p>Last updated: {{ last_updated }}</p>
<ul>
{% for article in articles %}
<li>
<a href="{{ article.link }}" target="_blank">{{ article.title }}</a>
<span> — {{ article.source }}</span>
{% if article.published %}<br><small>{{ article.published }}</small>{% endif %}
</li>
{% endfor %}
</ul>
</body>
</html>
What does this template do?
- {{ last_updated }} prints the timestamp that Flask passes to the template.
- {% for article in articles %} loops through every article in the list and creates one list item per article.
- Each article renders as a clickable link (opening in a new tab) with the source name and publication date below it.
- {% if article.published %} only shows the date if one exists. Not all sources include dates.
- Save templates/index.html.
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before moving on.
ⓧ I'd like to double check the full code
Here is the complete templates/index.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My News Aggregator</title>
</head>
<body>
<h1>My News Aggregator</h1>
<p>Last updated: {{ last_updated }}</p>
<ul>
{% for article in articles %}
<li>
<a href="{{ article.link }}" target="_blank">{{ article.title }}</a>
<span> — {{ article.source }}</span>
{% if article.published %}<br><small>{{ article.published }}</small>{% endif %}
</li>
{% endfor %}
</ul>
</body>
</html>
Update the route and check the browser
The template is ready, but the Flask route still returns plain text. You need to connect the function and template together by updating the route.
- In app.py, find this code near the bottom:
@app.route("/")
def index():
return "Hello, News!"
- Replace those three lines with this updated route:
@app.route("/")
def index():
bbc_articles = fetch_bbc_news()
all_articles = bbc_articles
last_updated = datetime.now().strftime("%B %d, %Y at %I:%M %p")
return render_template("index.html", articles=all_articles, last_updated=last_updated)
What does this route do now?
- fetch_bbc_news() calls your function to grab the latest 10 BBC headlines from the live RSS feed.
- datetime.now().strftime(...) creates a human-readable timestamp showing when the page was loaded.
- render_template() sends both the articles and the timestamp to your HTML template, which fills in the placeholders and returns a complete page.
- Save app.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved app.py before running the server.
ⓧ I'd like to double check the full code
Here is the complete app.py at this point:
from flask import Flask, render_template
import feedparser
from datetime import datetime
app = Flask(__name__)
def fetch_bbc_news():
"""Fetch news articles from BBC News RSS feed."""
feed = feedparser.parse("https://feeds.bbci.co.uk/news/rss.xml")
articles = []
for entry in feed.entries[:10]:
articles.append({
"title": entry.title,
"link": entry.link,
"published": entry.get("published", ""),
"source": "BBC News"
})
return articles
@app.route("/")
def index():
bbc_articles = fetch_bbc_news()
all_articles = bbc_articles
last_updated = datetime.now().strftime("%B %d, %Y at %I:%M %p")
return render_template("index.html", articles=all_articles, last_updated=last_updated)
if __name__ == "__main__":
app.run(debug=True)
Before you run the server, what do you expect to see when you open the page in your browser? Think about what data the function pulls and how the template displays it.
- Start the Flask server by running:
python3 app.py
- Open http://127.0.0.1:5000 in your browser.
You should see a page titled "My News Aggregator" with a list of BBC News headlines. Each headline is a clickable link that opens the full article in a new tab. Below each link, you will see the source name and publication date.
Seeing an error page or no articles?
- If you see a TemplateNotFound error, make sure your index.html file is inside the templates/ folder (not in the project root).
- If the page loads but shows no articles, check your internet connection. The function needs to reach the BBC feed URL.
- If you see a syntax error in the terminal, double check that your app.py matches the code above. Pay attention to indentation and quote marks.
Still stuck? Help me fix my Flask app that is not displaying BBC News articles.
Your news aggregator is live with real BBC headlines. Next up, you will add a second news source using a completely different technique: web scraping.
Add Web Scraping for Hacker News
Your news page is pulling live BBC headlines through an RSS feed. Now let's add a second source to make this a real aggregator.
Hacker News is a popular tech community where developers share interesting links. Your first instinct might be to use feedparser on it, just like you did for BBC. Let's test that assumption.
In this step, get ready to:
- Write a web scraping function for Hacker News using requests and Beautiful Soup.
- Combine BBC and Hacker News articles into a single feed.
- Group articles by source in the template.
Discover why RSS won't work here
You already know that feedparser can pull structured data from RSS feeds. Hacker News lives at https://news.ycombinator.com. Before writing any new code, let's see if feedparser can handle it.
Before you run this, do you think feedparser will find articles on the Hacker News homepage?
- Stop your running Flask server by pressing Ctrl+C in the terminal.
- Test feedparser on the Hacker News homepage by running:
python3 -c "import feedparser; feed = feedparser.parse('https://news.ycombinator.com'); print(len(feed.entries), 'entries found')"
You see 0 entries found. Nothing came back.
Why did feedparser return 0 entries?
feedparser only works with pages that serve RSS or Atom XML data. The Hacker News homepage at https://news.ycombinator.com is a regular HTML page, not an RSS feed. When feedparser encounters plain HTML, it returns an empty list of entries.
This is where web scraping comes in. Instead of relying on a site to publish a feed, you download the raw HTML and extract the data yourself using a parser like Beautiful Soup.
Write a scraping function with requests and Beautiful Soup
Web scraping has two steps: fetch the page's HTML with requests, then parse that HTML with Beautiful Soup to extract the pieces you want.
- In app.py, find the import lines at the top of the file.
- Add these two new imports below import feedparser:
import requests
from bs4 import BeautifulSoup
What do these imports do?
requests lets Python download web pages, just like your browser does. BeautifulSoup takes raw HTML and turns it into a searchable structure so you can pull out specific elements like links and titles.
- Below the fetch_bbc_news() function, add the following scraping function:
def scrape_hacker_news():
"""Scrape top stories from Hacker News."""
response = requests.get("https://news.ycombinator.com")
soup = BeautifulSoup(response.text, "html.parser")
articles = []
for item in soup.select(".titleline > a")[:10]:
articles.append({
"title": item.get_text(),
"link": item.get("href", ""),
"published": "",
"source": "Hacker News"
})
return articles
What does this code do?
requests.get() downloads the full HTML of the Hacker News homepage. BeautifulSoup then parses that HTML into a tree you can search.
soup.select(".titleline > a") uses a CSS selector to find all the article title links on the page. The [:10] slice grabs only the first 10 results, keeping things tidy.
Each article gets the same dictionary shape as the BBC articles (title, link, published, source) so your template can handle both sources identically.
- Save app.py.
- Verify the scraping function works by running:
python3 -c "from app import scrape_hacker_news; articles = scrape_hacker_news(); print(len(articles), 'articles scraped'); print(articles[0]['title'])"
You should see 10 articles scraped followed by the title of the top Hacker News story.
Seeing 0 articles scraped?
Make sure you saved app.py after adding both the imports and the function. Check that the import requests line sits directly below import feedparser at the top of the file.
Also confirm the CSS selector is exactly .titleline > a with the dot before titleline. A typo here means Beautiful Soup finds no matching elements.
Help me debug my scrape_hacker_news function
Combine both sources and update the template
Your scraping function works. Now you need to call it from your Flask route and update the template to display both sources separately.
- In app.py, find the index() function. Locate this line:
all_articles = bbc_articles
- Replace it with these two lines so the route calls your new function and combines both sources:
hn_articles = scrape_hacker_news()
all_articles = bbc_articles + hn_articles
What does this change?
hn_articles = scrape_hacker_news() calls your new function to get Hacker News articles. Then bbc_articles + hn_articles merges both lists into one, which gets passed to the template.
- Now open templates/index.html. Find the existing <ul> block that loops through all articles.
- Replace everything between <p>Last updated: {{ last_updated }}</p> and </body> with this grouped layout:
<h2>BBC News</h2>
<ul>
{% for article in articles %}
{% if article.source == "BBC News" %}
<li>
<a href="{{ article.link }}" target="_blank">{{ article.title }}</a>
{% if article.published %}<br><small>{{ article.published }}</small>{% endif %}
</li>
{% endif %}
{% endfor %}
</ul>
<h2>Hacker News</h2>
<ul>
{% for article in articles %}
{% if article.source == "Hacker News" %}
<li>
<a href="{{ article.link }}" target="_blank">{{ article.title }}</a>
</li>
{% endif %}
{% endfor %}
</ul>
What does this template do?
Instead of one big list, the template now loops through articles twice. The first loop uses {% if article.source == "BBC News" %} to show only BBC articles under a BBC heading.
The second loop does the same for Hacker News. This groups articles by source so you can see at a glance where each headline came from.
- Save both app.py and templates/index.html.
Before you restart the server, do you expect to see BBC and Hacker News articles in one mixed list, or separated by headings?
- Start your Flask server again by running:
python3 app.py
- Open http://127.0.0.1:5000 in your browser (or refresh the page if it's already open).
You should see two separate sections: a BBC News heading with BBC headlines below it, and a Hacker News heading with tech stories below it.
Only seeing one source or a blank page?
If you only see BBC News headlines, check that hn_articles = scrape_hacker_news() appears inside the index() function in app.py. The line must be indented to sit inside the function body.
If you see a Jinja2 template error, check that the {% if %} and {% endif %} tags are balanced in your HTML. Every {% if %} needs a matching {% endif %} before {% endfor %}.
Help me fix my news aggregator template
✔️ Awesome, I've got everything!
Great. Make sure both files are saved.
ⓧ I'd like to double check the full code
Your app.py should have these sections in order:
- Imports: Flask, render_template, feedparser, requests, BeautifulSoup, datetime.
- app = Flask(__name__).
- fetch_bbc_news() function parsing the BBC RSS feed for 10 entries.
- scrape_hacker_news() function using requests and BeautifulSoup with the .titleline > a selector for 10 items.
- index() route combining bbc_articles + hn_articles and passing all_articles and last_updated to the template.
- if __name__ == "__main__": app.run(debug=True).
Your templates/index.html should have:
- The standard <!DOCTYPE html> header with title My News Aggregator.
- An <h1> heading and the last_updated paragraph.
- A <h2>BBC News</h2> section with a {% for %} loop filtering for article.source == "BBC News".
- A <h2>Hacker News</h2> section with a {% for %} loop filtering for article.source == "Hacker News".
Your aggregator now pulls from two sources using two completely different techniques. Next, you'll transform it from plain text into a polished news site with CSS styling.
Style Your News Site
Your news aggregator pulls live headlines from two sources and displays them on a page you control. The data works perfectly. The problem is it looks like raw text from the 1990s.
A small amount of CSS transforms the page into something you would actually want to open every morning. In this step, you will add a clean newspaper-like layout that makes your aggregator feel like a real product.
In this step, get ready to:
- Add CSS for a centered layout with a styled header.
- Style source sections with distinct colors and hover effects.
- Add a styled timestamp showing when the page was last refreshed.
Add CSS for a centered container and header
Right now your page uses default browser styles. You will add a <style> block with CSS that resets browser defaults, centers the content, and creates a dark header bar.
- Open templates/index.html in VS Code.
- Select all the existing code and delete it. You will replace the entire file with a new styled version.
- Paste the following code to start the new file with base layout CSS:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My News Aggregator</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Georgia, serif;
background-color: #f4f4f4;
color: #333;
line-height: 1.6;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
What does this CSS do?
The * selector resets all elements to zero margin and padding, removing inconsistent browser defaults.
The body style sets a newspaper-like Georgia serif font on a light gray background. The .container class centers your content at 800px wide.
- Continue adding the following CSS directly below what you just pasted:
header {
background-color: #1a1a2e;
color: white;
padding: 20px;
text-align: center;
margin-bottom: 30px;
border-radius: 8px;
}
header h1 {
font-size: 2em;
margin-bottom: 5px;
}
.updated {
font-size: 0.85em;
color: #aaa;
}
.source-section {
margin-bottom: 30px;
}
.source-section h2 {
font-size: 1.3em;
padding: 10px 15px;
border-radius: 5px;
margin-bottom: 15px;
color: white;
}
What does this CSS do?
The header style creates a dark navy bar across the top with your site title centered in white text.
The .source-section h2 style turns each source heading into a colored banner. You will add the specific colors next.
Add source colors and hover effects
Each news source gets its own accent color so you can tell at a glance where a headline came from. Hover effects make the page feel interactive and responsive to your mouse.
- Continue adding the following CSS directly below the previous block:
.source-bbc h2 {
background-color: #bb1919;
}
.source-hackernews h2 {
background-color: #ff6600;
}
.article-card {
background: white;
padding: 15px 20px;
margin-bottom: 10px;
border-radius: 5px;
border-left: 4px solid #ddd;
transition: transform 0.2s, box-shadow 0.2s;
}
.article-card:hover {
transform: translateX(5px);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.source-bbc .article-card {
border-left-color: #bb1919;
}
.source-hackernews .article-card {
border-left-color: #ff6600;
}
What does this CSS do?
BBC News gets a red accent (#bb1919) and Hacker News gets orange (#ff6600). Each article sits in a white card with a colored left border matching its source.
The transition property makes the hover animation smooth. When you mouse over a card, it slides right and gains a subtle shadow.
- Add the final CSS rules below, then close the </style> and </head> tags:
.article-card a {
text-decoration: none;
color: #1a1a2e;
font-weight: bold;
font-size: 1.05em;
}
.article-card a:hover {
color: #0066cc;
}
.article-date {
font-size: 0.8em;
color: #888;
margin-top: 5px;
}
</style>
</head>
What does this CSS do?
Article links are bold with no underline for a clean look. They turn blue on hover to signal they are clickable. The .article-date class makes publication timestamps small and gray so they do not compete with headlines.
- Save templates/index.html and refresh your browser at http://127.0.0.1:5000.
You should see a gray background and Georgia serif font applied to the page. The layout still looks off because the HTML body has not been updated to use the new CSS classes yet.
Restructure the HTML to use your new styles
The CSS targets class names like .container, .article-card, and .source-bbc that do not exist in your HTML yet. You need to restructure the body to use these classes and add a proper header with the "Last updated" timestamp.
- In templates/index.html, delete everything from <body> through </html> (the old body content) and replace it with the following structured HTML:
<body>
<div class="container">
<header>
<h1>My News Aggregator</h1>
<p class="updated">Last updated: {{ last_updated }}</p>
</header>
<div class="source-section source-bbc">
<h2>BBC News</h2>
{% for article in articles %}
{% if article.source == "BBC News" %}
<div class="article-card">
<a href="{{ article.link }}" target="_blank">{{ article.title }}</a>
{% if article.published %}
<p class="article-date">{{ article.published }}</p>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
What changed in the HTML?
The old <ul> and <li> tags are gone. Each article now lives in a <div class="article-card"> so the CSS can style it as a card with borders and hover effects.
The <header> element wraps the site title and the "Last updated" timestamp together in the dark banner. The source-bbc class connects this section to the red accent color you defined in CSS.
- Add the Hacker News section and closing tags directly below:
<div class="source-section source-hackernews">
<h2>Hacker News</h2>
{% for article in articles %}
{% if article.source == "Hacker News" %}
<div class="article-card">
<a href="{{ article.link }}" target="_blank">{{ article.title }}</a>
{% if article.published %}
<p class="article-date">{{ article.published }}</p>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
</div>
</body>
</html>
Before you save and refresh, what do you think the page will look like now that the HTML uses all the CSS classes you defined?
- Save templates/index.html.
- Refresh your browser at http://127.0.0.1:5000.
You should see a dark navy header with "My News Aggregator" and the last-updated timestamp, a red BBC News section, an orange Hacker News section, and white article cards that slide right when you hover over them.
Page looks broken or unstyled?
- Make sure you deleted ALL the old body content. If any old <ul> or <li> tags remain, the CSS will not apply correctly.
- Check that the </style> closing tag appears after the last CSS rule. A missing closing tag breaks all styles.
- Confirm you have exactly one <head> section and one <body> section. Duplicate tags cause rendering issues.
Help me debug my styled news page
- Hover over a few article cards to see them slide right with a shadow effect.
- Click a headline link to confirm it still opens the article in a new tab.
✔️ Awesome, I've got everything!
Great. Double check you have saved templates/index.html with all the CSS and restructured HTML.
ⓧ I'd like to double check the full code
Scroll up and confirm your file contains each section in this order:
- The <!DOCTYPE html> and opening <head> with the viewport meta tag and <style> opening.
- CSS reset (*), body, and .container styles.
- The header, header h1, .updated, and .source-section styles.
- Source-specific colors (.source-bbc h2 and .source-hackernews h2).
- Article card styles with hover effects, source-specific border colors, link styles, and .article-date.
- Closing </style> and </head> tags.
- The <body> with a .container div, <header> element, BBC News section (source-section source-bbc), and Hacker News section (source-section source-hackernews).
- Each article in a <div class="article-card"> with a link and optional date paragraph.
The app.py file is unchanged in this step.
Your news aggregator now looks like something you would bookmark and check every morning. You have a working site that pulls live data from two sources and presents it in a clean, professional layout. Next up, take on the Secret Mission to make your site refresh itself automatically.
Secret mission
Add Automatic Background Refresh
Right now, your news aggregator only shows fresh articles when you restart the server or refresh the page. Real news sites update themselves in the background. Can you make yours do the same?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- A Python virtual environment inside your news-aggregator folder (takes disk space but costs nothing).
- A Flask development server running locally on port 5000.
- Project files: app.py and templates/index.html.
Keep everything running
No action needed. Choose this if you are still actively building or want to keep your news aggregator running on your machine.
- Your Flask server continues running at http://127.0.0.1:5000 and refreshes news automatically.
- You can deploy it to your own server anytime.
- The virtual environment stays on disk but costs nothing.
Pause - I'll come back to this later
Shut down the running server to free up your terminal. Your files stay on disk so you can restart anytime.
- In the terminal where Flask is running, press Ctrl+C to stop the server.
- To restart later, navigate back to your news-aggregator folder, activate the virtual environment, and run:
python3 app.py
Your news aggregator picks up right where you left off.
Delete - I don't want to use this again
Remove all project files and start fresh. This deletes your code, virtual environment, and all installed packages.
- In the terminal where Flask is running, press Ctrl+C to stop the server.
- Navigate to the parent folder (your Desktop) by running:
cd ~/Desktop
- Delete the entire project folder by running this command.
macOS
rm -rf news-aggregator
Windows
rmdir /s /q news-aggregator
- Confirm the folder is gone by running:
ls
You should no longer see news-aggregator in the output. Everything is cleaned up.
That's a Wrap!
That's a Wrap!
Nice work! You just built a personal news aggregator website from scratch that pulls live headlines from real news sources and displays them in a styled, newspaper-like layout you control.
You've learned how to:
- Parse RSS feeds with feedparser to pull structured news data from sites like BBC News.
- Scrape web pages with requests and Beautiful Soup to extract data from sites that don't offer RSS feeds.
- Build a Flask web application with Jinja2 templates and CSS styling that combines multiple data sources into a single unified page.
- Secret Mission: Add automatic background refresh with APScheduler so your site stays current without manual intervention.
Ready to quiz yourself?