@jjlmoya/utils-travel 1.20.0 → 1.22.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 +2 -2
- package/src/category/i18n/es.ts +2 -2
- package/src/category/i18n/fr.ts +2 -2
- package/src/category/i18n/pl.ts +1 -1
- package/src/category/i18n/pt.ts +1 -1
- package/src/category/i18n/ru.ts +1 -1
- package/src/category/index.ts +24 -24
- package/src/category/seo.astro +1 -1
- package/src/components/PreviewNavSidebar.astro +1 -1
- package/src/components/PreviewToolbar.astro +1 -1
- package/src/data.ts +1 -1
- package/src/env.d.ts +1 -1
- package/src/pages/[locale]/[slug].astro +164 -164
- package/src/pages/[locale].astro +278 -278
- package/src/pages/index.astro +1 -1
- package/src/tests/bibliography_wellformed_export.test.ts +46 -0
- package/src/tests/category_seo_quality.test.ts +74 -0
- package/src/tests/faq_count.test.ts +8 -8
- package/src/tests/mocks/astro_mock.js +1 -1
- package/src/tests/no_h1_in_components.test.ts +1 -1
- package/src/tests/seo_length.test.ts +8 -8
- package/src/tests/seo_parity.test.ts +2 -2
- package/src/tests/seo_translation_completeness.test.ts +18 -12
- package/src/tests/seo_wellformed_export.test.ts +65 -0
- package/src/tests/spanish_leakage.test.ts +175 -175
- package/src/tool/luggage-calculator/component.astro +181 -181
- package/src/tool/tip-calculator/i18n/ja.ts +5 -1
- package/src/tool/tip-calculator/i18n/ko.ts +5 -1
- package/src/tool/tip-calculator/i18n/sv.ts +5 -1
- package/src/tool/tip-calculator/i18n/tr.ts +5 -1
- package/src/tool/tip-calculator/i18n/zh.ts +5 -1
- package/src/tool/trip-expense-splitter/seo.astro +11 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jjlmoya/utils-travel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"lint": "eslint src/ --max-warnings 0 && stylelint \"src/**/*.{css,astro}\"",
|
|
28
28
|
"check": "astro check",
|
|
29
29
|
"type-check": "astro check",
|
|
30
|
-
"test": "vitest run",
|
|
30
|
+
"test": "vitest run --reporter=verbose --testTimeout=30000",
|
|
31
31
|
"preversion": "npm run lint && npm run test && npm run build",
|
|
32
32
|
"postversion": "git push && git push --tags",
|
|
33
33
|
"patch": "npm version patch",
|
package/src/category/i18n/es.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { CategoryLocaleContent } from '../../types';
|
|
|
3
3
|
export const content: CategoryLocaleContent = {
|
|
4
4
|
slug: 'viajes',
|
|
5
5
|
title: 'Herramientas y Calculadoras para Viajeros y Aventureros',
|
|
6
|
-
description: '
|
|
6
|
+
description: 'Herramientas gratuitas para equipaje, propinas internacionales, listas de maleta y retos diarios de viaje.',
|
|
7
7
|
seo: [
|
|
8
8
|
{
|
|
9
9
|
type: 'title',
|
|
@@ -184,4 +184,4 @@ export const content: CategoryLocaleContent = {
|
|
|
184
184
|
],
|
|
185
185
|
};
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
|
package/src/category/i18n/fr.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { CategoryLocaleContent } from '../../types';
|
|
|
3
3
|
export const content: CategoryLocaleContent = {
|
|
4
4
|
slug: 'voyages',
|
|
5
5
|
title: 'Outils et Calculateurs pour Voyageurs et Aventuriers',
|
|
6
|
-
description: '
|
|
6
|
+
description: 'Outils gratuits pour bagages, pourboires internationaux, listes de valise et défis quotidiens de voyage.',
|
|
7
7
|
seo: [
|
|
8
8
|
{
|
|
9
9
|
type: 'title',
|
|
@@ -59,7 +59,7 @@ export const content: CategoryLocaleContent = {
|
|
|
59
59
|
items: [
|
|
60
60
|
'<strong>Économie sur le Vol :</strong> Évitez les frais supplémentaires pour excès de bagages grâce à une planification technique.',
|
|
61
61
|
'<strong>Conscience Culturelle :</strong> Naviguez dans des environnements internationaux avec la confiance de connaître les normes locales.',
|
|
62
|
-
'<strong>Voyages Sûrs :</strong> Des checklists
|
|
62
|
+
'<strong>Voyages Sûrs :</strong> Des checklists pour organiser vos documents et médicaments nécessaires.',
|
|
63
63
|
'<strong>Gamification de l\'Exploration :</strong> Transformez vos trajets quotidiens en petites expéditions de découverte.',
|
|
64
64
|
],
|
|
65
65
|
},
|
package/src/category/i18n/pl.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { CategoryLocaleContent } from '../../types';
|
|
|
3
3
|
export const content: CategoryLocaleContent = {
|
|
4
4
|
slug: 'podroze',
|
|
5
5
|
title: 'Narzędzia i kalkulatory dla podróżników i poszukiwaczy przygód',
|
|
6
|
-
description: '
|
|
6
|
+
description: 'Darmowe narzędzia do bagażu, międzynarodowych napiwków, list pakowania i codziennych wyzwań podróżnych.',
|
|
7
7
|
seo: [
|
|
8
8
|
{
|
|
9
9
|
type: 'title',
|
package/src/category/i18n/pt.ts
CHANGED
|
@@ -59,7 +59,7 @@ export const content: CategoryLocaleContent = {
|
|
|
59
59
|
items: [
|
|
60
60
|
'<strong>Poupança em Voos:</strong> Evite custos extra por excesso de bagagem ou malas fora das medidas através de um planeamento técnico.',
|
|
61
61
|
'<strong>Consciência Cultural:</strong> Navegue em ambientes internacionais com a confiança de conhecer as normas de etiqueta locais.',
|
|
62
|
-
'<strong>Viagens Seguras:</strong> Checklists
|
|
62
|
+
'<strong>Viagens Seguras:</strong> Checklists para organizar a documentação e a medicação necessária.',
|
|
63
63
|
'<strong>Gamificação da Exploração:</strong> Transforme os seus trajetos diários em pequenas expedições de descoberta.',
|
|
64
64
|
],
|
|
65
65
|
},
|
package/src/category/i18n/ru.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { CategoryLocaleContent } from '../../types';
|
|
|
3
3
|
export const content: CategoryLocaleContent = {
|
|
4
4
|
slug: 'puteshestviya',
|
|
5
5
|
title: 'Инструменты и калькуляторы для путешественников и авантюристов',
|
|
6
|
-
description: '
|
|
6
|
+
description: 'Бесплатные инструменты для багажа, международных чаевых, списков вещей и ежедневных заданий путешественника.',
|
|
7
7
|
seo: [
|
|
8
8
|
{
|
|
9
9
|
type: 'title',
|
package/src/category/index.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import type { TravelCategoryEntry } from '../types';
|
|
2
|
-
|
|
3
|
-
export const travelCategory: TravelCategoryEntry = {
|
|
4
|
-
icon: 'mdi:map-marker-path',
|
|
5
|
-
tools: [],
|
|
6
|
-
i18n: {
|
|
7
|
-
de: () => import('./i18n/de').then((m) => m.content),
|
|
8
|
-
en: () => import('./i18n/en').then((m) => m.content),
|
|
9
|
-
es: () => import('./i18n/es').then((m) => m.content),
|
|
10
|
-
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
11
|
-
id: () => import('./i18n/id').then((m) => m.content),
|
|
12
|
-
it: () => import('./i18n/it').then((m) => m.content),
|
|
13
|
-
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
14
|
-
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
15
|
-
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
16
|
-
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
17
|
-
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
18
|
-
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
19
|
-
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
20
|
-
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
21
|
-
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
|
|
1
|
+
import type { TravelCategoryEntry } from '../types';
|
|
2
|
+
|
|
3
|
+
export const travelCategory: TravelCategoryEntry = {
|
|
4
|
+
icon: 'mdi:map-marker-path',
|
|
5
|
+
tools: [],
|
|
6
|
+
i18n: {
|
|
7
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
8
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
9
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
10
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
11
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
12
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
13
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
14
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
15
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
16
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
17
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
18
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
19
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
20
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
21
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
package/src/category/seo.astro
CHANGED
package/src/data.ts
CHANGED
package/src/env.d.ts
CHANGED
|
@@ -1,164 +1,164 @@
|
|
|
1
|
-
---
|
|
2
|
-
import PreviewLayout from "../../layouts/PreviewLayout.astro";
|
|
3
|
-
import PreviewNavSidebar from "../../components/PreviewNavSidebar.astro";
|
|
4
|
-
import { ALL_TOOLS } from "../../index";
|
|
5
|
-
import {
|
|
6
|
-
UtilityHeader,
|
|
7
|
-
FAQSection,
|
|
8
|
-
Bibliography,
|
|
9
|
-
SEORenderer,
|
|
10
|
-
} from "@jjlmoya/utils-shared";
|
|
11
|
-
import type { KnownLocale, ToolLocaleContent } from "../../types";
|
|
12
|
-
import type { UtilitySEOContent } from "@jjlmoya/utils-shared";
|
|
13
|
-
|
|
14
|
-
export async function getStaticPaths() {
|
|
15
|
-
const paths = [];
|
|
16
|
-
|
|
17
|
-
for (const { entry, Component: lazyComp } of ALL_TOOLS) {
|
|
18
|
-
const { default: Component } = (await lazyComp()) as { default: unknown };
|
|
19
|
-
const localeEntries = Object.entries(entry.i18n) as [
|
|
20
|
-
KnownLocale,
|
|
21
|
-
() => Promise<ToolLocaleContent>,
|
|
22
|
-
][];
|
|
23
|
-
const localeContents = await Promise.all(
|
|
24
|
-
localeEntries.map(async ([locale, loader]) => ({
|
|
25
|
-
locale,
|
|
26
|
-
content: await loader(),
|
|
27
|
-
})),
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
const localeUrls = Object.fromEntries(
|
|
31
|
-
localeContents.map(({ locale, content }) => [
|
|
32
|
-
locale,
|
|
33
|
-
`/${locale}/${content.slug}`,
|
|
34
|
-
]),
|
|
35
|
-
) as Partial<Record<KnownLocale, string>>;
|
|
36
|
-
|
|
37
|
-
const firstLoader = entry.i18n.en ?? Object.values(entry.i18n)[0];
|
|
38
|
-
const englishSlug = firstLoader ? (await firstLoader()).slug : entry.id;
|
|
39
|
-
|
|
40
|
-
for (const { locale, content } of localeContents) {
|
|
41
|
-
const allToolsNav = (
|
|
42
|
-
await Promise.all(
|
|
43
|
-
ALL_TOOLS.map(async ({ entry: navEntry }) => {
|
|
44
|
-
const loader = navEntry.i18n[locale] ?? navEntry.i18n.en;
|
|
45
|
-
if (!loader) return null;
|
|
46
|
-
const navContent = await loader();
|
|
47
|
-
return {
|
|
48
|
-
id: navEntry.id,
|
|
49
|
-
title: navContent.title,
|
|
50
|
-
href: `/${locale}/${navContent.slug}`,
|
|
51
|
-
isActive: navEntry.id === entry.id,
|
|
52
|
-
};
|
|
53
|
-
}),
|
|
54
|
-
)
|
|
55
|
-
).filter(Boolean) as NavItem[];
|
|
56
|
-
paths.push({
|
|
57
|
-
params: { locale, slug: content.slug },
|
|
58
|
-
props: { Component, locale, content, localeUrls, allToolsNav, englishSlug },
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return paths;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
interface NavItem {
|
|
67
|
-
id: string;
|
|
68
|
-
title: string;
|
|
69
|
-
href: string;
|
|
70
|
-
isActive?: boolean;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
interface Props {
|
|
74
|
-
Component: ToolComponent;
|
|
75
|
-
locale: KnownLocale;
|
|
76
|
-
content: ToolLocaleContent;
|
|
77
|
-
localeUrls: Partial<Record<KnownLocale, string>>;
|
|
78
|
-
allToolsNav: NavItem[];
|
|
79
|
-
englishSlug: string;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
type ToolComponent = (props: { ui: Record<string, unknown> }) => unknown;
|
|
83
|
-
|
|
84
|
-
const { Component, locale, content, localeUrls, allToolsNav, englishSlug } = Astro.props as Props;
|
|
85
|
-
|
|
86
|
-
// eslint-disable-next-line custom/no-css-comments
|
|
87
|
-
const cssFiles = import.meta.glob("../../tool/**/*.css", { query: "?raw", import: "default" });
|
|
88
|
-
const cssKey = Object.keys(cssFiles).find((k) => k.endsWith(`/${englishSlug}.css`));
|
|
89
|
-
const toolCss = cssKey ? await cssFiles[cssKey]() as string : "";
|
|
90
|
-
|
|
91
|
-
const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
|
|
92
|
-
|
|
93
|
-
const words = content.title.split(" ");
|
|
94
|
-
const titleHighlight = words[0] || "";
|
|
95
|
-
const titleBase = words.slice(1).join(" ") || "";
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
<PreviewLayout
|
|
99
|
-
title={content.title}
|
|
100
|
-
currentLocale={locale}
|
|
101
|
-
localeUrls={localeUrls}
|
|
102
|
-
hasSidebar={true}
|
|
103
|
-
>
|
|
104
|
-
<PreviewNavSidebar
|
|
105
|
-
slot="sidebar"
|
|
106
|
-
categoryTitle="Tools"
|
|
107
|
-
tools={allToolsNav}
|
|
108
|
-
/>
|
|
109
|
-
<Fragment slot="head">
|
|
110
|
-
{toolCss ? <Fragment set:html={`<style is:inline>${toolCss}</style>`} /> : null}
|
|
111
|
-
{
|
|
112
|
-
( content.schemas ?? []).map((schema: unknown) => (
|
|
113
|
-
<script
|
|
114
|
-
is:inline
|
|
115
|
-
type="application/ld+json"
|
|
116
|
-
set:html={JSON.stringify(schema)}
|
|
117
|
-
/>
|
|
118
|
-
))
|
|
119
|
-
}
|
|
120
|
-
</Fragment>
|
|
121
|
-
|
|
122
|
-
<div class="tool-page">
|
|
123
|
-
<UtilityHeader
|
|
124
|
-
titleHighlight={titleHighlight}
|
|
125
|
-
titleBase={titleBase}
|
|
126
|
-
description={content.description}
|
|
127
|
-
/>
|
|
128
|
-
|
|
129
|
-
<section class="section-tool">
|
|
130
|
-
<Component ui={content.ui} />
|
|
131
|
-
</section>
|
|
132
|
-
|
|
133
|
-
<section class="section-seo">
|
|
134
|
-
<SEORenderer content={seoContent} />
|
|
135
|
-
</section>
|
|
136
|
-
|
|
137
|
-
<section class="section-faq">
|
|
138
|
-
<FAQSection items={content.faq} />
|
|
139
|
-
</section>
|
|
140
|
-
|
|
141
|
-
<section class="section-bibliography">
|
|
142
|
-
<Bibliography links={content.bibliography} />
|
|
143
|
-
</section>
|
|
144
|
-
</div>
|
|
145
|
-
</PreviewLayout>
|
|
146
|
-
|
|
147
|
-
<style>
|
|
148
|
-
.tool-page {
|
|
149
|
-
display: flex;
|
|
150
|
-
flex-direction: column;
|
|
151
|
-
gap: 2rem;
|
|
152
|
-
}
|
|
153
|
-
.section-tool {
|
|
154
|
-
max-width: 1200px;
|
|
155
|
-
margin: 0 auto;
|
|
156
|
-
width: 100%;
|
|
157
|
-
}
|
|
158
|
-
.section-seo,
|
|
159
|
-
.section-faq,
|
|
160
|
-
.section-bibliography {
|
|
161
|
-
padding-top: 2rem;
|
|
162
|
-
border-top: 1px solid var(--border-color);
|
|
163
|
-
}
|
|
164
|
-
</style>
|
|
1
|
+
---
|
|
2
|
+
import PreviewLayout from "../../layouts/PreviewLayout.astro";
|
|
3
|
+
import PreviewNavSidebar from "../../components/PreviewNavSidebar.astro";
|
|
4
|
+
import { ALL_TOOLS } from "../../index";
|
|
5
|
+
import {
|
|
6
|
+
UtilityHeader,
|
|
7
|
+
FAQSection,
|
|
8
|
+
Bibliography,
|
|
9
|
+
SEORenderer,
|
|
10
|
+
} from "@jjlmoya/utils-shared";
|
|
11
|
+
import type { KnownLocale, ToolLocaleContent } from "../../types";
|
|
12
|
+
import type { UtilitySEOContent } from "@jjlmoya/utils-shared";
|
|
13
|
+
|
|
14
|
+
export async function getStaticPaths() {
|
|
15
|
+
const paths = [];
|
|
16
|
+
|
|
17
|
+
for (const { entry, Component: lazyComp } of ALL_TOOLS) {
|
|
18
|
+
const { default: Component } = (await lazyComp()) as { default: unknown };
|
|
19
|
+
const localeEntries = Object.entries(entry.i18n) as [
|
|
20
|
+
KnownLocale,
|
|
21
|
+
() => Promise<ToolLocaleContent>,
|
|
22
|
+
][];
|
|
23
|
+
const localeContents = await Promise.all(
|
|
24
|
+
localeEntries.map(async ([locale, loader]) => ({
|
|
25
|
+
locale,
|
|
26
|
+
content: await loader(),
|
|
27
|
+
})),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const localeUrls = Object.fromEntries(
|
|
31
|
+
localeContents.map(({ locale, content }) => [
|
|
32
|
+
locale,
|
|
33
|
+
`/${locale}/${content.slug}`,
|
|
34
|
+
]),
|
|
35
|
+
) as Partial<Record<KnownLocale, string>>;
|
|
36
|
+
|
|
37
|
+
const firstLoader = entry.i18n.en ?? Object.values(entry.i18n)[0];
|
|
38
|
+
const englishSlug = firstLoader ? (await firstLoader()).slug : entry.id;
|
|
39
|
+
|
|
40
|
+
for (const { locale, content } of localeContents) {
|
|
41
|
+
const allToolsNav = (
|
|
42
|
+
await Promise.all(
|
|
43
|
+
ALL_TOOLS.map(async ({ entry: navEntry }) => {
|
|
44
|
+
const loader = navEntry.i18n[locale] ?? navEntry.i18n.en;
|
|
45
|
+
if (!loader) return null;
|
|
46
|
+
const navContent = await loader();
|
|
47
|
+
return {
|
|
48
|
+
id: navEntry.id,
|
|
49
|
+
title: navContent.title,
|
|
50
|
+
href: `/${locale}/${navContent.slug}`,
|
|
51
|
+
isActive: navEntry.id === entry.id,
|
|
52
|
+
};
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
55
|
+
).filter(Boolean) as NavItem[];
|
|
56
|
+
paths.push({
|
|
57
|
+
params: { locale, slug: content.slug },
|
|
58
|
+
props: { Component, locale, content, localeUrls, allToolsNav, englishSlug },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return paths;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface NavItem {
|
|
67
|
+
id: string;
|
|
68
|
+
title: string;
|
|
69
|
+
href: string;
|
|
70
|
+
isActive?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface Props {
|
|
74
|
+
Component: ToolComponent;
|
|
75
|
+
locale: KnownLocale;
|
|
76
|
+
content: ToolLocaleContent;
|
|
77
|
+
localeUrls: Partial<Record<KnownLocale, string>>;
|
|
78
|
+
allToolsNav: NavItem[];
|
|
79
|
+
englishSlug: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type ToolComponent = (props: { ui: Record<string, unknown> }) => unknown;
|
|
83
|
+
|
|
84
|
+
const { Component, locale, content, localeUrls, allToolsNav, englishSlug } = Astro.props as Props;
|
|
85
|
+
|
|
86
|
+
// eslint-disable-next-line custom/no-css-comments
|
|
87
|
+
const cssFiles = import.meta.glob("../../tool/**/*.css", { query: "?raw", import: "default" });
|
|
88
|
+
const cssKey = Object.keys(cssFiles).find((k) => k.endsWith(`/${englishSlug}.css`));
|
|
89
|
+
const toolCss = cssKey ? await cssFiles[cssKey]() as string : "";
|
|
90
|
+
|
|
91
|
+
const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
|
|
92
|
+
|
|
93
|
+
const words = content.title.split(" ");
|
|
94
|
+
const titleHighlight = words[0] || "";
|
|
95
|
+
const titleBase = words.slice(1).join(" ") || "";
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
<PreviewLayout
|
|
99
|
+
title={content.title}
|
|
100
|
+
currentLocale={locale}
|
|
101
|
+
localeUrls={localeUrls}
|
|
102
|
+
hasSidebar={true}
|
|
103
|
+
>
|
|
104
|
+
<PreviewNavSidebar
|
|
105
|
+
slot="sidebar"
|
|
106
|
+
categoryTitle="Tools"
|
|
107
|
+
tools={allToolsNav}
|
|
108
|
+
/>
|
|
109
|
+
<Fragment slot="head">
|
|
110
|
+
{toolCss ? <Fragment set:html={`<style is:inline>${toolCss}</style>`} /> : null}
|
|
111
|
+
{
|
|
112
|
+
( content.schemas ?? []).map((schema: unknown) => (
|
|
113
|
+
<script
|
|
114
|
+
is:inline
|
|
115
|
+
type="application/ld+json"
|
|
116
|
+
set:html={JSON.stringify(schema)}
|
|
117
|
+
/>
|
|
118
|
+
))
|
|
119
|
+
}
|
|
120
|
+
</Fragment>
|
|
121
|
+
|
|
122
|
+
<div class="tool-page">
|
|
123
|
+
<UtilityHeader
|
|
124
|
+
titleHighlight={titleHighlight}
|
|
125
|
+
titleBase={titleBase}
|
|
126
|
+
description={content.description}
|
|
127
|
+
/>
|
|
128
|
+
|
|
129
|
+
<section class="section-tool">
|
|
130
|
+
<Component ui={content.ui} />
|
|
131
|
+
</section>
|
|
132
|
+
|
|
133
|
+
<section class="section-seo">
|
|
134
|
+
<SEORenderer content={seoContent} />
|
|
135
|
+
</section>
|
|
136
|
+
|
|
137
|
+
<section class="section-faq">
|
|
138
|
+
<FAQSection items={content.faq} />
|
|
139
|
+
</section>
|
|
140
|
+
|
|
141
|
+
<section class="section-bibliography">
|
|
142
|
+
<Bibliography links={content.bibliography} />
|
|
143
|
+
</section>
|
|
144
|
+
</div>
|
|
145
|
+
</PreviewLayout>
|
|
146
|
+
|
|
147
|
+
<style>
|
|
148
|
+
.tool-page {
|
|
149
|
+
display: flex;
|
|
150
|
+
flex-direction: column;
|
|
151
|
+
gap: 2rem;
|
|
152
|
+
}
|
|
153
|
+
.section-tool {
|
|
154
|
+
max-width: 1200px;
|
|
155
|
+
margin: 0 auto;
|
|
156
|
+
width: 100%;
|
|
157
|
+
}
|
|
158
|
+
.section-seo,
|
|
159
|
+
.section-faq,
|
|
160
|
+
.section-bibliography {
|
|
161
|
+
padding-top: 2rem;
|
|
162
|
+
border-top: 1px solid var(--border-color);
|
|
163
|
+
}
|
|
164
|
+
</style>
|