libreria-astro-lefebvre 0.1.37 → 0.1.39
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/carbins/Author_2025_Algarve.ts +3 -3
- package/src/carbins/Cabecera_2025_Barcelona.ts +1 -1
- package/src/carbins/SEO_Schema_Page.ts +24 -0
- package/src/carbins/Texto_2026_Alicante.ts +3 -3
- package/src/components/Astro/Author_2025_Algarve.astro +1 -14
- package/src/components/Astro/Cabecera_2025_Barcelona.astro +6 -5
- package/src/components/Astro/Card_2025_Malta.astro +8 -1
- package/src/components/Astro/Contenido_2025_Cordoba.astro +1 -14
- package/src/components/Astro/Contenido_2025_Granada.astro +1 -14
- package/src/components/Astro/Contenido_2026_Cabra.astro +8 -2
- package/src/components/Astro/Contenido_2026_Estocolmo.astro +8 -2
- package/src/components/Astro/Contenido_2026_Jaen.astro +0 -12
- package/src/components/Astro/Contenido_2026_Michigan.astro +7 -1
- package/src/components/Astro/Contenido_2026_Orcasitas.astro +7 -2
- package/src/components/Astro/Contenido_2026_Oslo.astro +8 -0
- package/src/components/Astro/Contenido_2026_Ubeda.astro +8 -1
- package/src/components/Astro/Contenido_2026_Yakarta.astro +7 -2
- package/src/components/Astro/Galeria_2026_Segorbe.astro +7 -2
- package/src/components/Astro/Indice_2025_Taiwan.astro +7 -1
- package/src/components/Astro/SEO_Schema_Page.astro +101 -49
- package/src/components/Astro/Tabla_2026_Cadiz.astro +12 -3
- package/src/components/Astro/Texto_2026_Alicante.astro +1 -13
- package/src/components/Astro/Titulo_2025_Algeciras.astro +1 -14
- package/src/components/Astro/Video_2025_Valencia.astro +7 -2
- package/src/lib/functions.js +14 -0
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@ export const metadata: ComponentMetadata = {
|
|
|
4
4
|
component_name: 'Author_2025_Algarve',
|
|
5
5
|
category: 'Contenido de Imagen',
|
|
6
6
|
name: 'Ficha de autor con imagen y tiempo de lectura 2025',
|
|
7
|
-
description: 'Ficha compacta de autor para cabeceras de artículo: imagen a la izquierda (con border-radius configurable), nombre subrayado y profesión a la derecha, y bloque opcional de "tiempo de lectura" al final.
|
|
7
|
+
description: 'Ficha compacta de autor para cabeceras de artículo: imagen a la izquierda (con border-radius configurable), nombre subrayado y profesión a la derecha, y bloque opcional de "tiempo de lectura" al final. Para la corporativa de Lefebvre',
|
|
8
8
|
framework: 'Astro',
|
|
9
9
|
priority: 1,
|
|
10
10
|
tags: ['autor', 'imagen', 'blog', 'cabecera', 'tiempo'],
|
|
@@ -12,7 +12,7 @@ export const metadata: ComponentMetadata = {
|
|
|
12
12
|
{
|
|
13
13
|
name: 'nameAuthor',
|
|
14
14
|
type: 'text',
|
|
15
|
-
help: 'Nombre del autor. Se renderiza como párrafo subrayado (Inter 16px color #262626) y admite HTML (set:html).
|
|
15
|
+
help: 'Nombre del autor. Se renderiza como párrafo subrayado (Inter 16px color #262626) y admite HTML (set:html). Conviene que ocupe una sola línea',
|
|
16
16
|
label: 'Nombre del autor',
|
|
17
17
|
mandatory: false,
|
|
18
18
|
example_value: 'María García López'
|
|
@@ -28,7 +28,7 @@ export const metadata: ComponentMetadata = {
|
|
|
28
28
|
{
|
|
29
29
|
name: 'descriptionAuthor',
|
|
30
30
|
type: 'text',
|
|
31
|
-
help: 'Profesión o descripción del autor. Se renderiza como párrafo (Inter 14px color #363942) debajo del nombre y admite HTML (set:html).
|
|
31
|
+
help: 'Profesión o descripción del autor. Se renderiza como párrafo (Inter 14px color #363942) debajo del nombre y admite HTML (set:html). Conviene que ocupe una sola línea',
|
|
32
32
|
label: 'Descripción del autor',
|
|
33
33
|
mandatory: false,
|
|
34
34
|
example_value: 'Abogada especialista en Derecho Fiscal con 15 años de experiencia'
|
|
@@ -50,7 +50,7 @@ export const metadata: ComponentMetadata = {
|
|
|
50
50
|
{
|
|
51
51
|
name: 'siteUrl',
|
|
52
52
|
type: 'text',
|
|
53
|
-
help: 'Origen público del sitio (protocolo + host, sin barra final, p. ej. "https://lefebvre.es"). Se usa para las URLs absolutas del BreadcrumbList en el structured data SEO. Necesario en SSR tras un proxy, donde Astro.url resuelve al host interno (localhost). Si se deja vacío, se usa Astro.site o el host de la petición',
|
|
53
|
+
help: 'Origen público del sitio (protocolo + host, sin barra final, p. ej. "https://lefebvre.es"). Se usa para las URLs absolutas de los niveles intermedios del BreadcrumbList en el structured data SEO. Necesario en SSR tras un proxy, donde Astro.url resuelve al host interno (localhost). Si se deja vacío, se usa Astro.site o el host de la petición',
|
|
54
54
|
label: 'URL del sitio',
|
|
55
55
|
mandatory: false,
|
|
56
56
|
example_value: ''
|
|
@@ -149,6 +149,30 @@ export const metadata: ComponentMetadata = {
|
|
|
149
149
|
mandatory: false,
|
|
150
150
|
example_value: 'https://www.lefebvre.es/#organization'
|
|
151
151
|
},
|
|
152
|
+
{
|
|
153
|
+
name: 'websiteId',
|
|
154
|
+
type: 'text',
|
|
155
|
+
help: '@id del nodo WebSite del sitio (el del @graph del Layout). Si se proporciona, el WebPage de la página añade isPartOf: { "@id": ... } enlazando la página con el sitio/organización.',
|
|
156
|
+
label: '@id del WebSite (isPartOf)',
|
|
157
|
+
mandatory: false,
|
|
158
|
+
example_value: 'https://genia-l.lefebvre.es/#website'
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'breadcrumbId',
|
|
162
|
+
type: 'text',
|
|
163
|
+
help: '@id de la BreadcrumbList de la página (el que emite Cabecera_2025_Barcelona, = {canonical}#breadcrumb). Si se proporciona, el WebPage añade breadcrumb: { "@id": ... }. No pasarlo si la página no tiene breadcrumb (evita referencia colgante).',
|
|
164
|
+
label: '@id del breadcrumb',
|
|
165
|
+
mandatory: false,
|
|
166
|
+
example_value: ''
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'mainEntityId',
|
|
170
|
+
type: 'text',
|
|
171
|
+
help: '@id del nodo principal de la página (la "entidad principal"), p. ej. el ItemList de un listado (#itemlist). Si se proporciona, el WebPage/CollectionPage añade mainEntity: { "@id": ... }. Solo aplica a tipos NO artículo/FAQ/ProfilePage (esos ya fijan su propio mainEntity). Usar @id relativo para que funcione también vía render API.',
|
|
172
|
+
label: '@id de la entidad principal (mainEntity)',
|
|
173
|
+
mandatory: false,
|
|
174
|
+
example_value: '#itemlist'
|
|
175
|
+
},
|
|
152
176
|
{
|
|
153
177
|
name: 'keywords',
|
|
154
178
|
type: 'text',
|
|
@@ -4,7 +4,7 @@ export const metadata: ComponentMetadata = {
|
|
|
4
4
|
component_name: 'Texto_2026_Alicante',
|
|
5
5
|
category: 'Contenido de Texto',
|
|
6
6
|
name: 'Bloque centrado con icono circular, título, descripción y botón CTA 2026',
|
|
7
|
-
description: 'Bloque de contenido centrado con cuatro elementos verticales: (1) icono SVG fijo (campana de notificación azul) dentro de un chip circular con fondo #F2F3F8 y padding de 12px; (2) título h3 en Poppins 20px / 24px en desktop semibold; (3) descripción en Inter 16px; (4) botón CTA primario con fondo azul #2134F1, hover invertido (fondo blanco, texto azul). Todos los elementos (icono, descripción y botón) son ocultables independientemente. El título se renderiza siempre si tiene valor
|
|
7
|
+
description: 'Bloque de contenido centrado con cuatro elementos verticales: (1) icono SVG fijo (campana de notificación azul) dentro de un chip circular con fondo #F2F3F8 y padding de 12px; (2) título h3 en Poppins 20px / 24px en desktop semibold; (3) descripción en Inter 16px; (4) botón CTA primario con fondo azul #2134F1, hover invertido (fondo blanco, texto azul). Todos los elementos (icono, descripción y botón) son ocultables independientemente. El título se renderiza siempre si tiene valor',
|
|
8
8
|
framework: 'Astro',
|
|
9
9
|
priority: 1,
|
|
10
10
|
tags: ['texto', 'icono', 'cta', 'boton', 'centrado'],
|
|
@@ -12,7 +12,7 @@ export const metadata: ComponentMetadata = {
|
|
|
12
12
|
{
|
|
13
13
|
name: 'title',
|
|
14
14
|
type: 'text',
|
|
15
|
-
help: 'Título principal del bloque (se renderiza como h3 en Poppins 20px en móvil / 24px en desktop, semibold). Admite HTML.
|
|
15
|
+
help: 'Título principal del bloque (se renderiza como h3 en Poppins 20px en móvil / 24px en desktop, semibold). Admite HTML. Si está vacío, el h3 se renderiza vacío',
|
|
16
16
|
label: 'Texto del título',
|
|
17
17
|
mandatory: false,
|
|
18
18
|
example_value: '¿Quieres saber más sobre GenIA-L?'
|
|
@@ -20,7 +20,7 @@ export const metadata: ComponentMetadata = {
|
|
|
20
20
|
{
|
|
21
21
|
name: 'description',
|
|
22
22
|
type: 'text',
|
|
23
|
-
help: 'Descripción que aparece debajo del título (Inter 16px, line-height 24px). Admite HTML. Solo se renderiza si "Mostrar descripción" está activado
|
|
23
|
+
help: 'Descripción que aparece debajo del título (Inter 16px, line-height 24px). Admite HTML. Solo se renderiza si "Mostrar descripción" está activado',
|
|
24
24
|
label: 'Texto de la descripción',
|
|
25
25
|
mandatory: false,
|
|
26
26
|
example_value: 'Solicita una demo personalizada y descubre cómo la IA jurídica puede transformar tu despacho.'
|
|
@@ -36,16 +36,6 @@ const sizeClassMap: Record<number, string> = {
|
|
|
36
36
|
const sizeClass = sizeClassMap[imageSize] ?? "max-w-[240px] max-h-[240px]";
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
const escapeJson = (s = "") => String(s).replace(/<[^>]*>/g, '').replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/[\n\r\t]+/g, ' ');
|
|
40
|
-
|
|
41
|
-
const structuredData = (nameAuthor || descriptionAuthor) ? `<script type="application/ld+json">
|
|
42
|
-
{
|
|
43
|
-
"@context": "https://schema.org",
|
|
44
|
-
"@type": "WebPageElement",
|
|
45
|
-
"name": "${escapeJson(nameAuthor)}"${descriptionAuthor ? `,
|
|
46
|
-
"description": "${escapeJson(descriptionAuthor)}"` : ''}
|
|
47
|
-
}
|
|
48
|
-
</script>` : '';
|
|
49
39
|
|
|
50
40
|
---
|
|
51
41
|
|
|
@@ -91,7 +81,4 @@ const structuredData = (nameAuthor || descriptionAuthor) ? `<script type="applic
|
|
|
91
81
|
</div>
|
|
92
82
|
{minutes && <Tiempo_2025_Londres minutes={minutes} />}
|
|
93
83
|
</div>
|
|
94
|
-
)}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
<Fragment set:html={structuredData} />
|
|
84
|
+
)}
|
|
@@ -7,12 +7,10 @@ const {
|
|
|
7
7
|
siteUrl = "",
|
|
8
8
|
} = Astro.props;
|
|
9
9
|
|
|
10
|
-
// Origen absoluto para
|
|
11
|
-
// al host interno
|
|
10
|
+
// Origen absoluto para las URLs de los niveles INTERMEDIOS del breadcrumb (otras páginas).
|
|
11
|
+
// En SSR tras un proxy, Astro.url resuelve al host interno, así que se prioriza `siteUrl`/`Astro.site`.
|
|
12
12
|
const origin = (siteUrl || Astro.site?.href || Astro.url.origin).replace(/\/+$/, '');
|
|
13
13
|
|
|
14
|
-
const currentUrl = new URL(subdirectory + Astro.url.pathname, origin).href;
|
|
15
|
-
|
|
16
14
|
const homeUrl = new URL(subdirectory + '/', origin).href;
|
|
17
15
|
|
|
18
16
|
const toAbsoluteUrl = (url: string) => {
|
|
@@ -27,12 +25,15 @@ const {
|
|
|
27
25
|
const allBreadcrumbItems = [
|
|
28
26
|
{ name: homeLabel, id: homeUrl },
|
|
29
27
|
...items.map((item: { text: string; url: string }) => ({ name: item.text, id: toAbsoluteUrl(item.url) })),
|
|
30
|
-
|
|
28
|
+
// La página actual referencia el nodo WebPage por @id RELATIVO (#webpage), el mismo que
|
|
29
|
+
// define SEO_Schema_Page → ambos bloques resuelven al MISMO nodo sin conocer la URL.
|
|
30
|
+
{ name: text, id: '#webpage' },
|
|
31
31
|
];
|
|
32
32
|
|
|
33
33
|
const breadcrumbStructuredData = {
|
|
34
34
|
"@context": "https://schema.org",
|
|
35
35
|
"@type": "BreadcrumbList",
|
|
36
|
+
"@id": '#breadcrumb',
|
|
36
37
|
"itemListElement": allBreadcrumbItems.map((item, i) => ({
|
|
37
38
|
"@type": "ListItem",
|
|
38
39
|
"position": i + 1,
|
|
@@ -5,16 +5,23 @@ interface MaltaItem {
|
|
|
5
5
|
link?: string;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
import { PAGE_ENTITY_ID } from '../../lib/functions.js';
|
|
9
|
+
|
|
8
10
|
const {
|
|
9
11
|
items = []
|
|
10
12
|
}: { items?: MaltaItem[] } = Astro.props;
|
|
11
13
|
|
|
14
|
+
// isPartOf hacia el nodo de página (#webpage por defecto). Genérico: opt-out con pageId="".
|
|
15
|
+
const { pageId = PAGE_ENTITY_ID, itemListId = '' } = Astro.props;
|
|
16
|
+
|
|
12
17
|
const escapeJson = (s = "") => String(s).replace(/<[^>]*>/g, '').replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/[\n\r\t]+/g, ' ');
|
|
13
18
|
|
|
14
19
|
const structuredData = `<script type="application/ld+json">
|
|
15
20
|
{
|
|
16
21
|
"@context": "https://schema.org",
|
|
17
|
-
"@type": "ItemList"
|
|
22
|
+
"@type": "ItemList",${itemListId ? `
|
|
23
|
+
"@id": "${itemListId}",` : ''}${pageId ? `
|
|
24
|
+
"isPartOf": { "@id": "${pageId}" },` : ''}
|
|
18
25
|
"numberOfItems": ${items.length},
|
|
19
26
|
"itemListElement": [${items.map((item: MaltaItem, i: number) => `{
|
|
20
27
|
"@type": "ListItem",
|
|
@@ -9,17 +9,6 @@ const {
|
|
|
9
9
|
flexOrientationIcoText = "flex-row",
|
|
10
10
|
} = Astro.props;
|
|
11
11
|
|
|
12
|
-
const escapeJson = (s = "") => String(s).replace(/<[^>]*>/g, '').replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/[\n\r\t]+/g, ' ');
|
|
13
|
-
|
|
14
|
-
const structuredData = (title || description) ? `<script type="application/ld+json">
|
|
15
|
-
{
|
|
16
|
-
"@context": "https://schema.org",
|
|
17
|
-
"@type": "WebPageElement",
|
|
18
|
-
"name": "${escapeJson(title)}"${description ? `,
|
|
19
|
-
"description": "${escapeJson(description)}"` : ''}
|
|
20
|
-
}
|
|
21
|
-
</script>` : '';
|
|
22
|
-
|
|
23
12
|
---
|
|
24
13
|
|
|
25
14
|
|
|
@@ -99,6 +88,4 @@ const structuredData = (title || description) ? `<script type="application/ld+js
|
|
|
99
88
|
}
|
|
100
89
|
<p class="text-[#363942] text-center">{description}</p>
|
|
101
90
|
</div>
|
|
102
|
-
</div>
|
|
103
|
-
|
|
104
|
-
<Fragment set:html={structuredData} />
|
|
91
|
+
</div>
|
|
@@ -10,17 +10,6 @@ const {
|
|
|
10
10
|
widthBlock = "lg:w-1/4 md:w-2/4", /* lg:w-1/3 */
|
|
11
11
|
} = Astro.props;
|
|
12
12
|
|
|
13
|
-
const escapeJson = (s = "") => String(s).replace(/<[^>]*>/g, '').replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/[\n\r\t]+/g, ' ');
|
|
14
|
-
|
|
15
|
-
const structuredData = (title || description) ? `<script type="application/ld+json">
|
|
16
|
-
{
|
|
17
|
-
"@context": "https://schema.org",
|
|
18
|
-
"@type": "WebPageElement",
|
|
19
|
-
"name": "${escapeJson(title)}"${description ? `,
|
|
20
|
-
"description": "${escapeJson(description)}"` : ''}
|
|
21
|
-
}
|
|
22
|
-
</script>` : '';
|
|
23
|
-
|
|
24
13
|
---
|
|
25
14
|
|
|
26
15
|
|
|
@@ -202,6 +191,4 @@ const structuredData = (title || description) ? `<script type="application/ld+js
|
|
|
202
191
|
}
|
|
203
192
|
<p class="text-[#363942]" set:html={description}></p>
|
|
204
193
|
</div>
|
|
205
|
-
</div>
|
|
206
|
-
|
|
207
|
-
<Fragment set:html={structuredData} />
|
|
194
|
+
</div>
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import Contenido_2026_Menorca from './Contenido_2026_Menorca.astro';
|
|
4
4
|
import Titulo_2025_Algeciras from './Titulo_2025_Algeciras.astro';
|
|
5
|
-
import { extractImageUrl } from '../../lib/functions.js';
|
|
5
|
+
import { extractImageUrl, PAGE_ENTITY_ID } from '../../lib/functions.js';
|
|
6
6
|
|
|
7
7
|
const {
|
|
8
8
|
items = [],
|
|
9
9
|
} = Astro.props;
|
|
10
10
|
|
|
11
|
+
// isPartOf hacia el nodo de página (#webpage por defecto). Genérico: opt-out con pageId="".
|
|
12
|
+
// itemListId opcional: @id del ItemList (para ser mainEntity de la CollectionPage).
|
|
13
|
+
const { pageId = PAGE_ENTITY_ID, itemListId = '' } = Astro.props;
|
|
14
|
+
|
|
11
15
|
type TabPoint = { title: string; description: string; icon: string; showIco: boolean };
|
|
12
16
|
type Tab = { name: string; buttonUrl: string; image: string; points: TabPoint[] };
|
|
13
17
|
|
|
@@ -38,7 +42,9 @@ const structuredData = `<script type="application/ld+json">
|
|
|
38
42
|
"@type": "WebPageElement",
|
|
39
43
|
"name": "${escapeJson(tabs.map((t: Tab) => t.name).filter(Boolean).join(' · '))}",
|
|
40
44
|
"mainEntity": {
|
|
41
|
-
"@type": "ItemList"
|
|
45
|
+
"@type": "ItemList",${itemListId ? `
|
|
46
|
+
"@id": "${itemListId}",` : ''}${pageId ? `
|
|
47
|
+
"isPartOf": { "@id": "${pageId}" },` : ''}
|
|
42
48
|
"numberOfItems": ${tabs.length},
|
|
43
49
|
"itemListElement": [${tabs.map((tab: Tab, index: number) => `{
|
|
44
50
|
"@type": "ListItem",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
|
|
3
3
|
import Contenido_2025_Montevideo from "./Contenido_2025_Montevideo.astro";
|
|
4
|
-
import { extractImageUrl } from '../../lib/functions.js';
|
|
4
|
+
import { extractImageUrl, PAGE_ENTITY_ID } from '../../lib/functions.js';
|
|
5
5
|
|
|
6
6
|
interface MontevideoItem {
|
|
7
7
|
link?: string;
|
|
@@ -23,12 +23,18 @@ const {
|
|
|
23
23
|
showAutor = false,
|
|
24
24
|
}: { items?: MontevideoItem[]; etiquetaVisible?: "fecha" | "keywords"; showAutor?: boolean } = Astro.props;
|
|
25
25
|
|
|
26
|
+
// isPartOf hacia el nodo de página (#webpage por defecto). Genérico: opt-out con pageId="".
|
|
27
|
+
// itemListId opcional: si se pasa, el ItemList lleva ese @id (para ser mainEntity de la CollectionPage).
|
|
28
|
+
const { pageId = PAGE_ENTITY_ID, itemListId = '' } = Astro.props;
|
|
29
|
+
|
|
26
30
|
const escapeJson = (s = "") => String(s).replace(/<[^>]*>/g, '').replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/[\n\r\t]+/g, ' ');
|
|
27
31
|
|
|
28
32
|
const structuredData = `<script type="application/ld+json">
|
|
29
33
|
{
|
|
30
34
|
"@context": "https://schema.org",
|
|
31
|
-
"@type": "ItemList"
|
|
35
|
+
"@type": "ItemList",${itemListId ? `
|
|
36
|
+
"@id": "${itemListId}",` : ''}${pageId ? `
|
|
37
|
+
"isPartOf": { "@id": "${pageId}" },` : ''}
|
|
32
38
|
"numberOfItems": ${items.length},
|
|
33
39
|
"itemListElement": [${items.map((item: MontevideoItem, i: number) => `{
|
|
34
40
|
"@type": "ListItem",
|
|
@@ -17,16 +17,6 @@ const {
|
|
|
17
17
|
|
|
18
18
|
import RRSS_2025_Pisa from "./RRSS_2025_Pisa.astro";
|
|
19
19
|
|
|
20
|
-
const escapeJson = (s = "") => String(s).replace(/<[^>]*>/g, '').replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/[\n\r\t]+/g, ' ');
|
|
21
|
-
|
|
22
|
-
const structuredData = (title || description) ? `<script type="application/ld+json">
|
|
23
|
-
{
|
|
24
|
-
"@context": "https://schema.org",
|
|
25
|
-
"@type": "WebPageElement",
|
|
26
|
-
"name": "${escapeJson(title)}"${description ? `,
|
|
27
|
-
"description": "${escapeJson(description)}"` : ''}
|
|
28
|
-
}
|
|
29
|
-
</script>` : '';
|
|
30
20
|
|
|
31
21
|
---
|
|
32
22
|
|
|
@@ -84,8 +74,6 @@ const structuredData = (title || description) ? `<script type="application/ld+js
|
|
|
84
74
|
)}
|
|
85
75
|
</div>
|
|
86
76
|
|
|
87
|
-
<Fragment set:html={structuredData} />
|
|
88
|
-
|
|
89
77
|
<style>
|
|
90
78
|
.moreDescription h2 {
|
|
91
79
|
color: #262626;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Formulario_2025_Teruel from './Formulario_2025_Teruel.astro';
|
|
3
|
+
import { PAGE_ENTITY_ID } from '../../lib/functions.js';
|
|
3
4
|
|
|
4
5
|
const {
|
|
5
6
|
orientation = 'left',
|
|
@@ -11,12 +12,17 @@ const {
|
|
|
11
12
|
items
|
|
12
13
|
} = Astro.props;
|
|
13
14
|
|
|
15
|
+
// isPartOf hacia el nodo de página (#webpage por defecto). Genérico: opt-out con pageId="".
|
|
16
|
+
const { pageId = PAGE_ENTITY_ID, itemListId = '' } = Astro.props;
|
|
17
|
+
|
|
14
18
|
const randomId = Math.floor(Math.random() * 1000);
|
|
15
19
|
|
|
16
20
|
const structuredData = `<script type="application/ld+json">
|
|
17
21
|
{
|
|
18
22
|
"@context": "https://schema.org",
|
|
19
|
-
"@type": "ItemList"
|
|
23
|
+
"@type": "ItemList",${itemListId ? `
|
|
24
|
+
"@id": "${itemListId}",` : ''}${pageId ? `
|
|
25
|
+
"isPartOf": { "@id": "${pageId}" },` : ''}
|
|
20
26
|
"numberOfItems": ${items.length},
|
|
21
27
|
"itemListElement": [
|
|
22
28
|
${items.map((item, index) => `{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { extractImageUrl } from '../../lib/functions.js';
|
|
2
|
+
import { extractImageUrl, PAGE_ENTITY_ID } from '../../lib/functions.js';
|
|
3
3
|
|
|
4
4
|
interface OrcasitasItem {
|
|
5
5
|
iframeSrc?: string;
|
|
@@ -21,6 +21,9 @@ const {
|
|
|
21
21
|
slidesToScroll = 1,
|
|
22
22
|
}: { loadScript?: boolean; items?: OrcasitasItem[]; slidesToShow?: number; slidesToScroll?: number } = Astro.props;
|
|
23
23
|
|
|
24
|
+
// isPartOf hacia el nodo de página (#webpage por defecto). Genérico: opt-out con pageId="".
|
|
25
|
+
const { pageId = PAGE_ENTITY_ID, itemListId = '' } = Astro.props;
|
|
26
|
+
|
|
24
27
|
const carouselId = "glider-carousel-"+Math.floor(Math.random()*1000);
|
|
25
28
|
const buttonsId = "glider-carousel-buttons-"+Math.floor(Math.random()*1000);
|
|
26
29
|
const prevId = "glider-carousel-prev-"+Math.floor(Math.random()*1000);
|
|
@@ -56,7 +59,9 @@ const buildImageItem = (item: OrcasitasItem) => {
|
|
|
56
59
|
const structuredData = items.length > 0 ? `<script type="application/ld+json">
|
|
57
60
|
{
|
|
58
61
|
"@context": "https://schema.org",
|
|
59
|
-
"@type": "ItemList"
|
|
62
|
+
"@type": "ItemList",${itemListId ? `
|
|
63
|
+
"@id": "${itemListId}",` : ''}${pageId ? `
|
|
64
|
+
"isPartOf": { "@id": "${pageId}" },` : ''}
|
|
60
65
|
"numberOfItems": ${items.length},
|
|
61
66
|
"itemListElement": [${items.map((item: OrcasitasItem, index: number) => `{
|
|
62
67
|
"@type": "ListItem",
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { PAGE_ENTITY_ID } from '../../lib/functions.js';
|
|
3
|
+
|
|
2
4
|
const {
|
|
3
5
|
items
|
|
4
6
|
} = Astro.props;
|
|
5
7
|
|
|
8
|
+
// isPartOf hacia el nodo de página (#webpage por defecto). Genérico: opt-out con pageId="".
|
|
9
|
+
// itemListId opcional: @id del ItemList (para ser mainEntity de la CollectionPage).
|
|
10
|
+
const { pageId = PAGE_ENTITY_ID, itemListId = '' } = Astro.props;
|
|
11
|
+
|
|
6
12
|
const randomId = Math.floor(Math.random() * 1000);
|
|
7
13
|
|
|
8
14
|
const structuredData = JSON.stringify({
|
|
9
15
|
"@context": "https://schema.org",
|
|
10
16
|
"@type": "ItemList",
|
|
17
|
+
...(itemListId ? { "@id": itemListId } : {}),
|
|
18
|
+
...(pageId ? { isPartOf: { "@id": pageId } } : {}),
|
|
11
19
|
"numberOfItems": items.length,
|
|
12
20
|
"itemListElement": items.map((item, index) => ({
|
|
13
21
|
"@type": "ListItem",
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { PAGE_ENTITY_ID } from '../../lib/functions.js';
|
|
3
|
+
|
|
2
4
|
const {
|
|
3
5
|
items = [],
|
|
4
6
|
} = Astro.props;
|
|
5
7
|
|
|
8
|
+
// isPartOf hacia el nodo de página (#webpage por defecto). Genérico: opt-out con pageId="".
|
|
9
|
+
const { pageId = PAGE_ENTITY_ID, itemListId = '' } = Astro.props;
|
|
10
|
+
|
|
6
11
|
const structuredData = `<script type="application/ld+json">
|
|
7
12
|
{
|
|
8
13
|
"@context": "https://schema.org",
|
|
9
|
-
"@type": "ItemList"
|
|
14
|
+
"@type": "ItemList",${itemListId ? `
|
|
15
|
+
"@id": "${itemListId}",` : ''}${pageId ? `
|
|
16
|
+
"isPartOf": { "@id": "${pageId}" },` : ''}
|
|
10
17
|
"numberOfItems": ${items.length},
|
|
11
18
|
"itemListElement": [
|
|
12
19
|
${items.map((item, index) => `{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { extractImageUrl } from '../../lib/functions.js';
|
|
2
|
+
import { extractImageUrl, PAGE_ENTITY_ID } from '../../lib/functions.js';
|
|
3
3
|
|
|
4
4
|
interface VideoItem {
|
|
5
5
|
name?: string;
|
|
@@ -17,6 +17,9 @@ const {
|
|
|
17
17
|
items = [],
|
|
18
18
|
}: { items?: VideoItem[] } = Astro.props;
|
|
19
19
|
|
|
20
|
+
// isPartOf hacia el nodo de página (#webpage por defecto). Genérico: opt-out con pageId="".
|
|
21
|
+
const { pageId = PAGE_ENTITY_ID, itemListId = '' } = Astro.props;
|
|
22
|
+
|
|
20
23
|
const idAccordion = 'js-acordeon-' + Math.random().toString(36).substring(2, 15);
|
|
21
24
|
|
|
22
25
|
const escapeJson = (s = "") => String(s).replace(/<[^>]*>/g, '').replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/[\n\r\t]+/g, ' ');
|
|
@@ -24,7 +27,9 @@ const escapeJson = (s = "") => String(s).replace(/<[^>]*>/g, '').replace(/\\/g,
|
|
|
24
27
|
const structuredData = items.length > 0 ? `<script type="application/ld+json">
|
|
25
28
|
{
|
|
26
29
|
"@context": "https://schema.org",
|
|
27
|
-
"@type": "ItemList"
|
|
30
|
+
"@type": "ItemList",${itemListId ? `
|
|
31
|
+
"@id": "${itemListId}",` : ''}${pageId ? `
|
|
32
|
+
"isPartOf": { "@id": "${pageId}" },` : ''}
|
|
28
33
|
"numberOfItems": ${items.length},
|
|
29
34
|
"itemListElement": [${items.map((item: VideoItem, index: number) => `{
|
|
30
35
|
"@type": "ListItem",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { extractImageUrl } from '../../lib/functions.js';
|
|
2
|
+
import { extractImageUrl, PAGE_ENTITY_ID } from '../../lib/functions.js';
|
|
3
3
|
|
|
4
4
|
interface GaleriaItem {
|
|
5
5
|
image?: string;
|
|
@@ -11,6 +11,9 @@ const {
|
|
|
11
11
|
items = []
|
|
12
12
|
}: { columns?: string; items?: GaleriaItem[] } = Astro.props;
|
|
13
13
|
|
|
14
|
+
// isPartOf hacia el nodo de página (#webpage por defecto). Genérico: opt-out con pageId="".
|
|
15
|
+
const { pageId = PAGE_ENTITY_ID, itemListId = '' } = Astro.props;
|
|
16
|
+
|
|
14
17
|
const lgColsClass = {
|
|
15
18
|
'2': 'lg:columns-2',
|
|
16
19
|
'3': 'lg:columns-3',
|
|
@@ -24,7 +27,9 @@ const escapeJson = (s = '') => String(s).replace(/<[^>]*>/g, '').replace(/\\/g,
|
|
|
24
27
|
const structuredData = `<script type="application/ld+json">
|
|
25
28
|
{
|
|
26
29
|
"@context": "https://schema.org",
|
|
27
|
-
"@type": "ItemList"
|
|
30
|
+
"@type": "ItemList",${itemListId ? `
|
|
31
|
+
"@id": "${itemListId}",` : ''}${pageId ? `
|
|
32
|
+
"isPartOf": { "@id": "${pageId}" },` : ''}
|
|
28
33
|
"numberOfItems": ${visibleItems.length},
|
|
29
34
|
"itemListElement": [${visibleItems.map((item: GaleriaItem, i: number) => `{
|
|
30
35
|
"@type": "ListItem",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { PAGE_ENTITY_ID } from '../../lib/functions.js';
|
|
2
3
|
|
|
3
4
|
const {
|
|
4
5
|
titulo,
|
|
@@ -7,6 +8,9 @@ const {
|
|
|
7
8
|
siteUrl = '',
|
|
8
9
|
} = Astro.props;
|
|
9
10
|
|
|
11
|
+
// isPartOf hacia el nodo de página (#webpage por defecto). Genérico: opt-out con pageId="".
|
|
12
|
+
const { pageId = PAGE_ENTITY_ID, itemListId = '' } = Astro.props;
|
|
13
|
+
|
|
10
14
|
interface IndiceItem {
|
|
11
15
|
enlace: string;
|
|
12
16
|
titulo: string;
|
|
@@ -27,7 +31,9 @@ const baseUrl = new URL(subdirectory + Astro.url.pathname, origin).href.split('#
|
|
|
27
31
|
const structuredData = `<script type="application/ld+json">
|
|
28
32
|
{
|
|
29
33
|
"@context": "https://schema.org",
|
|
30
|
-
"@type": "ItemList"
|
|
34
|
+
"@type": "ItemList",${itemListId ? `
|
|
35
|
+
"@id": "${itemListId}",` : ''}${pageId ? `
|
|
36
|
+
"isPartOf": { "@id": "${pageId}" },` : ''}
|
|
31
37
|
"name": "${escapeJson(titulo)}",
|
|
32
38
|
"numberOfItems": ${indiceItems.length},
|
|
33
39
|
"itemListElement": [${indiceItems.map((item, i) => `{
|
|
@@ -33,6 +33,9 @@ interface Props {
|
|
|
33
33
|
publisherLogo?: string;
|
|
34
34
|
publisherUrl?: string;
|
|
35
35
|
publisherId?: string;
|
|
36
|
+
websiteId?: string;
|
|
37
|
+
breadcrumbId?: string;
|
|
38
|
+
mainEntityId?: string;
|
|
36
39
|
keywords?: string;
|
|
37
40
|
inLanguage?: string;
|
|
38
41
|
articleSection?: string;
|
|
@@ -60,6 +63,9 @@ const {
|
|
|
60
63
|
publisherLogo = 'https://assets.lefebvre.es/media/logos-2/svg/lefebvre.svg',
|
|
61
64
|
publisherUrl = 'https://lefebvre.es',
|
|
62
65
|
publisherId,
|
|
66
|
+
websiteId,
|
|
67
|
+
breadcrumbId,
|
|
68
|
+
mainEntityId,
|
|
63
69
|
keywords,
|
|
64
70
|
inLanguage = 'es',
|
|
65
71
|
articleSection,
|
|
@@ -73,75 +79,121 @@ const escapeJson = (s = "") => String(s).replace(/<[^>]*>/g, '').replace(/\\/g,
|
|
|
73
79
|
|
|
74
80
|
const isArticleType = (['Article', 'BlogPosting', 'NewsArticle'] as PageType[]).includes(pageType);
|
|
75
81
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
};
|
|
82
|
+
// @id por-página RELATIVOS de fragmento: cualquier bloque del mismo documento (incluido el HTML
|
|
83
|
+
// inyectado por el render API del CMS) puede referenciarlos sin conocer la URL; Google los resuelve
|
|
84
|
+
// contra la página. Los @id de sitio/cross-página (websiteId, publisherId, authorId/#person) siguen
|
|
85
|
+
// absolutos y se reciben por props.
|
|
86
|
+
const webPageNodeId = '#webpage';
|
|
87
|
+
const primaryImageId = '#primaryimage';
|
|
88
|
+
const articleNodeId = '#article';
|
|
84
89
|
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
// Nodo ImageObject de la imagen principal (si hay), referenciable por @id.
|
|
91
|
+
const imageNode = image
|
|
92
|
+
? { '@type': 'ImageObject', '@id': primaryImageId, url: image }
|
|
93
|
+
: null;
|
|
87
94
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const author: Record<string, unknown> = { '@type': authorType, name: authorName };
|
|
98
|
-
if (authorUrl) author.url = authorUrl;
|
|
99
|
-
if (authorImage && authorType === 'Person') author.image = authorImage;
|
|
100
|
-
schema.author = author;
|
|
95
|
+
// Autor: nodo inline CON @id (autocontenido y fusionable entre páginas) si llega authorName;
|
|
96
|
+
// referencia pura { @id } si solo llega authorId (caso "Lefebvre" → #organization global).
|
|
97
|
+
const buildAuthor = (): Record<string, unknown> | null => {
|
|
98
|
+
if (authorName) {
|
|
99
|
+
const node: Record<string, unknown> = { '@type': authorType, name: authorName };
|
|
100
|
+
if (authorId) node['@id'] = authorId;
|
|
101
|
+
if (authorUrl) node.url = authorUrl;
|
|
102
|
+
if (authorImage && authorType === 'Person') node.image = authorImage;
|
|
103
|
+
return node;
|
|
101
104
|
}
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
if (authorId) return { '@id': authorId };
|
|
106
|
+
return null;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const buildPublisher = (): Record<string, unknown> =>
|
|
110
|
+
publisherId
|
|
111
|
+
? { '@id': publisherId }
|
|
112
|
+
: {
|
|
106
113
|
'@type': 'Organization',
|
|
107
114
|
name: publisherName,
|
|
108
115
|
url: publisherUrl,
|
|
109
116
|
logo: { '@type': 'ImageObject', url: publisherLogo },
|
|
110
117
|
};
|
|
111
|
-
|
|
118
|
+
|
|
119
|
+
// --- Nodo WebPage (siempre). Para tipos artículo el @type es WebPage; si no, el propio pageType.
|
|
120
|
+
const webPageType = isArticleType ? 'WebPage' : pageType;
|
|
121
|
+
const webPage: Record<string, unknown> = {
|
|
122
|
+
'@type': webPageType,
|
|
123
|
+
'@id': webPageNodeId,
|
|
124
|
+
url,
|
|
125
|
+
name,
|
|
126
|
+
inLanguage,
|
|
127
|
+
};
|
|
128
|
+
if (description) webPage.description = description;
|
|
129
|
+
if (websiteId) webPage.isPartOf = { '@id': websiteId };
|
|
130
|
+
if (breadcrumbId) webPage.breadcrumb = { '@id': breadcrumbId };
|
|
131
|
+
if (imageNode) webPage.primaryImageOfPage = { '@id': primaryImageId };
|
|
132
|
+
if (!isArticleType && keywords) webPage.keywords = keywords;
|
|
133
|
+
|
|
134
|
+
const graph: Array<Record<string, unknown>> = [webPage];
|
|
135
|
+
if (imageNode) graph.push(imageNode);
|
|
136
|
+
|
|
137
|
+
// --- Tipos artículo: nodo independiente (#article) enlazado al WebPage ---
|
|
138
|
+
if (isArticleType) {
|
|
139
|
+
webPage.mainEntity = { '@id': articleNodeId };
|
|
140
|
+
const article: Record<string, unknown> = {
|
|
141
|
+
'@type': pageType,
|
|
142
|
+
'@id': articleNodeId,
|
|
143
|
+
headline: headline || name,
|
|
144
|
+
isPartOf: { '@id': webPageNodeId },
|
|
145
|
+
mainEntityOfPage: { '@id': webPageNodeId },
|
|
146
|
+
inLanguage,
|
|
147
|
+
};
|
|
148
|
+
if (description) article.description = description;
|
|
149
|
+
if (datePublished) article.datePublished = datePublished;
|
|
150
|
+
if (dateModified) article.dateModified = dateModified;
|
|
151
|
+
if (articleSection) article.articleSection = articleSection;
|
|
152
|
+
if (keywords) article.keywords = keywords;
|
|
153
|
+
if (imageNode) article.image = { '@id': primaryImageId };
|
|
154
|
+
const author = buildAuthor();
|
|
155
|
+
if (author) article.author = author;
|
|
156
|
+
article.publisher = buildPublisher();
|
|
157
|
+
graph.push(article);
|
|
112
158
|
}
|
|
113
159
|
|
|
160
|
+
// --- FAQPage: las preguntas son el mainEntity del WebPage ---
|
|
114
161
|
if (pageType === 'FAQPage' && faqItems.length) {
|
|
115
|
-
|
|
162
|
+
webPage.mainEntity = faqItems.map((item) => ({
|
|
116
163
|
'@type': 'Question',
|
|
117
164
|
name: item.question,
|
|
118
|
-
acceptedAnswer: {
|
|
119
|
-
'@type': 'Answer',
|
|
120
|
-
text: item.answer,
|
|
121
|
-
},
|
|
165
|
+
acceptedAnswer: { '@type': 'Answer', text: item.answer },
|
|
122
166
|
}));
|
|
123
167
|
}
|
|
124
168
|
|
|
169
|
+
// --- ProfilePage: el mainEntity es la entidad (Person/Organization) del autor ---
|
|
125
170
|
if (pageType === 'ProfilePage' && (authorName || authorId)) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
if (authorUrl)
|
|
131
|
-
if (
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (authorUrl) person.url = authorUrl;
|
|
137
|
-
if (authorImage) person.image = authorImage;
|
|
138
|
-
if (jobTitle) person.jobTitle = jobTitle;
|
|
139
|
-
if (description) person.description = description;
|
|
140
|
-
if (sameAs && sameAs.length) person.sameAs = sameAs;
|
|
141
|
-
schema.mainEntity = person;
|
|
171
|
+
const entityId = authorId || `${url}#person`;
|
|
172
|
+
webPage.mainEntity = { '@id': entityId };
|
|
173
|
+
if (authorName) {
|
|
174
|
+
const entity: Record<string, unknown> = { '@type': authorType, '@id': entityId, name: authorName };
|
|
175
|
+
if (authorUrl) entity.url = authorUrl;
|
|
176
|
+
if (authorImage && authorType === 'Person') entity.image = authorImage;
|
|
177
|
+
if (jobTitle && authorType === 'Person') entity.jobTitle = jobTitle;
|
|
178
|
+
if (description) entity.description = description;
|
|
179
|
+
if (sameAs && sameAs.length) entity.sameAs = sameAs;
|
|
180
|
+
graph.push(entity);
|
|
142
181
|
}
|
|
182
|
+
// Si solo llega authorId (sin authorName), se referencia un nodo definido en otra parte
|
|
183
|
+
// (p. ej. la Organization global #organization del Layout) → no se redeclara aquí.
|
|
143
184
|
}
|
|
144
185
|
|
|
186
|
+
// --- mainEntity explícito (p. ej. CollectionPage → ItemList): se aplica solo si el tipo no la fija
|
|
187
|
+
// ya (artículo/FAQ/Profile). El consumidor pasa el @id del nodo principal (relativo, p. ej. "#itemlist").
|
|
188
|
+
if (mainEntityId && !webPage.mainEntity) {
|
|
189
|
+
webPage.mainEntity = { '@id': mainEntityId };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const schema: Record<string, unknown> = {
|
|
193
|
+
'@context': 'https://schema.org',
|
|
194
|
+
'@graph': graph,
|
|
195
|
+
};
|
|
196
|
+
|
|
145
197
|
const stringify = (value: unknown): string => {
|
|
146
198
|
if (value === null || value === undefined) return 'null';
|
|
147
199
|
if (typeof value === 'string') return `"${escapeJson(value)}"`;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { PAGE_ENTITY_ID } from '../../lib/functions.js';
|
|
3
|
+
|
|
2
4
|
const {
|
|
3
5
|
title = "",
|
|
4
6
|
description = "",
|
|
@@ -8,6 +10,11 @@ const {
|
|
|
8
10
|
rightItems = [],
|
|
9
11
|
} = Astro.props;
|
|
10
12
|
|
|
13
|
+
// isPartOf hacia el nodo de página (#webpage por defecto). Genérico: opt-out con pageId="".
|
|
14
|
+
// itemListId opcional: se aplica SOLO a la lista izquierda/principal (la derecha nunca lleva @id,
|
|
15
|
+
// para no duplicar el mismo @id en la página).
|
|
16
|
+
const { pageId = PAGE_ENTITY_ID, itemListId = '' } = Astro.props;
|
|
17
|
+
|
|
11
18
|
const hasLeft = leftTitle || leftItems.length > 0;
|
|
12
19
|
const hasRight = rightTitle || rightItems.length > 0;
|
|
13
20
|
const hasBoth = hasLeft && hasRight;
|
|
@@ -16,10 +23,12 @@ type Item = { text: string; link?: string; target?: string };
|
|
|
16
23
|
|
|
17
24
|
const escapeJson = (s = "") => String(s).replace(/<[^>]*>/g, '').replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/[\n\r\t]+/g, ' ');
|
|
18
25
|
|
|
19
|
-
const buildItemListScript = (listName: string, listItems: Item[]) => `<script type="application/ld+json">
|
|
26
|
+
const buildItemListScript = (listName: string, listItems: Item[], listId = '') => `<script type="application/ld+json">
|
|
20
27
|
{
|
|
21
28
|
"@context": "https://schema.org",
|
|
22
|
-
"@type": "ItemList"
|
|
29
|
+
"@type": "ItemList",${listId ? `
|
|
30
|
+
"@id": "${listId}",` : ''}${pageId ? `
|
|
31
|
+
"isPartOf": { "@id": "${pageId}" },` : ''}
|
|
23
32
|
"name": "${escapeJson(listName)}",
|
|
24
33
|
"numberOfItems": ${listItems.length},
|
|
25
34
|
"itemListElement": [${listItems.map((item: Item, index: number) => `{
|
|
@@ -32,7 +41,7 @@ const buildItemListScript = (listName: string, listItems: Item[]) => `<script ty
|
|
|
32
41
|
</script>`;
|
|
33
42
|
|
|
34
43
|
const structuredData = [
|
|
35
|
-
hasLeft ? buildItemListScript(leftTitle, leftItems) : '',
|
|
44
|
+
hasLeft ? buildItemListScript(leftTitle, leftItems, itemListId) : '',
|
|
36
45
|
hasRight ? buildItemListScript(rightTitle, rightItems) : '',
|
|
37
46
|
].filter(Boolean).join('\n');
|
|
38
47
|
---
|
|
@@ -15,16 +15,6 @@ const {
|
|
|
15
15
|
color = '#262626',
|
|
16
16
|
} = Astro.props;
|
|
17
17
|
|
|
18
|
-
const escapeJson = (s = "") => String(s).replace(/<[^>]*>/g, '').replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/[\n\r\t]+/g, ' ');
|
|
19
|
-
|
|
20
|
-
const structuredData = (title || description) ? `<script type="application/ld+json">
|
|
21
|
-
{
|
|
22
|
-
"@context": "https://schema.org",
|
|
23
|
-
"@type": "WebPageElement",
|
|
24
|
-
"name": "${escapeJson(title)}"${description ? `,
|
|
25
|
-
"description": "${escapeJson(description)}"` : ''}
|
|
26
|
-
}
|
|
27
|
-
</script>` : '';
|
|
28
18
|
---
|
|
29
19
|
|
|
30
20
|
<section class="w-full flex items-center justify-center p-6 md:p-0">
|
|
@@ -34,6 +24,4 @@ const structuredData = (title || description) ? `<script type="application/ld+js
|
|
|
34
24
|
{showDescription && <div class={`text-[${color}] font-inter text-base font-normal leading-[24px] mb-8`} set:html={description}></div>}
|
|
35
25
|
{showBtn && <a href={linkBtn} class="text-[#ffffff] font-inter font-normal bg-[#2134F1] px-5 py-3 border border-[#2134F1] rounded-xl hover:bg-[#ffffff] hover:border-[#2134F1] shadow-[0_2px_4px_-2px_rgba(0,0,0,0.08),0_4px_8px_-2px_rgba(0,0,0,0.04)] transition-all duration-300 hover:text-[#2134F1]">{txtBtn}</a>}
|
|
36
26
|
</article>
|
|
37
|
-
</section>
|
|
38
|
-
|
|
39
|
-
<Fragment set:html={structuredData} />
|
|
27
|
+
</section>
|
|
@@ -10,17 +10,6 @@ const {
|
|
|
10
10
|
subdescription = "",
|
|
11
11
|
} = Astro.props;
|
|
12
12
|
|
|
13
|
-
const escapeJson = (s = "") => String(s).replace(/<[^>]*>/g, '').replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/[\n\r\t]+/g, ' ');
|
|
14
|
-
|
|
15
|
-
const structuredData = (title || description) ? `<script type="application/ld+json">
|
|
16
|
-
{
|
|
17
|
-
"@context": "https://schema.org",
|
|
18
|
-
"@type": "WebPageElement",
|
|
19
|
-
"name": "${escapeJson(title)}"${description ? `,
|
|
20
|
-
"description": "${escapeJson(description)}"` : ''}
|
|
21
|
-
}
|
|
22
|
-
</script>` : '';
|
|
23
|
-
|
|
24
13
|
---
|
|
25
14
|
|
|
26
15
|
<div class={`w-full md:w-3/5 max-w-auto flex ${flexJustify === 'justify-center' ? 'justify-center' : 'justify-start'} flex-col p-4 md:p-0 mx-auto`}>
|
|
@@ -55,6 +44,4 @@ const structuredData = (title || description) ? `<script type="application/ld+js
|
|
|
55
44
|
{subdescription && subdescription.trim() !== "" && (
|
|
56
45
|
<div class={`w-full md:w-${descriptionWidth} text-[#363942] mx-auto max-w-[850px] font-poppins text-xl font-normal leading-7 mb-8 text-center`} set:html={subdescription}></div>
|
|
57
46
|
)}
|
|
58
|
-
</div>
|
|
59
|
-
|
|
60
|
-
<Fragment set:html={structuredData} />
|
|
47
|
+
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import Video_2025_Polop from './Video_2025_Polop.astro';
|
|
3
|
-
import { extractImageUrl } from '../../lib/functions.js';
|
|
3
|
+
import { extractImageUrl, PAGE_ENTITY_ID } from '../../lib/functions.js';
|
|
4
4
|
|
|
5
5
|
interface VideoItem {
|
|
6
6
|
title?: string;
|
|
@@ -17,6 +17,9 @@ const {
|
|
|
17
17
|
iframeSrc
|
|
18
18
|
}: { items?: VideoItem[]; iframeSrc?: string } = Astro.props;
|
|
19
19
|
|
|
20
|
+
// isPartOf hacia el nodo de página (#webpage por defecto). Genérico: opt-out con pageId="".
|
|
21
|
+
const { pageId = PAGE_ENTITY_ID, itemListId = '' } = Astro.props;
|
|
22
|
+
|
|
20
23
|
const idAccordion = 'js-acordeon-' + Math.random().toString(36).substring(2, 15);
|
|
21
24
|
|
|
22
25
|
const escapeJson = (s = "") => String(s).replace(/<[^>]*>/g, '').replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/[\n\r\t]+/g, ' ');
|
|
@@ -24,7 +27,9 @@ const escapeJson = (s = "") => String(s).replace(/<[^>]*>/g, '').replace(/\\/g,
|
|
|
24
27
|
const structuredData = items.length > 0 ? `<script type="application/ld+json">
|
|
25
28
|
{
|
|
26
29
|
"@context": "https://schema.org",
|
|
27
|
-
"@type": "ItemList"
|
|
30
|
+
"@type": "ItemList",${itemListId ? `
|
|
31
|
+
"@id": "${itemListId}",` : ''}${pageId ? `
|
|
32
|
+
"isPartOf": { "@id": "${pageId}" },` : ''}
|
|
28
33
|
"numberOfItems": ${items.length},
|
|
29
34
|
"itemListElement": [${items.map((item: VideoItem, index: number) => `{
|
|
30
35
|
"@type": "ListItem",
|
package/src/lib/functions.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { components } from '../generated/componentRegistry.ts';
|
|
2
2
|
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// 🎯 Grafo JSON-LD
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// @id RELATIVO de fragmento del nodo principal de la página (WebPage/CollectionPage…),
|
|
7
|
+
// que emite SEO_Schema_Page. Es la fuente única de la convención: cualquier bloque del mismo
|
|
8
|
+
// documento (incluido el HTML inyectado por el render API del CMS) puede colgar de la página con
|
|
9
|
+
// isPartOf: { "@id": PAGE_ENTITY_ID } sin necesidad de conocer la URL/canonical.
|
|
10
|
+
export const PAGE_ENTITY_ID = '#webpage';
|
|
11
|
+
|
|
12
|
+
// @id RELATIVO del ItemList principal de una página. Convención: el listado principal lleva este @id
|
|
13
|
+
// y la CollectionPage (vía SEO_Schema_Page mainEntityId) lo referencia como mainEntity. Relativo →
|
|
14
|
+
// resuelve contra la página, así que vale tanto para el front como para el render API del CMS.
|
|
15
|
+
export const MAIN_ITEMLIST_ID = '#itemlist';
|
|
16
|
+
|
|
3
17
|
// ============================================================================
|
|
4
18
|
// 🎯 Utilidades de Imagen para Limbo
|
|
5
19
|
// ============================================================================
|