@jjlmoya/utils-tabletop 1.36.0 → 1.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +18 -4
- package/scripts/postbuild.mjs +17 -0
- package/scripts/postinstall.mjs +2 -4
- package/src/components/ProductionBreadcrumb.astro +132 -0
- package/src/components/ProductionWidget.astro +182 -0
- package/src/i18n/header-ui.ts +15 -0
- package/src/i18n/language-ui.ts +9 -0
- package/src/i18n/languages.ts +24 -0
- package/src/identity/brands.ts +5 -0
- package/src/layouts/ProductionCategoryPage.astro +184 -0
- package/src/layouts/ProductionPage.astro +209 -0
- package/src/layouts/ProductionUtilityPage.astro +282 -0
- package/src/mfe/assets.ts +15 -0
- package/src/mfe/category-ui.ts +26 -0
- package/src/mfe/routes.ts +50 -0
- package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug].astro +100 -0
- package/src/pages/[locale]/[utilities]/[categories]/[category].astro +44 -0
- package/src/pages/index.astro +1 -1
- package/src/pages/mfe-sitemaps/[locale]/[vertical]/sitemap.xml.ts +75 -0
- package/src/pages/utilidades/[slug].astro +90 -0
- package/src/pages/utilidades/categorias/[category].astro +38 -0
- package/src/tests/category_ui.test.ts +16 -0
- package/src/tests/mfe_assets_contract.test.ts +44 -0
- package/src/tests/routing_contract.test.ts +41 -0
- package/src/tool/board-game-timer/component.astro +0 -1
- package/src/tool/card-draw-odds-calculator/component.astro +0 -1
- package/src/tool/dungeon-map-generator/component.astro +2 -2
- package/src/tool/fantasy-runes-translator/component.astro +0 -1
- package/src/tool/first-player-selector/component.astro +0 -1
- package/src/tool/hidden-role-dealer/component.astro +0 -1
- package/src/tool/lunar-tide-tracker/component.astro +0 -1
- package/src/tool/mus-scoreboard/component.astro +0 -1
- package/src/tool/rpg-initiative-tracker/component.astro +0 -1
- package/src/tool/rpg-settlement-exploration-map-generator/component.astro +0 -1
- package/src/tool/scatter-direction-selector/component.astro +0 -1
- package/src/tool/score-tracker/component.astro +0 -1
- package/src/tool/token-stamp-studio/component.astro +0 -1
- package/src/types.ts +6 -8
- package/src/worker.ts +26 -0
- package/src/pages/[locale]/[slug].astro +0 -162
- package/src/pages/[locale].astro +0 -251
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
import ProductionCategoryPage from "../../../../layouts/ProductionCategoryPage.astro";
|
|
3
|
+
import { ALL_ENTRIES, tabletopCategory } from "../../../../index";
|
|
4
|
+
import { LANGUAGE_CODES, type Language } from "../../../../i18n/languages";
|
|
5
|
+
import { getCategoryNamespace, getCategoryPath, getUtilityNamespace, getUtilityPath } from "../../../../mfe/routes";
|
|
6
|
+
import { CATEGORY_OG_IMAGE } from "../../../../mfe/assets";
|
|
7
|
+
|
|
8
|
+
export async function getStaticPaths() {
|
|
9
|
+
const categories = Object.fromEntries(await Promise.all(
|
|
10
|
+
LANGUAGE_CODES.map(async (language) => [language, await tabletopCategory.i18n[language]!()]),
|
|
11
|
+
));
|
|
12
|
+
return LANGUAGE_CODES.filter((language) => language !== "es").map((locale) => {
|
|
13
|
+
const category = categories[locale]!;
|
|
14
|
+
const alternates = LANGUAGE_CODES.map((language) => ({
|
|
15
|
+
language,
|
|
16
|
+
url: getCategoryPath(language, categories[language]!.slug),
|
|
17
|
+
}));
|
|
18
|
+
return {
|
|
19
|
+
params: { locale, utilities: getUtilityNamespace(locale), categories: getCategoryNamespace(locale), category: category.slug },
|
|
20
|
+
props: { locale, category, alternates },
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { locale, category, alternates } = Astro.props as {
|
|
26
|
+
locale: Exclude<Language, "es">;
|
|
27
|
+
category: Awaited<ReturnType<NonNullable<(typeof tabletopCategory.i18n)[Language]>>>;
|
|
28
|
+
alternates: { language: Language; url: string }[];
|
|
29
|
+
};
|
|
30
|
+
const tools = await Promise.all(ALL_ENTRIES.map(async (entry) => {
|
|
31
|
+
const loader = entry.i18n[locale] ?? entry.i18n.en;
|
|
32
|
+
if (!loader) throw new Error(`Missing ${locale} locale for ${entry.id}`);
|
|
33
|
+
const content = await loader();
|
|
34
|
+
return {
|
|
35
|
+
id: entry.id,
|
|
36
|
+
icon: entry.icons.fg,
|
|
37
|
+
title: content.title,
|
|
38
|
+
description: content.description,
|
|
39
|
+
href: getUtilityPath(locale, category.slug, content.slug),
|
|
40
|
+
};
|
|
41
|
+
}));
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
<ProductionCategoryPage locale={locale} {category} {tools} {alternates} image={CATEGORY_OG_IMAGE} />
|
package/src/pages/index.astro
CHANGED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { APIRoute } from "astro";
|
|
2
|
+
import { tabletopCategory } from "../../../../category";
|
|
3
|
+
import { ALL_ENTRIES } from "../../../../entries";
|
|
4
|
+
import { LANGUAGE_CODES, type Language } from "../../../../i18n/languages";
|
|
5
|
+
import { getCategoryPath, getUtilityPath } from "../../../../mfe/routes";
|
|
6
|
+
|
|
7
|
+
const GAMEBOB_URL = "https://www.gamebob.dev";
|
|
8
|
+
const JJLMOYA_URL = "https://www.jjlmoya.es";
|
|
9
|
+
|
|
10
|
+
type LocalizedEntry = {
|
|
11
|
+
i18n: Partial<Record<Language, () => Promise<{ slug: string }>>>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const absoluteUrl = (locale: Language, path: string): string =>
|
|
15
|
+
path.startsWith("http") ? path : `${locale === "es" ? JJLMOYA_URL : GAMEBOB_URL}${path}`;
|
|
16
|
+
|
|
17
|
+
const escapeXml = (value: string): string => value
|
|
18
|
+
.replaceAll("&", "&")
|
|
19
|
+
.replaceAll("\"", """)
|
|
20
|
+
.replaceAll("'", "'")
|
|
21
|
+
.replaceAll("<", "<")
|
|
22
|
+
.replaceAll(">", ">");
|
|
23
|
+
|
|
24
|
+
const getContent = async (entry: LocalizedEntry, locale: Language): Promise<{ slug: string }> => {
|
|
25
|
+
const loader = entry.i18n[locale] ?? entry.i18n.en;
|
|
26
|
+
if (!loader) throw new Error(`Missing ${locale} and English fallback`);
|
|
27
|
+
return loader();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const prerender = true;
|
|
31
|
+
|
|
32
|
+
export function getStaticPaths() {
|
|
33
|
+
return LANGUAGE_CODES.map((locale) => ({
|
|
34
|
+
params: { locale, vertical: "tabletop" },
|
|
35
|
+
props: { locale },
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const buildUrlEntry = async (locale: Language, path: string, index: number): Promise<string> => {
|
|
40
|
+
const links = await Promise.all(LANGUAGE_CODES.map(async (alternateLocale) => {
|
|
41
|
+
const alternateCategory = await getContent(tabletopCategory, alternateLocale);
|
|
42
|
+
const alternateContent = index === 0 ? null : await getContent(ALL_ENTRIES[index - 1], alternateLocale);
|
|
43
|
+
const alternatePath = alternateContent
|
|
44
|
+
? getUtilityPath(alternateLocale, alternateCategory.slug, alternateContent.slug)
|
|
45
|
+
: getCategoryPath(alternateLocale, alternateCategory.slug);
|
|
46
|
+
return ` <xhtml:link rel="alternate" hreflang="${alternateLocale}" href="${escapeXml(absoluteUrl(alternateLocale, alternatePath))}"/>`;
|
|
47
|
+
}));
|
|
48
|
+
return [
|
|
49
|
+
" <url>",
|
|
50
|
+
` <loc>${escapeXml(absoluteUrl(locale, path))}</loc>`,
|
|
51
|
+
...links,
|
|
52
|
+
` <changefreq>${index === 0 ? "weekly" : "monthly"}</changefreq>`,
|
|
53
|
+
` <priority>${index === 0 ? "0.7" : "0.6"}</priority>`,
|
|
54
|
+
" </url>",
|
|
55
|
+
].join("\n");
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const GET: APIRoute = async ({ props }) => {
|
|
59
|
+
const locale = props.locale as Language;
|
|
60
|
+
const category = await getContent(tabletopCategory, locale);
|
|
61
|
+
const contents = await Promise.all(ALL_ENTRIES.map((entry) => getContent(entry, locale)));
|
|
62
|
+
const paths = [
|
|
63
|
+
getCategoryPath(locale, category.slug),
|
|
64
|
+
...contents.map((content) => getUtilityPath(locale, category.slug, content.slug)),
|
|
65
|
+
];
|
|
66
|
+
const urls = await Promise.all(paths.map((path, index) => buildUrlEntry(locale, path, index)));
|
|
67
|
+
|
|
68
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">\n${urls.join("\n")}\n</urlset>`;
|
|
69
|
+
return new Response(xml, {
|
|
70
|
+
headers: {
|
|
71
|
+
"Content-Type": "application/xml; charset=utf-8",
|
|
72
|
+
"Cache-Control": "public, max-age=3600, s-maxage=3600, must-revalidate",
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
import ProductionUtilityPage from "../../layouts/ProductionUtilityPage.astro";
|
|
3
|
+
import { ALL_TOOLS, tabletopCategory } from "../../index";
|
|
4
|
+
import { LANGUAGE_CODES, type Language } from "../../i18n/languages";
|
|
5
|
+
import type { CategoryLocaleContent, ToolLocaleContent } from "../../types";
|
|
6
|
+
import type { AstroComponentFactory } from "astro/runtime/server/index.js";
|
|
7
|
+
import { getCategoryPath, getUtilityPath } from "../../mfe/routes";
|
|
8
|
+
import { getUtilityOgImage } from "../../mfe/assets";
|
|
9
|
+
|
|
10
|
+
type Loader<T> = () => Promise<{ default: T }>;
|
|
11
|
+
type LoadedTool = ToolLocaleContent;
|
|
12
|
+
|
|
13
|
+
export async function getStaticPaths() {
|
|
14
|
+
const category = await tabletopCategory.i18n.es!();
|
|
15
|
+
const categories = Object.fromEntries(await Promise.all(LANGUAGE_CODES.map(async (language) => [
|
|
16
|
+
language,
|
|
17
|
+
await tabletopCategory.i18n[language]!(),
|
|
18
|
+
]))) as Record<Language, CategoryLocaleContent>;
|
|
19
|
+
const paths = [];
|
|
20
|
+
for (const { entry, Component: componentLoader, SEOComponent: seoLoader, BibliographyComponent: bibliographyLoader } of ALL_TOOLS) {
|
|
21
|
+
const contents = Object.fromEntries(await Promise.all(LANGUAGE_CODES.map(async (language) => {
|
|
22
|
+
const loader = entry.i18n[language] ?? entry.i18n.en;
|
|
23
|
+
if (!loader) throw new Error(`Missing ${language} locale for ${entry.id}`);
|
|
24
|
+
return [language, await loader()];
|
|
25
|
+
}))) as Record<Language, LoadedTool>;
|
|
26
|
+
const { default: Component } = await (componentLoader as Loader<AstroComponentFactory>)();
|
|
27
|
+
const { default: SEOComponent } = await (seoLoader as Loader<AstroComponentFactory>)();
|
|
28
|
+
const { default: BibliographyComponent } = await (bibliographyLoader as Loader<AstroComponentFactory>)();
|
|
29
|
+
paths.push({
|
|
30
|
+
params: { slug: contents.es.slug },
|
|
31
|
+
props: {
|
|
32
|
+
locale: "es",
|
|
33
|
+
content: contents.es,
|
|
34
|
+
englishSlug: contents.en.slug,
|
|
35
|
+
category,
|
|
36
|
+
categorySlug: category.slug,
|
|
37
|
+
Component,
|
|
38
|
+
SEOComponent,
|
|
39
|
+
BibliographyComponent,
|
|
40
|
+
alternates: LANGUAGE_CODES.map((language) => ({
|
|
41
|
+
language,
|
|
42
|
+
url: getUtilityPath(language, categories[language].slug, contents[language].slug),
|
|
43
|
+
})),
|
|
44
|
+
image: getUtilityOgImage(contents.en.slug),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return paths;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const { locale, content, englishSlug, category, categorySlug, Component, SEOComponent, BibliographyComponent, alternates, image } = Astro.props as {
|
|
52
|
+
locale: "es";
|
|
53
|
+
content: LoadedTool;
|
|
54
|
+
englishSlug: string;
|
|
55
|
+
category: CategoryLocaleContent;
|
|
56
|
+
categorySlug: string;
|
|
57
|
+
Component: AstroComponentFactory;
|
|
58
|
+
SEOComponent: AstroComponentFactory;
|
|
59
|
+
BibliographyComponent: AstroComponentFactory;
|
|
60
|
+
alternates: { language: Language; url: string }[];
|
|
61
|
+
image?: string;
|
|
62
|
+
};
|
|
63
|
+
const relatedTools = (await Promise.all(ALL_TOOLS.map(async ({ entry: relatedEntry }) => {
|
|
64
|
+
const loader = relatedEntry.i18n.es ?? relatedEntry.i18n.en;
|
|
65
|
+
if (!loader) return null;
|
|
66
|
+
const relatedContent = await loader();
|
|
67
|
+
if (relatedContent.slug === content.slug) return null;
|
|
68
|
+
return {
|
|
69
|
+
icon: relatedEntry.icons.fg,
|
|
70
|
+
title: relatedContent.title,
|
|
71
|
+
description: relatedContent.description,
|
|
72
|
+
href: getUtilityPath("es", categorySlug, relatedContent.slug),
|
|
73
|
+
};
|
|
74
|
+
}))).filter((tool): tool is { icon: string; title: string; description: string; href: string } => tool !== null);
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
<ProductionUtilityPage
|
|
78
|
+
{locale}
|
|
79
|
+
{content}
|
|
80
|
+
{englishSlug}
|
|
81
|
+
categoryTitle={category.title}
|
|
82
|
+
{categorySlug}
|
|
83
|
+
categoryHref={getCategoryPath(locale, categorySlug)}
|
|
84
|
+
{relatedTools}
|
|
85
|
+
{Component}
|
|
86
|
+
{SEOComponent}
|
|
87
|
+
{BibliographyComponent}
|
|
88
|
+
{alternates}
|
|
89
|
+
{image}
|
|
90
|
+
/>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
import ProductionCategoryPage from "../../../layouts/ProductionCategoryPage.astro";
|
|
3
|
+
import { ALL_ENTRIES, tabletopCategory } from "../../../index";
|
|
4
|
+
import { LANGUAGE_CODES, type Language } from "../../../i18n/languages";
|
|
5
|
+
import { getCategoryPath, getUtilityPath } from "../../../mfe/routes";
|
|
6
|
+
import { CATEGORY_OG_IMAGE } from "../../../mfe/assets";
|
|
7
|
+
|
|
8
|
+
export async function getStaticPaths() {
|
|
9
|
+
const categories = Object.fromEntries(await Promise.all(
|
|
10
|
+
LANGUAGE_CODES.map(async (language) => [language, await tabletopCategory.i18n[language]!()]),
|
|
11
|
+
));
|
|
12
|
+
const category = categories.es!;
|
|
13
|
+
const alternates = LANGUAGE_CODES.map((language) => ({
|
|
14
|
+
language,
|
|
15
|
+
url: getCategoryPath(language, categories[language]!.slug),
|
|
16
|
+
}));
|
|
17
|
+
return [{ params: { category: category.slug }, props: { category, alternates } }];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { category, alternates } = Astro.props as {
|
|
21
|
+
category: Awaited<ReturnType<NonNullable<(typeof tabletopCategory.i18n)[Language]>>>;
|
|
22
|
+
alternates: { language: Language; url: string }[];
|
|
23
|
+
};
|
|
24
|
+
const tools = await Promise.all(ALL_ENTRIES.map(async (entry) => {
|
|
25
|
+
const loader = entry.i18n.es ?? entry.i18n.en;
|
|
26
|
+
if (!loader) throw new Error(`Missing Spanish locale for ${entry.id}`);
|
|
27
|
+
const content = await loader();
|
|
28
|
+
return {
|
|
29
|
+
id: entry.id,
|
|
30
|
+
icon: entry.icons.fg,
|
|
31
|
+
title: content.title,
|
|
32
|
+
description: content.description,
|
|
33
|
+
href: getUtilityPath("es", category.slug, content.slug),
|
|
34
|
+
};
|
|
35
|
+
}));
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
<ProductionCategoryPage locale="es" {category} {tools} {alternates} image={CATEGORY_OG_IMAGE} />
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { UTILITY_LOCALES } from '@jjlmoya/utils-shared/routing';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { CATEGORY_UI, getCategoryUi } from '../mfe/category-ui';
|
|
4
|
+
|
|
5
|
+
describe('category UI translations', () => {
|
|
6
|
+
it('provides a translated use-tool CTA for every supported locale', () => {
|
|
7
|
+
for (const locale of UTILITY_LOCALES) {
|
|
8
|
+
expect(getCategoryUi(locale).useTool.trim()).not.toBe('');
|
|
9
|
+
expect(getCategoryUi(locale).openTool.trim()).not.toBe('');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
expect(Object.keys(CATEGORY_UI).sort()).toEqual([...UTILITY_LOCALES].sort());
|
|
13
|
+
expect(UTILITY_LOCALES.filter((locale) => locale !== 'en').every((locale) => getCategoryUi(locale).useTool !== 'Use tool')).toBe(true);
|
|
14
|
+
expect(UTILITY_LOCALES.filter((locale) => locale !== 'en').every((locale) => getCategoryUi(locale).openTool !== 'Open tool')).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { ALL_TOOLS } from '../tools';
|
|
5
|
+
import {
|
|
6
|
+
CATEGORY_OG_IMAGE,
|
|
7
|
+
getUtilityCssPath,
|
|
8
|
+
getUtilityOgImage,
|
|
9
|
+
TABLETOP_ASSET_VERSION,
|
|
10
|
+
} from '../mfe/assets';
|
|
11
|
+
|
|
12
|
+
const assetRoot = join(process.cwd(), 'public', '_utilities', 'tabletop', 'images');
|
|
13
|
+
|
|
14
|
+
describe('MFE asset contract', () => {
|
|
15
|
+
it('keeps one English OG asset per category and tool', async () => {
|
|
16
|
+
const expectedSlugs = new Set(['tabletop']);
|
|
17
|
+
|
|
18
|
+
for (const { entry } of ALL_TOOLS) {
|
|
19
|
+
const loader = entry.i18n.en;
|
|
20
|
+
if (!loader) throw new Error(`Missing English locale for ${entry.id}`);
|
|
21
|
+
expectedSlugs.add((await loader()).slug);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const actualSlugs = new Set(
|
|
25
|
+
readdirSync(assetRoot)
|
|
26
|
+
.filter((filename) => filename.endsWith('.webp'))
|
|
27
|
+
.map((filename) => filename.slice(0, -'.webp'.length)),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(actualSlugs).toEqual(expectedSlugs);
|
|
31
|
+
for (const slug of expectedSlugs) {
|
|
32
|
+
expect(existsSync(join(assetRoot, `${slug}.webp`))).toBe(true);
|
|
33
|
+
expect(getUtilityOgImage(slug)).toBe(
|
|
34
|
+
`/_utilities/tabletop/images/${slug}.webp?version=${TABLETOP_ASSET_VERSION}`,
|
|
35
|
+
);
|
|
36
|
+
expect(getUtilityCssPath(slug)).toBe(
|
|
37
|
+
`/_utilities/tabletop/styles/${slug}.css?version=${TABLETOP_ASSET_VERSION}`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
expect(CATEGORY_OG_IMAGE).toBe(
|
|
41
|
+
`/_utilities/tabletop/images/tabletop.webp?version=${TABLETOP_ASSET_VERSION}`,
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
UTILITY_LOCALES,
|
|
4
|
+
getCategoryNamespace,
|
|
5
|
+
getCategoryPath,
|
|
6
|
+
getUtilitiesPath,
|
|
7
|
+
getUtilityPath,
|
|
8
|
+
getUtilityNamespace,
|
|
9
|
+
} from '@jjlmoya/utils-shared/routing';
|
|
10
|
+
|
|
11
|
+
describe('Public utility routing contract', () => {
|
|
12
|
+
it('keeps the complete fifteen-locale utility set', () => {
|
|
13
|
+
expect(UTILITY_LOCALES).toHaveLength(15);
|
|
14
|
+
expect(new Set(UTILITY_LOCALES).size).toBe(15);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('keeps Spanish on the external jjlmoya host', () => {
|
|
18
|
+
expect(getUtilitiesPath('es')).toBe('https://www.jjlmoya.es/utilidades/');
|
|
19
|
+
expect(getCategoryPath('es', 'juegos-de-mesa')).toBe(
|
|
20
|
+
'https://www.jjlmoya.es/utilidades/categorias/juegos-de-mesa/',
|
|
21
|
+
);
|
|
22
|
+
expect(getUtilityPath('es', 'juegos-de-mesa', 'tirador-de-dados')).toBe(
|
|
23
|
+
'https://www.jjlmoya.es/utilidades/tirador-de-dados/',
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('serializes every internal locale with both translated namespaces', () => {
|
|
28
|
+
for (const locale of UTILITY_LOCALES.filter((candidate) => candidate !== 'es')) {
|
|
29
|
+
expect(getUtilitiesPath(locale)).toBe(`/${locale}/${getUtilityNamespace(locale)}/`);
|
|
30
|
+
const category = getCategoryPath(locale, 'tabletop');
|
|
31
|
+
const utility = getUtilityPath(locale, 'tabletop', 'dice-roller-simulator');
|
|
32
|
+
|
|
33
|
+
expect(category).toBe(
|
|
34
|
+
`/${locale}/${getUtilityNamespace(locale)}/${getCategoryNamespace(locale)}/tabletop/`,
|
|
35
|
+
);
|
|
36
|
+
expect(utility).toBe(
|
|
37
|
+
`/${locale}/${getUtilityNamespace(locale)}/${getCategoryNamespace(locale)}/tabletop/dice-roller-simulator/`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -3,7 +3,6 @@ import { buildMapSvg } from './dom-views';
|
|
|
3
3
|
import { evaluateSettlement } from './evaluator';
|
|
4
4
|
import { DEFAULT_CONFIG, generateSettlement } from './logic';
|
|
5
5
|
import type { SettlementMapUI } from './ui';
|
|
6
|
-
import './rpg-settlement-exploration-map-generator.css';
|
|
7
6
|
interface Props { ui: SettlementMapUI; }
|
|
8
7
|
const { ui } = Astro.props;
|
|
9
8
|
const initialMap = generateSettlement(DEFAULT_CONFIG);
|
package/src/types.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { SEOSection } from '@jjlmoya/utils-shared';
|
|
2
|
+
import type { UtilityLocale } from '@jjlmoya/utils-shared/routing';
|
|
3
|
+
import type { AstroComponentFactory } from 'astro/runtime/server/index.js';
|
|
2
4
|
|
|
3
5
|
export type { SEOSection };
|
|
4
6
|
|
|
5
|
-
export type KnownLocale =
|
|
6
|
-
| 'ar' | 'da' | 'de' | 'en' | 'es' | 'fi'
|
|
7
|
-
| 'fr' | 'id' | 'it' | 'ja' | 'ko' | 'nb' | 'nl'
|
|
8
|
-
| 'pl' | 'pt' | 'ru' | 'sv' | 'tr' | 'zh';
|
|
7
|
+
export type KnownLocale = UtilityLocale;
|
|
9
8
|
|
|
10
9
|
export interface FAQItem {
|
|
11
10
|
question: string;
|
|
@@ -62,8 +61,7 @@ export interface TabletopCategoryEntry {
|
|
|
62
61
|
|
|
63
62
|
export interface ToolDefinition {
|
|
64
63
|
entry: TabletopToolEntry;
|
|
65
|
-
Component:
|
|
66
|
-
SEOComponent:
|
|
67
|
-
BibliographyComponent:
|
|
64
|
+
Component: () => Promise<{ default: AstroComponentFactory }>;
|
|
65
|
+
SEOComponent: () => Promise<{ default: AstroComponentFactory }>;
|
|
66
|
+
BibliographyComponent: () => Promise<{ default: AstroComponentFactory }>;
|
|
68
67
|
}
|
|
69
|
-
|
package/src/worker.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
interface UtilityMfeEnvironment {
|
|
2
|
+
ASSETS: { fetch(request: Request): Promise<Response> };
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
const LONG_LIVED_ASSET_CACHE = "public, max-age=31536000, immutable";
|
|
6
|
+
const SITEMAP_CACHE = "public, max-age=3600, s-maxage=3600, must-revalidate";
|
|
7
|
+
|
|
8
|
+
const getCacheControl = (pathname: string): string | undefined => {
|
|
9
|
+
if (pathname.endsWith("/sitemap.xml")) return SITEMAP_CACHE;
|
|
10
|
+
if (pathname.startsWith("/_utilities/")) {
|
|
11
|
+
return LONG_LIVED_ASSET_CACHE;
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
async fetch(request: Request, environment: UtilityMfeEnvironment): Promise<Response> {
|
|
18
|
+
const response = await environment.ASSETS.fetch(request);
|
|
19
|
+
const cacheControl = getCacheControl(new URL(request.url).pathname);
|
|
20
|
+
if (!cacheControl) return response;
|
|
21
|
+
|
|
22
|
+
const headers = new Headers(response.headers);
|
|
23
|
+
headers.set("Cache-Control", cacheControl);
|
|
24
|
+
return new Response(response.body, { status: response.status, headers });
|
|
25
|
+
},
|
|
26
|
+
};
|