@micazoyolli/foundation 0.1.2 → 0.3.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 +64 -65
- package/dist/a11y/focus.d.ts +15 -0
- package/dist/a11y/focus.js +57 -0
- package/dist/a11y/index.d.ts +5 -3
- package/dist/a11y/index.js +4 -3
- package/dist/a11y/protectedMedia.js +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -3
- package/dist/seo/dom.d.ts +9 -0
- package/dist/seo/dom.js +35 -0
- package/dist/seo/html.d.ts +22 -0
- package/dist/seo/html.js +71 -0
- package/dist/seo/index.d.ts +9 -0
- package/dist/seo/index.js +5 -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 +3 -2
package/README.md
CHANGED
|
@@ -1,92 +1,91 @@
|
|
|
1
1
|
# @micazoyolli/foundation
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Características
|
|
6
|
-
|
|
7
|
-
- Tokens SCSS base para spacing, radius, z-index y motion
|
|
8
|
-
- Breakpoints y mixins responsive reutilizables
|
|
9
|
-
- Helper `cx` para composición segura de clases
|
|
10
|
-
- Guards TypeScript pequeños para validaciones comunes
|
|
11
|
-
- Utilidades básicas de accesibilidad y protección selectiva de media
|
|
12
|
-
- Exports separados para TypeScript y SCSS
|
|
13
|
-
- Consumo desde npm sin credenciales especiales
|
|
14
|
-
|
|
15
|
-
## Tecnologías
|
|
16
|
-
|
|
17
|
-
- TypeScript
|
|
18
|
-
- SCSS
|
|
19
|
-
- Yarn 1
|
|
20
|
-
- npm
|
|
21
|
-
|
|
22
|
-
## Estructura
|
|
23
|
-
|
|
24
|
-
```txt
|
|
25
|
-
src/
|
|
26
|
-
├── a11y/
|
|
27
|
-
├── scss/
|
|
28
|
-
│ ├── mixins/
|
|
29
|
-
│ └── tokens/
|
|
30
|
-
├── utils/
|
|
31
|
-
└── index.ts
|
|
32
|
-
|
|
33
|
-
examples/
|
|
34
|
-
dist/
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Scripts
|
|
3
|
+
Fundamentos compartidos no visuales para los proyectos de Micazoyolli.
|
|
38
4
|
|
|
39
|
-
|
|
40
|
-
yarn install
|
|
41
|
-
yarn build
|
|
42
|
-
yarn clean
|
|
43
|
-
yarn prepack
|
|
44
|
-
```
|
|
5
|
+
`@micazoyolli/foundation` centraliza utilidades pequenas y estables que ya se repiten entre repos independientes: tokens SCSS base, mixins responsive, helpers TypeScript, accesibilidad ligera y utilidades SEO/build. No incluye React, componentes visuales, tokens de marca, layouts ni metadata especifica de ningun proyecto.
|
|
45
6
|
|
|
46
|
-
##
|
|
7
|
+
## Instalacion
|
|
47
8
|
|
|
48
9
|
```bash
|
|
49
10
|
yarn add @micazoyolli/foundation
|
|
50
11
|
```
|
|
51
12
|
|
|
52
|
-
##
|
|
13
|
+
## Entrypoints
|
|
53
14
|
|
|
54
15
|
TypeScript:
|
|
55
16
|
|
|
56
17
|
```ts
|
|
57
|
-
import { cx,
|
|
58
|
-
|
|
59
|
-
const className = cx('button', isActive && 'button--active');
|
|
60
|
-
|
|
61
|
-
if (isElement(event.target)) {
|
|
62
|
-
event.target.closest('[data-protected-media]');
|
|
63
|
-
}
|
|
18
|
+
import { cx, getCanonicalUrl } from '@micazoyolli/foundation';
|
|
64
19
|
```
|
|
65
20
|
|
|
66
21
|
SCSS:
|
|
67
22
|
|
|
68
23
|
```scss
|
|
69
24
|
@use '@micazoyolli/foundation/scss' as foundation;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Documentacion
|
|
28
|
+
|
|
29
|
+
### Primeros pasos
|
|
30
|
+
|
|
31
|
+
- [Getting Started](./docs/getting-started.md)
|
|
32
|
+
- [Installation](./docs/installation.md)
|
|
33
|
+
|
|
34
|
+
### SCSS
|
|
70
35
|
|
|
71
|
-
.
|
|
72
|
-
|
|
73
|
-
|
|
36
|
+
- [Breakpoints](./docs/scss/breakpoints.md)
|
|
37
|
+
- [Spacing](./docs/scss/spacing.md)
|
|
38
|
+
- [Motion](./docs/scss/motion.md)
|
|
39
|
+
- [Mixins](./docs/scss/mixins.md)
|
|
74
40
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
41
|
+
### TypeScript
|
|
42
|
+
|
|
43
|
+
- [DOM y accesibilidad](./docs/typescript/dom.md)
|
|
44
|
+
- [SEO y build](./docs/typescript/seo.md)
|
|
45
|
+
- [Utils](./docs/typescript/utils.md)
|
|
46
|
+
|
|
47
|
+
### Proyecto
|
|
48
|
+
|
|
49
|
+
- [Changelog](./docs/changelog.md)
|
|
50
|
+
- [Roadmap](./docs/roadmap.md)
|
|
51
|
+
|
|
52
|
+
## Que incluye
|
|
53
|
+
|
|
54
|
+
- Tokens SCSS base para spacing, radius, z-index, motion y breakpoints.
|
|
55
|
+
- Mixins SCSS para responsive y reduced motion.
|
|
56
|
+
- `cx` para composicion simple de clases.
|
|
57
|
+
- Guards TypeScript pequenos.
|
|
58
|
+
- Helpers DOM/a11y para targets, teclado y media protegida.
|
|
59
|
+
- Helpers DOM/a11y para foco, scroll lock, Escape y focus trap.
|
|
60
|
+
- Helpers SEO/build para escaping, canonical, sitemap, metadata DOM y HTML estatico.
|
|
61
|
+
|
|
62
|
+
## Que NO incluye
|
|
63
|
+
|
|
64
|
+
- Componentes React.
|
|
65
|
+
- Botones, cards, layouts o grids visuales.
|
|
66
|
+
- Tokens finales de marca, color o tipografia.
|
|
67
|
+
- Metadata especifica de proyectos.
|
|
68
|
+
- Logica de negocio.
|
|
69
|
+
- Dependencias UI pesadas.
|
|
70
|
+
|
|
71
|
+
## Scripts
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
yarn install
|
|
75
|
+
yarn build
|
|
76
|
+
yarn clean
|
|
77
|
+
yarn prepack
|
|
79
78
|
```
|
|
80
79
|
|
|
81
|
-
##
|
|
80
|
+
## Proyectos que ya lo consumen
|
|
82
81
|
|
|
83
|
-
-
|
|
84
|
-
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
82
|
+
- TeInvitaASu Invitaciones
|
|
83
|
+
- TeInvitaASu.Party
|
|
84
|
+
- Micazoyolli
|
|
85
|
+
- Estilo Natura
|
|
86
|
+
- OhMamaMXX
|
|
87
|
+
- WTFashion
|
|
89
88
|
|
|
90
89
|
## Autora
|
|
91
90
|
|
|
92
|
-
Una
|
|
91
|
+
Una creacion de [`<micazoyolli />`](https://nadia.dev)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const FOCUSABLE_SELECTOR: string;
|
|
2
|
+
export type BodyScrollLock = {
|
|
3
|
+
document: Document;
|
|
4
|
+
previousOverflow: string;
|
|
5
|
+
};
|
|
6
|
+
type FocusableElement = HTMLElement & {
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare const getFocusableElements: (container: Pick<ParentNode, "querySelectorAll">) => FocusableElement[];
|
|
10
|
+
export declare const lockBodyScroll: (documentRef?: Document) => BodyScrollLock;
|
|
11
|
+
export declare const unlockBodyScroll: (lock: BodyScrollLock) => void;
|
|
12
|
+
export declare const restoreFocus: (element: Element | null | undefined, documentRef?: Document) => void;
|
|
13
|
+
export declare const trapTabKey: (event: Pick<KeyboardEvent, "key" | "shiftKey" | "preventDefault">, container: Pick<ParentNode, "querySelectorAll">, activeElement?: Element | null | undefined) => boolean;
|
|
14
|
+
export declare const createEscapeKeyHandler: (onEscape: () => void) => (event: Pick<KeyboardEvent, "key">) => void;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { KEYBOARD_KEYS } from './keyboard.js';
|
|
2
|
+
export const FOCUSABLE_SELECTOR = [
|
|
3
|
+
'a[href]',
|
|
4
|
+
'button:not([disabled])',
|
|
5
|
+
'input:not([disabled])',
|
|
6
|
+
'select:not([disabled])',
|
|
7
|
+
'textarea:not([disabled])',
|
|
8
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
9
|
+
].join(',');
|
|
10
|
+
const canReceiveFocus = (element) => !element.disabled
|
|
11
|
+
&& element.getAttribute('aria-hidden') !== 'true'
|
|
12
|
+
&& element.tabIndex !== -1;
|
|
13
|
+
export const getFocusableElements = (container) => Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter(canReceiveFocus);
|
|
14
|
+
export const lockBodyScroll = (documentRef = document) => {
|
|
15
|
+
const previousOverflow = documentRef.body.style.overflow;
|
|
16
|
+
documentRef.body.style.overflow = 'hidden';
|
|
17
|
+
return {
|
|
18
|
+
document: documentRef,
|
|
19
|
+
previousOverflow,
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export const unlockBodyScroll = (lock) => {
|
|
23
|
+
lock.document.body.style.overflow = lock.previousOverflow;
|
|
24
|
+
};
|
|
25
|
+
export const restoreFocus = (element, documentRef = document) => {
|
|
26
|
+
if (!element || !documentRef.body.contains(element))
|
|
27
|
+
return;
|
|
28
|
+
const focusable = element;
|
|
29
|
+
if (typeof focusable.focus === 'function') {
|
|
30
|
+
focusable.focus();
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
export const trapTabKey = (event, container, activeElement = document.activeElement) => {
|
|
34
|
+
if (event.key !== 'Tab')
|
|
35
|
+
return false;
|
|
36
|
+
const focusableElements = getFocusableElements(container);
|
|
37
|
+
const firstElement = focusableElements[0];
|
|
38
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
39
|
+
if (!firstElement || !lastElement)
|
|
40
|
+
return false;
|
|
41
|
+
if (event.shiftKey && activeElement === firstElement) {
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
lastElement.focus();
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (!event.shiftKey && activeElement === lastElement) {
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
firstElement.focus();
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
};
|
|
53
|
+
export const createEscapeKeyHandler = (onEscape) => (event) => {
|
|
54
|
+
if (event.key === KEYBOARD_KEYS.escape) {
|
|
55
|
+
onEscape();
|
|
56
|
+
}
|
|
57
|
+
};
|
package/dist/a11y/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
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';
|
|
4
|
+
export { FOCUSABLE_SELECTOR, createEscapeKeyHandler, getFocusableElements, lockBodyScroll, restoreFocus, trapTabKey, unlockBodyScroll, } from './focus.js';
|
|
5
|
+
export type { BodyScrollLock } from './focus.js';
|
package/dist/a11y/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
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';
|
|
4
|
+
export { FOCUSABLE_SELECTOR, createEscapeKeyHandler, getFocusableElements, lockBodyScroll, restoreFocus, trapTabKey, unlockBodyScroll, } from './focus.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export { KEYBOARD_KEYS, PROTECTED_MEDIA_SELECTOR, isElement, isHTMLElement, isKeyboardActivation, isProtectedMediaTarget, } from './a11y';
|
|
2
|
-
export {
|
|
3
|
-
export
|
|
4
|
-
export {
|
|
1
|
+
export { FOCUSABLE_SELECTOR, KEYBOARD_KEYS, PROTECTED_MEDIA_SELECTOR, createEscapeKeyHandler, getFocusableElements, isElement, isHTMLElement, isKeyboardActivation, isProtectedMediaTarget, lockBodyScroll, restoreFocus, trapTabKey, unlockBodyScroll, } from './a11y/index.js';
|
|
2
|
+
export type { BodyScrollLock } from './a11y/index.js';
|
|
3
|
+
export { cx } from './utils/cx.js';
|
|
4
|
+
export type { ClassValue } from './utils/cx.js';
|
|
5
|
+
export { isNonEmptyString } from './utils/guards.js';
|
|
6
|
+
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 { FOCUSABLE_SELECTOR, KEYBOARD_KEYS, PROTECTED_MEDIA_SELECTOR, createEscapeKeyHandler, getFocusableElements, isElement, isHTMLElement, isKeyboardActivation, isProtectedMediaTarget, lockBodyScroll, restoreFocus, trapTabKey, unlockBodyScroll, } 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,9 @@
|
|
|
1
|
+
export type MetaAttributes = {
|
|
2
|
+
content: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
property?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare const updateDocumentTitle: (title: string, documentRef?: Document) => void;
|
|
7
|
+
export declare const upsertMeta: (selector: string, attributes: MetaAttributes, documentRef?: Document) => HTMLMetaElement;
|
|
8
|
+
export declare const upsertCanonical: (href: string, documentRef?: Document) => HTMLLinkElement;
|
|
9
|
+
export declare const upsertAlternate: (hreflang: string, href: string, documentRef?: Document) => HTMLLinkElement;
|
package/dist/seo/dom.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const updateDocumentTitle = (title, documentRef = document) => {
|
|
2
|
+
documentRef.title = title;
|
|
3
|
+
};
|
|
4
|
+
export const upsertMeta = (selector, attributes, documentRef = document) => {
|
|
5
|
+
let element = documentRef.head.querySelector(selector);
|
|
6
|
+
if (!element) {
|
|
7
|
+
element = documentRef.createElement('meta');
|
|
8
|
+
documentRef.head.appendChild(element);
|
|
9
|
+
}
|
|
10
|
+
Object.entries(attributes).forEach(([name, value]) => {
|
|
11
|
+
element?.setAttribute(name, value);
|
|
12
|
+
});
|
|
13
|
+
return element;
|
|
14
|
+
};
|
|
15
|
+
export const upsertCanonical = (href, documentRef = document) => {
|
|
16
|
+
let element = documentRef.head.querySelector('link[rel="canonical"]');
|
|
17
|
+
if (!element) {
|
|
18
|
+
element = documentRef.createElement('link');
|
|
19
|
+
element.rel = 'canonical';
|
|
20
|
+
documentRef.head.appendChild(element);
|
|
21
|
+
}
|
|
22
|
+
element.href = href;
|
|
23
|
+
return element;
|
|
24
|
+
};
|
|
25
|
+
export const upsertAlternate = (hreflang, href, documentRef = document) => {
|
|
26
|
+
let element = documentRef.head.querySelector(`link[rel="alternate"][hreflang="${hreflang}"]`);
|
|
27
|
+
if (!element) {
|
|
28
|
+
element = documentRef.createElement('link');
|
|
29
|
+
element.rel = 'alternate';
|
|
30
|
+
element.hreflang = hreflang;
|
|
31
|
+
documentRef.head.appendChild(element);
|
|
32
|
+
}
|
|
33
|
+
element.href = href;
|
|
34
|
+
return element;
|
|
35
|
+
};
|
|
@@ -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,9 @@
|
|
|
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';
|
|
8
|
+
export { updateDocumentTitle, upsertAlternate, upsertCanonical, upsertMeta, } from './dom.js';
|
|
9
|
+
export type { MetaAttributes } from './dom.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
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';
|
|
5
|
+
export { updateDocumentTitle, upsertAlternate, upsertCanonical, upsertMeta, } from './dom.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));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@micazoyolli/foundation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Fundamentos compartidos no visuales para los proyectos de Micazoyolli.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"build": "tsc -p tsconfig.json && yarn copy-scss",
|
|
26
26
|
"clean": "rm -rf dist",
|
|
27
27
|
"copy-scss": "rsync -av --include='*/' --include='*.scss' --exclude='*' src/scss/ dist/scss/",
|
|
28
|
-
"prepack": "yarn build"
|
|
28
|
+
"prepack": "yarn build",
|
|
29
|
+
"test": "yarn build && node --test tests/*.test.mjs"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"typescript": "6.0.3"
|