From 88753076685ddee1b67b4dd903cab91c47877667 Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Sun, 7 Jun 2026 23:24:23 +0200 Subject: [PATCH] grav interation --- .../app/controllers/api/wiki_controller.rb | 10 ++ apps/api/app/services/wiki_service.rb | 46 ++++++++++ apps/api/config/routes.rb | 1 + apps/frontend/src/api/wiki.api.ts | 91 +++++++------------ apps/frontend/vite.config.ts | 8 -- docker-compose.yaml | 2 +- 6 files changed, 89 insertions(+), 69 deletions(-) create mode 100644 apps/api/app/controllers/api/wiki_controller.rb create mode 100644 apps/api/app/services/wiki_service.rb diff --git a/apps/api/app/controllers/api/wiki_controller.rb b/apps/api/app/controllers/api/wiki_controller.rb new file mode 100644 index 0000000..474bcd1 --- /dev/null +++ b/apps/api/app/controllers/api/wiki_controller.rb @@ -0,0 +1,10 @@ +class Api::WikiController < ApiController + # GET /api/wiki/pages?tag=blog|howto[&limit=30][&body=1] + def index + render json: WikiService.new.pages( + tag: params[:tag], + limit: params[:limit], + body: params[:body] + ) + end +end diff --git a/apps/api/app/services/wiki_service.rb b/apps/api/app/services/wiki_service.rb new file mode 100644 index 0000000..6038f7d --- /dev/null +++ b/apps/api/app/services/wiki_service.rb @@ -0,0 +1,46 @@ +require "net/http" +require "json" + +# Fetches wiki pages (blog, howto, …) from the Grav backend, filtered by tag. +# +# Grav exposes: GET {WIKI_GRAV_URL}/api/pages.json?tag=[&limit=][&body=1] +# and returns: +# { +# "tag": "howto", "count": 24, +# "pages": [ +# { "id", "path", "title", "description", +# "createdAt", "updatedAt", "locale", "route", "tags", +# # with body=1 also: +# "render" (rendered HTML), +# "content" (raw markdown) } +# ] +# } +# +# This replaces the frontend's direct WikiJS GraphQL calls with a simple GET, +# and points at Grav (which is taking over from wiki.teletypegames.org). +class WikiService + GRAV_URL = ENV.fetch("WIKI_GRAV_URL", "http://localhost:8080").freeze + + def pages(tag:, limit: nil, body: nil) + query = { tag: tag } + query[:limit] = limit if limit.present? + query[:body] = body if body.present? + + uri = URI.parse("#{GRAV_URL}/api/pages.json") + uri.query = URI.encode_www_form(query) + + response = Net::HTTP.start( + uri.host, uri.port, + use_ssl: uri.scheme == "https", + open_timeout: 5, read_timeout: 10 + ) { |http| http.get(uri.request_uri) } + + unless response.is_a?(Net::HTTPSuccess) + return { "tag" => tag, "count" => 0, "pages" => [], "error" => "grav responded #{response.code}" } + end + + JSON.parse(response.body) + rescue StandardError => e + { "tag" => tag, "count" => 0, "pages" => [], "error" => e.message } + end +end diff --git a/apps/api/config/routes.rb b/apps/api/config/routes.rb index d3fab11..1d6429d 100644 --- a/apps/api/config/routes.rb +++ b/apps/api/config/routes.rb @@ -8,6 +8,7 @@ Rails.application.routes.draw do get "events", to: "events#index" get "members", to: "members#index" get "image/:id", to: "images#show" + get "wiki/pages", to: "wiki#index" end get "update", to: "update#update" diff --git a/apps/frontend/src/api/wiki.api.ts b/apps/frontend/src/api/wiki.api.ts index c1de244..43f18a2 100644 --- a/apps/frontend/src/api/wiki.api.ts +++ b/apps/frontend/src/api/wiki.api.ts @@ -1,45 +1,28 @@ import type { WikiPage, WikiPageWithContent, WikiPageContent } from '../lib/interfaces/wiki.interface' -const BASE = import.meta.env.VITE_WIKI_BASE || 'https://wiki.teletypegames.org' -const TOKEN = import.meta.env.WEBAPP_WIKIJS_TOKEN +// Public content base — used only for building external links to wiki pages. +// Points at Grav (which is taking over from wiki.teletypegames.org). +const WIKI_BASE = import.meta.env.VITE_WIKI_BASE || 'https://wiki.teletypegames.org' -function buildHeaders(): Record { - const h: Record = { 'Content-Type': 'application/json', 'Accept': 'application/json' } - if (TOKEN) h['Authorization'] = `Bearer ${TOKEN}` - return h -} +// Pages come from our own Rails API (same-origin), which proxies the Grav +// content backend: GET /api/wiki/pages?tag=[&limit=][&body=1] +async function fetchPages( + tag: string, + opts: { body?: boolean; limit?: number } = {}, +): Promise { + const params = new URLSearchParams({ tag }) + if (opts.body) params.set('body', '1') + if (opts.limit) params.set('limit', String(opts.limit)) -async function gql(query: string): Promise { - const res = await fetch(`${BASE}/graphql`, { - method: 'POST', - headers: buildHeaders(), - body: JSON.stringify({ query }), - }) + const res = await fetch(`/api/wiki/pages?${params.toString()}`) + if (!res.ok) return [] const json = await res.json() - if (json.errors) throw new Error(json.errors.map((e: any) => e.message).join(', ')) - return json.data + return json?.pages ?? [] } const listBlogPages = async (): Promise => { - const data = await gql(`{ - pages { - list(orderBy: CREATED, orderByDirection: DESC, tags: ["blog"]) { - id path title description updatedAt createdAt locale - } - } - }`) - const pages: any[] = data?.pages?.list ?? [] - - const pagesWithContent = await Promise.all(pages.map(async (p: any) => { - try { - const contentData = await gql(`{ pages { single(id: ${p.id}) { content } } }`) - return { ...p, content: contentData?.pages?.single?.content ?? '' } - } catch { - return { ...p, content: '' } - } - })) - - return pagesWithContent.map((p: any): WikiPageWithContent => ({ + const pages = await fetchPages('blog', { body: true }) + return pages.map((p: any): WikiPageWithContent => ({ id: p.id, path: p.path, title: p.title || p.path, @@ -52,14 +35,7 @@ const listBlogPages = async (): Promise => { } const getBlogPage = async (slug: string): Promise => { - const data = await gql(`{ - pages { - list(orderBy: CREATED, orderByDirection: DESC, tags: ["blog"]) { - id path - } - } - }`) - const pages: any[] = data?.pages?.list ?? [] + const pages = await fetchPages('blog', { body: true }) const matched = pages.find((p: any) => { const pageSlug = p.path.startsWith('blog/') ? p.path.replace('blog/', '') : p.path @@ -68,25 +44,20 @@ const getBlogPage = async (slug: string): Promise => { if (!matched) return null - const singleData = await gql(`{ - pages { - single(id: ${matched.id}) { - id path title description render updatedAt createdAt locale - } - } - }`) - return singleData?.pages?.single ?? null + return { + id: matched.id, + path: matched.path, + title: matched.title || matched.path, + description: matched.description ?? '', + render: matched.render ?? '', + updatedAt: matched.updatedAt, + createdAt: matched.createdAt, + locale: matched.locale, + } } const listHowtoPages = async (): Promise => { - const data = await gql(`{ - pages { - list(orderBy: UPDATED, orderByDirection: DESC, tags: ["howto"]) { - id path title description updatedAt createdAt locale - } - } - }`) - const pages: any[] = data?.pages?.list ?? [] + const pages = await fetchPages('howto', { limit: 30 }) return pages.map((p: any): WikiPage => ({ id: p.id, path: p.path, @@ -95,8 +66,8 @@ const listHowtoPages = async (): Promise => { updatedAt: p.updatedAt, createdAt: p.createdAt, locale: p.locale, - })).slice(0, 30) + })) } -export { BASE as WIKI_BASE } +export { WIKI_BASE } export default { listBlogPages, getBlogPage, listHowtoPages } diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index c822df7..94ef03c 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -13,14 +13,6 @@ export default defineConfig({ changeOrigin: true, rewrite: (path) => path.replace(/^\/proxy\/wiki/, '') }, - '/_assets': { - target: 'https://wiki.teletypegames.org', - changeOrigin: true, - }, - '/_error': { - target: 'https://wiki.teletypegames.org', - changeOrigin: true, - }, '/proxy/git': { target: 'https://git.teletypegames.org', changeOrigin: true, diff --git a/docker-compose.yaml b/docker-compose.yaml index 6b56430..b9fe5ad 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -125,7 +125,6 @@ services: - NODE_ENV=development - HOST=0.0.0.0 - WEBAPP_GITEA_TOKEN=${WEBAPP_GITEA_TOKEN} - - WEBAPP_WIKIJS_TOKEN=${WEBAPP_WIKIJS_TOKEN} - DISCORD_INVITE_LINK=${DISCORD_INVITE_LINK} - YOUTUBE_API_KEY=${YOUTUBE_API_KEY} - YOUTUBE_CHANNEL_ID=${YOUTUBE_CHANNEL_ID} @@ -161,6 +160,7 @@ services: - SECRET_KEY_BASE=${SECRET_KEY_BASE:-changeme_in_production_use_long_random_string} - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@teletype.hu} - ADMIN_PASSWORD=${ADMIN_PASSWORD:-password123} + - WIKI_GRAV_URL=${WIKI_GRAV_URL:-https://wiki.teletypegames.org} depends_on: mysql: condition: service_healthy