@jjlmoya/utils-aquarium 1.61.0 → 1.63.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.
@@ -0,0 +1,49 @@
1
+ import type { APIRoute } from "astro";
2
+ import { ALL_TOOLS, aquariumCategory } 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 aquariumCategory.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,100 @@
1
+ ---
2
+ import ProductionUtilityPage from "../../../../../layouts/ProductionUtilityPage.astro";
3
+ import { ALL_TOOLS, aquariumCategory } 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 { getCategoryNamespace, getCategoryPath, getUtilityNamespace, 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 paths = [];
15
+ for (const { entry, Component: componentLoader, SEOComponent: seoLoader, BibliographyComponent: bibliographyLoader } of ALL_TOOLS) {
16
+ const contents = Object.fromEntries(await Promise.all(LANGUAGE_CODES.map(async (language) => {
17
+ const loader = entry.i18n[language] ?? entry.i18n.en;
18
+ if (!loader) throw new Error(`Missing ${language} locale for ${entry.id}`);
19
+ return [language, await loader()];
20
+ }))) as Record<Language, LoadedTool>;
21
+ const categories = Object.fromEntries(await Promise.all(LANGUAGE_CODES.map(async (language) => [
22
+ language,
23
+ await aquariumCategory.i18n[language]!(),
24
+ ]))) as Record<Language, CategoryLocaleContent>;
25
+ const { default: Component } = await (componentLoader as Loader<AstroComponentFactory>)();
26
+ const { default: SEOComponent } = await (seoLoader as Loader<AstroComponentFactory>)();
27
+ const { default: BibliographyComponent } = await (bibliographyLoader as Loader<AstroComponentFactory>)();
28
+ for (const locale of LANGUAGE_CODES.filter((candidate) => candidate !== "es")) {
29
+ const content = contents[locale];
30
+ const category = categories[locale];
31
+ paths.push({
32
+ params: {
33
+ locale,
34
+ utilities: getUtilityNamespace(locale),
35
+ categories: getCategoryNamespace(locale),
36
+ category: category.slug,
37
+ slug: content.slug,
38
+ },
39
+ props: {
40
+ locale,
41
+ content,
42
+ englishSlug: contents.en.slug,
43
+ category,
44
+ categorySlug: category.slug,
45
+ Component,
46
+ SEOComponent,
47
+ BibliographyComponent,
48
+ alternates: LANGUAGE_CODES.map((language) => ({
49
+ language,
50
+ url: getUtilityPath(language, categories[language].slug, contents[language].slug),
51
+ })),
52
+ image: getUtilityOgImage(contents.en.slug),
53
+ },
54
+ });
55
+ }
56
+ }
57
+ return paths;
58
+ }
59
+
60
+ const { locale, content, englishSlug, category, categorySlug, Component, SEOComponent, BibliographyComponent, relatedTools, alternates, image } = Astro.props as {
61
+ locale: Exclude<Language, "es">;
62
+ content: LoadedTool;
63
+ englishSlug: string;
64
+ category: CategoryLocaleContent;
65
+ categorySlug: string;
66
+ Component: AstroComponentFactory;
67
+ SEOComponent: AstroComponentFactory;
68
+ BibliographyComponent: AstroComponentFactory;
69
+ relatedTools?: { icon: string; title: string; description: string; href: string }[];
70
+ alternates: { language: Language; url: string }[];
71
+ image?: string;
72
+ };
73
+ const related = relatedTools ?? (await Promise.all(ALL_TOOLS.map(async ({ entry: relatedEntry }) => {
74
+ const loader = relatedEntry.i18n[locale] ?? relatedEntry.i18n.en;
75
+ if (!loader) return null;
76
+ const relatedContent = await loader();
77
+ if (relatedContent.slug === content.slug) return null;
78
+ return {
79
+ icon: relatedEntry.icons.fg,
80
+ title: relatedContent.title,
81
+ description: relatedContent.description,
82
+ href: getUtilityPath(locale, categorySlug, relatedContent.slug),
83
+ };
84
+ }))).filter((tool): tool is { icon: string; title: string; description: string; href: string } => tool !== null);
85
+ ---
86
+
87
+ <ProductionUtilityPage
88
+ {locale}
89
+ {content}
90
+ {englishSlug}
91
+ categoryTitle={category.title}
92
+ {categorySlug}
93
+ categoryHref={getCategoryPath(locale, categorySlug)}
94
+ relatedTools={related}
95
+ {Component}
96
+ {SEOComponent}
97
+ {BibliographyComponent}
98
+ {alternates}
99
+ {image}
100
+ />
@@ -0,0 +1,44 @@
1
+ ---
2
+ import ProductionCategoryPage from "../../../../layouts/ProductionCategoryPage.astro";
3
+ import { ALL_TOOLS, aquariumCategory } 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 aquariumCategory.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 aquariumCategory.i18n)[Language]>>>;
28
+ alternates: { language: Language; url: string }[];
29
+ };
30
+ const tools = await Promise.all(ALL_TOOLS.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} />
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  ---
3
- <meta http-equiv="refresh" content="0;url=/es" />
3
+ <meta http-equiv="refresh" content="0;url=/utilidades/categorias/acuarios/" />
4
4
 
