blog permalink
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user