<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RSS Feed to HTML Converter</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #6366f1;
--primary-dark: #4f46e5;
--secondary: #8b5cf6;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--dark: #1f2937;
--gray: #6b7280;
--light: #f3f4f6;
--white: #ffffff;
--border: #e5e7eb;
--shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);
--shadow-lg: 0 10px 25px rgba(0,0,0,0.1);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
color: var(--dark);
}
.container {
max-width: 1400px;
margin: 0 auto;
}
header {
text-align: center;
color: var(--white);
margin-bottom: 30px;
animation: fadeInDown 0.6s ease;
}
header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
header p {
font-size: 1.1rem;
opacity: 0.95;
}
.main-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
animation: fadeIn 0.8s ease;
}
.card {
background: var(--white);
border-radius: 12px;
padding: 25px;
box-shadow: var(--shadow-lg);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 15px 35px rgba(0,0,0,0.15);
}
.card-title {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 20px;
color: var(--dark);
display: flex;
align-items: center;
gap: 10px;
}
.icon {
width: 24px;
height: 24px;
display: inline-block;
}
.input-group {
margin-bottom: 20px;
}
.input-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--gray);
}
.input-group input,
.input-group textarea,
.input-group select {
width: 100%;
padding: 12px;
border: 2px solid var(--border);
border-radius: 8px;
font-size: 1rem;
transition: all 0.3s ease;
}
.input-group input:focus,
.input-group textarea:focus,
.input-group select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.input-group textarea {
min-height: 150px;
resize: vertical;
font-family: 'Courier New', monospace;
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
border-bottom: 2px solid var(--border);
}
.tab {
padding: 10px 20px;
background: none;
border: none;
color: var(--gray);
cursor: pointer;
font-size: 1rem;
font-weight: 500;
transition: all 0.3s ease;
position: relative;
}
.tab.active {
color: var(--primary);
}
.tab.active::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 2px;
background: var(--primary);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
animation: fadeIn 0.3s ease;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: var(--primary);
color: var(--white);
}
.btn-primary:hover {
background: var(--primary-dark);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
}
.btn-secondary {
background: var(--secondary);
color: var(--white);
}
.btn-secondary:hover {
background: #7c3aed;
transform: translateY(-2px);
}
.btn-success {
background: var(--success);
color: var(--white);
}
.btn-success:hover {
background: #059669;
}
.btn-group {
display: flex;
gap: 10px;
margin-top: 20px;
}
.options-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-bottom: 20px;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox-group input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
}
.preview-container {
background: var(--light);
border-radius: 8px;
padding: 20px;
min-height: 400px;
max-height: 600px;
overflow-y: auto;
}
.preview-container h3 {
margin-bottom: 15px;
color: var(--dark);
}
.post-preview {
background: var(--white);
border-radius: 8px;
padding: 20px;
margin-bottom: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.post-preview h2 {
color: var(--primary);
margin-bottom: 10px;
}
.post-meta {
color: var(--gray);
font-size: 0.9rem;
margin-bottom: 15px;
}
.post-content {
line-height: 1.6;
}
.code-preview {
background: #1e1e1e;
color: #d4d4d4;
padding: 20px;
border-radius: 8px;
overflow-x: auto;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
line-height: 1.5;
}
.alert {
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
animation: slideIn 0.3s ease;
}
.alert-success {
background: #d1fae5;
color: #065f46;
border: 1px solid #6ee7b7;
}
.alert-error {
background: #fee2e2;
color: #991b1b;
border: 1px solid #fca5a5;
}
.alert-warning {
background: #fed7aa;
color: #92400e;
border: 1px solid #fdba74;
}
.loading {
display: none;
text-align: center;
padding: 20px;
}
.spinner {
border: 3px solid var(--light);
border-top: 3px solid var(--primary);
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
background: var(--light);
padding: 15px;
border-radius: 8px;
text-align: center;
}
.stat-value {
font-size: 1.5rem;
font-weight: bold;
color: var(--primary);
}
.stat-label {
font-size: 0.9rem;
color: var(--gray);
margin-top: 5px;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (max-width: 968px) {
.main-content {
grid-template-columns: 1fr;
}
header h1 {
font-size: 2rem;
}
.options-grid {
grid-template-columns: 1fr;
}
.stats {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>📡 RSS Feed to HTML Converter</h1>
<p>Transform your RSS/Blogger feeds into clean, semantic HTML</p>
</header>
<div class="main-content">
<!-- Input Section -->
<div class="card">
<h2 class="card-title">
<span class="icon">📥</span>
Feed Input
</h2>
<div class="tabs">
<button class="tab active" onclick="switchTab('url')">Feed URL</button>
<button class="tab" onclick="switchTab('xml')">Direct XML</button>
</div>
<div id="url-tab" class="tab-content active">
<div class="input-group">
<label for="feed-url">RSS/Atom Feed URL</label>
<input type="url" id="feed-url" placeholder="https://example.com/feed.xml">
</div>
<button class="btn btn-primary" onclick="fetchFeed()">
<span>🔍</span> Fetch Feed
</button>
</div>
<div id="xml-tab" class="tab-content">
<div class="input-group">
<label for="feed-xml">Paste RSS/Atom XML</label>
<textarea id="feed-xml" placeholder="<?xml version="1.0" encoding="UTF-8"?> <rss version="2.0">..."></textarea>
</div>
<button class="btn btn-primary" onclick="parseDirectXML()">
<span>📄</span> Parse XML
</button>
</div>
<div class="loading" id="loading">
<div class="spinner"></div>
<p style="margin-top: 10px; color: var(--gray);">Processing feed...</p>
</div>
<div class="alert alert-success" id="success-alert">
<strong>Success!</strong> Feed parsed successfully.
</div>
<div class="alert alert-error" id="error-alert">
<strong>Error!</strong> <span id="error-message"></span>
</div>
<div class="alert alert-warning" id="warning-alert">
<strong>Warning!</strong> <span id="warning-message"></span>
</div>
<!-- Conversion Options -->
<h3 style="margin-top: 30px; margin-bottom: 15px;">Conversion Options</h3>
<div class="options-grid">
<div class="checkbox-group">
<input type="checkbox" id="include-title" checked>
<label for="include-title">Include Title</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="include-date" checked>
<label for="include-date">Include Date</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="include-author" checked>
<label for="include-author">Include Author</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="include-content" checked>
<label for="include-content">Include Content</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="include-link" checked>
<label for="include-link">Include Link</label>
</div>
<div class="checkbox-group">
<input type="checkbox" id="semantic-html" checked>
<label for="semantic-html">Semantic HTML</label>
</div>
</div>
<div class="input-group">
<label for="date-format">Date Format</label>
<select id="date-format">
<option value="full">Full (Month DD, YYYY)</option>
<option value="short">Short (MM/DD/YYYY)</option>
<option value="iso">ISO (YYYY-MM-DD)</option>
<option value="relative">Relative (2 days ago)</option>
</select>
</div>
<div class="input-group">
<label for="max-posts">Maximum Posts</label>
<input type="number" id="max-posts" value="10" min="1" max="100">
</div>
<button class="btn btn-secondary" onclick="convertToHTML()">
<span>🔄</span> Convert to HTML
</button>
</div>
<!-- Output Section -->
<div class="card">
<h2 class="card-title">
<span class="icon">📤</span>
Output
</h2>
<div class="stats" id="stats" style="display: none;">
<div class="stat-card">
<div class="stat-value" id="total-posts">0</div>
<div class="stat-label">Total Posts</div>
</div>
<div class="stat-card">
<div class="stat-value" id="converted-posts">0</div>
<div class="stat-label">Converted</div>
</div>
<div class="stat-card">
<div class="stat-value" id="feed-type">-</div>
<div class="stat-label">Feed Type</div>
</div>
</div>
<div class="tabs">
<button class="tab active" onclick="switchOutputTab('preview')">Preview</button>
<button class="tab" onclick="switchOutputTab('code')">HTML Code</button>
</div>
<div id="preview-tab" class="tab-content active">
<div class="preview-container">
<h3>Preview</h3>
<div id="preview-content">
<p style="color: var(--gray); text-align: center; padding: 40px;">
Your converted HTML will appear here
</p>
</div>
</div>
</div>
<div id="code-tab" class="tab-content">
<div class="code-preview" id="html-code">
<!-- HTML code will appear here -->
</div>
</div>
<div class="btn-group">
<button class="btn btn-success" onclick="downloadHTML()">
<span>💾</span> Download HTML
</button>
<button class="btn btn-primary" onclick="copyToClipboard()">
<span>📋</span> Copy Code
</button>
</div>
</div>
</div>
</div>
<script>
let parsedFeed = null;
let convertedHTML = '';
function switchTab(tabName) {
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
event.target.classList.add('active');
document.getElementById(`${tabName}-tab`).classList.add('active');
}
function switchOutputTab(tabName) {
document.querySelectorAll('.card:last-child .tab').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.card:last-child .tab-content').forEach(content => content.classList.remove('active'));
event.target.classList.add('active');
document.getElementById(`${tabName}-tab`).classList.add('active');
}
function showAlert(type, message) {
const alertElement = document.getElementById(`${type}-alert`);
if (type === 'error') {
document.getElementById('error-message').textContent = message;
} else if (type === 'warning') {
document.getElementById('warning-message').textContent = message;
}
alertElement.style.display = 'block';
setTimeout(() => {
alertElement.style.display = 'none';
}, 5000);
}
function showLoading(show) {
document.getElementById('loading').style.display = show ? 'block' : 'none';
}
async function fetchFeed() {
const url = document.getElementById('feed-url').value.trim();
if (!url) {
showAlert('error', 'Please enter a valid feed URL');
return;
}
showLoading(true);
try {
// Use a CORS proxy for demonstration (in production, you'd use your own proxy)
const proxyUrl = `https://api.allorigins.win/get?url=${encodeURIComponent(url)}`;
const response = await fetch(proxyUrl);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const xmlString = data.contents;
if (!xmlString) {
throw new Error('No content received from the feed');
}
parseXML(xmlString);
} catch (error) {
// Fallback: Try direct fetch (might work for some feeds)
try {
const response = await fetch(url);
const xmlString = await response.text();
parseXML(xmlString);
} catch (fallbackError) {
showAlert('error', `Failed to fetch feed: ${error.message}`);
}
} finally {
showLoading(false);
}
}
function parseDirectXML() {
const xmlString = document.getElementById('feed-xml').value.trim();
if (!xmlString) {
showAlert('error', 'Please paste valid RSS/Atom XML');
return;
}
parseXML(xmlString);
}
function parseXML(xmlString) {
try {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlString, 'text/xml');
// Check for parsing errors
const parserError = xmlDoc.querySelector('parsererror');
if (parserError) {
throw new Error('Invalid XML format');
}
// Determine feed type
const rssElement = xmlDoc.querySelector('rss');
const feedElement = xmlDoc.querySelector('feed');
if (rssElement) {
parsedFeed = parseRSS(xmlDoc);
document.getElementById('feed-type').textContent = 'RSS';
} else if (feedElement) {
parsedFeed = parseAtom(xmlDoc);
document.getElementById('feed-type').textContent = 'Atom';
} else {
throw new Error('Unsupported feed format');
}
document.getElementById('total-posts').textContent = parsedFeed.items.length;
document.getElementById('stats').style.display = 'grid';
showAlert('success', `Successfully parsed ${parsedFeed.items.length} posts`);
// Auto-convert after parsing
convertToHTML();
} catch (error) {
showAlert('error', `Failed to parse XML: ${error.message}`);
}
}
function parseRSS(xmlDoc) {
const channel = xmlDoc.querySelector('channel');
const items = xmlDoc.querySelectorAll('item');
const feed = {
title: getElementText(channel, 'title'),
description: getElementText(channel, 'description'),
link: getElementText(channel, 'link'),
items: []
};
items.forEach(item => {
feed.items.push({
title: getElementText(item, 'title'),
description: getElementText(item, 'description'),
content: getElementText(item, 'content:encoded') || getElementText(item, 'description'),
link: getElementText(item, 'link'),
pubDate: getElementText(item, 'pubDate'),
author: getElementText(item, 'dc:creator') || getElementText(item, 'author'),
guid: getElementText(item, 'guid'),
categories: Array.from(item.querySelectorAll('category')).map(cat => cat.textContent)
});
});
return feed;
}
function parseAtom(xmlDoc) {
const feed = xmlDoc.querySelector('feed');
const entries = xmlDoc.querySelectorAll('entry');
const parsedFeed = {
title: getElementText(feed, 'title'),
description: getElementText(feed, 'subtitle'),
link: feed.querySelector('link')?.getAttribute('href'),
items: []
};
entries.forEach(entry => {
parsedFeed.items.push({
title: getElementText(entry, 'title'),
description: getElementText(entry, 'summary'),
content: getElementText(entry, 'content') || getElementText(entry, 'summary'),
link: entry.querySelector('link')?.getAttribute('href'),
pubDate: getElementText(entry, 'published') || getElementText(entry, 'updated'),
author: getElementText(entry, 'author name'),
id: getElementText(entry, 'id'),
categories: Array.from(entry.querySelectorAll('category')).map(cat => cat.getAttribute('term'))
});
});
return parsedFeed;
}
function getElementText(parent, selector) {
const element = parent.querySelector(selector);
return element ? element.textContent.trim() : '';
}
function formatDate(dateString, format) {
if (!dateString) return '';
const date = new Date(dateString);
switch(format) {
case 'full':
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
case 'short':
return date.toLocaleDateString('en-US');
case 'iso':
return date.toISOString().split('T')[0];
case 'relative':
const now = new Date();
const diff = now - date;
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days === 0) return 'Today';
if (days === 1) return 'Yesterday';
if (days < 7) return `${days} days ago`;
if (days < 30) return `${Math.floor(days / 7)} weeks ago`;
if (days < 365) return `${Math.floor(days / 30)} months ago`;
return `${Math.floor(days / 365)} years ago`;
default:
return dateString;
}
}
function convertToHTML() {
if (!parsedFeed) {
showAlert('warning', 'Please parse a feed first');
return;
}
const options = {
includeTitle: document.getElementById('include-title').checked,
includeDate: document.getElementById('include-date').checked,
includeAuthor: document.getElementById('include-author').checked,
includeContent: document.getElementById('include-content').checked,
includeLink: document.getElementById('include-link').checked,
semanticHTML: document.getElementById('semantic-html').checked,
dateFormat: document.getElementById('date-format').value,
maxPosts: parseInt(document.getElementById('max-posts').value)
};
const items = parsedFeed.items.slice(0, options.maxPosts);
let html = '';
if (options.semanticHTML) {
html = generateSemanticHTML(items, options);
} else {
html = generateBasicHTML(items, options);
}
convertedHTML = html;
// Update preview
document.getElementById('preview-content').innerHTML = html;
document.getElementById('html-code').textContent = html;
document.getElementById('converted-posts').textContent = items.length;
}
function generateSemanticHTML(items, options) {
let html = '<article class="feed-container">\n';
if (parsedFeed.title) {
html += ' <header class="feed-header">\n';
html += ` <h1 class="feed-title">${escapeHtml(parsedFeed.title)}</h1>\n`;
if (parsedFeed.description) {
html += ` <p class="feed-description">${escapeHtml(parsedFeed.description)}</p>\n`;
}
html += ' </header>\n';
}
html += ' <main class="feed-content">\n';
items.forEach((item, index) => {
html += ` <article class="post-item" data-post-id="${index}">\n`;
if (options.includeTitle && item.title) {
html += ` <header>\n`;
html += ` <h2 class="post-title">\n`;
if (options.includeLink && item.link) {
html += ` <a href="${escapeHtml(item.link)}" target="_blank" rel="noopener">${escapeHtml(item.title)}</a>\n`;
} else {
html += ` ${escapeHtml(item.title)}\n`;
}
html += ` </h2>\n`;
html += ` </header>\n`;
}
html += ` <div class="post-meta">\n`;
if (options.includeDate && item.pubDate) {
html += ` <time class="post-date" datetime="${new Date(item.pubDate).toISOString()}">${formatDate(item.pubDate, options.dateFormat)}</time>\n`;
}
if (options.includeAuthor && item.author) {
html += ` <address class="post-author">By ${escapeHtml(item.author)}</address>\n`;
}
html += ` </div>\n`;
if (options.includeContent) {
html += ` <div class="post-content">\n`;
const content = item.content || item.description || '';
html += ` ${content}\n`;
html += ` </div>\n`;
}
if (options.includeLink && item.link && !options.includeTitle) {
html += ` <footer>\n`;
html += ` <a href="${escapeHtml(item.link)}" class="read-more" target="_blank" rel="noopener">Read more →</a>\n`;
html += ` </footer>\n`;
}
html += ` </article>\n\n`;
});
html += ' </main>\n';
html += '</article>';
return html;
}
function generateBasicHTML(items, options) {
let html = '<div class="feed">\n';
if (parsedFeed.title) {
html += ` <h1>${escapeHtml(parsedFeed.title)}</h1>\n`;
}
items.forEach(item => {
html += ' <div class="post">\n';
if (options.includeTitle && item.title) {
if (options.includeLink && item.link) {
html += ` <h2><a href="${escapeHtml(item.link)}">${escapeHtml(item.title)}</a></h2>\n`;
} else {
html += ` <h2>${escapeHtml(item.title)}</h2>\n`;
}
}
if (options.includeDate && item.pubDate) {
html += ` <p class="date">${formatDate(item.pubDate, options.dateFormat)}</p>\n`;
}
if (options.includeAuthor && item.author) {
html += ` <p class="author">By ${escapeHtml(item.author)}</p>\n`;
}
if (options.includeContent) {
const content = item.content || item.description || '';
html += ` <div class="content">${content}</div>\n`;
}
html += ' </div>\n\n';
});
html += '</div>';
return html;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function downloadHTML() {
if (!convertedHTML) {
showAlert('warning', 'No HTML to download. Please convert a feed first.');
return;
}
const blob = new Blob([convertedHTML], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'rss-feed.html';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showAlert('success', 'HTML file downloaded successfully');
}
function copyToClipboard() {
if (!convertedHTML) {
showAlert('warning', 'No HTML to copy. Please convert a feed first.');
return;
}
navigator.clipboard.writeText(convertedHTML).then(() => {
showAlert('success', 'HTML code copied to clipboard');
}).catch(err => {
showAlert('error', 'Failed to copy to clipboard');
});
}
// Add sample feed for demonstration
document.addEventListener('DOMContentLoaded', () => {
const sampleFeed = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Sample Tech Blog</title>
<description>Latest technology news and tutorials</description>
<link>https://example.com</link>
<item>
<title>Getting Started with Web Development</title>
<description>Learn the basics of HTML, CSS, and JavaScript in this comprehensive guide.</description>
<content:encoded><![CDATA[
<p>Web development is an exciting field that combines creativity with technical skills. In this article, we'll explore the fundamental technologies that power the web.</p>
<h3>HTML - The Structure</h3>
<p>HTML provides the semantic structure of web pages...</p>
]]></content:encoded>
<link>https://example.com/web-dev-basics</link>
<pubDate>Mon, 15 Jan 2024 10:00:00 GMT</pubDate>
<author>John Doe</author>
</item>
<item>
<title>Understanding CSS Grid Layout</title>
<description>Master CSS Grid for creating responsive and flexible layouts.</description>
<content:encoded><![CDATA[
<p>CSS Grid is a powerful layout system that makes it easy to create complex responsive designs.</p>
<h3>Grid Basics</h3>
<p>Let's start with the fundamentals...</p>
]]></content:encoded>
<link>https://example.com/css-grid</link>
<pubDate>Sun, 14 Jan 2024 15:30:00 GMT</pubDate>
<author>Jane Smith</author>
</item>
</channel>
</rss>`;
document.getElementById('feed-xml').value = sampleFeed;
});
</script>
</body>
</html>
No comments:
Post a Comment