@jjlmoya/utils-tabletop 1.39.0 → 1.40.1
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 +1 -1
- package/src/components/ProductionStructuredData.astro +46 -0
- package/src/layouts/ProductionPage.astro +3 -1
- package/src/layouts/ProductionUtilityPage.astro +4 -3
- package/src/mfe/manifest.ts +45 -0
- package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug]/manifest.json.ts +49 -0
- package/src/pages/utilidades/[slug]/manifest.json.ts +28 -0
- package/src/tests/mfe_manifest.test.ts +28 -0
package/package.json
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { KnownLocale, ToolLocaleContent } from "../types";
|
|
3
|
+
import { TABLETOP_ASSET_VERSION } from "../mfe/assets";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
locale: KnownLocale;
|
|
7
|
+
content: ToolLocaleContent;
|
|
8
|
+
publicPath: string;
|
|
9
|
+
categoryTitle: string;
|
|
10
|
+
categoryHref: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { locale, content, publicPath, categoryTitle, categoryHref } = Astro.props;
|
|
14
|
+
const siteOrigin = locale === "es" ? "https://www.jjlmoya.es" : "https://www.gamebob.dev";
|
|
15
|
+
const fullUrl = new URL(publicPath, siteOrigin).href;
|
|
16
|
+
const categoryUrl = new URL(categoryHref, siteOrigin).href;
|
|
17
|
+
const brandName = locale === "es" ? "jjlmoya" : "GameBob";
|
|
18
|
+
const schemas = (content.schemas ?? []).filter((schema) => (schema as unknown as Record<string, unknown>)["@type"] !== "FAQPage");
|
|
19
|
+
const schemaTypes = new Set(schemas.map((schema) => (schema as unknown as Record<string, unknown>)["@type"]));
|
|
20
|
+
const breadcrumbSchema = {
|
|
21
|
+
"@context": "https://schema.org",
|
|
22
|
+
"@type": "BreadcrumbList",
|
|
23
|
+
itemListElement: [
|
|
24
|
+
{ "@type": "ListItem", position: 1, name: brandName, item: `${siteOrigin}/${locale === "es" ? "" : `${locale}/`}` },
|
|
25
|
+
{ "@type": "ListItem", position: 2, name: categoryTitle, item: categoryUrl },
|
|
26
|
+
{ "@type": "ListItem", position: 3, name: content.title, item: fullUrl },
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
const webApplicationSchema = {
|
|
30
|
+
"@context": "https://schema.org",
|
|
31
|
+
"@type": "WebApplication",
|
|
32
|
+
name: content.title,
|
|
33
|
+
description: content.description,
|
|
34
|
+
url: fullUrl,
|
|
35
|
+
applicationCategory: "UtilityApplication",
|
|
36
|
+
operatingSystem: "Any",
|
|
37
|
+
softwareVersion: TABLETOP_ASSET_VERSION,
|
|
38
|
+
author: { "@type": "Person", name: "jjlmoya", url: siteOrigin },
|
|
39
|
+
publisher: { "@type": "Organization", name: brandName, url: siteOrigin },
|
|
40
|
+
inLanguage: locale,
|
|
41
|
+
offers: { "@type": "Offer", price: "0", priceCurrency: "EUR" },
|
|
42
|
+
};
|
|
43
|
+
---
|
|
44
|
+
<script is:inline type="application/ld+json" set:html={JSON.stringify(breadcrumbSchema)} />
|
|
45
|
+
{!schemaTypes.has("WebApplication") && <script is:inline type="application/ld+json" set:html={JSON.stringify(webApplicationSchema)} />}
|
|
46
|
+
{schemas.map((schema) => <script is:inline type="application/ld+json" set:html={JSON.stringify(schema)} />)}
|
|
@@ -22,10 +22,11 @@ interface Props {
|
|
|
22
22
|
image?: string | undefined;
|
|
23
23
|
toolCss?: string | undefined;
|
|
24
24
|
publicPath: string;
|
|
25
|
+
manifestPath?: string | undefined;
|
|
25
26
|
alternates: readonly AlternateLink[];
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
const { title, description, locale, image, toolCss, publicPath, alternates } = Astro.props;
|
|
29
|
+
const { title, description, locale, image, toolCss, publicPath, manifestPath, alternates } = Astro.props;
|
|
29
30
|
const brand = getBrand(locale);
|
|
30
31
|
const brandFavicon = getBrandAssetPath(brand, "favicon");
|
|
31
32
|
const brandSizedFavicon = getBrandAssetPath(brand, "faviconSized");
|
|
@@ -67,6 +68,7 @@ const xDefaultUrl = alternates.find(({ language }) => language === "en")?.url ??
|
|
|
67
68
|
<link rel="icon" type="image/x-icon" href={brandFavicon} />
|
|
68
69
|
<link rel="icon" type="image/webp" sizes={brand === "jjlmoya" ? "32x32" : "48x48"} href={brandSizedFavicon} />
|
|
69
70
|
<link rel="apple-touch-icon" href={brandAppleTouchIcon} />
|
|
71
|
+
{manifestPath && <link rel="manifest" href={manifestPath} />}
|
|
70
72
|
<link rel="canonical" href={canonical} />
|
|
71
73
|
{alternates.map(({ language, url }) => <link rel="alternate" hreflang={language} href={absoluteUrl(language, url)} />)}
|
|
72
74
|
<link rel="alternate" hreflang="x-default" href={absoluteUrl("en", xDefaultUrl)} />
|
|
@@ -4,10 +4,11 @@ import { FAQSection, UtilityHeader } from "@jjlmoya/utils-shared";
|
|
|
4
4
|
import { Icon } from "astro-icon/components";
|
|
5
5
|
import ProductionBreadcrumb from "../components/ProductionBreadcrumb.astro";
|
|
6
6
|
import ProductionWidget from "../components/ProductionWidget.astro";
|
|
7
|
+
import ProductionStructuredData from "../components/ProductionStructuredData.astro";
|
|
7
8
|
import type { KnownLocale, ToolLocaleContent } from "../types";
|
|
8
9
|
import { getCategoryUi } from "../mfe/category-ui";
|
|
9
10
|
import { getCategoryPath, getUtilityPath } from "../mfe/routes";
|
|
10
|
-
import { getUtilityCssPath } from "../mfe/assets";
|
|
11
|
+
import { getUtilityCssPath, TABLETOP_ASSET_VERSION } from "../mfe/assets";
|
|
11
12
|
import type { AstroComponentFactory } from "astro/runtime/server/index.js";
|
|
12
13
|
|
|
13
14
|
interface RelatedTool { title: string; description: string; href: string; icon: string; }
|
|
@@ -21,7 +22,7 @@ const publicCategoryPath = getCategoryPath(locale, categorySlug);
|
|
|
21
22
|
const ui = getCategoryUi(locale);
|
|
22
23
|
---
|
|
23
24
|
|
|
24
|
-
<ProductionPage title={content.title} description={content.description} {locale} {publicPath} {alternates} {image} toolCss={getUtilityCssPath(englishSlug)}>
|
|
25
|
+
<ProductionPage title={content.title} description={content.description} {locale} {publicPath} manifestPath={`${publicPath}manifest.json?version=${TABLETOP_ASSET_VERSION}`} {alternates} {image} toolCss={getUtilityCssPath(englishSlug)}>
|
|
25
26
|
<div class="utility-production">
|
|
26
27
|
<div class="utility-production-inner">
|
|
27
28
|
<ProductionBreadcrumb locale={locale} categoryTitle={categoryTitle} categoryHref={categoryHref || publicCategoryPath} currentTitle={content.title} />
|
|
@@ -34,7 +35,7 @@ const ui = getCategoryUi(locale);
|
|
|
34
35
|
<ProductionWidget locale={locale} slug={content.slug} />
|
|
35
36
|
{content.faq.length > 0 && <section class="utility-section-production"><FAQSection items={content.faq} /></section>}
|
|
36
37
|
<Fragment slot="head">
|
|
37
|
-
{
|
|
38
|
+
<ProductionStructuredData locale={locale} {content} {publicPath} categoryTitle={categoryTitle} categoryHref={categoryHref || publicCategoryPath} />
|
|
38
39
|
</Fragment>
|
|
39
40
|
<section class="utility-section-production"><SEOComponent locale={locale} /></section>
|
|
40
41
|
{content.bibliography.length > 0 && <section class="utility-section-production"><BibliographyComponent locale={locale} /></section>}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { getUtilityOgImage } from "./assets";
|
|
2
|
+
|
|
3
|
+
export interface UtilityManifestInput {
|
|
4
|
+
title: string;
|
|
5
|
+
description: string;
|
|
6
|
+
startUrl: string;
|
|
7
|
+
englishSlug: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const buildUtilityManifest = ({
|
|
11
|
+
title,
|
|
12
|
+
description,
|
|
13
|
+
startUrl,
|
|
14
|
+
englishSlug,
|
|
15
|
+
}: UtilityManifestInput) => {
|
|
16
|
+
const shortName = title.length > 12
|
|
17
|
+
? title.split(/\s+/).map((word) => word[0]).join("")
|
|
18
|
+
: title;
|
|
19
|
+
const manifest = {
|
|
20
|
+
name: title,
|
|
21
|
+
short_name: shortName,
|
|
22
|
+
description,
|
|
23
|
+
start_url: startUrl,
|
|
24
|
+
scope: startUrl,
|
|
25
|
+
icons: [{
|
|
26
|
+
src: getUtilityOgImage(englishSlug),
|
|
27
|
+
sizes: "512x512",
|
|
28
|
+
type: "image/webp",
|
|
29
|
+
purpose: "any",
|
|
30
|
+
}],
|
|
31
|
+
theme_color: "#0f172a",
|
|
32
|
+
background_color: "#0f172a",
|
|
33
|
+
display: "standalone",
|
|
34
|
+
orientation: "portrait",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return manifest;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const createUtilityManifestResponse = (input: UtilityManifestInput): Response => new Response(JSON.stringify(buildUtilityManifest(input), null, 2), {
|
|
41
|
+
headers: {
|
|
42
|
+
"Content-Type": "application/manifest+json",
|
|
43
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
44
|
+
},
|
|
45
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { APIRoute } from "astro";
|
|
2
|
+
import { ALL_TOOLS, tabletopCategory } from "../../../../../../index";
|
|
3
|
+
import type { CategoryLocaleContent, ToolLocaleContent } from "../../../../../../types";
|
|
4
|
+
import { LANGUAGE_CODES, type Language } from "../../../../../../i18n/languages";
|
|
5
|
+
import { getCategoryNamespace, getUtilityNamespace, getUtilityPath } from "../../../../../../mfe/routes";
|
|
6
|
+
import { createUtilityManifestResponse, type UtilityManifestInput } from "../../../../../../mfe/manifest";
|
|
7
|
+
|
|
8
|
+
const loadCategories = async () => Object.fromEntries(await Promise.all(LANGUAGE_CODES.map(async (language) => [
|
|
9
|
+
language,
|
|
10
|
+
await tabletopCategory.i18n[language]!(),
|
|
11
|
+
]))) as Record<Language, CategoryLocaleContent>;
|
|
12
|
+
|
|
13
|
+
const loadContents = async (entry: (typeof ALL_TOOLS)[number]["entry"]) => Object.fromEntries(await Promise.all(LANGUAGE_CODES.map(async (language) => {
|
|
14
|
+
const loader = entry.i18n[language] ?? entry.i18n.en;
|
|
15
|
+
if (!loader) throw new Error(`Missing ${language} locale for ${entry.id}`);
|
|
16
|
+
return [language, await loader()];
|
|
17
|
+
}))) as Record<Language, ToolLocaleContent>;
|
|
18
|
+
|
|
19
|
+
const createManifestPath = (locale: Exclude<Language, "es">, category: CategoryLocaleContent, content: ToolLocaleContent, englishContent: ToolLocaleContent) => ({
|
|
20
|
+
params: {
|
|
21
|
+
locale,
|
|
22
|
+
utilities: getUtilityNamespace(locale),
|
|
23
|
+
categories: getCategoryNamespace(locale),
|
|
24
|
+
category: category.slug,
|
|
25
|
+
slug: content.slug,
|
|
26
|
+
},
|
|
27
|
+
props: {
|
|
28
|
+
title: content.title,
|
|
29
|
+
description: content.description,
|
|
30
|
+
startUrl: getUtilityPath(locale, category.slug, content.slug),
|
|
31
|
+
englishSlug: englishContent.slug,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export async function getStaticPaths() {
|
|
36
|
+
const categories = await loadCategories();
|
|
37
|
+
const paths = [];
|
|
38
|
+
for (const { entry } of ALL_TOOLS) {
|
|
39
|
+
const contents = await loadContents(entry);
|
|
40
|
+
for (const locale of LANGUAGE_CODES.filter((candidate) => candidate !== "es")) {
|
|
41
|
+
const content = contents[locale];
|
|
42
|
+
const category = categories[locale];
|
|
43
|
+
paths.push(createManifestPath(locale, category, content, contents.en));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return paths;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const GET: APIRoute = ({ props }) => createUtilityManifestResponse(props as unknown as UtilityManifestInput);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { APIRoute } from "astro";
|
|
2
|
+
import { ALL_TOOLS, tabletopCategory } from "../../../index";
|
|
3
|
+
import type { ToolLocaleContent } from "../../../types";
|
|
4
|
+
import { getUtilityPath } from "../../../mfe/routes";
|
|
5
|
+
import { createUtilityManifestResponse, type UtilityManifestInput } from "../../../mfe/manifest";
|
|
6
|
+
|
|
7
|
+
export async function getStaticPaths() {
|
|
8
|
+
const category = await tabletopCategory.i18n.es!();
|
|
9
|
+
const paths = [];
|
|
10
|
+
for (const { entry } of ALL_TOOLS) {
|
|
11
|
+
const loader = entry.i18n.es ?? entry.i18n.en;
|
|
12
|
+
if (!loader) continue;
|
|
13
|
+
const content = await loader() as ToolLocaleContent;
|
|
14
|
+
const englishContent = await (entry.i18n.en ?? loader)() as ToolLocaleContent;
|
|
15
|
+
paths.push({
|
|
16
|
+
params: { slug: content.slug },
|
|
17
|
+
props: {
|
|
18
|
+
title: content.title,
|
|
19
|
+
description: content.description,
|
|
20
|
+
startUrl: getUtilityPath("es", category.slug, content.slug),
|
|
21
|
+
englishSlug: englishContent.slug,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return paths;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const GET: APIRoute = ({ props }) => createUtilityManifestResponse(props as unknown as UtilityManifestInput);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createUtilityManifestResponse } from "../mfe/manifest";
|
|
3
|
+
|
|
4
|
+
describe("MFE utility manifest", () => {
|
|
5
|
+
it("returns a versioned installable manifest for the current utility", async () => {
|
|
6
|
+
const response = createUtilityManifestResponse({
|
|
7
|
+
title: "Card draw odds calculator",
|
|
8
|
+
description: "Calculate card draw probabilities.",
|
|
9
|
+
startUrl: "/en/utilities/categories/tabletop/card-draw-odds-calculator/",
|
|
10
|
+
englishSlug: "card-draw-odds-calculator",
|
|
11
|
+
});
|
|
12
|
+
const manifest = await response.json() as {
|
|
13
|
+
name: string;
|
|
14
|
+
start_url: string;
|
|
15
|
+
scope: string;
|
|
16
|
+
icons: { src: string; sizes: string; type: string; purpose: string }[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
expect(response.headers.get("Content-Type")).toContain("application/manifest+json");
|
|
20
|
+
expect(response.headers.get("Cache-Control")).toContain("immutable");
|
|
21
|
+
expect(manifest.name).toBe("Card draw odds calculator");
|
|
22
|
+
expect(manifest.start_url).toBe(manifest.scope);
|
|
23
|
+
expect(manifest.icons[0]?.src).toContain("/_utilities/tabletop/images/card-draw-odds-calculator.webp?version=");
|
|
24
|
+
expect(manifest.icons[0]?.sizes).toBe("512x512");
|
|
25
|
+
expect(manifest.icons[0]?.type).toBe("image/webp");
|
|
26
|
+
expect(manifest.icons[0]?.purpose).toBe("any");
|
|
27
|
+
});
|
|
28
|
+
});
|