next-tribune-blog
Version:
Automatic blog generator for Next.js from Tribune.sh blockchain articles
588 lines (526 loc) • 13.6 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateBlogPages = generateBlogPages;
const fs_extra_1 = __importDefault(require("fs-extra"));
const path_1 = __importDefault(require("path"));
const blockchain_1 = require("./blockchain");
function convertArticleToPost(article) {
// Combine all body parts
const content = [
article.body1,
article.body2,
article.body3,
article.body4,
article.body5
].filter(Boolean).join('');
// Create a slug from the title
const slug = article.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '') || `article-${article.id}`;
return {
slug,
title: article.title,
description: article.description,
content,
coverImage: article.header || undefined,
author: article.owner,
date: new Date(Number(article.timestamp) * 1000).toISOString(),
tokenAddress: article.tokenAddress,
blockchainId: article.id,
};
}
async function generateBlogPages(options) {
const { outputDir, walletAddress, contractAddress, rpcUrl } = options;
console.log('🚀 Generating Tribune blog pages...');
// Ensure output directory exists
await fs_extra_1.default.ensureDir(outputDir);
let articles = [];
try {
if (walletAddress && walletAddress !== '0x0') {
// Fetch articles for specific wallet
articles = await (0, blockchain_1.fetchArticlesByOwner)(walletAddress, contractAddress, rpcUrl);
}
else {
// Fetch all articles
articles = await (0, blockchain_1.fetchAllArticles)(contractAddress, rpcUrl);
}
}
catch (error) {
console.error('Failed to fetch articles from blockchain:', error);
console.log('⚠️ Generating example blog structure...');
// Generate example structure
await generateExampleStructure(outputDir);
return;
}
if (articles.length === 0) {
console.log('⚠️ No articles found on blockchain');
await generateExampleStructure(outputDir);
return;
}
const posts = articles.map(convertArticleToPost);
// Generate individual post pages
for (const post of posts) {
const postDir = path_1.default.join(outputDir, post.slug);
await fs_extra_1.default.ensureDir(postDir);
const postPageContent = generatePostPage(post);
await fs_extra_1.default.writeFile(path_1.default.join(postDir, 'page.tsx'), postPageContent);
}
// Generate blog list page
const listPageContent = generateListPage(posts);
await fs_extra_1.default.writeFile(path_1.default.join(outputDir, 'page.tsx'), listPageContent);
// Create styles in the blog directory
const stylesContent = getStylesContent();
await fs_extra_1.default.writeFile(path_1.default.join(outputDir, 'styles.module.css'), stylesContent);
console.log(`✅ Successfully generated ${posts.length} Tribune blog posts!`);
}
function generatePostPage(post) {
const escapedContent = post.content.replace(/`/g, '\\`').replace(/\$/g, '\\$');
return `import styles from '../styles.module.css';
import type { Metadata } from 'next';
import Link from 'next/link';
export const metadata: Metadata = {
title: '${post.title}',
description: '${post.description}',
};
export default function BlogPost() {
return (
<article className={styles.article}>
<header className={styles.header}>
${post.coverImage ? `<img src="${post.coverImage}" alt="${post.title}" className={styles.coverImage} />` : ''}
<h1 className={styles.title}>${post.title}</h1>
<div className={styles.metadata}>
<time className={styles.date}>${new Date(post.date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}</time>
<span className={styles.divider}>•</span>
<Link
href={\`https://tribune.sh/article/${post.blockchainId}\`}
target="_blank"
className={styles.tribuneLink}
>
View on Tribune.sh
</Link>
</div>
<div className={styles.author}>
<span>By </span>
<a
href={\`https://abstractscan.com/address/${post.author}\`}
target="_blank"
rel="noopener noreferrer"
className={styles.authorLink}
>
${post.author.slice(0, 6)}...${post.author.slice(-4)}
</a>
</div>
</header>
<div
className={styles.content}
dangerouslySetInnerHTML={{ __html: \`${escapedContent}\` }}
/>
<footer className={styles.footer}>
<Link
href={\`https://abstractscan.com/token/${post.tokenAddress}\`}
target="_blank"
className={styles.tokenLink}
>
View Article Token
</Link>
</footer>
</article>
);
}`;
}
function generateListPage(posts) {
const sortedPosts = posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
const postsData = sortedPosts.map(({ content, ...rest }) => rest);
return `import Link from 'next/link';
import styles from './styles.module.css';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Tribune Blog',
description: 'Blog posts from Tribune.sh',
};
const posts = ${JSON.stringify(postsData, null, 2)};
export default function BlogList() {
return (
<div className={styles.container}>
<h1 className={styles.pageTitle}>Tribune Blog</h1>
<p className={styles.subtitle}>Articles published on Tribune.sh blockchain</p>
<div className={styles.grid}>
{posts.map((post) => (
<Link
key={post.slug}
href={\`/blog/\${post.slug}\`}
className={styles.card}
>
{post.coverImage && (
<img
src={post.coverImage}
alt={post.title}
className={styles.cardImage}
/>
)}
<div className={styles.cardContent}>
<h2 className={styles.cardTitle}>{post.title}</h2>
{post.description && (
<p className={styles.cardDescription}>{post.description}</p>
)}
<div className={styles.cardMeta}>
<time className={styles.cardDate}>
{new Date(post.date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</time>
<span className={styles.cardAuthor}>
{post.author.slice(0, 6)}...{post.author.slice(-4)}
</span>
</div>
</div>
</Link>
))}
</div>
</div>
);
}`;
}
async function generateExampleStructure(outputDir) {
const examplePageContent = `import styles from './styles.module.css';
export default function BlogList() {
return (
<div className={styles.container}>
<h1 className={styles.pageTitle}>Tribune Blog</h1>
<p className={styles.subtitle}>No articles found. Configure your wallet address in next.config.js</p>
<div className={styles.emptyState}>
<p>To display your Tribune articles:</p>
<ol>
<li>Add your wallet address to the plugin configuration</li>
<li>Ensure you have published articles on Tribune.sh</li>
<li>Run the build command again</li>
</ol>
</div>
</div>
);
}`;
await fs_extra_1.default.writeFile(path_1.default.join(outputDir, 'page.tsx'), examplePageContent);
// Also create styles for example structure
const stylesContent = getStylesContent();
await fs_extra_1.default.writeFile(path_1.default.join(outputDir, 'styles.module.css'), stylesContent);
}
function getStylesContent() {
return `/* Container and Layout */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.pageTitle {
font-size: 3rem;
font-weight: 800;
margin-bottom: 1rem;
text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle {
text-align: center;
color: #6b7280;
font-size: 1.25rem;
margin-bottom: 3rem;
}
/* Blog Grid */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 2rem;
}
/* Blog Card */
.card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 16px;
overflow: hidden;
transition: all 0.3s ease;
text-decoration: none;
color: inherit;
display: block;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
}
.cardImage {
width: 100%;
height: 200px;
object-fit: cover;
}
.cardContent {
padding: 1.5rem;
}
.cardTitle {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.75rem;
color: #111827;
line-height: 1.3;
}
.cardDescription {
color: #6b7280;
line-height: 1.6;
margin-bottom: 1rem;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.cardMeta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.875rem;
}
.cardDate {
color: #9ca3af;
font-weight: 500;
}
.cardAuthor {
color: #6366f1;
font-family: monospace;
font-size: 0.875rem;
}
/* Article Styles */
.article {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.header {
margin-bottom: 3rem;
padding-bottom: 2rem;
border-bottom: 1px solid #e5e7eb;
}
.coverImage {
width: 100%;
max-height: 400px;
object-fit: cover;
border-radius: 12px;
margin-bottom: 2rem;
}
.title {
font-size: 2.5rem;
font-weight: 800;
margin-bottom: 1rem;
color: #111827;
line-height: 1.2;
}
.metadata {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
color: #6b7280;
}
.date {
font-size: 1rem;
}
.divider {
color: #d1d5db;
}
.tribuneLink {
color: #6366f1;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.tribuneLink:hover {
color: #4f46e5;
text-decoration: underline;
}
.author {
color: #4b5563;
font-size: 1.125rem;
}
.authorLink {
color: #6366f1;
text-decoration: none;
font-family: monospace;
font-weight: 500;
}
.authorLink:hover {
text-decoration: underline;
}
/* Content Styles */
.content {
line-height: 1.8;
font-size: 1.125rem;
color: #374151;
}
.content h1,
.content h2,
.content h3 {
font-weight: 700;
margin-top: 2.5rem;
margin-bottom: 1rem;
color: #111827;
}
.content h1 {
font-size: 2rem;
}
.content h2 {
font-size: 1.75rem;
}
.content h3 {
font-size: 1.5rem;
}
.content p {
margin-bottom: 1.5rem;
}
.content ul,
.content ol {
margin-bottom: 1.5rem;
padding-left: 2rem;
}
.content li {
margin-bottom: 0.5rem;
}
.content pre {
background: #1f2937;
color: #e5e7eb;
padding: 1.5rem;
border-radius: 8px;
overflow-x: auto;
margin-bottom: 1.5rem;
}
.content code {
background: #f3f4f6;
color: #dc2626;
padding: 0.125rem 0.375rem;
border-radius: 4px;
font-size: 0.875em;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
}
.content pre code {
background: transparent;
color: inherit;
padding: 0;
}
.content blockquote {
border-left: 4px solid #6366f1;
padding-left: 1.5rem;
margin: 1.5rem 0;
font-style: italic;
color: #6b7280;
}
.content a {
color: #6366f1;
text-decoration: underline;
transition: color 0.2s ease;
}
.content a:hover {
color: #4f46e5;
}
.content img {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 2rem 0;
}
/* Footer */
.footer {
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid #e5e7eb;
text-align: center;
}
.tokenLink {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: #6366f1;
text-decoration: none;
font-weight: 500;
padding: 0.75rem 1.5rem;
border: 2px solid #6366f1;
border-radius: 8px;
transition: all 0.2s ease;
}
.tokenLink:hover {
background: #6366f1;
color: white;
}
/* Empty State */
.emptyState {
text-align: center;
padding: 4rem 2rem;
background: #f9fafb;
border-radius: 12px;
margin-top: 2rem;
}
.emptyState p {
color: #6b7280;
font-size: 1.125rem;
margin-bottom: 1.5rem;
}
.emptyState ol {
text-align: left;
max-width: 500px;
margin: 0 auto;
color: #4b5563;
}
.emptyState li {
margin-bottom: 0.75rem;
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.card {
background: #1f2937;
border-color: #374151;
}
.card:hover {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
}
.cardTitle,
.title,
.content h1,
.content h2,
.content h3 {
color: #f9fafb;
}
.content {
color: #e5e7eb;
}
.content code {
background: #374151;
color: #f87171;
}
.header,
.footer {
border-color: #374151;
}
.emptyState {
background: #111827;
}
.emptyState p,
.emptyState li {
color: #d1d5db;
}
.tokenLink {
border-color: #6366f1;
}
.tokenLink:hover {
background: #6366f1;
color: white;
}
}`;
}
//# sourceMappingURL=generator.js.map