@frontity/head-tags
Version:
Integrate your Frontity site with REST API - Head Tags by Frontity
289 lines (257 loc) • 7.83 kB
text/typescript
import { warn } from "frontity";
import { State } from "frontity/types";
import { Packages, HeadTag, WithHeadTags } from "../../types";
import {
isPostType,
isTerm,
isAuthor,
isPostTypeArchive,
} from "@frontity/source";
// Attributes that could contain links.
const possibleLink = ["href", "content"];
/**
* Iterates over an object, executing the suplied function.
*
* @param obj - The object that will be iterated.
* @param func - The function that will be executed. The first parameter is the
* object property and the rest are defined in `args`.
* @param args - The rest of the parameters of the function.
*/
const deepTransform = <
Func extends (value: string, ...args: Args) => string,
Args extends any[]
>(
obj: any,
func: Func,
...args: Args
): void => {
// Iterate over keys.
for (const key in obj) {
// Get value.
const value = obj[key];
// Transform value if it is a string.
if (typeof value === "string") obj[key] = func(value, ...args);
// Iterate over props if it is an object.
else if (typeof value === "object") deepTransform(value, func, ...args);
}
};
/**
* Checks if the link should be transformed to the Frontity URL or left as it
* is, usually the WordPress URL.
*
* @param link - The link that will be checked.
* @param base - The base that the link needs to have, usually the WordPress
* URL.
* @param ignore - A Regexp (in a string format) used to know if the link
* should be changed or not.
*
* @returns A boolean indicating if the link should be transformed.
*/
const shouldTransform = (link: string, base: string, ignore: string) => {
return (
link.startsWith(base) && !new RegExp(ignore).test(link.replace(base, ""))
);
};
/**
* The function that actually transforms the link in the new one.
*
* @param link - The original link.
* @param base - The old base (hostname and path). Usually the WordPress URL.
* @param newBase - The new base (hostname and path). Usually the Frontity URL.
*
* @returns The transformed link.
*/
const getNewLink = (link: string, base: string, newBase: string) => {
const { pathname, search, hash } = new URL(link);
const finalPathname = pathname.replace(
new RegExp(`^${new URL(base).pathname}`),
"/"
);
return `${newBase.replace(/\/?$/, "")}${finalPathname}${search}${hash}`;
};
/**
* The options of the {@link transformLink} function.
*/
interface TransformLinkOptions {
/**
* The link that will be changed.
*/
link: string;
/**
* The old base (hostname and path) that will be changed by the `newBase`.
*/
base: string;
/**
* The new base that will replace the old `base`.
*/
newBase: string;
/**
* A Regexp (in string format) so that if the link matches the transform
* doesn't happen.
*/
ignore: string;
}
/**
* Changes the URL hostname to the Frontity one, instead of the WordPress one.
*
* @param options - Defined in {@link TransformLinkOptions}.
*
* @returns The new link.
*/
export const transformLink = ({
link,
ignore,
base,
newBase,
}: TransformLinkOptions) => {
if (shouldTransform(link, base, ignore))
return getNewLink(link, base, newBase);
return link;
};
/**
* The options of the {@link transformHeadTags} function.
*/
interface TransformHeadTagsOptions {
/**
* The Frontity state.
*/
state: State<Packages>;
/**
* The `head_tags` array that usually comes from the REST API.
*/
headTags: HeadTag[];
}
/**
* Return a copy of the given head tags, transforming all links found that
* should point to the Frontity server according to
* `state.source.transformLinks`.
*
* @param options - Defined in {@link TransformHeadTagsOptions}.
*
* @returns The modified head tags array.
*/
export const transformHeadTags = ({
state,
headTags,
}: TransformHeadTagsOptions) => {
/**
* At this point we assume that `state.headTags.transformLinks` and
* `state.frontity.url` are defined.
*/
if (!state.headTags.transformLinks || !state.frontity.url) return headTags;
// Prefix of links to change.
const base =
state.headTags.transformLinks.base || state.source.url.replace(/\/?$/, "/");
const ignore = state.headTags.transformLinks.ignore;
// The site URL.
const newBase = state.frontity.url;
// For each head tag...
return headTags.map(({ tag, attributes, content }) => {
// Init processed head tag.
const processed: HeadTag = { tag };
if (content) {
// Set initial content value.
processed.content = content;
// Transform URLs inside JSON content.
if (
attributes &&
attributes.type &&
attributes.type.endsWith("ld+json")
) {
// Try to parse the tag content.
let json: any;
try {
json = JSON.parse(content);
} catch (e) {
warn(
`The following content of a <script type="ld+json"> tag is not a valid JSON. Links in that tag will not be changed.
${content}`
);
}
// Iterate over json props.
if (json) {
deepTransform(json, (link: string) => {
return transformLink({ link, ignore, base, newBase });
});
// Stringify json again.
processed.content = JSON.stringify(json);
}
}
}
// Process Attributes.
if (attributes) {
processed.attributes = Object.entries(attributes)
.map(([key, link]) => {
// Change link if it's a WP blog link.
if (possibleLink.includes(key)) {
link = transformLink({ link, ignore, base, newBase });
}
// Return the entry.
return [key, link];
})
.reduce((result, [key, link]) => {
result[key] = link;
return result;
}, {});
}
// Return processed head tag.
return processed;
});
};
/**
* The options of the {@link getHeadTags} function.
*/
interface GetHeadTagsOptions {
/**
* The Frontity state.
*/
state: State<Packages>;
/**
* The link (URL) of the entity.
*/
link: string;
}
/**
* Get the head tags stored in the entity pointed by `link`, or an empty array
* if there is no entity nor head tags.
*
* @param options - Defined in {@link HeadTagsOptions}.
*
* @returns Either the transformed head tags or an empty array if they are not
* found.
*/
export const getHeadTags = ({ state, link }: GetHeadTagsOptions) => {
// Get the data object associated to link.
const data = state.source.get(link);
// Get the entity pointed by the given link.
let entity: WithHeadTags = null;
// Entities are stored in different places depending on their type.
if (isPostType(data)) {
const { type, id } = data;
entity = state.source[type][id];
} else if (isTerm(data)) {
const { taxonomy, id } = data;
entity = state.source[taxonomy][id];
} else if (isAuthor(data)) {
const { id } = data;
entity = state.source.author[id];
} else if (isPostTypeArchive(data)) {
const { type } = data;
entity = state.source.type[type];
}
// Get the `head_tags` field from the entity.
const headTags = entity?.head_tags;
// Return an empty array if there is no entity or head tags.
if (!headTags) return [];
// Leave head tags unchanged if `transform` is set to false.
if (!state.headTags.transformLinks) return headTags;
// Do not change links if `state.frontity.url` is not defined.
if (!state.frontity || !state.frontity.url) {
warn(
"Property `state.headTags.links.transform` is defined but `state.frontity.url` is not. All links in <head> tags pointing to other site (e.g. WordPress) instead to the Frontity site won't be changed."
);
return headTags;
}
// Transform links.
return transformHeadTags({ state, headTags });
};