9
common/resources/client/seo/default-meta-tags.tsx
Executable file
9
common/resources/client/seo/default-meta-tags.tsx
Executable file
@@ -0,0 +1,9 @@
|
||||
import {Helmet} from './helmet';
|
||||
import {useBootstrapData} from '../core/bootstrap-data/bootstrap-data-context';
|
||||
|
||||
export function DefaultMetaTags() {
|
||||
const {
|
||||
data: {default_meta_tags},
|
||||
} = useBootstrapData();
|
||||
return <Helmet tags={default_meta_tags} />;
|
||||
}
|
||||
150
common/resources/client/seo/helmet.tsx
Executable file
150
common/resources/client/seo/helmet.tsx
Executable file
@@ -0,0 +1,150 @@
|
||||
import {Children, memo, ReactElement} from 'react';
|
||||
import {shallowEqual} from '../utils/shallow-equal';
|
||||
import {MetaTag} from './meta-tag';
|
||||
import {TitleMetaTagChildren} from './static-page-title';
|
||||
import {useTrans, UseTransReturn} from '../i18n/use-trans';
|
||||
import {isSsr} from '@common/utils/dom/is-ssr';
|
||||
|
||||
const rafPolyfill = (() => {
|
||||
let clock = Date.now();
|
||||
|
||||
return (callback: Function) => {
|
||||
const currentTime = Date.now();
|
||||
|
||||
if (currentTime - clock > 16) {
|
||||
clock = currentTime;
|
||||
callback(currentTime);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
rafPolyfill(callback);
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
const cafPolyfill = (id: string | number) => clearTimeout(id);
|
||||
|
||||
const requestAnimationFrame = !isSsr()
|
||||
? window.requestAnimationFrame
|
||||
: global.requestAnimationFrame || rafPolyfill;
|
||||
|
||||
const cancelAnimationFrame = !isSsr()
|
||||
? window.cancelAnimationFrame
|
||||
: global.cancelAnimationFrame || cafPolyfill;
|
||||
|
||||
export const helmetAttribute = 'data-be-helmet';
|
||||
let rafId: number | null;
|
||||
|
||||
interface HelmetProps {
|
||||
children?: ReactElement | ReactElement[];
|
||||
tags?: MetaTag[];
|
||||
}
|
||||
export const Helmet = memo(({children, tags}: HelmetProps) => {
|
||||
const {trans} = useTrans();
|
||||
|
||||
if (isSsr()) return null;
|
||||
|
||||
if (!tags && children) {
|
||||
tags = mapChildrenToTags(children, trans);
|
||||
}
|
||||
|
||||
updateTags(tags);
|
||||
|
||||
return null;
|
||||
}, shallowEqual);
|
||||
|
||||
function mapChildrenToTags(
|
||||
children: ReactElement | ReactElement[],
|
||||
trans: UseTransReturn['trans'],
|
||||
): MetaTag[] {
|
||||
return Children.map(children, child => {
|
||||
switch (child.type) {
|
||||
case 'title':
|
||||
return {
|
||||
nodeName: 'title',
|
||||
_text: titleTagChildrenToString(child.props.children, trans),
|
||||
};
|
||||
case 'meta':
|
||||
return {...child.props, nodeName: 'meta'};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function titleTagChildrenToString(
|
||||
children: TitleMetaTagChildren,
|
||||
trans: UseTransReturn['trans'],
|
||||
): string {
|
||||
if (children == null) return '';
|
||||
if (typeof children === 'string') return children;
|
||||
if (Array.isArray(children)) {
|
||||
return children.map(c => titleTagChildrenToString(c, trans)).join('');
|
||||
}
|
||||
if ('message' in children) {
|
||||
return trans(children);
|
||||
}
|
||||
return trans(children.props);
|
||||
}
|
||||
|
||||
function removeOldTags() {
|
||||
if (isSsr()) return;
|
||||
document.head
|
||||
.querySelectorAll(
|
||||
'meta:not([data-keep]), script[type="application/ld+json"]:not([data-keep]), title, link[rel="canonical"]',
|
||||
)
|
||||
.forEach(tag => {
|
||||
document.head.removeChild(tag);
|
||||
});
|
||||
}
|
||||
|
||||
function updateTags(tags?: MetaTag[] | string) {
|
||||
if (rafId) {
|
||||
cancelAnimationFrame(rafId);
|
||||
}
|
||||
rafId = requestAnimationFrame(() => {
|
||||
removeOldTags();
|
||||
|
||||
if (typeof tags === 'string') {
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = tags;
|
||||
template.content.childNodes.forEach(node => {
|
||||
if (node instanceof HTMLElement) {
|
||||
node.setAttribute(helmetAttribute, 'true');
|
||||
document.head.prepend(node);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
tags?.forEach(tag => {
|
||||
updateTag(tag);
|
||||
});
|
||||
}
|
||||
|
||||
rafId = null;
|
||||
});
|
||||
}
|
||||
|
||||
function updateTag(tag: MetaTag) {
|
||||
// update title
|
||||
if (tag.nodeName === 'title') {
|
||||
if (typeof tag._text !== 'undefined' && document.title !== tag._text) {
|
||||
document.title = tag._text;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// update <meta> tag
|
||||
const newElement = document.createElement(tag.nodeName);
|
||||
for (const key in tag) {
|
||||
const attribute = key as keyof MetaTag;
|
||||
if (attribute === 'nodeName') continue;
|
||||
if (attribute === '_text') {
|
||||
newElement.textContent =
|
||||
typeof tag._text === 'string' ? tag._text : JSON.stringify(tag._text);
|
||||
} else {
|
||||
const value = tag[attribute] == null ? '' : tag[attribute];
|
||||
newElement.setAttribute(attribute, value as string);
|
||||
}
|
||||
}
|
||||
|
||||
newElement.setAttribute(helmetAttribute, 'true');
|
||||
document.head.prepend(newElement);
|
||||
}
|
||||
9
common/resources/client/seo/meta-tag.tsx
Executable file
9
common/resources/client/seo/meta-tag.tsx
Executable file
@@ -0,0 +1,9 @@
|
||||
export interface MetaTag {
|
||||
nodeName: 'meta' | 'script' | 'title' | 'link';
|
||||
type?: string;
|
||||
content?: string;
|
||||
property?: string;
|
||||
_text?: string;
|
||||
href?: string;
|
||||
rel?: string;
|
||||
}
|
||||
30
common/resources/client/seo/static-page-title.tsx
Executable file
30
common/resources/client/seo/static-page-title.tsx
Executable file
@@ -0,0 +1,30 @@
|
||||
import {Helmet} from './helmet';
|
||||
import {ReactElement} from 'react';
|
||||
import {MessageDescriptor} from '../i18n/message-descriptor';
|
||||
import {useSettings} from '../core/settings/use-settings';
|
||||
|
||||
type TitleChild =
|
||||
| string
|
||||
| null
|
||||
| ReactElement<MessageDescriptor>
|
||||
| MessageDescriptor;
|
||||
export type TitleMetaTagChildren = TitleChild | TitleChild[];
|
||||
|
||||
interface StaticPageTitleProps {
|
||||
children: TitleMetaTagChildren;
|
||||
}
|
||||
export function StaticPageTitle({children}: StaticPageTitleProps) {
|
||||
const {
|
||||
branding: {site_name},
|
||||
} = useSettings();
|
||||
return (
|
||||
<Helmet>
|
||||
{children ? (
|
||||
// @ts-ignore
|
||||
<title>
|
||||
{children as any} - {site_name}
|
||||
</title>
|
||||
) : undefined}
|
||||
</Helmet>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user