blog permalink

This commit is contained in:
2026-03-06 18:47:12 +01:00
parent 7a2942b7e2
commit f96eec440a
2 changed files with 266 additions and 75 deletions
+22 -75
View File
@@ -69,53 +69,6 @@ try {
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}`;
@@ -135,6 +88,11 @@ function timeAgo(dateStr: string): string {
if (diffDays < 30) return `${diffDays}d ago`;
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
}
function getPermalink(path: string): string {
const slug = path.startsWith('blog/') ? path.replace('blog/', '') : path;
return `/blog/${slug}`;
}
---
<Layout>
@@ -170,16 +128,6 @@ function timeAgo(dateStr: string): string {
</div>
)}
{permissionWarning && (
<div class="warning-banner" role="alert">
<span class="text-2xl">️</span>
<div>
<h3 class="warning-banner-title">Limited Access</h3>
<p class="warning-banner-desc">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="empty-state">
<div class="empty-state-icon">📭</div>
@@ -192,6 +140,7 @@ function timeAgo(dateStr: string): string {
<div class="blog-posts-list">
{blogPages.map((page) => {
const isNew = (new Date().getTime() - new Date(page.createdAt).getTime()) < 7 * 24 * 60 * 60 * 1000;
const permalink = getPermalink(page.path);
return (
<article class="blog-post-card">
@@ -211,7 +160,9 @@ function timeAgo(dateStr: string): string {
</div>
<h2 class="blog-post-title">
{page.title}
<a href={permalink} class="hover:text-indigo-600 transition-colors">
{page.title}
</a>
</h2>
{page.description && (
@@ -220,15 +171,14 @@ function timeAgo(dateStr: string): string {
</p>
)}
{page.render ? (
<div class="wiki-content max-w-none text-gray-700 leading-relaxed" set:html={page.render} />
) : (
!page.description && (
<div class="blog-post-fallback">
No preview available for this post.
</div>
)
)}
<div class="mt-6">
<a href={permalink} class="read-more-btn">
Read more
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</a>
</div>
</div>
</article>
);
@@ -250,7 +200,7 @@ function timeAgo(dateStr: string): string {
@apply absolute inset-0 opacity-30;
}
.blog-main {
@apply w-full px-4 md:px-8 -mt-12 relative z-10;
@apply max-w-5xl mx-auto px-4 md:px-8 -mt-12 relative z-10;
}
/* Banners */
@@ -261,10 +211,6 @@ function timeAgo(dateStr: string): string {
.error-banner-title { @apply text-red-800 font-bold text-lg; }
.error-banner-desc { @apply text-red-700 mt-1; }
.warning-banner { @apply banner-base bg-amber-50 border-amber-500; }
.warning-banner-title { @apply text-amber-800 font-bold text-lg; }
.warning-banner-desc { @apply text-amber-700 mt-1; }
/* Empty State */
.empty-state {
@apply max-w-7xl mx-auto bg-white rounded-2xl shadow-xl p-12 text-center border border-gray-100;
@@ -299,10 +245,11 @@ function timeAgo(dateStr: string): string {
@apply text-2xl md:text-3xl font-black text-gray-900 mb-4 leading-tight;
}
.blog-post-description {
@apply text-lg text-gray-500 mb-8 font-medium leading-relaxed;
@apply text-lg text-gray-500 mb-2 font-medium leading-relaxed;
}
.blog-post-fallback {
@apply text-gray-600 text-base leading-relaxed whitespace-pre-wrap;
.read-more-btn {
@apply inline-flex items-center text-indigo-600 font-bold hover:text-indigo-800 transition-colors;
}
</style>
@@ -0,0 +1,244 @@
---
import Layout from '../../layouts/Layout.astro';
export async function getStaticPaths() {
const WIKIJS_BASE_URL = 'https://wiki.teletype.hu';
const WIKIJS_TOKEN = import.meta.env.WEBAPP_WIKIJS_TOKEN;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
if (WIKIJS_TOKEN) {
headers['Authorization'] = `Bearer ${WIKIJS_TOKEN}`;
}
const LIST_QUERY = `
{
pages {
list(orderBy: UPDATED, orderByDirection: DESC, tags: ["blog"]) {
id
path
title
description
updatedAt
createdAt
locale
}
}
}
`;
try {
const listResponse = await fetch(`${WIKIJS_BASE_URL}/graphql`, {
method: 'POST',
headers,
body: JSON.stringify({ query: LIST_QUERY }),
});
const listJson = await listResponse.json();
const pages = listJson?.data?.pages?.list ?? [];
return pages.map((page: any) => {
// WikiJS paths usually look like "blog/my-post"
// We want to match it against /blog/[...slug]
// If path is "blog/post-1", slug should be "post-1"
const slug = page.path.startsWith('blog/')
? page.path.replace('blog/', '')
: page.path;
return {
params: { slug },
props: { pageId: page.id },
};
});
} catch (e) {
console.error('Error fetching paths for blog posts:', e);
return [];
}
}
const { slug } = Astro.params;
const { pageId } = Astro.props;
const WIKIJS_BASE_URL = 'https://wiki.teletype.hu';
const WIKIJS_TOKEN = import.meta.env.WEBAPP_WIKIJS_TOKEN;
let pageContent = null;
let error = null;
try {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
if (WIKIJS_TOKEN) {
headers['Authorization'] = `Bearer ${WIKIJS_TOKEN}`;
}
const SINGLE_PAGE_QUERY = `
{
pages {
single(id: ${pageId}) {
id
path
title
description
render
updatedAt
createdAt
}
}
}
`;
const response = await fetch(`${WIKIJS_BASE_URL}/graphql`, {
method: 'POST',
headers,
body: JSON.stringify({ query: SINGLE_PAGE_QUERY }),
});
const json = await response.json();
pageContent = json?.data?.pages?.single;
if (!pageContent) {
error = "Post not found";
}
} catch (e: any) {
error = 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="blog-container">
<header class="blog-header">
<div class="blog-header-decor">
<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">
<a href="/blog" class="back-link">
← Back to Blog
</a>
{pageContent && (
<>
<h1 class="hero-title mt-4">
{pageContent.title}
</h1>
<p class="hero-subtitle mb-4">
{pageContent.description}
</p>
<div class="blog-post-timestamp text-indigo-200">
{timeAgo(pageContent.updatedAt)}
</div>
</>
)}
</div>
</header>
<main class="blog-main">
{error && (
<div class="error-banner" role="alert">
<span class="text-2xl">⚠️</span>
<div>
<h3 class="error-banner-title">Error</h3>
<p class="error-banner-desc">{error}</p>
</div>
</div>
)}
{pageContent && (
<article class="blog-post-card">
<div class="blog-post-content">
{pageContent.render ? (
<div class="wiki-content max-w-none text-gray-700 leading-relaxed" set:html={pageContent.render} />
) : (
<div class="blog-post-fallback">
No content available for this post.
</div>
)}
</div>
</article>
)}
</main>
</div>
</Layout>
<style>
.blog-container {
@apply bg-gray-50 min-h-screen pb-20;
}
.blog-header {
@apply relative overflow-hidden py-24 text-white text-center bg-indigo-900;
}
.blog-header-decor {
@apply absolute inset-0 opacity-30;
}
.hero-container {
@apply relative z-10 max-w-4xl mx-auto px-4;
}
.back-link {
@apply inline-block text-indigo-300 hover:text-white transition-colors font-medium;
}
.hero-title {
@apply text-4xl md:text-5xl font-black mb-4 leading-tight;
}
.hero-subtitle {
@apply text-xl text-indigo-100 font-medium;
}
.blog-main {
@apply max-w-4xl mx-auto px-4 md:px-8 -mt-12 relative z-10;
}
.blog-post-card {
@apply bg-white rounded-2xl shadow-xl border border-gray-100 overflow-hidden;
}
.blog-post-content {
@apply p-6 md:p-12;
}
.blog-post-timestamp {
@apply text-sm font-semibold uppercase tracking-wider;
}
.error-banner {
@apply 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;
}
.error-banner-title { @apply text-red-800 font-bold text-lg; }
.error-banner-desc { @apply text-red-700 mt-1; }
</style>
<style is:global>
.wiki-content h1 { @apply text-2xl font-bold mt-8 mb-4 text-gray-900; }
.wiki-content h2 { @apply text-xl font-bold mt-6 mb-3 text-gray-800; }
.wiki-content h3 { @apply text-lg font-bold mt-4 mb-2 text-gray-800; }
.wiki-content p { @apply mb-4 text-gray-700 leading-relaxed; }
.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>