Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions RSS_FEATURE_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# RSS Feed Aggregation Feature

This feature aggregates external blog posts from RSS feeds and displays them on the homepage Recent Posts section, integrated seamlessly with local blog posts.

## Features

- **Homepage Integration**: External posts appear in the Recent Posts section alongside local posts
- **Visual Distinction**: External posts have distinct styling with source badges and external link indicators
- **Responsive Design**: Works on all device sizes with dark mode support
- **Automatic Updates**: Posts are fetched at build time (development uses mock data)
- **Deduplication**: Prevents duplicate posts from appearing
- **Graceful Fallback**: Falls back to mock data when RSS feeds are unavailable

## Configuration

### External Blog Configuration (`data/external-blogs.yaml`)

```yaml
feeds:
- name: "DevJev.nl"
url: "https://www.devjev.nl/"
rss_url: "https://www.devjev.nl/feed.xml"
author: "DevJev"

- name: "Bearman.nl"
url: "https://bearman.nl/"
rss_url: "https://bearman.nl/feed.xml"
author: "Bearman"

- name: "Wesley Camargo"
url: "https://wesleycamargo.github.io/"
rss_url: "https://wesleycamargo.github.io/feed.xml"
author: "Wesley Camargo"

config:
max_posts_per_feed: 5
enable_aggregation: true
```

### Recent Posts Section (`data/en/sections/posts.yaml`)

```yaml
section:
name: Recent Posts
id: recent-posts
enable: true
weight: 6
numShow: 6 # Number of posts to show (combines local + external)
```

## Technical Implementation

### Components

1. **RSS Aggregator** (`themes/toha/layouts/partials/helpers/rss-aggregator.html`)
- Fetches RSS feeds using Hugo's `resources.GetRemote`
- Supports both RSS 2.0 and Atom formats
- Extracts post metadata (title, description, date, images)
- Falls back to mock data in development

2. **External Post Card** (`themes/toha/layouts/partials/cards/external-post.html`)
- Custom template for displaying external posts
- Includes source badges and external link indicators
- Opens links in new tabs

3. **Recent Posts Section** (`themes/toha/layouts/partials/sections/recent-posts.html`)
- Modified to fetch and merge external posts with local posts
- Sorts all posts by date (newest first)
- Displays mixed content seamlessly

4. **Styling** (`themes/toha/assets/styles/sections/external-posts.scss`)
- Custom styling for external posts
- Blue borders and gradient backgrounds
- Dark mode compatibility
- Responsive design

### Data Flow

1. **Build Time**: RSS aggregator fetches external feeds
2. **Data Processing**: Posts are parsed and standardized
3. **Merging**: External and local posts are combined and sorted by date
4. **Display**: Recent Posts section renders mixed content with appropriate templates

## Usage

### Adding New RSS Feeds

1. Edit `data/external-blogs.yaml`
2. Add new feed configuration with name, URL, RSS URL, and author
3. The posts will automatically appear on next build

### Customizing Display

- Modify `numShow` in `data/en/sections/posts.yaml` to change number of posts
- Edit styling in `themes/toha/assets/styles/sections/external-posts.scss`
- Customize external post template in `themes/toha/layouts/partials/cards/external-post.html`

## Development

For development, the system uses mock data from `data/mock-external-posts.yaml` to ensure consistent behavior when RSS feeds are not accessible.

## Production Deployment

In production, set the Hugo environment to "production" to enable live RSS feed fetching:

```bash
hugo --environment production
```

## Error Handling

- RSS feed failures are logged but don't break the build
- Graceful fallback to mock data ensures site remains functional
- Missing images are handled with placeholder icons
- Invalid RSS data is skipped silently

## Performance

- RSS feeds are fetched once per build (not on every page load)
- Data is cached by Hugo's resource system
- Only essential post metadata is extracted and stored
- Image extraction is optimized with regex parsing
1 change: 1 addition & 0 deletions data/en/sections/posts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ section:
enable: true
weight: 6
showOnNavbar: true
numShow: 6
# Can optionally hide the title in sections
# hideTitle: true

Expand Down
34 changes: 34 additions & 0 deletions data/external-blogs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
# External blog RSS feeds configuration
# Note: RSS feed URLs should be verified in production environment
feeds:
- name: "DevJev.nl"
url: "https://www.devjev.nl/"
rss_url: "https://www.devjev.nl/feed.xml" # Try also: /rss.xml, /index.xml
author: "DevJev"
favicon: ""

- name: "Bearman.nl"
url: "https://bearman.nl/"
rss_url: "https://bearman.nl/feed.xml" # Try also: /rss.xml, /index.xml
author: "Bearman"
favicon: ""

