blog
This commit is contained in:
@@ -20,6 +20,7 @@ import '../styles/global.css';
|
||||
<nav class="hidden md:flex space-x-4">
|
||||
<a href="/" class="p-2 hover:text-purple-300 transition-colors duration-200">Home</a>
|
||||
<a href="/catalog" class="p-2 hover:text-purple-300 transition-colors duration-200">Catalog</a>
|
||||
<a href="/blog" class="p-2 hover:text-purple-300 transition-colors duration-200">Blog</a>
|
||||
<a href="/howtos" class="p-2 hover:text-purple-300 transition-colors duration-200">How-tos</a>
|
||||
<a href="/code" class="p-2 hover:text-purple-300 transition-colors duration-200">Code</a>
|
||||
</nav>
|
||||
@@ -35,6 +36,7 @@ import '../styles/global.css';
|
||||
<nav id="mobile-menu" class="hidden md:hidden absolute top-full left-0 w-full bg-gray-800 flex-col items-center py-4 space-y-2 z-50">
|
||||
<a href="/" class="block p-2 w-full text-center hover:bg-gray-700 transition-colors duration-200">Home</a>
|
||||
<a href="/catalog" class="block p-2 w-full text-center hover:bg-gray-700 transition-colors duration-200">Catalog</a>
|
||||
<a href="/blog" class="block p-2 w-full text-center hover:bg-gray-700 transition-colors duration-200">Blog</a>
|
||||
<a href="/howtos" class="block p-2 w-full text-center hover:bg-gray-700 transition-colors duration-200">How-tos</a>
|
||||
<a href="/code" class="block p-2 w-full text-center hover:bg-gray-700 transition-colors duration-200">Code</a>
|
||||
</nav>
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
|
||||
const WIKIJS_BASE_URL = 'https://wiki.teletype.hu';
|
||||
const WIKIJS_TOKEN = import.meta.env.WEBAPP_WIKIJS_TOKEN;
|
||||
|
||||
interface WikiPage {
|
||||
id: number;
|
||||
path: string;
|
||||
title: string;
|
||||
description: string;
|
||||
render?: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
let blogPages: WikiPage[] = [];
|
||||
let error: string | null = null;
|
||||
let permissionWarning = false;
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
if (WIKIJS_TOKEN) {
|
||||
headers['Authorization'] = `Bearer ${WIKIJS_TOKEN}`;
|
||||
}
|
||||
|
||||
// 1. Get the list of pages with the "blog" tag
|
||||
const LIST_QUERY = `
|
||||
{
|
||||
pages {
|
||||
list(orderBy: UPDATED, orderByDirection: DESC, tags: ["blog"]) {
|
||||
id
|
||||
path
|
||||
title
|
||||
description
|
||||
updatedAt
|
||||
createdAt
|
||||
locale
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const listResponse = await fetch(`${WIKIJS_BASE_URL}/graphql`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ query: LIST_QUERY }),
|
||||
});
|
||||
|
||||
const listJson = await listResponse.json();
|
||||
|
||||
if (listJson.errors) {
|
||||
throw new Error(`GraphQL error: ${listJson.errors.map((e: any) => e.message).join(', ')}`);
|
||||
}
|
||||
|
||||
const pages = listJson?.data?.pages?.list ?? [];
|
||||
blogPages = pages.map((p: any) => ({
|
||||
id: p.id,
|
||||
path: p.path,
|
||||
title: p.title || p.path,
|
||||
description: p.description ?? '',
|
||||
updatedAt: p.updatedAt,
|
||||
createdAt: p.createdAt,
|
||||
locale: p.locale,
|
||||
}));
|
||||
|
||||
// 2. Try to fetch rendered HTML for each page
|
||||
const idsToFetch = blogPages.slice(0, 10).map(p => p.id);
|
||||
|
||||
if (idsToFetch.length > 0) {
|
||||
const SINGLE_PAGES_QUERY = `
|
||||
{
|
||||
${idsToFetch.map((id: number, index: number) => `
|
||||
page_${index}: pages {
|
||||
single(id: ${id}) {
|
||||
id
|
||||
render
|
||||
}
|
||||
}
|
||||
`).join('\n')}
|
||||
}
|
||||
`;
|
||||
|
||||
const contentResponse = await fetch(`${WIKIJS_BASE_URL}/graphql`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ query: SINGLE_PAGES_QUERY }),
|
||||
});
|
||||
|
||||
const contentJson = await contentResponse.json();
|
||||
|
||||
if (contentJson.errors) {
|
||||
if (contentJson.errors.some((e: any) => e.message.includes('authorized') || e.message.includes('permission'))) {
|
||||
console.warn('WikiJS: Permission denied for pages.single. Falling back to descriptions.');
|
||||
permissionWarning = true;
|
||||
} else {
|
||||
console.error('WikiJS Content Error:', contentJson.errors);
|
||||
}
|
||||
}
|
||||
|
||||
// Merge rendered HTML if available
|
||||
if (contentJson.data) {
|
||||
Object.values(contentJson.data).forEach((item: any) => {
|
||||
if (item?.single) {
|
||||
const match = blogPages.find(p => p.id === item.single.id);
|
||||
if (match) {
|
||||
match.render = item.single.render;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: any) {
|
||||
console.error('Failed to fetch WikiJS data:', e);
|
||||
error = `Failed to fetch wiki data: ${e.message}`;
|
||||
}
|
||||
|
||||
function timeAgo(dateStr: string): string {
|
||||
const now = new Date();
|
||||
const date = new Date(dateStr);
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffMins < 1) return 'just now';
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
if (diffDays < 30) return `${diffDays}d ago`;
|
||||
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
}
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<div class="bg-gray-50 min-h-screen pb-20">
|
||||
<header class="relative overflow-hidden py-24 text-white text-center bg-indigo-900">
|
||||
<div class="absolute inset-0 opacity-30">
|
||||
<div class="hero-decor-blob -top-24 -left-24 h-96 w-96 bg-purple-600"></div>
|
||||
<div class="hero-decor-blob -bottom-24 -right-24 h-96 w-96 bg-blue-600"></div>
|
||||
</div>
|
||||
|
||||
<div class="hero-container">
|
||||
<div class="hero-badge">
|
||||
Community Stories
|
||||
</div>
|
||||
<h1 class="hero-title">
|
||||
Developer <span class="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-400">Blog</span>
|
||||
</h1>
|
||||
<p class="hero-subtitle mb-10">
|
||||
News, updates, and behind-the-scenes stories from the Teletype Games community.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="w-full px-4 md:px-8 -mt-12 relative z-10">
|
||||
|
||||
{error && (
|
||||
<div class="max-w-7xl mx-auto bg-red-50 border-l-4 border-red-500 p-6 rounded-r-xl shadow-lg mb-12 flex items-start gap-4" role="alert">
|
||||
<span class="text-2xl">⚠️</span>
|
||||
<div>
|
||||
<h3 class="text-red-800 font-bold text-lg">Failed to connect to Wiki</h3>
|
||||
<p class="text-red-700 mt-1">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{permissionWarning && (
|
||||
<div class="max-w-7xl mx-auto bg-amber-50 border-l-4 border-amber-500 p-6 rounded-r-xl shadow-lg mb-12 flex items-start gap-4" role="alert">
|
||||
<span class="text-2xl">ℹ️</span>
|
||||
<div>
|
||||
<h3 class="text-amber-800 font-bold text-lg">Limited Access</h3>
|
||||
<p class="text-amber-700 mt-1">Full content is hidden because the API token lacks <code>manage:pages</code> or <code>delete:pages</code> permissions in Wiki.js. Showing summaries instead.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!error && blogPages.length === 0 && (
|
||||
<div class="max-w-7xl mx-auto bg-white rounded-2xl shadow-xl p-12 text-center border border-gray-100">
|
||||
<div class="text-6xl mb-4">📭</div>
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-2">No blog posts found</h2>
|
||||
<p class="text-gray-500 max-w-md mx-auto">We haven't published any blog posts yet. Check back soon!</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!error && blogPages.length > 0 && (
|
||||
<div class="flex flex-col gap-12">
|
||||
{blogPages.map((page) => {
|
||||
const pageUrl = `${WIKIJS_BASE_URL}/${page.locale}/${page.path}`;
|
||||
const isNew = (new Date().getTime() - new Date(page.createdAt).getTime()) < 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
return (
|
||||
<article class="bg-white rounded-2xl shadow-xl border border-gray-100 overflow-hidden">
|
||||
<div class="p-8 md:p-12">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
{isNew && (
|
||||
<span class="flex items-center gap-1 text-green-600 bg-green-50 px-2 py-0.5 rounded-full border border-green-100">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse"></span>
|
||||
New Post
|
||||
</span>
|
||||
)}
|
||||
<span>{timeAgo(page.updatedAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href={pageUrl} target="_blank" class="text-indigo-600 hover:text-indigo-800 font-bold flex items-center gap-2 transition-colors">
|
||||
View on Wiki <span>→</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h2 class="text-3xl md:text-4xl font-black text-gray-900 mb-8 leading-tight">
|
||||
{page.title}
|
||||
</h2>
|
||||
|
||||
{page.render ? (
|
||||
<div class="wiki-content prose prose-lg max-w-none text-gray-700 leading-relaxed" set:html={page.render} />
|
||||
) : (
|
||||
<div class="text-gray-600 text-lg leading-relaxed whitespace-pre-wrap">
|
||||
{page.description || "No preview available. Click 'View on Wiki' to read the full post."}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style is:global>
|
||||
.wiki-content h1 { @apply text-3xl font-bold mt-8 mb-4 text-gray-900; }
|
||||
.wiki-content h2 { @apply text-2xl font-bold mt-6 mb-3 text-gray-800; }
|
||||
.wiki-content h3 { @apply text-xl font-bold mt-4 mb-2 text-gray-800; }
|
||||
.wiki-content p { @apply mb-4 text-gray-700; }
|
||||
.wiki-content ul { @apply list-disc ml-6 mb-4 text-gray-700; }
|
||||
.wiki-content ol { @apply list-decimal ml-6 mb-4 text-gray-700; }
|
||||
.wiki-content li { @apply mb-1; }
|
||||
.wiki-content a { @apply text-indigo-600 hover:underline; }
|
||||
.wiki-content img { @apply max-w-full h-auto rounded-lg my-6 shadow-md; }
|
||||
.wiki-content pre { @apply bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto my-6 font-mono text-sm; }
|
||||
.wiki-content code { @apply bg-gray-100 text-pink-600 px-1 rounded font-mono text-sm; }
|
||||
.wiki-content blockquote { @apply border-l-4 border-gray-300 pl-4 italic my-6 text-gray-600; }
|
||||
.wiki-content table { @apply w-full border-collapse my-6; }
|
||||
.wiki-content th, .wiki-content td { @apply border border-gray-200 p-3 text-left; }
|
||||
.wiki-content th { @apply bg-gray-50 font-bold; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user