@@ -0,0 +1,28 @@
1
+ import type { APIRoute } from "astro";
2
+ import { ALL_TOOLS, aquariumCategory } 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 aquariumCategory.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,90 @@
1
+ ---
2
+ import ProductionUtilityPage from "../../layouts/ProductionUtilityPage.astro";
3
+ import { ALL_TOOLS, aquariumCategory } 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 aquariumCategory.i18n.es!();
15
+ const categories = Object.fromEntries(await Promise.all(LANGUAGE_CODES.map(async (language) => [
16
+ language,
17
+ await aquariumCategory.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_TOOLS, aquariumCategory } 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 aquariumCategory.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 aquariumCategory.i18n)[Language]>>>;
22
+ alternates: { language: Language; url: string }[];
23
+ };
24
+ const tools = await Promise.all(ALL_TOOLS.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,11 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { getCacheControl, LONG_LIVED_ASSET_CACHE } from "../worker";
3
+
4
+ describe("MFE cache contract", () => {
5
+ it("caches versioned assets and manifests for one year", () => {
6
+ expect(getCacheControl("/_utilities/aquarium/images/tool.webp")).toBe(LONG_LIVED_ASSET_CACHE);
7
+ expect(getCacheControl("/_utilities/aquarium/styles/tool.css")).toBe(LONG_LIVED_ASSET_CACHE);
8
+ expect(getCacheControl("/en/utilities/categories/aquarium/tool/manifest.json")).toBe(LONG_LIVED_ASSET_CACHE);
9
+ });
10
+
11
+ });
@@ -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/aquarium/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/aquarium/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
+ });
package/src/types.ts CHANGED
@@ -1,18 +1,16 @@
1
1
  import type { SEOSection } from '@jjlmoya/utils-shared';
2
+ import type { UtilityLocale } from '@jjlmoya/utils-shared/routing';
2
3
  import type { WithContext, Thing } from 'schema-dts';
3
4
 
4
5
  export type { SEOSection };
5
6
 
6
- export type KnownLocale =
7
- | 'ar' | 'da' | 'de' | 'en' | 'es' | 'fi'
8
- | 'fr' | 'id' | 'it' | 'ja' | 'ko' | 'nb' | 'nl'
9
- | 'pl' | 'pt' | 'ru' | 'sv' | 'tr' | 'zh';
7
+ export type KnownLocale = UtilityLocale;
10
8
 
11
9
  export interface FAQItem { question: string; answer: string; }
12
10
  export interface BibliographyEntry { name: string; url: string; }
13
11
  export interface HowToStep { name: string; text: string; }
14
12
 