- name: "Wesley Camargo"
url: "https://wesleycamargo.github.io/"
rss_url: "https://wesleycamargo.github.io/feed.xml" # Try also: /rss.xml, /index.xml
author: "Wesley Camargo"
favicon: ""

# Configuration for RSS aggregation
config:
max_posts_per_feed: 5
cache_duration: "1h"
enable_aggregation: true

# RSS Feed URL Discovery Notes:
# Common RSS feed paths to try:
# - /feed.xml, /rss.xml, /index.xml
# - /feed/, /rss/, /atom.xml
# - /feeds/all.atom.xml
# - Check HTML <link> tags: <link rel="alternate" type="application/rss+xml" href="/feed.xml">
62 changes: 62 additions & 0 deletions data/mock-external-posts.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Mock external blog posts for testing
# This file simulates RSS feed data when actual feeds are not available
mock_posts:
- title: "Advanced Cloud Security Patterns"
url: "https://www.devjev.nl/posts/cloud-security-patterns"
description: "Exploring advanced security patterns for cloud-native applications, including zero-trust architecture and secure service mesh implementations."
date: "2024-01-15T10:30:00Z"
author: "DevJev"
source: "DevJev.nl"
source_url: "https://www.devjev.nl/"
image: "https://via.placeholder.com/400x200/0066cc/ffffff?text=Cloud+Security"
external: true

- title: "Kubernetes Networking Deep Dive"
url: "https://bearman.nl/posts/kubernetes-networking"
description: "A comprehensive guide to understanding Kubernetes networking, covering CNI plugins, service mesh, and network policies for enterprise deployments."
date: "2024-01-12T14:15:00Z"
author: "Bearman"
source: "Bearman.nl"
source_url: "https://bearman.nl/"
image: "https://via.placeholder.com/400x200/28a745/ffffff?text=K8s+Networking"
external: true

- title: "Infrastructure as Code Best Practices"
url: "https://wesleycamargo.github.io/posts/iac-best-practices"
description: "Essential best practices for Infrastructure as Code implementation, covering Terraform, Ansible, and GitOps workflows for scalable cloud infrastructure."
date: "2024-01-10T09:45:00Z"
author: "Wesley Camargo"
source: "Wesley Camargo"
source_url: "https://wesleycamargo.github.io/"
image: "https://via.placeholder.com/400x200/dc3545/ffffff?text=IaC+Best+Practices"
external: true

- title: "Serverless Architecture Patterns"
url: "https://www.devjev.nl/posts/serverless-patterns"
description: "Comprehensive overview of serverless architecture patterns using AWS Lambda, Azure Functions, and Google Cloud Functions for modern applications."
date: "2024-01-08T16:20:00Z"
author: "DevJev"
source: "DevJev.nl"
source_url: "https://www.devjev.nl/"
image: "https://via.placeholder.com/400x200/ffc107/000000?text=Serverless"
external: true

- title: "Container Security Fundamentals"
url: "https://bearman.nl/posts/container-security"
description: "Essential security practices for containerized applications, including image scanning, runtime protection, and compliance frameworks."
date: "2024-01-05T11:30:00Z"
author: "Bearman"
source: "Bearman.nl"
source_url: "https://bearman.nl/"
image: "https://via.placeholder.com/400x200/6f42c1/ffffff?text=Container+Security"
external: true

- title: "Multi-Cloud Strategy Implementation"
url: "https://wesleycamargo.github.io/posts/multi-cloud-strategy"
description: "Strategic approach to implementing multi-cloud architectures, covering vendor management, cost optimization, and operational complexity."
date: "2024-01-03T08:15:00Z"
author: "Wesley Camargo"
source: "Wesley Camargo"
source_url: "https://wesleycamargo.github.io/"
image: "https://via.placeholder.com/400x200/17a2b8/ffffff?text=Multi-Cloud"
external: true
59 changes: 59 additions & 0 deletions layouts/blog/list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{{ define "navbar" }}
{{ partial "navigators/navbar.html" . }}
{{ end }}

{{ define "sidebar" }}
{{ $homePage:="#" }}
{{ if hugo.IsMultilingual }}
{{ $homePage = (path.Join (cond ( eq .Language.Lang "en") "" .Language.Lang) .Type) }}
{{ end }}

<section class="sidebar-section" id="sidebar-section">
<div class="sidebar-holder">
<div class="sidebar" id="sidebar">
<form class="mx-auto" method="get" action="{{ "search" | relLangURL }}">
<input type="text" name="keyword" value="" placeholder="{{ i18n "search" }}" data-search="" id="search-box" />
</form>
<div class="sidebar-tree">
<ul class="tree" id="tree">
<li id="list-heading"><a href="{{ .Type | relLangURL }}" data-filter="all">{{ i18n .Type }}</a></li>
<div class="subtree">
{{ partial "navigators/sidebar.html" (dict "menuName" "sidebar" "menuItems" site.Menus.sidebar "ctx" .) }}
</div>
</ul>
</div>
</div>
</div>
</section>
{{ end }}

