From f2c07c83c542ebc27aa169441c1c1036ac36a2c9 Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Wed, 5 Aug 2026 07:29:07 +0200 Subject: [PATCH] Serve engine-tagged wiki pages on a new /engines section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontend: /engines index + /engines/:slug detail routes, nav menu item and en/hu translations. Engine pages are few, so the index uses an emphasized poster-style design (dark slate, emerald accents, numbered full-width cards with content preview) instead of the blog/howtos layouts. Slugs are the last wiki path segment, since engine pages live scattered in the wiki tree. API: /api/rss/engines feed linking to the site's engine pages, and a WikiService#pages alias for #index — RssService called the alias-less name, so the blog and howtos feeds were raising NoMethodError. --- .../api/app/controllers/api/rss_controller.rb | 7 + .../app/controllers/api/wiki_controller.rb | 2 +- apps/api/app/services/rss_service.rb | 22 +++ apps/api/app/services/wiki_service.rb | 3 + apps/api/config/routes.rb | 1 + apps/frontend/src/api/wiki.api.ts | 40 ++++- apps/frontend/src/i18n/locales/en.ts | 15 ++ apps/frontend/src/i18n/locales/hu.ts | 15 ++ apps/frontend/src/layout/AppLayout.vue | 6 + .../src/page/engines/EngineShowPage.vue | 110 ++++++++++++ .../src/page/engines/EnginesIndexPage.vue | 164 ++++++++++++++++++ apps/frontend/src/router/engines.router.ts | 6 + apps/frontend/src/router/index.router.ts | 2 + apps/frontend/src/stores/engines.store.ts | 40 +++++ 14 files changed, 430 insertions(+), 3 deletions(-) create mode 100644 apps/frontend/src/page/engines/EngineShowPage.vue create mode 100644 apps/frontend/src/page/engines/EnginesIndexPage.vue create mode 100644 apps/frontend/src/router/engines.router.ts create mode 100644 apps/frontend/src/stores/engines.store.ts diff --git a/apps/api/app/controllers/api/rss_controller.rb b/apps/api/app/controllers/api/rss_controller.rb index f48b444..0f5d8bc 100644 --- a/apps/api/app/controllers/api/rss_controller.rb +++ b/apps/api/app/controllers/api/rss_controller.rb @@ -24,4 +24,11 @@ class Api::RssController < ApiController xml = RssService.new.howtos_feed render xml: xml, content_type: "application/rss+xml" end + + api :GET, "/api/rss/engines", "Engines RSS feed" + returns code: 200, desc: "RSS XML feed of in-house engines" + def engines + xml = RssService.new.engines_feed + render xml: xml, content_type: "application/rss+xml" + end end diff --git a/apps/api/app/controllers/api/wiki_controller.rb b/apps/api/app/controllers/api/wiki_controller.rb index 0e6d865..98ba040 100644 --- a/apps/api/app/controllers/api/wiki_controller.rb +++ b/apps/api/app/controllers/api/wiki_controller.rb @@ -4,7 +4,7 @@ class Api::WikiController < ApiController end api :GET, "/api/wiki/pages", "List wiki pages filtered by tag" - param :tag, String, required: false, desc: "Filter by tag (blog, howto)" + param :tag, String, required: false, desc: "Filter by tag (blog, howto, engine)" param :limit, :number, required: false, desc: "Limit number of results" param :body, String, required: false, desc: "Include body content (1 = yes)" returns code: 200, desc: "Wiki pages response" do diff --git a/apps/api/app/services/rss_service.rb b/apps/api/app/services/rss_service.rb index 9bbe087..28f26e3 100644 --- a/apps/api/app/services/rss_service.rb +++ b/apps/api/app/services/rss_service.rb @@ -50,6 +50,28 @@ class RssService end.to_s end + def engines_feed + pages = WikiService.new.pages(tag: "engine", limit: 30) + items = pages.fetch("pages", []) + + RSS::Maker.make("2.0") do |maker| + maker.channel.title = "Teletype Games Engines" + maker.channel.link = "#{SITE_URL}/engines" + maker.channel.description = "In-house engines and frameworks from Teletype Games" + maker.channel.language = "hu" + + items.each do |page| + maker.items.new_item do |item| + slug = page["path"].to_s.split("/").last + item.title = page["title"] + item.link = "#{SITE_URL}/engines/#{slug}" + item.description = page["description"].to_s + item.pubDate = Time.parse(page["createdAt"]) rescue Time.current + end + end + end.to_s + end + def howtos_feed pages = WikiService.new.pages(tag: "howto", limit: 30) items = pages.fetch("pages", []) diff --git a/apps/api/app/services/wiki_service.rb b/apps/api/app/services/wiki_service.rb index e51d508..b8c7335 100644 --- a/apps/api/app/services/wiki_service.rb +++ b/apps/api/app/services/wiki_service.rb @@ -43,4 +43,7 @@ class WikiService rescue StandardError => e { "tag" => tag, "count" => 0, "pages" => [], "error" => e.message } end + + # Az RSS feedek ezen a néven hívják. + alias_method :pages, :index end diff --git a/apps/api/config/routes.rb b/apps/api/config/routes.rb index 4789d76..810ba35 100644 --- a/apps/api/config/routes.rb +++ b/apps/api/config/routes.rb @@ -11,6 +11,7 @@ Rails.application.routes.draw do get "rss/blog", to: "rss#blog" get "rss/releases", to: "rss#releases" get "rss/howtos", to: "rss#howtos" + get "rss/engines", to: "rss#engines" end # Utolsó sor: a host route-jai nyernek, a katalógus-útvonalakat diff --git a/apps/frontend/src/api/wiki.api.ts b/apps/frontend/src/api/wiki.api.ts index 9192fa0..6a3c799 100644 --- a/apps/frontend/src/api/wiki.api.ts +++ b/apps/frontend/src/api/wiki.api.ts @@ -67,6 +67,42 @@ const getBlogPage = async (slug: string): Promise => { } } +// Engine pages live scattered in the wiki tree (infrastructure/, others/, …), +// so the site slug is the last path segment: others/rubbs -> rubbs. +const engineSlug = (path: string): string => path.split('/').filter(Boolean).pop() ?? path + +const listEnginePages = async (): Promise => { + const pages = await fetchPages('engine', { body: true }) + return pages.map((p): WikiPageWithContent => ({ + id: p.id, + path: p.path, + title: p.title || p.path, + description: p.description ?? '', + content: p.content ?? '', + updatedAt: p.updatedAt, + createdAt: p.createdAt, + locale: p.locale, + })) +} + +const getEnginePage = async (slug: string): Promise => { + const pages = await fetchPages('engine', { body: true }) + + const matched = pages.find((p) => engineSlug(p.path) === slug) + if (!matched) return 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 pages = await fetchPages('howto', { limit: 30 }) return pages.map((p): WikiPage => ({ @@ -80,5 +116,5 @@ const listHowtoPages = async (): Promise => { })) } -export { WIKI_BASE } -export default { listBlogPages, getBlogPage, listHowtoPages } +export { WIKI_BASE, engineSlug } +export default { listBlogPages, getBlogPage, listHowtoPages, listEnginePages, getEnginePage } diff --git a/apps/frontend/src/i18n/locales/en.ts b/apps/frontend/src/i18n/locales/en.ts index 13f8b47..2bf4ea5 100644 --- a/apps/frontend/src/i18n/locales/en.ts +++ b/apps/frontend/src/i18n/locales/en.ts @@ -4,6 +4,7 @@ export default { catalog: 'Catalog', blog: 'Blog', howtos: 'How-tos', + engines: 'Engines', code: 'Code', team: 'Team', contact: 'Contact us', @@ -168,6 +169,20 @@ export default { title: 'Our Team', subtitle: 'Meet the brilliant minds behind Teletype Games.', }, + engines: { + badge: 'In-house Tech', + titleLead: 'Our', + titleAccent: 'Engines', + subtitle: 'The engines and frameworks we build, maintain and ship our games and services on.', + errorTitle: 'Failed to connect to Wiki', + noPagesTitle: 'No engines found', + noPagesDesc: "It seems like there aren't any engine pages available on the wiki at the moment.", + new: 'New', + explore: 'Explore', + openWiki: 'Open in Wiki', + back: 'Back to Engines', + noContent: 'No content available for this engine.', + }, howtos: { badge: 'Knowledge Base', title: 'Tech HowTo Center', diff --git a/apps/frontend/src/i18n/locales/hu.ts b/apps/frontend/src/i18n/locales/hu.ts index ef610ab..d608090 100644 --- a/apps/frontend/src/i18n/locales/hu.ts +++ b/apps/frontend/src/i18n/locales/hu.ts @@ -4,6 +4,7 @@ export default { catalog: 'Katalógus', blog: 'Blog', howtos: 'Hogyan csináld', + engines: 'Engine-ek', code: 'Kód', team: 'Csapat', contact: 'Kapcsolat', @@ -168,6 +169,20 @@ export default { title: 'Csapatunk', subtitle: 'Ismerd meg a Teletype Games mögött álló zseniális elméket.', }, + engines: { + badge: 'Saját technológia', + titleLead: 'Saját', + titleAccent: 'Engine-jeink', + subtitle: 'Az általunk épített és karbantartott engine-ek és keretrendszerek, amelyekre a játékaink és szolgáltatásaink épülnek.', + errorTitle: 'Nem sikerült csatlakozni a Wikihez', + noPagesTitle: 'Nem találhatók engine-ek', + noPagesDesc: 'Úgy tűnik, jelenleg nincsenek engine-oldalak a wikin.', + new: 'Új', + explore: 'Felfedezés', + openWiki: 'Megnyitás a Wikiben', + back: 'Vissza az Engine-ekhez', + noContent: 'Ehhez az engine-hez nincs elérhető tartalom.', + }, howtos: { badge: 'Tudásbázis', title: 'Tech HowTo Központ', diff --git a/apps/frontend/src/layout/AppLayout.vue b/apps/frontend/src/layout/AppLayout.vue index db10cbd..f11ac82 100644 --- a/apps/frontend/src/layout/AppLayout.vue +++ b/apps/frontend/src/layout/AppLayout.vue @@ -9,6 +9,7 @@ {{ t('nav.catalog') }} {{ t('nav.blog') }} {{ t('nav.howtos') }} + {{ t('nav.engines') }} {{ t('nav.code') }} {{ t('nav.team') }} {{ t('nav.contact') }} @@ -33,6 +34,7 @@ {{ t('nav.catalog') }} {{ t('nav.blog') }} {{ t('nav.howtos') }} + {{ t('nav.engines') }} {{ t('nav.code') }} {{ t('nav.team') }} {{ t('nav.contact') }} @@ -66,6 +68,10 @@ HowTos + | + + Engines + diff --git a/apps/frontend/src/page/engines/EngineShowPage.vue b/apps/frontend/src/page/engines/EngineShowPage.vue new file mode 100644 index 0000000..558ec42 --- /dev/null +++ b/apps/frontend/src/page/engines/EngineShowPage.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/apps/frontend/src/page/engines/EnginesIndexPage.vue b/apps/frontend/src/page/engines/EnginesIndexPage.vue new file mode 100644 index 0000000..2af16cf --- /dev/null +++ b/apps/frontend/src/page/engines/EnginesIndexPage.vue @@ -0,0 +1,164 @@ + + + + + diff --git a/apps/frontend/src/router/engines.router.ts b/apps/frontend/src/router/engines.router.ts new file mode 100644 index 0000000..b67cba8 --- /dev/null +++ b/apps/frontend/src/router/engines.router.ts @@ -0,0 +1,6 @@ +import type { RouteRecordRaw } from 'vue-router' + +export const enginesRouter: RouteRecordRaw[] = [ + { path: '/engines', name: 'enginesIndex', component: () => import('../page/engines/EnginesIndexPage.vue') }, + { path: '/engines/:slug', name: 'engineShow', component: () => import('../page/engines/EngineShowPage.vue') }, +] diff --git a/apps/frontend/src/router/index.router.ts b/apps/frontend/src/router/index.router.ts index aaa4d13..5d2a99a 100644 --- a/apps/frontend/src/router/index.router.ts +++ b/apps/frontend/src/router/index.router.ts @@ -4,6 +4,7 @@ import { blogRouter } from './blog.router' import { catalogRouter } from './catalog.router' import { codeRouter } from './code.router' import { contactRouter } from './contact.router' +import { enginesRouter } from './engines.router' import { howtosRouter } from './howtos.router' import { teamRouter } from './team.router' import { buildsRouter } from './builds.router' @@ -16,6 +17,7 @@ export const router = createRouter({ ...catalogRouter, ...codeRouter, ...contactRouter, + ...enginesRouter, ...howtosRouter, ...teamRouter, ...buildsRouter, diff --git a/apps/frontend/src/stores/engines.store.ts b/apps/frontend/src/stores/engines.store.ts new file mode 100644 index 0000000..c9df43d --- /dev/null +++ b/apps/frontend/src/stores/engines.store.ts @@ -0,0 +1,40 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import wikiApi, { engineSlug } from '../api/wiki.api' +import { isNew } from '../lib/softwareUtils' +import { useLoadable } from '../composables/useLoadable' +import type { WikiPageWithContent, WikiPageContent } from '../lib/interfaces/wiki.interface' + +export const useEnginesStore = defineStore('engines', () => { + const pages = ref([]) + const { loading, error, withCache, invalidate } = useLoadable() + + const currentPage = ref(null) + const { loading: pageLoading, error: pageError, withCache: withPageCache } = useLoadable(0) + + async function fetch() { + await withCache(async () => { + pages.value = await wikiApi.listEnginePages() + }) + } + + async function fetchPage(slug: string) { + currentPage.value = null + await withPageCache(async () => { + const result = await wikiApi.getEnginePage(slug) + if (!result) throw new Error('Engine not found') + currentPage.value = result + }) + } + + function getPermalink(path: string): string { + return `/engines/${engineSlug(path)}` + } + + function getCleanPreview(content: string): string { + if (!content) return '' + return content.replace(/[#*`_[\]()>|-]/g, '').replace(/\s+/g, ' ').trim().slice(0, 260) + '...' + } + + return { pages, loading, error, currentPage, pageLoading, pageError, fetch, fetchPage, isNew, getPermalink, getCleanPreview, invalidate } +})