UNPKG

frontitygit

Version:

A Frontity source package for the REST API of self-hosted and WordPress.com sites

355 lines (320 loc) 10.8 kB
import { error, batch } from "frontity"; import WpSource from "../types"; import { addFinalSlash, concatLink } from "./libraries/route-utils"; import { wpOrg, wpCom } from "./libraries/patterns"; import { getMatch } from "./libraries/get-match"; import { postTypeHandler, postTypeArchiveHandler, postTypeWithQueryHandler, taxonomyHandler, } from "./libraries/handlers"; import { ErrorData } from "@frontity/source/types/data"; import { ServerError, isError, isSearch } from "@frontity/source"; import { is404Redirection, isEagerRedirection, fetchRedirection, } from "./utils"; const actions: WpSource["actions"]["source"] = { fetch: ({ state, libraries }) => async (...params) => { const [resource, options] = params; const { source } = state; // Get the normalize and parse from libraries instead of importing them. // This way they can be e.g. overriden at runtime by another package const { normalize, parse, stringify } = libraries.source; // Get link and link params. const link = normalize(resource); const linkParams = parse(resource); const { query, page, hash } = linkParams; // Get options. const force = options ? options.force : false; // Get current data object. let data = source.data[link]; // Initialize `data` if it does not exist yet. Also, reinitialize it only in // the case `data` is an error and `{ force: true }` is used. if (!data || (force && isError(data))) { data = source.data[link] = { isFetching: true, isReady: false, route: linkParams.route, link, query, page, }; } else { // Reassign `route`, `link`, `query`, `page` to fix custom handlers that // do not add them. Object.assign(data, { route: linkParams.route, link, query, page, }); // Stop fetching if data is ready or being fetched and `{ force: true }` // is not used. if (!force && (data.isReady || data.isFetching)) return; // Reached this point, make sure `isFetching` is true. data.isFetching = true; } // Get and execute the corresponding handler based on path. try { let { route } = linkParams; const redirection = getMatch( { route, link }, libraries.source.redirections ); // Create a variable that holds the value of the link that we will later // pass to the `getMatch()` function. If there is a redirection, we update // the value of `route`, so we have to derive the link again based on the // "new" (redirected) route. let linkForHandler = link; if (redirection) { // Transform route if there is some redirection. route = redirection.func(redirection.params); // Derive the link from the "redirected" route. // We have to add the route, page, query and hash back by calling `stringify()` // because they might have been removed by the redirection. linkForHandler = addFinalSlash(stringify({ route, page, query, hash })); } // Check if we need to check if it is a 30X redirection before fetching // the backend. if (isEagerRedirection(state.source.redirections, link)) { const redirection = await fetchRedirection({ link, state }); // If there is a redirection, populate the data object and finish here. if (redirection?.isRedirection) { batch(() => Object.assign(source.data[link], redirection)); return; } } // Get the handler for this route. const handler = getMatch( { route, link: linkForHandler }, libraries.source.handlers ); // Return a 404 error if no handler has matched. if (!handler) throw new ServerError( `No handler has matched for the given link: "${link}"`, 404 ); // Execute the handler. await handler.func({ link, // This `route` parameter does not match `data.route`. It is the name // given to `link` prior to @frontity/wp-source@1.6.0. route: link, params: handler.params, state, libraries, force, }); // Check if this is the homepage URL if it is either the root "/" or the // subdirectory "/folder/", but only in the case that it is not a search // and the matched handler is not used to match for queries (the ones that // start with "RegExp:"). const isHome = !handler.pattern.startsWith("RegExp:") && isSearch(source.data[link]) !== true && source.data[link].route === normalize(state.source.subdirectory || "/"); // Mark the data object as ready. // TODO: We should remove data.isHome to the handlers in Source v2. batch(() => Object.assign(source.data[link], { ...(isHome && { isHome: true }), isFetching: false, isReady: true, }) ); } catch (error) { // If it's NOT a server error (4xx or 5xx) it means it is an error in the // code, so we should throw. if (error.name !== "ServerError") { throw error; } // Check it there is a 301 redirection stored in the backend. if (error.status === 404 && is404Redirection(state.source.redirections)) { const redirection = await fetchRedirection({ link, state }); // If there is a redirection, populate the data object and finish here. if (redirection?.isRedirection) { batch(() => Object.assign(source.data[link], redirection)); return; } } const errorData: ErrorData = { isError: true, isReady: true, isFetching: false, [`is${error.status}`]: true, errorStatus: error.status, errorStatusText: error.statusText, route: linkParams.route, link, query, page, }; source.data[link] = errorData; } }, init: ({ state, libraries }) => { const { api, isWpCom } = state.source; try { new URL(api); } catch (e) { error("Add the URL of your WordPress REST API in state.source.api."); } libraries.source.api.init({ api, isWpCom }); // If the URL contains an auth token, then add it to the state. This is // normally the case e.g, when accessing the post preview. const auth = state.frontity?.options?.sourceAuth; const authFromEnv = process.env.FRONTITY_SOURCE_AUTH; if (auth || authFromEnv) { state.source.auth = auth || authFromEnv; } // Handlers & redirections. const { handlers, redirections } = libraries.source; const patterns = isWpCom ? wpCom : wpOrg; handlers.push(...patterns); // Add handlers for custom post types. state.source.postTypes.forEach(({ type, endpoint, archive }) => { // Single page. handlers.push({ name: type, priority: 10, pattern: concatLink(type, "/(.*)?/:slug"), func: postTypeHandler({ endpoints: [endpoint] }), }); // Query permalink (mainly for drafts). handlers.push({ name: `${type} with query permalink`, priority: 9, pattern: `RegExp:(\\?|&)p=\\d+&post_type=${type}`, func: postTypeWithQueryHandler({ type, endpoint }), }); // Archive. if (archive) handlers.push({ name: `${type} archive`, priority: 10, pattern: concatLink(archive), func: postTypeArchiveHandler({ type, endpoint }), }); }); // Add handlers for custom taxonomies. state.source.taxonomies.forEach( ({ taxonomy, endpoint, postTypeEndpoint, params }) => { handlers.push({ name: taxonomy, priority: 10, pattern: concatLink(taxonomy, "/(.*)?/:slug"), func: taxonomyHandler({ taxonomy, endpoint, postTypeEndpoint, params, }), }); } ); const { subdirectory, homepage, postsPage, categoryBase, tagBase, authorBase, } = state.source; if (homepage) { const pattern = concatLink(subdirectory); redirections.push({ name: "homepage", priority: 10, pattern, func: () => concatLink(homepage), }); } if (postsPage) { const pattern = concatLink(subdirectory, postsPage); redirections.push({ name: "posts page", priority: 10, pattern, func: () => "/", }); } if (categoryBase) { // Add new direction. const pattern = concatLink(subdirectory, categoryBase, "/:subpath+"); redirections.push({ name: "category base", priority: 10, pattern, func: ({ subpath }) => `/category/${subpath}/`, }); // Remove old direction. redirections.push({ name: "category base (reverse)", priority: 10, pattern: concatLink(subdirectory, "/category/(.*)/"), func: () => "", }); } if (tagBase) { // Add new direction. const pattern = concatLink(subdirectory, tagBase, "/:subpath+"); redirections.push({ name: "tag base", priority: 10, pattern, func: ({ subpath }) => `/tag/${subpath}/`, }); // Remove old direction. redirections.push({ name: "tag base (reverse)", priority: 10, pattern: concatLink(subdirectory, "/tag/(.*)/"), func: () => "", }); } if (authorBase) { // Add new direction. const pattern = concatLink(subdirectory, authorBase, "/:subpath+"); redirections.push({ name: "author base", priority: 10, pattern, func: ({ subpath }) => `/author/${subpath}/`, }); // Remove old direction. redirections.push({ name: "author base (reverse)", priority: 10, pattern: concatLink(subdirectory, "/author/(.*)/"), func: () => "", }); } if (subdirectory) { // Add new direction. const pattern = concatLink(subdirectory, "/:subpath*"); redirections.push({ name: "subdirectory", priority: 10, pattern, func: ({ subpath = "" }) => `/${subpath}${subpath ? "/" : ""}`, }); // Remove old direction. redirections.push({ name: "subdirectory (reverse)", priority: 10, pattern: "/(.*)", func: () => "", }); } }, afterSSR: ({ state }) => { // Remove the auth tokens that were used in the server. delete state.source.auth; delete state.frontity.options.sourceAuth; }, }; export default actions;