wiki multilang support
ci/woodpecker/push/deploy Pipeline was successful

This commit is contained in:
2026-08-28 20:52:49 +02:00
parent 653894a876
commit 9ffbc7a2ca
25 changed files with 351 additions and 52 deletions
@@ -5,23 +5,26 @@ class Api::RssController < ApiController
end
api :GET, "/api/rss/blog", "Blog RSS feed"
param :lang, String, required: false, desc: "Feed language (en, hu). Unknown values fall back to en"
returns code: 200, desc: "RSS XML feed of blog posts"
def blog
xml = Rss::BlogFeed.new.xml
xml = Rss::BlogFeed.new(lang: params[:lang]).xml
render xml: xml, content_type: "application/rss+xml"
end
api :GET, "/api/rss/releases", "Software releases RSS feed"
param :lang, String, required: false, desc: "Feed language (en, hu). Unknown values fall back to en"
returns code: 200, desc: "RSS XML feed of software releases"
def releases
xml = Rss::ReleasesFeed.new.xml
xml = Rss::ReleasesFeed.new(lang: params[:lang]).xml
render xml: xml, content_type: "application/rss+xml"
end
api :GET, "/api/rss/howtos", "HowTos RSS feed"
param :lang, String, required: false, desc: "Feed language (en, hu). Unknown values fall back to en"
returns code: 200, desc: "RSS XML feed of tech howtos"
def howtos
xml = Rss::HowtosFeed.new.xml
xml = Rss::HowtosFeed.new(lang: params[:lang]).xml
render xml: xml, content_type: "application/rss+xml"
end
end
@@ -7,8 +7,10 @@ class Api::WikiController < ApiController
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)"
param :lang, String, required: false, desc: "Content language (en, hu). Unknown values fall back to en"
returns code: 200, desc: "Wiki pages response" do
property :tag, String, desc: "Applied tag filter"
property :lang, String, desc: "Requested content language"
property :count, Integer, desc: "Number of pages returned"
property :pages, Array, desc: "Array of wiki pages" do
property :id, String, desc: "Page ID"
@@ -17,7 +19,7 @@ class Api::WikiController < ApiController
property :description, String, desc: "Short description"
property :createdAt, String, desc: "Created date (ISO 8601)"
property :updatedAt, String, desc: "Updated date (ISO 8601)"
property :locale, String, desc: "Locale code"
property :locale, String, desc: "Language the page body is actually written in (differs from lang when the page has no translation)"
property :route, String, desc: "URL slug"
property :tags, Array, of: String, desc: "Tags"
property :repo, String, desc: "Git repository URL (from page metadata, engines)"
@@ -31,7 +33,8 @@ class Api::WikiController < ApiController
render json: Wiki::Pages.new.fetch(
tag: params[:tag],
limit: params[:limit],
body: params[:body]
body: params[:body],
lang: params[:lang]
)
end
end
+3 -3
View File
@@ -2,12 +2,12 @@ module Rss
class BlogFeed < Feed
private
def title = "Teletype Games Blog"
def title = translate("rss.blog.title")
def link = "#{site_url}/blog"
def description = "Latest blog posts from Teletype Games"
def description = translate("rss.blog.description")
def items
Wiki::Pages.new.all(tag: "blog", limit: 30).map do |page|
Wiki::Pages.new.all(tag: "blog", limit: 30, lang: lang).map do |page|
{
title: page["title"],
link: "#{site_url}/blog/#{page['route']}",
+10 -2
View File
@@ -2,12 +2,16 @@ require "rss"
module Rss
class Feed
def initialize(lang: nil)
@lang = Locales.resolve(lang)
end
def xml
RSS::Maker.make("2.0") do |maker|
maker.channel.title = title
maker.channel.link = link
maker.channel.description = description
maker.channel.language = "hu"
maker.channel.language = lang
items.each do |item|
maker.items.new_item do |rss_item|
@@ -22,9 +26,13 @@ module Rss
private
attr_reader :lang
def site_url = Rails.configuration.x.site_url
def wiki_url = Rails.configuration.x.wiki_url
def wiki_url = "#{Rails.configuration.x.wiki_url}#{Wiki::Pages.path_prefix(lang)}"
def translate(key, **options) = I18n.t(key, locale: lang, **options)
def parse_time(value)
Time.parse(value.to_s)
+3 -3
View File
@@ -2,12 +2,12 @@ module Rss
class HowtosFeed < Feed
private
def title = "Teletype Games HowTos"
def title = translate("rss.howtos.title")
def link = "#{site_url}/howtos"
def description = "Latest tech howtos from Teletype Games"
def description = translate("rss.howtos.description")
def items
Wiki::Pages.new.all(tag: "howto", limit: 30).map do |page|
Wiki::Pages.new.all(tag: "howto", limit: 30, lang: lang).map do |page|
{
title: page["title"],
link: "#{wiki_url}/#{page['path']}",
+3 -3
View File
@@ -2,9 +2,9 @@ module Rss
class ReleasesFeed < Feed
private
def title = "Teletype Games Releases"
def title = translate("rss.releases.title")
def link = "#{site_url}/catalog"
def description = "Latest game releases from Teletype Games"
def description = translate("rss.releases.description")
def items
WarpEngine::Release.includes(:software).order(created_at: :desc).limit(50).filter_map do |release|
@@ -14,7 +14,7 @@ module Rss
{
title: "#{software.title} v#{release.version}",
link: "#{site_url}/catalog/#{software.name}",
description: "#{software.title} #{release.version} released #{software.desc}",
description: translate("rss.releases.item_description", title: software.title, version: release.version, desc: software.desc),
published_at: release.created_at.to_time
}
end
+14 -8
View File
@@ -3,12 +3,18 @@ require "json"
module Wiki
class Pages
def fetch(tag:, limit: nil, body: nil)
def self.path_prefix(language)
language == Locales::DEFAULT ? "" : "/#{language}"
end
def fetch(tag:, limit: nil, body: nil, lang: nil)
language = Locales.resolve(lang)
query = { tag: tag }
query[:limit] = limit if limit.present?
query[:body] = body if body.present?
uri = URI.parse("#{Rails.configuration.x.wiki_url}/custom/pages.json")
uri = URI.parse("#{Rails.configuration.x.wiki_url}#{self.class.path_prefix(language)}/custom/pages.json")
uri.query = URI.encode_www_form(query)
response = Net::HTTP.start(
@@ -17,21 +23,21 @@ module Wiki
open_timeout: 5, read_timeout: 10
) { |http| http.get(uri.request_uri) }
return empty(tag, "grav responded #{response.code}") unless response.is_a?(Net::HTTPSuccess)
return empty(tag, language, "grav responded #{response.code}") unless response.is_a?(Net::HTTPSuccess)
JSON.parse(response.body)
rescue StandardError => e
empty(tag, e.message)
empty(tag, language, e.message)
end
def all(tag:, limit: nil)
fetch(tag: tag, limit: limit).fetch("pages", [])
def all(tag:, limit: nil, lang: nil)
fetch(tag: tag, limit: limit, lang: lang).fetch("pages", [])
end
private
def empty(tag, error)
{ "tag" => tag, "count" => 0, "pages" => [], "error" => error }
def empty(tag, language, error)
{ "tag" => tag, "lang" => language, "count" => 0, "pages" => [], "error" => error }
end
end
end
+11
View File
@@ -5,3 +5,14 @@ en:
date:
formats:
long: "%Y-%m-%d"
rss:
blog:
title: "Teletype Games Blog"
description: "Latest blog posts from Teletype Games"
howtos:
title: "Teletype Games HowTos"
description: "Latest tech howtos from Teletype Games"
releases:
title: "Teletype Games Releases"
description: "Latest game releases from Teletype Games"
item_description: "%{title} %{version} released %{desc}"
+18
View File
@@ -0,0 +1,18 @@
hu:
time:
formats:
long: "%Y-%m-%d %H:%M"
date:
formats:
long: "%Y-%m-%d"
rss:
blog:
title: "Teletype Games blog"
description: "A Teletype Games legfrissebb blogbejegyzései"
howtos:
title: "Teletype Games útmutatók"
description: "A Teletype Games legfrissebb technikai útmutatói"
releases:
title: "Teletype Games kiadások"
description: "A Teletype Games legfrissebb játékkiadásai"
item_description: "Megjelent a %{title} %{version} %{desc}"
+9
View File
@@ -0,0 +1,9 @@
module Locales
SUPPORTED = %w[en hu].freeze
DEFAULT = "en"
def self.resolve(value)
normalized = value.to_s.strip.downcase
SUPPORTED.include?(normalized) ? normalized : DEFAULT
end
end
+20
View File
@@ -0,0 +1,20 @@
require "rails_helper"
RSpec.describe Locales do
describe ".resolve" do
it "accepts the supported languages" do
expect(Locales.resolve("en")).to eq("en")
expect(Locales.resolve("hu")).to eq("hu")
end
it "normalises case and whitespace" do
expect(Locales.resolve(" HU ")).to eq("hu")
end
it "falls back to the default for anything else" do
expect(Locales.resolve("de")).to eq(Locales::DEFAULT)
expect(Locales.resolve(nil)).to eq(Locales::DEFAULT)
expect(Locales.resolve("")).to eq(Locales::DEFAULT)
end
end
end
+31
View File
@@ -0,0 +1,31 @@
require "rails_helper"
RSpec.describe "Api::Wiki", type: :request do
let(:service) { instance_double(Wiki::Pages) }
before do
host! "teletypegames.org"
allow(Wiki::Pages).to receive(:new).and_return(service)
end
it "passes the requested language through to the wiki service" do
expect(service).to receive(:fetch)
.with(tag: "howto", limit: nil, body: nil, lang: "hu")
.and_return({ "tag" => "howto", "lang" => "hu", "count" => 0, "pages" => [] })
get "/api/wiki/pages", params: { tag: "howto", lang: "hu" }
expect(response).to have_http_status(:ok)
expect(response.parsed_body["lang"]).to eq("hu")
end
it "leaves the fallback to the service when no language is asked for" do
expect(service).to receive(:fetch)
.with(tag: "howto", limit: nil, body: nil, lang: nil)
.and_return({ "tag" => "howto", "lang" => "en", "count" => 0, "pages" => [] })
get "/api/wiki/pages", params: { tag: "howto" }
expect(response.parsed_body["lang"]).to eq("en")
end
end
+46
View File
@@ -0,0 +1,46 @@
require "rails_helper"
RSpec.describe Wiki::Pages do
let(:requested_paths) { [] }
before do
http = instance_double(Net::HTTP)
response = Net::HTTPOK.new("1.1", "200", "OK")
allow(response).to receive(:body).and_return({ tag: "howto", lang: "en", count: 0, pages: [] }.to_json)
allow(http).to receive(:get) do |path|
requested_paths << path
response
end
allow(Net::HTTP).to receive(:start) { |*_args, **_opts, &block| block.call(http) }
end
describe "#fetch" do
it "requests the default language without a path prefix" do
described_class.new.fetch(tag: "howto")
expect(requested_paths.first).to start_with("/custom/pages.json")
end
it "prefixes the path with the requested language" do
described_class.new.fetch(tag: "howto", lang: "hu")
expect(requested_paths.first).to start_with("/hu/custom/pages.json")
end
it "falls back to the default language for an unsupported one" do
described_class.new.fetch(tag: "howto", lang: "de")
expect(requested_paths.first).to start_with("/custom/pages.json")
end
it "reports the resolved language when grav is unreachable" do
allow(Net::HTTP).to receive(:start).and_raise(Errno::ECONNREFUSED)
result = described_class.new.fetch(tag: "howto", lang: "hu")
expect(result["lang"]).to eq("hu")
expect(result["pages"]).to eq([])
expect(result["error"]).to be_present
end
end
end
@@ -0,0 +1,35 @@
import { describe, it, expect, afterEach } from 'vitest'
import { i18n } from '../../i18n'
import { WIKI_BASE, wikiUrl } from '../wiki.api'
afterEach(() => {
i18n.global.locale.value = 'en'
})
describe('wikiUrl', () => {
it('leaves English unprefixed', () => {
expect(wikiUrl('development/godot', 'en')).toBe(`${WIKI_BASE}/development/godot`)
})
it('prefixes Hungarian with /hu', () => {
expect(wikiUrl('development/godot', 'hu')).toBe(`${WIKI_BASE}/hu/development/godot`)
})
it('accepts a route that already starts with a slash', () => {
expect(wikiUrl('/development/godot', 'hu')).toBe(`${WIKI_BASE}/hu/development/godot`)
})
it('returns the wiki root when no page is given', () => {
expect(wikiUrl('', 'hu')).toBe(`${WIKI_BASE}/hu`)
expect(wikiUrl()).toBe(WIKI_BASE)
})
it('falls back to English for an unsupported language', () => {
expect(wikiUrl('development/godot', 'de')).toBe(`${WIKI_BASE}/development/godot`)
})
it('follows the active interface language when none is passed', () => {
i18n.global.locale.value = 'hu'
expect(wikiUrl('development/godot')).toBe(`${WIKI_BASE}/hu/development/godot`)
})
})
+29 -11
View File
@@ -1,9 +1,27 @@
import type { WikiPage, WikiPageWithContent, WikiPageContent } from '../lib/interfaces/wiki.interface'
import { CONFIG } from '../lib/config'
import { i18n } from '../i18n'
const WIKI_BASE = CONFIG.wikiBase
const WIKI_LOCALES = ['en', 'hu'] as const
const DEFAULT_WIKI_LOCALE = 'en'
function resolveLocale(locale?: string): string {
const value = (locale ?? String(i18n.global.locale.value)).toLowerCase()
return (WIKI_LOCALES as readonly string[]).includes(value) ? value : DEFAULT_WIKI_LOCALE
}
// Grav serves English unprefixed and Hungarian under /hu, so a page link is the
// base plus the prefix plus the language-neutral route.
function wikiUrl(path = '', locale?: string): string {
const lang = resolveLocale(locale)
const prefix = lang === DEFAULT_WIKI_LOCALE ? '' : `/${lang}`
const route = path ? `/${path.replace(/^\/+/, '')}` : ''
return `${WIKI_BASE}${prefix}${route}`
}
interface RawWikiPage {
id: number
path: string
@@ -22,9 +40,9 @@ const HIGHLIGHTED_TAG = 'highlighted'
async function fetchPages(
tag: string,
opts: { body?: boolean; limit?: number } = {},
opts: { body?: boolean; limit?: number; locale?: string } = {},
): Promise<RawWikiPage[]> {
const params = new URLSearchParams({ tag })
const params = new URLSearchParams({ tag, lang: resolveLocale(opts.locale) })
if (opts.body) params.set('body', '1')
if (opts.limit) params.set('limit', String(opts.limit))
@@ -34,8 +52,8 @@ async function fetchPages(
return json?.pages ?? []
}
const listBlogPages = async (): Promise<WikiPageWithContent[]> => {
const pages = await fetchPages('blog', { body: true })
const listBlogPages = async (locale?: string): Promise<WikiPageWithContent[]> => {
const pages = await fetchPages('blog', { body: true, locale })
return pages.map((p): WikiPageWithContent => ({
id: p.id,
path: p.path,
@@ -48,8 +66,8 @@ const listBlogPages = async (): Promise<WikiPageWithContent[]> => {
}))
}
const getBlogPage = async (slug: string): Promise<WikiPageContent | null> => {
const pages = await fetchPages('blog', { body: true })
const getBlogPage = async (slug: string, locale?: string): Promise<WikiPageContent | null> => {
const pages = await fetchPages('blog', { body: true, locale })
const matched = pages.find((p) => {
const pageSlug = p.path.startsWith('blog/') ? p.path.replace('blog/', '') : p.path
@@ -70,8 +88,8 @@ const getBlogPage = async (slug: string): Promise<WikiPageContent | null> => {
}
}
const listEnginePages = async (): Promise<WikiPageWithContent[]> => {
const pages = await fetchPages('engine', { body: true })
const listEnginePages = async (locale?: string): Promise<WikiPageWithContent[]> => {
const pages = await fetchPages('engine', { body: true, locale })
return pages
.filter((p) => (p.tags ?? []).includes(HIGHLIGHTED_TAG))
.map((p): WikiPageWithContent => ({
@@ -87,8 +105,8 @@ const listEnginePages = async (): Promise<WikiPageWithContent[]> => {
}))
}
const listHowtoPages = async (): Promise<WikiPage[]> => {
const pages = await fetchPages('howto', { limit: 30 })
const listHowtoPages = async (locale?: string): Promise<WikiPage[]> => {
const pages = await fetchPages('howto', { limit: 30, locale })
return pages.map((p): WikiPage => ({
id: p.id,
path: p.path,
@@ -100,5 +118,5 @@ const listHowtoPages = async (): Promise<WikiPage[]> => {
}))
}
export { WIKI_BASE }
export { WIKI_BASE, WIKI_LOCALES, DEFAULT_WIKI_LOCALE, wikiUrl }
export default { listBlogPages, getBlogPage, listHowtoPages, listEnginePages }
@@ -0,0 +1,13 @@
import { watch } from 'vue'
import { i18n } from '../i18n'
// Wiki content is fetched per language, so anything cached from the wiki has to
// be dropped and re-read when the visitor switches EN/HU.
export function useLocaleReload(reload: () => void | Promise<void>) {
watch(
() => i18n.global.locale.value,
() => {
void reload()
},
)
}
+2 -2
View File
@@ -53,7 +53,7 @@
</nav>
<div class="footer-meta">
<a :href="WIKI_BASE" target="_blank" rel="noopener noreferrer" class="footer-link">{{ t('footer.wiki') }}</a>
<a :href="wikiUrl()" target="_blank" rel="noopener noreferrer" class="footer-link">{{ t('footer.wiki') }}</a>
<a :href="GIT_BASE" target="_blank" rel="noopener noreferrer" class="footer-link">{{ t('footer.git') }}</a>
<span class="footer-rss">
<Rss v-bind="ICON" aria-hidden="true" />
@@ -89,7 +89,7 @@ import { Menu, Moon, Rss, Sun, X } from 'lucide-vue-next'
import { useUiStore } from '../stores/ui.store'
import { getCookie } from '../lib/cookie'
import { ICON, ICON_CONTROL } from '../components/icons'
import { WIKI_BASE } from '../api/wiki.api'
import { wikiUrl } from '../api/wiki.api'
const GIT_BASE = 'https://git.teletypegames.org'
@@ -48,7 +48,7 @@
<a :href="exploreUrl(page)" target="_blank" rel="noopener" class="btn-accent">
{{ t('engines.explore') }}
</a>
<a :href="`${WIKI_BASE}/${page.path}`" target="_blank" rel="noopener" class="btn-ghost">
<a :href="wikiUrl(page.path)" target="_blank" rel="noopener" class="btn-ghost">
{{ t('engines.openWiki') }}
</a>
</div>
@@ -63,7 +63,7 @@
import { computed, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'
import { WIKI_BASE } from '../../api/wiki.api'
import { wikiUrl } from '../../api/wiki.api'
import { useEnginesStore, getEngineDigest } from '../../stores/engines.store'
import type { WikiPageWithContent } from '../../lib/interfaces/wiki.interface'
import SkeletonCard from '../../components/SkeletonCard.vue'
@@ -79,7 +79,7 @@ const cards = computed(() =>
)
const exploreUrl = (page: WikiPageWithContent): string =>
page.repo || `${WIKI_BASE}/${page.path}`
page.repo || wikiUrl(page.path)
onMounted(() => store.fetch())
</script>
@@ -6,7 +6,7 @@
<h1 class="hero-title">{{ t('howtos.title') }}</h1>
<p class="hero-subtitle">{{ t('howtos.subtitle') }}</p>
<div class="hero-actions">
<a :href="WIKI_BASE" target="_blank" rel="noopener noreferrer" class="btn-accent">
<a :href="wikiUrl()" target="_blank" rel="noopener noreferrer" class="btn-accent">
{{ t('howtos.knowledgeBase') }}
</a>
</div>
@@ -30,7 +30,7 @@
<a
v-for="page in recentPages"
:key="page.id"
:href="`${WIKI_BASE}/${page.path}`"
:href="wikiUrl(page.path)"
target="_blank"
rel="noopener noreferrer"
class="howto-card"
@@ -50,7 +50,7 @@
</div>
<div v-if="!error && recentPages.length > 0" class="focus-row">
<a :href="WIKI_BASE" target="_blank" rel="noopener noreferrer" class="link text-sm">
<a :href="wikiUrl()" target="_blank" rel="noopener noreferrer" class="link text-sm">
{{ t('howtos.browseWiki') }}
</a>
</div>
@@ -63,7 +63,7 @@ import { onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'
import { formatDateTime } from '../../lib/dateFormat'
import { WIKI_BASE } from '../../api/wiki.api'
import { wikiUrl } from '../../api/wiki.api'
import { useHowtosStore } from '../../stores/howtos.store'
import SkeletonCard from '../../components/SkeletonCard.vue'
@@ -30,7 +30,7 @@
<a :href="client.releasesUrl" target="_blank" rel="noopener noreferrer" class="btn-accent">
{{ t('stores.clientDownload') }}
</a>
<a :href="client.wikiUrl" target="_blank" rel="noopener noreferrer" class="btn-ghost">
<a :href="wikiUrl(client.wikiPath)" target="_blank" rel="noopener noreferrer" class="btn-ghost">
{{ t('stores.docs') }}
</a>
<a :href="client.repoUrl" target="_blank" rel="noopener noreferrer" class="btn-ghost">
@@ -82,7 +82,7 @@
</div>
<div class="st-links st-links-after">
<a :href="current.wikiUrl" target="_blank" rel="noopener noreferrer" class="btn-ghost">
<a :href="wikiUrl(current.wikiPath)" target="_blank" rel="noopener noreferrer" class="btn-ghost">
{{ t('stores.docs') }}
</a>
<a :href="current.repoUrl" target="_blank" rel="noopener noreferrer" class="btn-ghost">
@@ -116,7 +116,7 @@
import { computed, ref } from 'vue'
import { RouterLink, useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { CONFIG } from '../../lib/config'
import { wikiUrl } from '../../api/wiki.api'
import CommandBlock from './CommandBlock.vue'
import { BrandIcon } from '../../components/icons'
import clientScreenshot from '../../assets/warpengine-client.webp'
@@ -130,7 +130,7 @@ const FORGE = 'https://git.teletypegames.org/stores'
const CLIENT = {
repoUrl: `${FORGE}/warp-engine-client`,
releasesUrl: `${FORGE}/warp-engine-client/releases`,
wikiUrl: `${CONFIG.wikiBase}/stores/warp-engine-client`,
wikiPath: 'stores/warp-engine-client',
}
type DeviceId = 'batocera' | 'retroarch'
@@ -150,7 +150,7 @@ const devices = [
id: 'batocera' as DeviceId,
projectUrl: 'https://batocera.org',
repoUrl: `${FORGE}/ttg-batocera-store`,
wikiUrl: `${CONFIG.wikiBase}/stores/ttg-batocera-store`,
wikiPath: 'stores/ttg-batocera-store',
platforms: [
...CARTRIDGE_PLATFORMS,
{ platform: 'ebitengine', label: 'Ebitengine', ext: '.zip', note: 'x86_64 · ARM64' },
@@ -172,7 +172,7 @@ const devices = [
id: 'retroarch' as DeviceId,
projectUrl: 'https://www.retroarch.com',
repoUrl: `${FORGE}/ttg-retroarch-store`,
wikiUrl: `${CONFIG.wikiBase}/stores/ttg-retroarch-store`,
wikiPath: 'stores/ttg-retroarch-store',
platforms: CARTRIDGE_PLATFORMS,
installCmd: `curl -fsSL ${FORGE}/ttg-retroarch-store/raw/branch/master/install.sh | sh`,
afterInstallCmd: '',
@@ -74,3 +74,31 @@ describe('getEngineDigest', () => {
expect(getEngineDigest('')).toEqual({ intro: '', highlightsTitle: '', highlights: [] })
})
})
const SAMPLE_MARKDOWN_HU = `
> A példamotor egy **minta** keretrendszer, ami markdownból landing-kártyát csinál.
# Amit kapsz
- **Első funkció**: rövid magyarázattal.
- Sima felsorolás, félkövér nyitás nélkül.
# Későbbi szakasz
- **Nem kerül be**: egy lista egy későbbi cím alatt.
`
describe('getEngineDigest, Hungarian page', () => {
it('recognises the Hungarian highlights heading', () => {
const digest = getEngineDigest(SAMPLE_MARKDOWN_HU)
expect(digest.highlightsTitle).toBe('Amit kapsz')
expect(digest.highlights).toHaveLength(2)
expect(digest.highlights[0]).toEqual({ title: 'Első funkció', text: 'rövid magyarázattal.' })
})
it('still stops at the next heading', () => {
expect(getEngineDigest(SAMPLE_MARKDOWN_HU).highlights.map((h) => h.title)).not.toContain(
'Nem kerül be',
)
})
})
+10
View File
@@ -3,6 +3,7 @@ import { ref } from 'vue'
import wikiApi from '../api/wiki.api'
import { isNew } from '../lib/softwareUtils'
import { useLoadable } from '../composables/useLoadable'
import { useLocaleReload } from '../composables/useLocaleReload'
import type { WikiPageWithContent, WikiPageContent } from '../lib/interfaces/wiki.interface'
export const useBlogStore = defineStore('blog', () => {
@@ -18,7 +19,10 @@ export const useBlogStore = defineStore('blog', () => {
})
}
const currentSlug = ref<string | null>(null)
async function fetchPage(slug: string) {
currentSlug.value = slug
currentPage.value = null
await withPageCache(async () => {
const result = await wikiApi.getBlogPage(slug)
@@ -37,5 +41,11 @@ export const useBlogStore = defineStore('blog', () => {
return content.replace(/[#*`_[\]()]/g, '').trim().slice(0, 300) + '...'
}
useLocaleReload(async () => {
invalidate()
await fetch()
if (currentSlug.value) await fetchPage(currentSlug.value)
})
return { pages, loading, error, currentPage, pageLoading, pageError, fetch, fetchPage, isNew, getPermalink, getCleanPreview, invalidate }
})
+8 -2
View File
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
import { ref } from 'vue'
import wikiApi from '../api/wiki.api'
import { useLoadable } from '../composables/useLoadable'
import { useLocaleReload } from '../composables/useLocaleReload'
import type { WikiPageWithContent } from '../lib/interfaces/wiki.interface'
export interface EngineHighlight {
@@ -15,7 +16,7 @@ export interface EngineDigest {
highlights: EngineHighlight[]
}
const HIGHLIGHTS_HEADING = 'what you get'
const HIGHLIGHTS_HEADINGS = ['what you get', 'amit kapsz']
const MAX_HIGHLIGHTS = 6
function stripInline(md: string): string {
@@ -39,7 +40,7 @@ export function getEngineDigest(content: string): EngineDigest {
if (heading) {
if (inHighlights) break
const title = stripInline(heading[1])
if (title.toLowerCase() !== HIGHLIGHTS_HEADING) break
if (!HIGHLIGHTS_HEADINGS.includes(title.toLowerCase())) break
inHighlights = true
digest.highlightsTitle = title
continue
@@ -81,5 +82,10 @@ export const useEnginesStore = defineStore('engines', () => {
return content.replace(/[#*`_[\]()>|-]/g, '').replace(/\s+/g, ' ').trim().slice(0, 260) + '...'
}
useLocaleReload(async () => {
invalidate()
await fetch()
})
return { pages, loading, error, fetch, getCleanPreview, getEngineDigest, invalidate }
})
+6
View File
@@ -3,6 +3,7 @@ import { ref } from 'vue'
import wikiApi from '../api/wiki.api'
import { isNew } from '../lib/softwareUtils'
import { useLoadable } from '../composables/useLoadable'
import { useLocaleReload } from '../composables/useLocaleReload'
import type { WikiPage } from '../lib/interfaces/wiki.interface'
export const useHowtosStore = defineStore('howtos', () => {
@@ -15,5 +16,10 @@ export const useHowtosStore = defineStore('howtos', () => {
})
}
useLocaleReload(async () => {
invalidate()
await fetch()
})
return { pages, loading, error, fetch, isNew, invalidate }
})