@micazoyolli/foundation 0.1.2 → 0.2.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/README.md +21 -1
- package/dist/a11y/index.d.ts +3 -3
- package/dist/a11y/index.js +3 -3
- package/dist/a11y/protectedMedia.js +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/seo/html.d.ts +22 -0
- package/dist/seo/html.js +71 -0
- package/dist/seo/index.d.ts +7 -0
- package/dist/seo/index.js +4 -0
- package/dist/seo/sitemap.d.ts +10 -0
- package/dist/seo/sitemap.js +21 -0
- package/dist/seo/text.d.ts +2 -0
- package/dist/seo/text.js +6 -0
- package/dist/seo/url.d.ts +9 -0
- package/dist/seo/url.js +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ Paquete público de fundamentos compartidos no visuales para los proyectos de Mi
|
|
|
9
9
|
- Helper `cx` para composición segura de clases
|
|
10
10
|
- Guards TypeScript pequeños para validaciones comunes
|
|
11
11
|
- Utilidades básicas de accesibilidad y protección selectiva de media
|
|
12
|
+
- Helpers SEO/build puros para HTML estático, canonical y sitemap
|
|
12
13
|
- Exports separados para TypeScript y SCSS
|
|
13
14
|
- Consumo desde npm sin credenciales especiales
|
|
14
15
|
|
|
@@ -27,6 +28,7 @@ src/
|
|
|
27
28
|
├── scss/
|
|
28
29
|
│ ├── mixins/
|
|
29
30
|
│ └── tokens/
|
|
31
|
+
├── seo/
|
|
30
32
|
├── utils/
|
|
31
33
|
└── index.ts
|
|
32
34
|
|
|
@@ -63,6 +65,24 @@ if (isElement(event.target)) {
|
|
|
63
65
|
}
|
|
64
66
|
```
|
|
65
67
|
|
|
68
|
+
SEO/build:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import {
|
|
72
|
+
applyHtmlMetadata,
|
|
73
|
+
buildSitemapXml,
|
|
74
|
+
getCanonicalUrl,
|
|
75
|
+
} from '@micazoyolli/foundation';
|
|
76
|
+
|
|
77
|
+
const canonical = getCanonicalUrl('https://example.com', '/contacto');
|
|
78
|
+
const html = applyHtmlMetadata(template, {
|
|
79
|
+
canonical,
|
|
80
|
+
title: 'Contacto',
|
|
81
|
+
description: 'Hablemos de tu proyecto.',
|
|
82
|
+
});
|
|
83
|
+
const sitemap = buildSitemapXml([{ loc: canonical, priority: '0.8' }]);
|
|
84
|
+
```
|
|
85
|
+
|
|
66
86
|
SCSS:
|
|
67
87
|
|
|
68
88
|
```scss
|
|
@@ -81,7 +101,7 @@ SCSS:
|
|
|
81
101
|
## Buenas prácticas
|
|
82
102
|
|
|
83
103
|
- Mantener el paquete pequeño y estrictamente no visual
|
|
84
|
-
- Evitar tokens de marca, layouts, componentes React,
|
|
104
|
+
- Evitar tokens de marca, layouts, componentes React, metadata específica o tipografías finales
|
|
85
105
|
- Publicar cambios mediante versiones semánticas
|
|
86
106
|
- Exportar solo utilidades compartibles entre repos independientes
|
|
87
107
|
- Evitar dependencias UI pesadas
|
package/dist/a11y/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { isElement, isHTMLElement } from './targets';
|
|
2
|
-
export { KEYBOARD_KEYS, isKeyboardActivation } from './keyboard';
|
|
3
|
-
export { PROTECTED_MEDIA_SELECTOR, isProtectedMediaTarget, } from './protectedMedia';
|
|
1
|
+
export { isElement, isHTMLElement } from './targets.js';
|
|
2
|
+
export { KEYBOARD_KEYS, isKeyboardActivation } from './keyboard.js';
|
|
3
|
+
export { PROTECTED_MEDIA_SELECTOR, isProtectedMediaTarget, } from './protectedMedia.js';
|
package/dist/a11y/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { isElement, isHTMLElement } from './targets';
|
|
2
|
-
export { KEYBOARD_KEYS, isKeyboardActivation } from './keyboard';
|
|
3
|
-
export { PROTECTED_MEDIA_SELECTOR, isProtectedMediaTarget, } from './protectedMedia';
|
|
1
|
+
export { isElement, isHTMLElement } from './targets.js';
|
|
2
|
+
export { KEYBOARD_KEYS, isKeyboardActivation } from './keyboard.js';
|
|
3
|
+
export { PROTECTED_MEDIA_SELECTOR, isProtectedMediaTarget, } from './protectedMedia.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { KEYBOARD_KEYS, PROTECTED_MEDIA_SELECTOR, isElement, isHTMLElement, isKeyboardActivation, isProtectedMediaTarget, } from './a11y';
|
|
2
|
-
export { cx } from './utils/cx';
|
|
3
|
-
export type { ClassValue } from './utils/cx';
|
|
4
|
-
export { isNonEmptyString } from './utils/guards';
|
|
1
|
+
export { KEYBOARD_KEYS, PROTECTED_MEDIA_SELECTOR, isElement, isHTMLElement, isKeyboardActivation, isProtectedMediaTarget, } from './a11y/index.js';
|
|
2
|
+
export { cx } from './utils/cx.js';
|
|
3
|
+
export type { ClassValue } from './utils/cx.js';
|
|
4
|
+
export { isNonEmptyString } from './utils/guards.js';
|
|
5
|
+
export * from './seo/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { KEYBOARD_KEYS, PROTECTED_MEDIA_SELECTOR, isElement, isHTMLElement, isKeyboardActivation, isProtectedMediaTarget, } from './a11y';
|
|
2
|
-
export { cx } from './utils/cx';
|
|
3
|
-
export { isNonEmptyString } from './utils/guards';
|
|
1
|
+
export { KEYBOARD_KEYS, PROTECTED_MEDIA_SELECTOR, isElement, isHTMLElement, isKeyboardActivation, isProtectedMediaTarget, } from './a11y/index.js';
|
|
2
|
+
export { cx } from './utils/cx.js';
|
|
3
|
+
export { isNonEmptyString } from './utils/guards.js';
|
|
4
|
+
export * from './seo/index.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type HtmlMetadata = {
|
|
2
|
+
canonical?: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
image?: string;
|
|
5
|
+
lang?: string;
|
|
6
|
+
ogImageHeight?: number | string;
|
|
7
|
+
ogImageWidth?: number | string;
|
|
8
|
+
robots?: string;
|
|
9
|
+
siteName?: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
twitterCard?: string;
|
|
12
|
+
};
|
|
13
|
+
export type AlternateLink = {
|
|
14
|
+
href: string;
|
|
15
|
+
hreflang: string;
|
|
16
|
+
};
|
|
17
|
+
export declare const upsertMetaTag: (html: string, selector: string, tag: string) => string;
|
|
18
|
+
export declare const upsertLinkTag: (html: string, selector: string, tag: string) => string;
|
|
19
|
+
export declare const removeAlternateLinks: (html: string) => string;
|
|
20
|
+
export declare const getAlternateLinkTags: (alternates: readonly AlternateLink[]) => string;
|
|
21
|
+
export declare const applyHtmlMetadata: (html: string, metadata: HtmlMetadata, alternates?: readonly AlternateLink[]) => string;
|
|
22
|
+
export declare const getStaticRouteOutputPath: (routePath: string) => string;
|
package/dist/seo/html.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2
|
+
const insertBeforeHeadEnd = (html, tag) => html.includes('</head>') ? html.replace('</head>', ` ${tag}\n </head>`) : html;
|
|
3
|
+
export const upsertMetaTag = (html, selector, tag) => {
|
|
4
|
+
const pattern = new RegExp(`<meta\\s+${escapeRegExp(selector)}[^>]*>`);
|
|
5
|
+
if (pattern.test(html)) {
|
|
6
|
+
return html.replace(pattern, tag);
|
|
7
|
+
}
|
|
8
|
+
return insertBeforeHeadEnd(html, tag);
|
|
9
|
+
};
|
|
10
|
+
export const upsertLinkTag = (html, selector, tag) => {
|
|
11
|
+
const pattern = new RegExp(`<link\\s+${escapeRegExp(selector)}[^>]*>`);
|
|
12
|
+
if (pattern.test(html)) {
|
|
13
|
+
return html.replace(pattern, tag);
|
|
14
|
+
}
|
|
15
|
+
return insertBeforeHeadEnd(html, tag);
|
|
16
|
+
};
|
|
17
|
+
export const removeAlternateLinks = (html) => html.replace(/\s*<link\s+rel="alternate"\s+hreflang="[^"]+"\s+href="[^"]+"\s*\/?>\n?/g, '');
|
|
18
|
+
export const getAlternateLinkTags = (alternates) => alternates
|
|
19
|
+
.map(({ href, hreflang }) => `<link rel="alternate" hreflang="${hreflang}" href="${href}" />`)
|
|
20
|
+
.join('\n ');
|
|
21
|
+
export const applyHtmlMetadata = (html, metadata, alternates = []) => {
|
|
22
|
+
let next = html;
|
|
23
|
+
if (metadata.lang) {
|
|
24
|
+
next = next.replace(/<html\s+lang="[^"]*">/, `<html lang="${metadata.lang}">`);
|
|
25
|
+
}
|
|
26
|
+
if (metadata.title) {
|
|
27
|
+
next = next.replace(/<title>[\s\S]*?<\/title>/, `<title>${metadata.title}</title>`);
|
|
28
|
+
next = upsertMetaTag(next, 'name="title"', `<meta name="title" content="${metadata.title}" />`);
|
|
29
|
+
next = upsertMetaTag(next, 'property="og:title"', `<meta property="og:title" content="${metadata.title}" />`);
|
|
30
|
+
next = upsertMetaTag(next, 'name="twitter:title"', `<meta name="twitter:title" content="${metadata.title}" />`);
|
|
31
|
+
}
|
|
32
|
+
if (metadata.description) {
|
|
33
|
+
next = upsertMetaTag(next, 'name="description"', `<meta name="description" content="${metadata.description}" />`);
|
|
34
|
+
next = upsertMetaTag(next, 'property="og:description"', `<meta property="og:description" content="${metadata.description}" />`);
|
|
35
|
+
next = upsertMetaTag(next, 'name="twitter:description"', `<meta name="twitter:description" content="${metadata.description}" />`);
|
|
36
|
+
}
|
|
37
|
+
if (metadata.canonical) {
|
|
38
|
+
next = upsertMetaTag(next, 'property="og:url"', `<meta property="og:url" content="${metadata.canonical}" />`);
|
|
39
|
+
next = upsertLinkTag(next, 'rel="canonical"', `<link rel="canonical" href="${metadata.canonical}" />`);
|
|
40
|
+
}
|
|
41
|
+
if (metadata.image) {
|
|
42
|
+
next = upsertMetaTag(next, 'property="og:image"', `<meta property="og:image" content="${metadata.image}" />`);
|
|
43
|
+
next = upsertMetaTag(next, 'name="twitter:image"', `<meta name="twitter:image" content="${metadata.image}" />`);
|
|
44
|
+
}
|
|
45
|
+
if (metadata.robots) {
|
|
46
|
+
next = upsertMetaTag(next, 'name="robots"', `<meta name="robots" content="${metadata.robots}" />`);
|
|
47
|
+
}
|
|
48
|
+
if (metadata.siteName) {
|
|
49
|
+
next = upsertMetaTag(next, 'property="og:site_name"', `<meta property="og:site_name" content="${metadata.siteName}" />`);
|
|
50
|
+
}
|
|
51
|
+
if (metadata.ogImageWidth) {
|
|
52
|
+
next = upsertMetaTag(next, 'property="og:image:width"', `<meta property="og:image:width" content="${metadata.ogImageWidth}" />`);
|
|
53
|
+
}
|
|
54
|
+
if (metadata.ogImageHeight) {
|
|
55
|
+
next = upsertMetaTag(next, 'property="og:image:height"', `<meta property="og:image:height" content="${metadata.ogImageHeight}" />`);
|
|
56
|
+
}
|
|
57
|
+
if (metadata.twitterCard) {
|
|
58
|
+
next = upsertMetaTag(next, 'name="twitter:card"', `<meta name="twitter:card" content="${metadata.twitterCard}" />`);
|
|
59
|
+
}
|
|
60
|
+
if (alternates.length > 0) {
|
|
61
|
+
next = removeAlternateLinks(next);
|
|
62
|
+
next = insertBeforeHeadEnd(next, getAlternateLinkTags(alternates));
|
|
63
|
+
}
|
|
64
|
+
return next;
|
|
65
|
+
};
|
|
66
|
+
export const getStaticRouteOutputPath = (routePath) => {
|
|
67
|
+
const normalized = routePath === '' ? '/' : routePath;
|
|
68
|
+
if (normalized === '/')
|
|
69
|
+
return 'index.html';
|
|
70
|
+
return `${normalized.replace(/^\/+/, '').replace(/\/+$/, '')}/index.html`;
|
|
71
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { applyHtmlMetadata, getAlternateLinkTags, getStaticRouteOutputPath, removeAlternateLinks, upsertLinkTag, upsertMetaTag, } from './html.js';
|
|
2
|
+
export type { AlternateLink, HtmlMetadata } from './html.js';
|
|
3
|
+
export { buildSitemapXml } from './sitemap.js';
|
|
4
|
+
export type { SitemapEntry, SitemapOptions } from './sitemap.js';
|
|
5
|
+
export { escapeHtml, escapeXml } from './text.js';
|
|
6
|
+
export { getAbsoluteUrl, getCanonicalUrl, isAbsoluteUrl, normalizeBaseUrl, normalizeRoutePath, } from './url.js';
|
|
7
|
+
export type { CanonicalUrlOptions } from './url.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { applyHtmlMetadata, getAlternateLinkTags, getStaticRouteOutputPath, removeAlternateLinks, upsertLinkTag, upsertMetaTag, } from './html.js';
|
|
2
|
+
export { buildSitemapXml } from './sitemap.js';
|
|
3
|
+
export { escapeHtml, escapeXml } from './text.js';
|
|
4
|
+
export { getAbsoluteUrl, getCanonicalUrl, isAbsoluteUrl, normalizeBaseUrl, normalizeRoutePath, } from './url.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type SitemapEntry = {
|
|
2
|
+
changefreq?: string;
|
|
3
|
+
lastmod?: string;
|
|
4
|
+
loc: string;
|
|
5
|
+
priority?: string;
|
|
6
|
+
};
|
|
7
|
+
export type SitemapOptions = {
|
|
8
|
+
lastmod?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare const buildSitemapXml: (entries: readonly SitemapEntry[], options?: SitemapOptions) => string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { escapeXml } from './text.js';
|
|
2
|
+
export const buildSitemapXml = (entries, options = {}) => {
|
|
3
|
+
const body = entries
|
|
4
|
+
.map((entry) => {
|
|
5
|
+
const lastmod = entry.lastmod ?? options.lastmod;
|
|
6
|
+
const parts = [
|
|
7
|
+
' <url>',
|
|
8
|
+
` <loc>${escapeXml(entry.loc)}</loc>`,
|
|
9
|
+
];
|
|
10
|
+
if (lastmod)
|
|
11
|
+
parts.push(` <lastmod>${escapeXml(lastmod)}</lastmod>`);
|
|
12
|
+
if (entry.changefreq)
|
|
13
|
+
parts.push(` <changefreq>${escapeXml(entry.changefreq)}</changefreq>`);
|
|
14
|
+
if (entry.priority)
|
|
15
|
+
parts.push(` <priority>${escapeXml(entry.priority)}</priority>`);
|
|
16
|
+
parts.push(' </url>');
|
|
17
|
+
return parts.join('\n');
|
|
18
|
+
})
|
|
19
|
+
.join('\n');
|
|
20
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${body}\n</urlset>\n`;
|
|
21
|
+
};
|
package/dist/seo/text.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type CanonicalUrlOptions = {
|
|
2
|
+
rootTrailingSlash?: boolean;
|
|
3
|
+
trailingSlash?: boolean;
|
|
4
|
+
};
|
|
5
|
+
export declare const normalizeBaseUrl: (baseUrl: string) => string;
|
|
6
|
+
export declare const normalizeRoutePath: (routePath: string) => string;
|
|
7
|
+
export declare const isAbsoluteUrl: (value: string) => boolean;
|
|
8
|
+
export declare const getCanonicalUrl: (baseUrl: string, routePath: string, options?: CanonicalUrlOptions) => string;
|
|
9
|
+
export declare const getAbsoluteUrl: (value: string, baseUrl: string, options?: CanonicalUrlOptions) => string;
|
package/dist/seo/url.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const normalizeBaseUrl = (baseUrl) => baseUrl.replace(/\/+$/, '');
|
|
2
|
+
export const normalizeRoutePath = (routePath) => {
|
|
3
|
+
const path = routePath.trim();
|
|
4
|
+
if (!path || path === '/')
|
|
5
|
+
return '/';
|
|
6
|
+
return `/${path.replace(/^\/+/, '').replace(/\/+$/, '')}`;
|
|
7
|
+
};
|
|
8
|
+
export const isAbsoluteUrl = (value) => /^https?:\/\//i.test(value);
|
|
9
|
+
export const getCanonicalUrl = (baseUrl, routePath, options = {}) => {
|
|
10
|
+
const base = normalizeBaseUrl(baseUrl);
|
|
11
|
+
const path = normalizeRoutePath(routePath);
|
|
12
|
+
if (path === '/') {
|
|
13
|
+
return options.rootTrailingSlash ? `${base}/` : base;
|
|
14
|
+
}
|
|
15
|
+
const canonical = `${base}${path}`;
|
|
16
|
+
return options.trailingSlash ? `${canonical}/` : canonical;
|
|
17
|
+
};
|
|
18
|
+
export const getAbsoluteUrl = (value, baseUrl, options = {}) => (isAbsoluteUrl(value) ? value : getCanonicalUrl(baseUrl, value, options));
|