{{ define "content" }}
<section class="content-section" id="content-section">
<div class="content container-fluid" id="content">
<div class="container-fluid post-card-holder" id="post-card-holder">
{{/* Get external posts */}}
{{ $externalPosts := partial "helpers/rss-aggregator.html" . }}

{{/* Display external posts first */}}
{{ range first 6 $externalPosts }}
{{ partial "cards/external-post.html" . }}
{{ end }}

{{/* Then display local posts */}}
{{ $localPosts := where .RegularPagesRecursive "Layout" "!=" "search" }}
{{ $numShow := site.Params.features.pagination.maxPostsPerPage | default 12}}
{{ $paginator := .Paginate $localPosts $numShow }}
{{ range $paginator.Pages }}
{{ if .Layout }}
{{/* ignore the search.md file*/}}
{{ else }}
{{ partial "cards/post.html" . }}
{{ end }}
{{ end }}
</div>
<div class="paginator">
{{ partial "pagination.html" . }}
</div>
</div>
</section>
{{ end }}
96 changes: 96 additions & 0 deletions layouts/posts/list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
{{ define "navbar" }}
{{ partial "navigators/navbar.html" . }}
{{ end }}

{{ define "sidebar" }}
{{ $homePage:="#" }}
{{ if hugo.IsMultilingual }}
{{ $homePage = (path.Join (cond ( eq .Language.Lang "en") "" .Language.Lang) .Type) }}
{{ end }}

<section class="sidebar-section" id="sidebar-section">
<div class="sidebar-holder">
<div class="sidebar" id="sidebar">
<form class="mx-auto" method="get" action="{{ "search" | relLangURL }}">
<input type="text" name="keyword" value="" placeholder="{{ i18n "search" }}" data-search="" id="search-box" />
</form>
<div class="sidebar-tree">
<ul class="tree" id="tree">
<li id="list-heading"><a href="{{ .Type | relLangURL }}" data-filter="all">{{ i18n .Type }}</a></li>
<div class="subtree">
{{ partial "navigators/sidebar.html" (dict "menuName" "sidebar" "menuItems" site.Menus.sidebar "ctx" .) }}
</div>
</ul>
</div>
</div>
</div>
</section>
{{ end }}

{{ define "content" }}
<section class="content-section" id="content-section">
<div class="content container-fluid" id="content">
<div class="container-fluid post-card-holder" id="post-card-holder">
{{/* Get local posts */}}
{{ $localPosts := where .RegularPagesRecursive "Layout" "!=" "search" }}

{{/* Get external posts */}}
{{ $externalPosts := partial "helpers/rss-aggregator.html" . }}

{{/* Combine and sort posts by date */}}
{{ $allPosts := slice }}

{{/* Add local posts to collection */}}
{{ range $localPosts }}
{{ $post := dict
"title" .Title
"url" .RelPermalink
"date" .Date
"summary" .Summary
"external" false
"page" .
}}
{{ $allPosts = $allPosts | append $post }}
{{ end }}

{{/* Add external posts to collection */}}
{{ range $externalPosts }}
{{ $post := dict
"title" .title
"url" .url
"date" (.date | time.AsTime)
"summary" .description
"external" true
"externalData" .
}}
{{ $allPosts = $allPosts | append $post }}
{{ end }}

{{/* Sort all posts by date */}}
{{ $sortedPosts := sort $allPosts "date" "desc" }}

{{/* Simple pagination - use Hugo's built-in pagination with local posts only for now */}}
{{ $numShow := site.Params.features.pagination.maxPostsPerPage | default 12}}
{{ $paginator := .Paginate $localPosts $numShow }}

{{/* Display external posts first (latest ones) */}}
{{ $externalPostsToShow := first $numShow $externalPosts }}
{{ range $externalPostsToShow }}
{{ partial "cards/external-post.html" . }}
{{ end }}

{{/* Then display local posts */}}
{{ range $paginator.Pages }}
{{ if .Layout }}
{{/* ignore the search.md file*/}}
{{ else }}
{{ partial "cards/post.html" . }}
{{ end }}
{{ end }}
</div>
<div class="paginator">
{{ partial "pagination.html" . }}
</div>
</div>
</section>
{{ end }}
1 change: 1 addition & 0 deletions themes/toha/assets/styles/application.template.scss
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
@import './sections/education';
@import './sections/projects';
@import './sections/recent-posts';
@import './sections/external-posts';
@import './sections/achievements';
@import './sections/accomplishments';
@import './sections/publications';
Expand Down
Loading
Loading