15
- export interface ToolLocaleContent<TUI extends Record<string, string> = Record<string, string>> {
13
+ export interface ToolLocaleContent<TUI extends object = Record<string, string>> {
16
14
  slug: string;
17
15
  title: string;
18
16
  description: string;
@@ -34,7 +32,7 @@ export interface CategoryLocaleContent {
34
32
  export type LocaleLoader<T> = () => Promise<T>;
35
33
  export type LocaleMap<T> = Partial<Record<KnownLocale, LocaleLoader<T>>>;
36
34
 
37
- export interface AquariumToolEntry<TUI extends Record<string, string> = Record<string, string>> {
35
+ export interface AquariumToolEntry<TUI extends object = Record<string, string>> {
38
36
  id: string;
39
37
  icons: { bg: string; fg: string; };
40
38
  i18n: LocaleMap<ToolLocaleContent<TUI>>;
package/src/worker.ts ADDED
@@ -0,0 +1,25 @@
1
+ interface UtilityMfeEnvironment {
2
+ ASSETS: { fetch(request: Request): Promise<Response> };
3
+ }
4
+
5
+ export const LONG_LIVED_ASSET_CACHE = "public, max-age=31536000, immutable";
6
+
7
+ export const getCacheControl = (pathname: string): string | undefined => {
8
+ if (pathname.endsWith("/manifest.json")) return LONG_LIVED_ASSET_CACHE;
9
+ if (pathname.startsWith("/_utilities/")) {
10
+ return LONG_LIVED_ASSET_CACHE;
11
+ }
12
+ return undefined;
13
+ };
14
+
15
+ export default {
16
+ async fetch(request: Request, environment: UtilityMfeEnvironment): Promise<Response> {
17
+ const response = await environment.ASSETS.fetch(request);
18
+ const cacheControl = getCacheControl(new URL(request.url).pathname);
19
+ if (!cacheControl) return response;
20
+
21
+ const headers = new Headers(response.headers);
22
+ headers.set("Cache-Control", cacheControl);
23
+ return new Response(response.body, { status: response.status, headers });
24
+ },
25
+ };
@@ -1,65 +0,0 @@
1
- ---
2
- import PreviewLayout from '../../layouts/PreviewLayout.astro';
3
- import PreviewNavSidebar from '../../components/PreviewNavSidebar.astro';
4
- import { ALL_TOOLS } from '../../index';
5
- import { UtilityHeader, FAQSection, Bibliography, SEORenderer } from '@jjlmoya/utils-shared';
6
- import type { KnownLocale, ToolLocaleContent } from '../../types';
7
- import type { UtilitySEOContent } from '@jjlmoya/utils-shared';
8
-
9
- export async function getStaticPaths() {
10
- const paths = [];
11
- for (const { entry, Component: lazyComponent } of ALL_TOOLS) {
12
- const { default: Component } = await lazyComponent();
13
- const localeContents = await Promise.all(Object.entries(entry.i18n).map(async ([locale, loader]) => ({
14
- locale: locale as KnownLocale,
15
- content: await loader(),
16
- })));
17
- const localeUrls = Object.fromEntries(localeContents.map(({ locale, content }) => [locale, '/' + locale + '/' + content.slug]));
18
- for (const { locale, content } of localeContents) {
19
- const allToolsNav = (await Promise.all(ALL_TOOLS.map(async ({ entry: navEntry }) => {
20
- const loader = navEntry.i18n[locale] ?? navEntry.i18n.en;
21
- if (loader === undefined) return null;
22
- const navContent = await loader();
23
- return { id: navEntry.id, title: navContent.title, href: '/' + locale + '/' + navContent.slug, isActive: navEntry.id === entry.id };
24
- }))).filter((item): item is { id: string; title: string; href: string; isActive: boolean } => item !== null);
25
- const englishLoader = entry.i18n.en ?? Object.values(entry.i18n)[0];
26
- const englishSlug = englishLoader === undefined ? entry.id : (await englishLoader()).slug;
27
- paths.push({ params: { locale, slug: content.slug }, props: { Component, locale, content, localeUrls, allToolsNav, englishSlug } });
28
- }
29
- }
30
- return paths;
31
- }
32
-
33
- const { Component, locale, content, localeUrls, allToolsNav, englishSlug } = Astro.props as {
34
- Component: (_props: Record<string, unknown>) => unknown;
35
- locale: KnownLocale;
36
- content: ToolLocaleContent;
37
- localeUrls: Partial<Record<KnownLocale, string>>;
38
- allToolsNav: { id: string; title: string; href: string; isActive?: boolean }[];
39
- englishSlug: string;
40
- };
41
-
42
- const cssFiles = import.meta.glob('../../tool/**' + '/' + '*.css', { query: '?raw', import: 'default' });
43
- const cssKey = Object.keys(cssFiles).find((key) => key.endsWith('/' + englishSlug + '.css'));
44
- const cssLoader = cssKey === undefined ? undefined : cssFiles[cssKey];
45
- const toolCss = cssLoader === undefined ? '' : await cssLoader() as string;
46
- const seoContent: UtilitySEOContent = { locale, sections: content.seo };
47
- const words = content.title.split(' ');
48
- const titleHighlight = words[0] ?? '';
49
- const titleBase = words.slice(1).join(' ');
50
- ---
51
-
52
- <PreviewLayout title={content.title} currentLocale={locale} localeUrls={localeUrls} hasSidebar={true}>
53
- <PreviewNavSidebar slot="sidebar" categoryTitle="Aquarium tools" tools={allToolsNav} />
54
- <Fragment slot="head">
55
- {toolCss !== '' && <Fragment set:html={'<style is:inline>' + toolCss + '</style>'} />}
56
- {content.schemas.map((schema: unknown) => <script is:inline type="application/ld+json" set:html={JSON.stringify(schema)} />)}
57
- </Fragment>
58
- <div class="tool-page">
59
- <UtilityHeader titleHighlight={titleHighlight} titleBase={titleBase} description={content.description} />
60
- <section class="section-tool"><Component ui={content.ui} /></section>
61
- <section class="section-seo"><SEORenderer content={seoContent} /></section>
62
- <section class="section-faq"><FAQSection items={content.faq} /></section>
63
- <section class="section-bibliography"><Bibliography links={content.bibliography} /></section>
64
- </div>
65
- </PreviewLayout>
@@ -1,69 +0,0 @@
1
- ---
2
- import PreviewLayout from '../layouts/PreviewLayout.astro';
3
- import PreviewNavSidebar from '../components/PreviewNavSidebar.astro';
4
- import { aquariumCategory, ALL_TOOLS } from '../index';
5
- import { Icon } from 'astro-icon/components';
6
- import type { KnownLocale, ToolLocaleContent } from '../types';
7
-
8
- export async function getStaticPaths() {
9
- const locales = ['en'] as KnownLocale[];
10
- return locales.map((locale) => ({ params: { locale } }));
11
- }
12
-
13
- const currentLocale = (Astro.params.locale ?? 'en') as KnownLocale;
14
- const categoryLoader = aquariumCategory.i18n[currentLocale] ?? aquariumCategory.i18n.en;
15
- const categoryContent = categoryLoader === undefined ? {
16
- slug: 'aquarium',
17
- title: 'Aquarium Tools',
18
- description: 'Aquarium volume tools',
19
- seo: [],
20
- } : await categoryLoader();
21
-
22
- const toolsWithContent = await Promise.all(
23
- ALL_TOOLS.map(async ({ entry, Component }) => {
24
- const localeEntries = await Promise.all(
25
- Object.entries(entry.i18n).map(async ([locale, loader]) => [locale, await loader()] as const),
26
- );
27
- const localeContents = Object.fromEntries(localeEntries) as Record<string, ToolLocaleContent>;
28
- const locale = localeContents[currentLocale] ?? localeContents.en;
29
- const availableLocales: Record<string, string> = {};
30
- for (const [localeCode, content] of Object.entries(localeContents)) {
31
- availableLocales[localeCode] = '/' + localeCode + '/' + content.slug;
32
- }
33
- return { entry, Component, locale, availableLocales };
34
- }),
35
- );
36
- ---
37
-
38
- <PreviewLayout title={categoryContent.title} currentLocale={currentLocale} hasSidebar={true}>
39
- <PreviewNavSidebar
40
- slot="sidebar"
41
- categoryTitle={categoryContent.title}
42
- tools={toolsWithContent.map(({ entry, locale, availableLocales }) => ({
43
- id: entry.id,
44
- title: locale?.title ?? entry.id,
45
- href: availableLocales[currentLocale] ?? '#',
46
- }))}
47
- />
48
- <div class="dashboard">
49
- <header class="preview-header">
50
- <span class="badge">preview · @jjlmoya/utils-aquarium</span>
51
- <h1>{categoryContent.title}</h1>
52
- <p>{categoryContent.description}</p>
53
- </header>
54
- <div class="tool-list">
55
- {toolsWithContent.map(({ entry, locale, availableLocales }) => (
56
- <article class="tool-card">
57
- <a href={availableLocales[currentLocale] ?? '#'} class="tool-card-link">
58
- <div class="tool-icons"><div class="icon-wrapper bg"><Icon name={entry.icons.bg} /></div><div class="icon-wrapper fg"><Icon name={entry.icons.fg} /></div></div>
59
- <div class="tool-card-content"><h2 class="tool-title">{locale?.title}</h2><p class="tool-description">{locale?.description}</p></div>
60
- <div class="tool-card-meta"><span class="tool-id">{entry.id}</span></div>
61
- </a>
62
- <div class="tool-locales">
63
- {Object.entries(availableLocales).map(([localeCode, url]) => <a href={url} class:list={{ active: localeCode === currentLocale }} class="locale-badge">{localeCode.toUpperCase()}</a>)}
64
- </div>
65
- </article>
66
- ))}
67
- </div>
68
- </div>
69
- </PreviewLayout>