From 2fc23f1c9320b95c02cb98a51a2aee0f8f641284 Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Wed, 4 Mar 2026 00:18:56 +0100 Subject: [PATCH] blog --- apps/frontend/src/layouts/Layout.astro | 2 + apps/frontend/src/pages/blog.astro | 254 +++++++++++++++++++++++++ docker-compose.yml | 1 + env-example | 1 + 4 files changed, 258 insertions(+) create mode 100644 apps/frontend/src/pages/blog.astro diff --git a/apps/frontend/src/layouts/Layout.astro b/apps/frontend/src/layouts/Layout.astro index 96c0e23..cf5c54a 100644 --- a/apps/frontend/src/layouts/Layout.astro +++ b/apps/frontend/src/layouts/Layout.astro @@ -20,6 +20,7 @@ import '../styles/global.css'; @@ -35,6 +36,7 @@ import '../styles/global.css'; diff --git a/apps/frontend/src/pages/blog.astro b/apps/frontend/src/pages/blog.astro new file mode 100644 index 0000000..2ca21d5 --- /dev/null +++ b/apps/frontend/src/pages/blog.astro @@ -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 = { + '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' }); +} +--- + + +
+
+
+
+
+
+ +
+
+ Community Stories +
+

+ Developer Blog +

+

+ News, updates, and behind-the-scenes stories from the Teletype Games community. +

+
+
+ +
+ + {error && ( + + )} + + {permissionWarning && ( + + )} + + {!error && blogPages.length === 0 && ( +
+
📭
+

No blog posts found

+

We haven't published any blog posts yet. Check back soon!

+
+ )} + + {!error && blogPages.length > 0 && ( +
+ {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 ( +
+
+
+
+
+ {isNew && ( + + + New Post + + )} + {timeAgo(page.updatedAt)} +
+
+ + View on Wiki → + +
+ +

+ {page.title} +

+ + {page.render ? ( +
+ ) : ( +
+ {page.description || "No preview available. Click 'View on Wiki' to read the full post."} +
+ )} +
+
+ ); + })} +
+ )} +
+
+
+ + diff --git a/docker-compose.yml b/docker-compose.yml index 70ad94f..dbf7f37 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -119,6 +119,7 @@ services: - NODE_ENV=development - HOST=0.0.0.0 - WEBAPP_GITEA_TOKEN=${WEBAPP_GITEA_TOKEN} + - WEBAPP_WIKIJS_TOKEN=${WEBAPP_WIKIJS_TOKEN} volumes: - ./apps/frontend:/app - /app/node_modules diff --git a/env-example b/env-example index 6502745..d289378 100644 --- a/env-example +++ b/env-example @@ -24,3 +24,4 @@ MYSQL_ROOT_PASSWORD= DB_PASSWORD= UPDATE_SECRET= DROP_PASSWORD= +WEBAPP_WIKIJS_TOKEN= \ No newline at end of file