From 0bc6a5cd78c76cacc151e470888b9c5b07a40113 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sun, 11 Oct 2026 00:19:55 +0200 Subject: [PATCH 1/2] feat(pages): add a page on how to join the server Add /de/join and /en/join: a step-by-step guide for joining with the Java and Bedrock editions. The addresses come from content/server/*/connect.json through the existing ServerAddresses component, so the home page and the join page read one source. Entry points: a Community navigation item, a link from the home addresses section, and a link from the editions FAQ answer. Claude-Session: https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX --- content/faq/de/editions.md | 3 +- content/faq/en/editions.md | 3 +- i18n/locales/de.json | 30 +++++- i18n/locales/en.json | 30 +++++- layers/home/components/ServerAddresses.vue | 8 +- layers/home/composables/useServerConnect.ts | 20 ++++ layers/navigation/navItems.ts | 1 + pages/join.vue | 111 ++++++++++++++++++++ tests/join/content.spec.ts | 92 ++++++++++++++++ tests/join/page.spec.ts | 53 ++++++++++ tests/navigation/structure.spec.ts | 4 + 11 files changed, 350 insertions(+), 5 deletions(-) create mode 100644 layers/home/composables/useServerConnect.ts create mode 100644 pages/join.vue create mode 100644 tests/join/content.spec.ts create mode 100644 tests/join/page.spec.ts diff --git a/content/faq/de/editions.md b/content/faq/de/editions.md index a9e1e332..250b90aa 100644 --- a/content/faq/de/editions.md +++ b/content/faq/de/editions.md @@ -6,4 +6,5 @@ order: 2 Unser Server unterstützt sowohl die Java Edition als auch die Bedrock Edition. Die genauen Adressen findest du im Abschnitt „Mit dem Server verbinden" auf -dieser Seite. +dieser Seite. Eine Schritt-für-Schritt-Anleitung für beide Editionen findest du auf +[der Seite Minecraft beitreten](/de/join). diff --git a/content/faq/en/editions.md b/content/faq/en/editions.md index 25cae490..2706525b 100644 --- a/content/faq/en/editions.md +++ b/content/faq/en/editions.md @@ -5,4 +5,5 @@ order: 2 --- Our server supports both the Java Edition and Bedrock Edition. The exact -addresses are listed in the "Connect to the server" section on this page. +addresses are listed in the "Connect to the server" section on this page. A step-by-step guide for both +editions is on the [Join Minecraft page](/en/join). diff --git a/i18n/locales/de.json b/i18n/locales/de.json index 8a94afee..4cb7a2b7 100644 --- a/i18n/locales/de.json +++ b/i18n/locales/de.json @@ -16,6 +16,7 @@ "more": "Mehr", "status": "Status", "builds": "Bauwerke", + "join": "Beitreten", "home_link": "OneLiteFeather – Startseite", "community_overview": "Übersicht", "events": "Events", @@ -66,7 +67,8 @@ "copy_port": "Port kopieren", "copy_port_aria": "Port kopieren: {port}", "port_label": "Port", - "clipboard_note": "Dein Browser unterstützt das Kopieren in die Zwischenablage nicht. Bitte kopiere die Adresse manuell." + "clipboard_note": "Dein Browser unterstützt das Kopieren in die Zwischenablage nicht. Bitte kopiere die Adresse manuell.", + "more_link": "Mehr zum Beitreten" } }, "blog": { @@ -596,6 +598,32 @@ "intro": "Dieser Bau zeigt die folgenden Projekte im Einsatz." } }, + "join": { + "title": "Minecraft beitreten: Java & Bedrock", + "description": "So trittst du dem OneLiteFeather Minecraft-Netzwerk bei: Adresse für Java oder Bedrock kopieren und in wenigen Schritten loslegen.", + "heading": "Minecraft beitreten", + "intro": "OneLiteFeather ist ein Minecraft-Netzwerk, gegründet 2019. Du kannst mit der Java Edition oder der Bedrock Edition beitreten. Kopiere die passende Adresse und folge den Schritten für deine Edition.", + "java": { + "heading": "Mit der Java Edition beitreten", + "step_1": "Starte Minecraft Java Edition und öffne den Mehrspielermodus.", + "step_2": "Klicke auf „Server hinzufügen“.", + "step_3": "Trage als Serveradresse {address} ein. Den Servernamen wählst du selbst.", + "step_4": "Klicke auf „Fertig“, wähle den Server in der Liste aus und klicke auf „Server beitreten“." + }, + "bedrock": { + "heading": "Mit der Bedrock Edition beitreten", + "step_1": "Starte Minecraft Bedrock Edition und öffne den Reiter „Spielen“.", + "step_2": "Wechsle zum Reiter „Server“ und wähle „Server hinzufügen“.", + "step_3": "Trage einen Servernamen deiner Wahl ein, als Serveradresse {host} und als Port {port}.", + "step_4": "Speichere den Eintrag und verbinde dich mit dem Server." + }, + "help": { + "heading": "Hilfe und Fragen", + "text": "Klappt die Verbindung nicht? Unser Discord hilft dir weiter. Antworten auf häufige Fragen findest du auf der Startseite.", + "discord": "Discord", + "faq": "Häufige Fragen" + } + }, "about": { "title": "Über uns", "description": "OneLiteFeather ist ein Minecraft-Netzwerk, das von seiner Community lebt: Gemeinschaftsbauten, offene Software, eigene Events und transparente Finanzierung.", diff --git a/i18n/locales/en.json b/i18n/locales/en.json index db9dbb50..c3034907 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -16,6 +16,7 @@ "more": "More", "status": "Status", "builds": "Builds", + "join": "Join", "home_link": "OneLiteFeather – Home", "community_overview": "Overview", "events": "Events", @@ -66,7 +67,8 @@ "copy_port": "Copy port", "copy_port_aria": "Copy port: {port}", "port_label": "Port", - "clipboard_note": "Your browser does not support copying to the clipboard. Please copy the address manually." + "clipboard_note": "Your browser does not support copying to the clipboard. Please copy the address manually.", + "more_link": "More on joining" } }, "index": { @@ -596,6 +598,32 @@ "intro": "This build shows the following projects in use." } }, + "join": { + "title": "Join Minecraft: Java & Bedrock", + "description": "Join the OneLiteFeather Minecraft network: copy the address for Java or Bedrock, add the server in your game and start playing in a few steps.", + "heading": "Join Minecraft", + "intro": "OneLiteFeather is a Minecraft network founded in 2019. You can join with the Java Edition or the Bedrock Edition. Copy the matching address and follow the steps for your edition.", + "java": { + "heading": "Join with Java Edition", + "step_1": "Start Minecraft Java Edition and open Multiplayer.", + "step_2": "Click Add Server.", + "step_3": "Enter {address} as the server address. You can choose the server name yourself.", + "step_4": "Click Done, select the server in the list and click Join Server." + }, + "bedrock": { + "heading": "Join with Bedrock Edition", + "step_1": "Start Minecraft Bedrock Edition and open the Play tab.", + "step_2": "Switch to the Servers tab and choose Add Server.", + "step_3": "Enter any server name, {host} as the server address and {port} as the port.", + "step_4": "Save the entry and connect to the server." + }, + "help": { + "heading": "Help and questions", + "text": "Can't connect? Our Discord can help. Answers to common questions are on the home page.", + "discord": "Discord", + "faq": "Common questions" + } + }, "about": { "title": "About us", "description": "OneLiteFeather is a Minecraft network that lives on its community: community builds, open software, events of our own and transparent funding.", diff --git a/layers/home/components/ServerAddresses.vue b/layers/home/components/ServerAddresses.vue index 8337892d..f6355681 100644 --- a/layers/home/components/ServerAddresses.vue +++ b/layers/home/components/ServerAddresses.vue @@ -20,7 +20,7 @@ const props = withDefaults(defineProps(), { bedrockPort: undefined }) -const { t } = useI18n() +const { t, locale } = useI18n() // Prefer props if provided, otherwise fall back to i18n const displayTitle = computed(() => props.title ?? t('server.connect.title')) @@ -128,6 +128,12 @@ const panelClass /> +
+ + {{ t('server.connect.more_link') }} + +
+

{{ t('server.connect.clipboard_note') }} diff --git a/layers/home/composables/useServerConnect.ts b/layers/home/composables/useServerConnect.ts new file mode 100644 index 00000000..5e88f216 --- /dev/null +++ b/layers/home/composables/useServerConnect.ts @@ -0,0 +1,20 @@ +import type { Locale } from '#layers/content-core' +import type { ServerConnectDocument } from '../types' + +/** + * The server connect document (Java and Bedrock addresses) for the active + * locale. Shares its cache key with `useHomeContent`, so both read one payload. + */ +export async function useServerConnect() { + const { locale } = useI18n() + const repo = useContentRepository() + const activeLocale = computed(() => (locale?.value || 'de') as Locale) + + const { data: connect } = await useAsyncData( + () => `server-connect-${activeLocale.value}`, + () => repo.getServerConnect(activeLocale.value), + { watch: [activeLocale] } + ) + + return { connect } +} diff --git a/layers/navigation/navItems.ts b/layers/navigation/navItems.ts index 8020acaa..f48219ed 100644 --- a/layers/navigation/navItems.ts +++ b/layers/navigation/navItems.ts @@ -49,6 +49,7 @@ export function buildNavConfig({ eventsTopLevel }: { eventsTopLevel: boolean }): icon: ['fas', 'handshake'], children: [ { type: 'link', textKey: 'navigation.community_overview', routeName: 'community', icon: ['fas', 'handshake'] }, + { type: 'link', textKey: 'navigation.join', routeName: 'join', icon: ['fas', 'play'] }, { type: 'link', textKey: 'navigation.builds', routeName: 'community-poi', icon: ['fas', 'location-dot'] }, { type: 'link', textKey: 'navigation.projects', routeName: 'projects', icon: ['fas', 'code'] }, ...(eventsTopLevel ? [] : [eventsLink]) diff --git a/pages/join.vue b/pages/join.vue new file mode 100644 index 00000000..f3cb5c0c --- /dev/null +++ b/pages/join.vue @@ -0,0 +1,111 @@ + + + diff --git a/tests/join/content.spec.ts b/tests/join/content.spec.ts new file mode 100644 index 00000000..f8b0567b --- /dev/null +++ b/tests/join/content.spec.ts @@ -0,0 +1,92 @@ +import { readFileSync } from 'node:fs' +import { describe, expect, it } from 'vitest' +import { repoRoot } from '../helpers/sources' + +type Tree = Record + +const messages = (locale: string): Tree => JSON.parse(readFileSync(`${repoRoot}/i18n/locales/${locale}.json`, 'utf8')) +const read = (file: string) => readFileSync(`${repoRoot}/${file}`, 'utf8') +function lookup(tree: Tree, dotted: string): unknown { + return dotted.split('.').reduce((node, part) => (node as Tree | undefined)?.[part], tree) +} + +const LOCALES = ['de', 'en'] as const + +const KEYS = [ + 'join.title', + 'join.description', + 'join.heading', + 'join.intro', + 'join.java.heading', + 'join.java.step_1', + 'join.java.step_2', + 'join.java.step_3', + 'join.java.step_4', + 'join.bedrock.heading', + 'join.bedrock.step_1', + 'join.bedrock.step_2', + 'join.bedrock.step_3', + 'join.bedrock.step_4', + 'join.help.heading', + 'join.help.text', + 'join.help.discord', + 'join.help.faq', + 'navigation.join', + 'server.connect.more_link' +] + +describe('join page messages', () => { + it.each(LOCALES)('has every join key filled in %s', (locale) => { + const tree = messages(locale) + for (const key of KEYS) { + const value = lookup(tree, key) + expect(typeof value, `${locale}: ${key}`).toBe('string') + expect((value as string).trim(), `${locale}: ${key}`).not.toBe('') + } + }) + + it.each(LOCALES)('keeps the address placeholders in the steps in %s', (locale) => { + const tree = messages(locale) + expect(lookup(tree, 'join.java.step_3')).toContain('{address}') + expect(lookup(tree, 'join.bedrock.step_3')).toContain('{host}') + expect(lookup(tree, 'join.bedrock.step_3')).toContain('{port}') + }) + + it.each(LOCALES)('keeps the title short enough for a search result in %s', (locale) => { + const title = lookup(messages(locale), 'join.title') as string + expect(title.length).toBeLessThanOrEqual(39) + }) + + it.each(LOCALES)('writes a meta description of 120 to 155 characters in %s', (locale) => { + const description = lookup(messages(locale), 'join.description') as string + expect(description.length).toBeGreaterThanOrEqual(120) + expect(description.length).toBeLessThanOrEqual(155) + }) + + it('uses umlauts and the du-form in the German copy', () => { + const copy = JSON.stringify(messages('de').join) + expect(copy).toMatch(/[äöüß]/) + expect(copy).toMatch(/\b(du|dir|deine|deinen|dein)\b/) + }) +}) + +describe('entry points to the join page', () => { + it('links the German editions FAQ answer to /de/join', () => { + expect(read('content/faq/de/editions.md')).toContain('](/de/join)') + }) + + it('links the English editions FAQ answer to /en/join', () => { + expect(read('content/faq/en/editions.md')).toContain('](/en/join)') + }) + + it('links the home server addresses section to the join page in the current locale', () => { + const addresses = read('layers/home/components/ServerAddresses.vue') + expect(addresses).toMatch(/:to="`\/\$\{locale\}\/join`"/) + expect(addresses).toContain("t('server.connect.more_link')") + }) + + it('points the join page FAQ link at the heading the FAQ section renders', () => { + expect(read('layers/home/components/FaqSection.vue')).toContain('id="faq-heading"') + expect(read('pages/join.vue')).toContain('#faq-heading') + }) +}) diff --git a/tests/join/page.spec.ts b/tests/join/page.spec.ts new file mode 100644 index 00000000..3fef9b07 --- /dev/null +++ b/tests/join/page.spec.ts @@ -0,0 +1,53 @@ +import { readFileSync } from 'node:fs' +import { describe, expect, it } from 'vitest' +import { repoRoot } from '../helpers/sources' + +const read = (file: string) => readFileSync(`${repoRoot}/${file}`, 'utf8') + +describe('join page', () => { + const page = read('pages/join.vue') + + it('has exactly one h1', () => { + expect(page.match(/]/g)).toHaveLength(1) + }) + + it('describes itself with a title, description and breadcrumb', () => { + expect(page).toMatch(/usePageSeo\(\{\s*title: t\('join\.title'\)/) + expect(page).toContain("description: t('join.description')") + expect(page).toMatch(/useBreadcrumbs\(/) + }) + + it('takes both addresses from the connect document, never from literals', () => { + expect(page).toMatch(/useServerConnect\(\)/) + expect(page).toMatch(/]*:java-address="connect\.javaAddress"/) + expect(page).toMatch(/]*:bedrock-host="connect\.bedrockHost"/) + expect(page).toMatch(/]*:bedrock-port="connect\.bedrockPort"/) + expect(page).not.toMatch(/onelitefeather\.(net|com)/) + expect(page).not.toMatch(/19132/) + }) + + it('links to Discord through the shared runtime invite', () => { + expect(page).toContain('discordUrl') + }) + + it('reads no query, cookie or header', () => { + expect(page).not.toMatch(/route\.query|useCookie|useRequestHeaders/) + }) + + it('renders no raw HTML', () => { + expect(page).not.toMatch(/v-html/) + }) +}) + +describe('server connect composable', () => { + const composable = read('layers/home/composables/useServerConnect.ts') + + it('loads the connect document for the active locale through the repository', () => { + expect(composable).toContain('useContentRepository()') + expect(composable).toContain('repo.getServerConnect(activeLocale.value)') + }) + + it('shares its cache key with the home page, which loads the same document', () => { + expect(composable).toContain('`server-connect-${activeLocale.value}`') + }) +}) diff --git a/tests/navigation/structure.spec.ts b/tests/navigation/structure.spec.ts index c5fb30a9..03d17b0a 100644 --- a/tests/navigation/structure.spec.ts +++ b/tests/navigation/structure.spec.ts @@ -61,6 +61,7 @@ describe('main navigation structure', () => { it('keeps events last in the community group while no event is live', () => { expect(childrenOf(buildNavConfig({ eventsTopLevel: false }), 'navigation.community').map(child => child.routeName)) .toEqual(['community', +'join', 'community-poi', 'projects', 'events']) @@ -69,6 +70,7 @@ describe('main navigation structure', () => { it('drops events from the community group once it is on the top level', () => { expect(childrenOf(buildNavConfig({ eventsTopLevel: true }), 'navigation.community').map(child => child.routeName)) .toEqual(['community', +'join', 'community-poi', 'projects']) }) @@ -92,6 +94,7 @@ describe('main navigation structure', () => { const reachable = new Set([...links(config), playLink].map(link => link.routeName ?? link.path)) const expected = [ 'blog', + 'join', 'team', 'community-poi', 'events', @@ -131,6 +134,7 @@ describe('main navigation structure', () => { const nav = messages(locale) const keys = [ 'builds', + 'join', 'home_link', 'community_overview', 'about', From c8ed249dc9736bc2f879b0ad8ae68de18b84ace8 Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Sun, 11 Oct 2026 00:21:51 +0200 Subject: [PATCH 2/2] feat(pages): name the server and its language on the join page Say that OneLiteFeather is a German-speaking network in the intro and the meta description, since that is the main search intent. Name the server in the heading and the title: "Minecraft-Server beitreten" and "Server beitreten: Java & Bedrock" (EN: "Join the Minecraft server" and "Join the server: Java & Bedrock"). The titles stay within 39 characters. Claude-Session: https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX --- i18n/locales/de.json | 8 ++++---- i18n/locales/en.json | 8 ++++---- pages/join.vue | 2 +- tests/join/content.spec.ts | 25 +++++++++++++++++++++++++ 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/i18n/locales/de.json b/i18n/locales/de.json index 4cb7a2b7..9d61644f 100644 --- a/i18n/locales/de.json +++ b/i18n/locales/de.json @@ -599,10 +599,10 @@ } }, "join": { - "title": "Minecraft beitreten: Java & Bedrock", - "description": "So trittst du dem OneLiteFeather Minecraft-Netzwerk bei: Adresse für Java oder Bedrock kopieren und in wenigen Schritten loslegen.", - "heading": "Minecraft beitreten", - "intro": "OneLiteFeather ist ein Minecraft-Netzwerk, gegründet 2019. Du kannst mit der Java Edition oder der Bedrock Edition beitreten. Kopiere die passende Adresse und folge den Schritten für deine Edition.", + "title": "Server beitreten: Java & Bedrock", + "description": "So trittst du dem deutschsprachigen OneLiteFeather Minecraft-Netzwerk bei: Adresse für Java oder Bedrock kopieren und in wenigen Schritten loslegen.", + "heading": "Minecraft-Server beitreten", + "intro": "OneLiteFeather ist ein deutschsprachiges Minecraft-Netzwerk, gegründet 2019. Du kannst mit der Java Edition oder der Bedrock Edition beitreten. Kopiere die passende Adresse und folge den Schritten für deine Edition.", "java": { "heading": "Mit der Java Edition beitreten", "step_1": "Starte Minecraft Java Edition und öffne den Mehrspielermodus.", diff --git a/i18n/locales/en.json b/i18n/locales/en.json index c3034907..ec4c35c0 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -599,10 +599,10 @@ } }, "join": { - "title": "Join Minecraft: Java & Bedrock", - "description": "Join the OneLiteFeather Minecraft network: copy the address for Java or Bedrock, add the server in your game and start playing in a few steps.", - "heading": "Join Minecraft", - "intro": "OneLiteFeather is a Minecraft network founded in 2019. You can join with the Java Edition or the Bedrock Edition. Copy the matching address and follow the steps for your edition.", + "title": "Join the server: Java & Bedrock", + "description": "Join the German-speaking OneLiteFeather Minecraft network: copy the Java or Bedrock address, add the server in your game and start playing in a few steps.", + "heading": "Join the Minecraft server", + "intro": "OneLiteFeather is a German-speaking Minecraft network, founded in 2019. You can join with the Java Edition or the Bedrock Edition. Copy the matching address and follow the steps for your edition.", "java": { "heading": "Join with Java Edition", "step_1": "Start Minecraft Java Edition and open Multiplayer.", diff --git a/pages/join.vue b/pages/join.vue index f3cb5c0c..37e7cc48 100644 --- a/pages/join.vue +++ b/pages/join.vue @@ -22,7 +22,7 @@ usePageSeo({ schemaType: 'WebPage', keywords: [ 'Minecraft Server', - 'Minecraft beitreten', + 'Minecraft-Server beitreten', 'Java Edition', 'Bedrock Edition', 'Join Minecraft' diff --git a/tests/join/content.spec.ts b/tests/join/content.spec.ts index f8b0567b..a4c4bc5c 100644 --- a/tests/join/content.spec.ts +++ b/tests/join/content.spec.ts @@ -70,6 +70,31 @@ describe('join page messages', () => { }) }) +describe('name and language of the network', () => { + const LANGUAGE = { + de: { intro: 'deutschsprachiges Minecraft-Netzwerk', description: 'deutschsprachigen' }, + en: { intro: 'German-speaking Minecraft network', description: 'German-speaking' } + } as const + + it.each(LOCALES)('names the server in the heading and the title in %s', (locale) => { + const tree = messages(locale) + expect(lookup(tree, 'join.heading') as string).toMatch(/server/i) + expect(lookup(tree, 'join.title') as string).toMatch(/server/i) + }) + + it.each(LOCALES)('says the network is German-speaking in the intro in %s', (locale) => { + expect(lookup(messages(locale), 'join.intro') as string).toContain(LANGUAGE[locale].intro) + }) + + it.each(LOCALES)('says the network is German-speaking in the meta description in %s', (locale) => { + expect(lookup(messages(locale), 'join.description') as string).toContain(LANGUAGE[locale].description) + }) + + it.each(LOCALES)('states the founding year in the intro in %s', (locale) => { + expect(lookup(messages(locale), 'join.intro') as string).toContain('2019') + }) +}) + describe('entry points to the join page', () => { it('links the German editions FAQ answer to /de/join', () => { expect(read('content/faq/de/editions.md')).toContain('](/de/join)')