feat: implement comprehensive blog system

Add dual-source blog system with Editor.js integration:
- Blog storage supporting repo-based (JSON) and database sources
- Admin panel with rich text editor using Editor.js
- Public news page with infinite scroll
- Individual blog post viewer page
- Categories management in admin
- Image upload functionality
- 4 SEO blog posts about WordPress with PluginCompass promotion
- 3 News blog posts about Plugin Compass
- API endpoints for CRUD operations
- Security and validation for admin operations

Closes blog feature request
This commit is contained in:
southseact-3d
2026-02-10 13:23:37 +00:00
parent 82ae9687b8
commit cfd8d9c706
17 changed files with 5126 additions and 10 deletions

454
chat/public/blog.html Normal file
View File

@@ -0,0 +1,454 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Loading blog post...">
<title>Blog - Plugin Compass</title>
<link rel="icon" type="image/png" href="/assets/Plugin.png">
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script>
(function applyTailwindConfig(cfg, attempts = 0) {
try {
if (typeof window.tailwind !== 'undefined') {
window.tailwind.config = cfg;
return;
}
} catch (e) {}
if (attempts >= 20) return;
setTimeout(() => applyTailwindConfig(cfg, attempts + 1), 100);
})({
theme: {
extend: {
fontFamily: { sans: ['Inter', 'sans-serif'] },
colors: {
brand: {
50: '#eef2ff', 100: '#e0e7ff', 200: '#c7d2fe', 300: '#a5b4fc',
400: '#818cf8', 500: '#6366f1', 600: '#4f46e5', 700: '#4338ca',
800: '#3730a3', 900: '#312e81'
}
}
}
}
});
</script>
<style>
body { font-family: 'Inter', sans-serif; }
/* Blog content styles */
.blog-content {
font-size: 1.125rem;
line-height: 1.75;
color: #374151;
}
.blog-content h2 {
font-size: 1.875rem;
font-weight: 700;
margin-top: 2.5rem;
margin-bottom: 1rem;
color: #111827;
}
.blog-content h3 {
font-size: 1.5rem;
font-weight: 600;
margin-top: 2rem;
margin-bottom: 0.75rem;
color: #111827;
}
.blog-content h4 {
font-size: 1.25rem;
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 0.5rem;
color: #111827;
}
.blog-content p {
margin-bottom: 1.25rem;
}
.blog-content ul, .blog-content ol {
margin-bottom: 1.25rem;
padding-left: 1.5rem;
}
.blog-content li {
margin-bottom: 0.5rem;
}
.blog-content a {
color: #4f46e5;
text-decoration: underline;
}
.blog-content a:hover {
color: #4338ca;
}
.blog-content blockquote {
border-left: 4px solid #e5e7eb;
padding-left: 1rem;
margin: 1.5rem 0;
font-style: italic;
color: #6b7280;
}
.blog-content blockquote cite {
display: block;
margin-top: 0.5rem;
font-size: 0.875rem;
font-style: normal;
color: #9ca3af;
}
.blog-content pre {
background: #f3f4f6;
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin-bottom: 1.25rem;
}
.blog-content code {
background: #f3f4f6;
padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
font-family: monospace;
font-size: 0.875rem;
}
.blog-content pre code {
background: none;
padding: 0;
}
.blog-content img {
max-width: 100%;
height: auto;
border-radius: 0.5rem;
margin: 1.5rem 0;
}
.blog-content figure {
margin: 1.5rem 0;
}
.blog-content figcaption {
text-align: center;
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.5rem;
}
.blog-content table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1.25rem;
}
.blog-content th, .blog-content td {
border: 1px solid #e5e7eb;
padding: 0.75rem;
text-align: left;
}
.blog-content th {
background: #f9fafb;
font-weight: 600;
}
.blog-content .embed {
position: relative;
padding-bottom: 56.25%;
height: 0;
overflow: hidden;
margin: 1.5rem 0;
}
.blog-content .embed iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* Loading animation */
.loading-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: .5; }
}
</style>
<!-- PostHog Analytics -->
<script src="/posthog.js"></script>
</head>
<body class="bg-gray-50 text-gray-900">
<!-- Navigation -->
<nav class="bg-white shadow-sm border-b border-gray-200 sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<a href="/" class="flex items-center gap-2">
<img src="/assets/Plugin.png" alt="Plugin Compass" class="h-8 w-8">
<span class="font-bold text-xl text-gray-900">Plugin Compass</span>
</a>
</div>
<div class="flex items-center gap-6">
<a href="/" class="text-gray-600 hover:text-gray-900 font-medium">Home</a>
<a href="/news" class="text-gray-600 hover:text-gray-900 font-medium">News</a>
<a href="/features" class="text-gray-600 hover:text-gray-900 font-medium">Features</a>
<a href="/pricing" class="text-gray-600 hover:text-gray-900 font-medium">Pricing</a>
<a href="/builder" class="bg-brand-600 text-white px-4 py-2 rounded-lg hover:bg-brand-700 transition">Get Started</a>
</div>
</div>
</div>
</nav>
<!-- Loading State -->
<div id="loading-state" class="min-h-screen flex items-center justify-center">
<div class="text-center">
<i class="fas fa-spinner fa-spin text-4xl text-brand-600 mb-4"></i>
<p class="text-gray-600">Loading article...</p>
</div>
</div>
<!-- Error State -->
<div id="error-state" class="min-h-screen flex items-center justify-center hidden">
<div class="text-center">
<i class="fas fa-exclamation-circle text-6xl text-red-400 mb-4"></i>
<h1 class="text-2xl font-bold text-gray-900 mb-2">Article Not Found</h1>
<p class="text-gray-600 mb-6">The blog post you're looking for doesn't exist or has been removed.</p>
<a href="/news" class="inline-flex items-center gap-2 text-brand-600 hover:text-brand-700 font-medium">
<i class="fas fa-arrow-left"></i>
Back to News
</a>
</div>
</div>
<!-- Article Content -->
<article id="article-content" class="hidden">
<!-- Hero Section -->
<header class="bg-white border-b border-gray-200">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="flex items-center gap-3 mb-6">
<span id="post-category" class="px-3 py-1 bg-brand-100 text-brand-700 rounded-full text-sm font-medium">Category</span>
<span id="post-type" class="text-gray-400 text-sm"></span>
</div>
<h1 id="post-title" class="text-4xl md:text-5xl font-bold text-gray-900 mb-6 leading-tight"></h1>
<div class="flex items-center gap-4 text-gray-600">
<div class="flex items-center gap-2">
<div class="w-10 h-10 rounded-full bg-brand-100 flex items-center justify-center">
<i class="fas fa-user text-brand-600"></i>
</div>
<div>
<p id="post-author" class="font-medium text-gray-900"></p>
<p id="post-date" class="text-sm"></p>
</div>
</div>
</div>
</div>
</header>
<!-- Featured Image -->
<div id="featured-image-container" class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 -mt-6 mb-12 hidden">
<img id="featured-image" src="" alt="" class="w-full h-64 md:h-96 object-cover rounded-xl shadow-lg">
</div>
<!-- Main Content -->
<main class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-16">
<div id="post-content" class="blog-content">
<!-- Content will be loaded here -->
</div>
<!-- Tags -->
<div id="post-tags" class="mt-12 pt-8 border-t border-gray-200">
<!-- Tags will be loaded here -->
</div>
<!-- Share -->
<div class="mt-8 flex items-center gap-4">
<span class="text-gray-600 font-medium">Share this article:</span>
<div class="flex gap-2">
<button onclick="shareTwitter()" class="w-10 h-10 rounded-full bg-blue-400 text-white flex items-center justify-center hover:bg-blue-500 transition">
<i class="fab fa-twitter"></i>
</button>
<button onclick="shareFacebook()" class="w-10 h-10 rounded-full bg-blue-600 text-white flex items-center justify-center hover:bg-blue-700 transition">
<i class="fab fa-facebook-f"></i>
</button>
<button onclick="shareLinkedIn()" class="w-10 h-10 rounded-full bg-blue-700 text-white flex items-center justify-center hover:bg-blue-800 transition">
<i class="fab fa-linkedin-in"></i>
</button>
<button onclick="copyLink()" class="w-10 h-10 rounded-full bg-gray-600 text-white flex items-center justify-center hover:bg-gray-700 transition" title="Copy link">
<i class="fas fa-link"></i>
</button>
</div>
</div>
</main>
</article>
<!-- Footer -->
<footer class="bg-gray-900 text-gray-300 py-12">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<div>
<div class="flex items-center gap-2 mb-4">
<img src="/assets/Plugin.png" alt="Plugin Compass" class="h-8 w-8">
<span class="font-bold text-white text-lg">Plugin Compass</span>
</div>
<p class="text-sm">Build custom WordPress plugins with AI. Replace expensive subscriptions with tailored solutions.</p>
</div>
<div>
<h4 class="font-semibold text-white mb-4">Product</h4>
<ul class="space-y-2 text-sm">
<li><a href="/features" class="hover:text-white">Features</a></li>
<li><a href="/pricing" class="hover:text-white">Pricing</a></li>
<li><a href="/builder" class="hover:text-white">Builder</a></li>
</ul>
</div>
<div>
<h4 class="font-semibold text-white mb-4">Resources</h4>
<ul class="space-y-2 text-sm">
<li><a href="/news" class="hover:text-white">News</a></li>
<li><a href="/docs" class="hover:text-white">Documentation</a></li>
<li><a href="/faq" class="hover:text-white">FAQ</a></li>
</ul>
</div>
<div>
<h4 class="font-semibold text-white mb-4">Legal</h4>
<ul class="space-y-2 text-sm">
<li><a href="/privacy" class="hover:text-white">Privacy Policy</a></li>
<li><a href="/terms" class="hover:text-white">Terms of Service</a></li>
</ul>
</div>
</div>
<div class="border-t border-gray-800 mt-8 pt-8 text-center text-sm">
<p>&copy; 2026 Plugin Compass. All rights reserved.</p>
</div>
</div>
</footer>
<script>
let postData = null;
// Initialize
document.addEventListener('DOMContentLoaded', async () => {
const slug = window.location.pathname.split('/blog/')[1];
if (!slug) {
showError();
return;
}
await loadPost(slug);
});
// Load post
async function loadPost(slug) {
try {
const response = await fetch(`/api/blogs/${slug}`);
if (!response.ok) {
showError();
return;
}
const data = await response.json();
postData = data.post;
if (!postData) {
showError();
return;
}
renderPost(postData);
} catch (error) {
console.error('Failed to load post:', error);
showError();
}
}
// Render post
function renderPost(post) {
// Update meta tags
document.title = `${post.title} - Plugin Compass`;
document.querySelector('meta[name="description"]').content = post.excerpt || post.meta_description || '';
// Update page content
document.getElementById('post-title').textContent = post.title;
document.getElementById('post-author').textContent = post.author || 'Plugin Compass Team';
document.getElementById('post-date').textContent = formatDate(post.published_at);
document.getElementById('post-category').textContent = post.category || 'Article';
document.getElementById('post-type').textContent = post.type === 'seo' ? 'SEO Article' : 'News';
// Featured image
if (post.featured_image) {
document.getElementById('featured-image').src = post.featured_image;
document.getElementById('featured-image').alt = post.title;
document.getElementById('featured-image-container').classList.remove('hidden');
}
// Content
document.getElementById('post-content').innerHTML = post.html || '<p>No content available.</p>';
// Tags
if (post.tags && post.tags.length > 0) {
document.getElementById('post-tags').innerHTML = `
<div class="flex flex-wrap gap-2">
${post.tags.map(tag => `
<span class="px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm">${escapeHtml(tag)}</span>
`).join('')}
</div>
`;
}
// Show content, hide loading
document.getElementById('loading-state').classList.add('hidden');
document.getElementById('article-content').classList.remove('hidden');
}
// Show error state
function showError() {
document.getElementById('loading-state').classList.add('hidden');
document.getElementById('error-state').classList.remove('hidden');
}
// Share functions
function shareTwitter() {
const url = encodeURIComponent(window.location.href);
const text = encodeURIComponent(postData?.title || 'Check out this article');
window.open(`https://twitter.com/intent/tweet?url=${url}&text=${text}`, '_blank', 'width=600,height=400');
}
function shareFacebook() {
const url = encodeURIComponent(window.location.href);
window.open(`https://www.facebook.com/sharer/sharer.php?u=${url}`, '_blank', 'width=600,height=400');
}
function shareLinkedIn() {
const url = encodeURIComponent(window.location.href);
const title = encodeURIComponent(postData?.title || '');
window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${url}&title=${title}`, '_blank', 'width=600,height=400');
}
function copyLink() {
navigator.clipboard.writeText(window.location.href).then(() => {
// Show feedback
const btn = event.target.closest('button');
const original = btn.innerHTML;
btn.innerHTML = '<i class="fas fa-check"></i>';
setTimeout(() => {
btn.innerHTML = original;
}, 2000);
});
}
// Utility functions
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
</script>
</body>
</html>