From 41e98ee0f6e713d4a3db61bab25dcb95936b0302 Mon Sep 17 00:00:00 2001 From: Hugo Montenegro Date: Sun, 15 Mar 2026 18:12:02 +0000 Subject: [PATCH 1/2] fix: hide today's word on archive, fix unicode in word-image API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Words archive (/lang/words): today's word now shows a mystery card with "?" tiles and "Play to reveal!" CTA instead of spoiling the word - API: today's entry returns null word/definition/stats to prevent spoilers even via API - word-image & definition endpoints: normalize URL word param to NFC to fix 403 errors for words with diacritics (ö, é, etc.) --- pages/[lang]/words.vue | 141 ++++++++++++--------- server/api/[lang]/definition/[word].get.ts | 2 +- server/api/[lang]/word-image/[word].get.ts | 2 +- server/api/[lang]/words.get.ts | 11 +- 4 files changed, 93 insertions(+), 63 deletions(-) diff --git a/pages/[lang]/words.vue b/pages/[lang]/words.vue index dc18dd60..1b30d810 100644 --- a/pages/[lang]/words.vue +++ b/pages/[lang]/words.vue @@ -169,69 +169,96 @@ function winRate(stats: { total: number; wins: number }): number {
- - -
-
+ + +
+
+ ? +
+
+

+ #{{ w.day_idx }} · {{ formatDate(w.date) }} +

+

- {{ letter }} + Today's word — Play to reveal! +

+
+ + + + +
+
+ {{ letter }} +
-
- -

- #{{ w.day_idx }} · {{ formatDate(w.date) }} -

+ +

+ #{{ w.day_idx }} · {{ formatDate(w.date) }} +

- -

- {{ - w.definition.definition.length > 80 - ? w.definition.definition.slice(0, 80) + '\u2026' - : w.definition.definition - }} -

+ +

+ {{ + w.definition.definition.length > 80 + ? w.definition.definition.slice(0, 80) + '\u2026' + : w.definition.definition + }} +

- -
- {{ w.stats.total }} plays - {{ winRate(w.stats) }}% win -
+ +
+ {{ w.stats.total }} plays + {{ winRate(w.stats) }}% win +
- - - + + + +
diff --git a/server/api/[lang]/definition/[word].get.ts b/server/api/[lang]/definition/[word].get.ts index 5db7a4ca..6b073493 100644 --- a/server/api/[lang]/definition/[word].get.ts +++ b/server/api/[lang]/definition/[word].get.ts @@ -6,7 +6,7 @@ import { fetchDefinition } from '../../../utils/definitions'; export default defineEventHandler(async (event) => { const lang = getRouterParam(event, 'lang')!; - const word = getRouterParam(event, 'word')!; + const word = getRouterParam(event, 'word')!.normalize('NFC'); const data = loadAllData(); if (!data.languageCodes.includes(lang)) { diff --git a/server/api/[lang]/word-image/[word].get.ts b/server/api/[lang]/word-image/[word].get.ts index c2248219..4cc4844c 100644 --- a/server/api/[lang]/word-image/[word].get.ts +++ b/server/api/[lang]/word-image/[word].get.ts @@ -47,7 +47,7 @@ const IMAGE_MIN_DAY_IDX = 1708; export default defineEventHandler(async (event) => { const lang = getRouterParam(event, 'lang')!; - const word = getRouterParam(event, 'word')!; + const word = getRouterParam(event, 'word')!.normalize('NFC'); const data = loadAllData(); if (!data.languageCodes.includes(lang)) { diff --git a/server/api/[lang]/words.get.ts b/server/api/[lang]/words.get.ts index bbb9aa09..93ae144b 100644 --- a/server/api/[lang]/words.get.ts +++ b/server/api/[lang]/words.get.ts @@ -61,24 +61,27 @@ export default defineEventHandler((event) => { const words: Array<{ day_idx: number; - word: string; + word: string | null; date: string; definition: { definition: string; part_of_speech?: string } | null; stats: { total: number; wins: number } | null; + is_today: boolean; }> = []; for (let idx = startIdx; idx > endIdx; idx--) { + const isToday = idx === todaysIdx; const word = getWordForDay(lang, idx); const date = idxToDate(idx); - const stats = loadWordStats(lang, idx); - const defResult = readCachedDefinition(word, lang); + const stats = isToday ? null : loadWordStats(lang, idx); + const defResult = isToday ? null : readCachedDefinition(word, lang); words.push({ day_idx: idx, - word, + word: isToday ? null : word, date: date.toISOString().slice(0, 10), definition: defResult, stats: stats ? { total: stats.total, wins: stats.wins } : null, + is_today: isToday, }); } From bb3ea779a471122f2a3da07e2a2cbed8caf565a1 Mon Sep 17 00:00:00 2001 From: Hugo Montenegro Date: Sun, 15 Mar 2026 18:17:25 +0000 Subject: [PATCH 2/2] fix: handle null word in JSON-LD schema and update API test - Filter out today's null word from structured data ItemList to prevent SSR crash (toUpperCase on null) - Update API test to verify today's word is hidden (null) and past words are visible --- pages/[lang]/words.vue | 14 ++++++++------ tests/api.test.ts | 17 ++++++++++++----- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/pages/[lang]/words.vue b/pages/[lang]/words.vue index 1b30d810..0de5a5e0 100644 --- a/pages/[lang]/words.vue +++ b/pages/[lang]/words.vue @@ -83,12 +83,14 @@ useHead({ mainEntity: { '@type': 'ItemList', numberOfItems: words.value.length, - itemListElement: words.value.map((w, i) => ({ - '@type': 'ListItem', - position: i + 1 + (page.value - 1) * 30, - url: `https://wordle.global/${lang}/word/${w.day_idx}`, - name: `${w.word.toUpperCase()} \u2014 Wordle ${langNameNative.value} #${w.day_idx}`, - })), + itemListElement: words.value + .filter((w) => w.word) + .map((w, i) => ({ + '@type': 'ListItem', + position: i + 1 + (page.value - 1) * 30, + url: `https://wordle.global/${lang}/word/${w.day_idx}`, + name: `${w.word!.toUpperCase()} \u2014 Wordle ${langNameNative.value} #${w.day_idx}`, + })), }, }) ), diff --git a/tests/api.test.ts b/tests/api.test.ts index d2f050aa..b3091601 100644 --- a/tests/api.test.ts +++ b/tests/api.test.ts @@ -127,11 +127,18 @@ describe('API Routes', () => { expect(data.words.length).toBeGreaterThan(0); expect(data.words.length).toBeLessThanOrEqual(30); - // Check word entry shape - const word = data.words[0]; - expect(word.day_idx).toBeDefined(); - expect(typeof word.word).toBe('string'); - expect(word.date).toMatch(/^\d{4}-\d{2}-\d{2}$/); + // First entry is today's word (hidden) + const todayWord = data.words[0]; + expect(todayWord.day_idx).toBeDefined(); + expect(todayWord.is_today).toBe(true); + expect(todayWord.word).toBeNull(); + + // Second entry is a past word (visible) + if (data.words.length > 1) { + const pastWord = data.words[1]; + expect(typeof pastWord.word).toBe('string'); + expect(pastWord.date).toMatch(/^\d{4}-\d{2}-\d{2}$/); + } }); });