How to create and use a new ACF field
In this scenario, editors want to be able to add "read more" links to pages that will be displayed after the main content of the page. We will solve this by adding an ACF field for the links and then display them in the front-end via shadowing.
Step 1: Create field group
The first step is to create the ACF field for the links. Here is the recommended way to do that:
- Create a php-file in /wp-content/mu-plugins.
- Add an action for the
"acf/init"hook. Using that hook instead of"init"ensures that all ACF API functions are loaded, - Use
acf_add_local_field_groupto create a field group oracf_add_local_fieldto add a field to an existing one. - Field group keys should be start with
group_and then the plugin name (ormuif it is an MU plugin), followed by a brief reference to the purpose of the fields, e.g."group_mu_readmore"for a "Read More" field group. - Field keys should be start with
field_, followed by the group key (withoutgroup_) and then the name of the field, e.g."field_mu_readmore_links". - Provide as little config as possible. A lot of options for ACF fields can be left out and use the default values.
- For fields to be avaible in the GraphQL API, you must set
"show_in_graphql"totrueon both field groups and fields. - Make sure to make all strings available for translation by using the
__and_xfunctions. Usemu-pluginas the text domain for MU plugins. - Set
"map_graphql_types_from_location_rules"tofalseand use the"graphql_types”option if you need to override where the field will show up in GraphQL.
Here’s a complete example:
# /wp-content/mu-plugins/readmore.php
add_action("acf/init", function () {
acf_add_local_field_group([
"key" => "group_mu_readmore",
"title" => __("Read more", "mu-plugin"),
"fields" => [
[
"key" => "field_mu_readmore_links",
"label" => __("Links", "mu-plugin"),
"name" => "links",
"type" => "repeater",
"button_label" => __("Add link", 'mu-plugin'),
"sub_fields" => [
[
"key" => "field_mu_readmore_links_link",
"label" => "Länk",
"name" => "link",
"type" => "link",
"show_in_graphql" => 1,
],
],
],
],
"location" => [
[
[
"param" => "post_type",
"operator" => "==",
"value" => "page",
],
],
],
"show_in_graphql" => 1,
"graphql_field_name" => "readmoreLinks",
"map_graphql_types_from_location_rules" => 1,
"graphql_types" => [
"Page",
],
]);
});
If you are unsure what options are available you can instead create the field group using the UI in WP-admin and then export it as PHP. Make sure to remove any unecessary options from the resulting array though.
Use the GraphiQL IDE to check that your new field has been registered correctly.
Step 2: Fetch the field value in Gatsby
In our example we’ve added a readmoreLinks field to the Page GraphQL type
via ACF. This means that data can now be fetched in the GraphQL queries. We want
to fetch the links when we fetch the other data for the pages, which is done
with the createPages API in @whitespace/gatsby-theme-wordpress-basic. To add
more fields to that query we alter the fragments in src/fragments.
Open src/fragments/WP_PageForPage.js and add the following to the fragment:
# /src/fragments/WP_PageForPage.js
fragment WP_PageForPage on WP_Page {
readmoreLinks {
links {
link {
title
url
target
}
}
}
}
Step 3: Use the field data
The data will now be available whenever you use the usePageContext hook.
To display the links right after the page text we will have to make changes to a
component. In this case <ArticleBody> is a good choice. It’s provided by
@whitespace/gatsby-theme-wordpress-basic and located at
src/components/ArticleBody.js inside that package. We cannot change this file
directyl though since it’s inside node_modules and handled by Yarn. Instead we
use a Gatbsy featured called
shadowing.
Note however that ArticleBody.js has already been shadowed in
@municipio/gatsby-theme-basic so we base our shadow on that shadow instead of
the original, or else we will lose some functionality. That shadow is located at
node_modules/@municipio/gatsby-theme-basic/src/@whitespace/gatsby-theme-wordpress-basic/components/ArticleBody.js.
The content of that file is this:
export { default } from "../../../components/ArticleBody";
The actual component that replaces <ArticleBody> in
@whitespace/gatsby-theme-wordpress-basic is therefore in this file:
node_modules/@municipio/gatsby-theme-basic/src/components/ArticleBody.js, so
that is the one we want to duplicate. In this perticular case we can choose to
shadow the original in @whitespace/gatsby-theme-wordpress-basic or this new
file (referenced in the shadow) in @municipio/gatsby-theme-basic, so that our
file will be located at either
src/@whitespace/gatsby-theme-wordpress-basic/components/ArticleBody.js or
src/@municipio/gatsby-theme-basic/src/components/ArticleBody.js. This is not
the most common scenario though, and in most cases the shadow should be placed
in a folder that matched the original file, and in this case that is also a
possible option and we will do that for simplicity.
Copy node_modules/@municipio/gatsby-theme-basic/src/components/ArticleBody.js
to src/@whitespace/gatsby-theme-wordpress-basic/components/ArticleBody.js. You
must also change any imports in this file if they contain relative paths so that
it points back to their original position.
// src/@whitespace/gatsby-theme-wordpress-basic/components/ArticleBody.js
import { PageContent } from "@whitespace/gatsby-theme-wordpress-basic/src/components";
import * as defaultStyles from "@whitespace/gatsby-theme-wordpress-basic/src/components/ArticleBody.module.css";
import TextContent from "@whitespace/gatsby-theme-wordpress-basic/src/components/TextContent";
import WPBlocks from "@whitespace/gatsby-theme-wordpress-basic/src/components/WPBlocks";
import { usePageContext } from "@whitespace/gatsby-theme-wordpress-basic/src/hooks";
import clsx from "clsx";
import PropTypes from "prop-types";
import React from "react";
// Replaced relative imports:
// import ModularityArea from "./ModularityArea";
// New absolute imports:
import ModularityArea from "@municipio/gatsby-theme-basic/src/components/ModularityArea";
ArticleBody.propTypes = {
styles: PropTypes.objectOf(PropTypes.string),
className: PropTypes.string,
};
export default function ArticleBody({ styles = defaultStyles, ...restProps }) {
let {
contentNode: {
blocksJSON,
content: contentHTML,
contentArea,
contentMedia,
},
} = usePageContext();
return (
<TextContent {...restProps}>
{blocksJSON ? (
<>
<WPBlocks
blocks={JSON.parse(blocksJSON)}
contentMedia={contentMedia}
/>
</>
) : (
<PageContent input={contentHTML} contentMedia={contentMedia}>
{({ preamble, content }) => (
<>
{preamble && (
<div className={clsx(styles.preamble)}>{preamble}</div>
)}
{content}
<ModularityArea area={contentArea} />
</>
)}
</PageContent>
)}
</TextContent>
);
}
We should then add a "Read more" section directly after the "content area"
modularity area. It is recommended to separate code into components as much as
possible, so one good way forward here would be to simply create a new component
for that section and include it in <ArticleBody>:
// src/@whitespace/gatsby-theme-wordpress-basic/components/ArticleBody.js
// ...
import ArticleReadMoreSection from "../../../components/ArticleReadMoreSection";
export default function ArticleBody({ styles = defaultStyles, ...restProps }) {
// ...
return (
<TextContent {...restProps}>
{blocksJSON ? (
<>{/* ... */}</>
) : (
<PageContent input={contentHTML} contentMedia={contentMedia}>
{({ preamble, content }) => (
<>
{/* ... */}
<ModularityArea area={contentArea} />
<ArticleReadMoreSection />
</>
)}
</PageContent>
)}
</TextContent>
);
}
Then create the component. Please read the comments in the following example of such a component for more tips and tricks:
// src/components/ArticleReadMoreSection.js
import { H } from "@jfrk/react-heading-levels";
import { Link } from "@whitespace/components";
import { usePageContext } from "@whitespace/gatsby-theme-wordpress-basic/src/hooks";
import React from "react";
import { useTranslation } from "react-i18next";
export default function ArticleReadMoreSection() {
// To translate the heading of this section we use i18next.
const { t } = useTranslation();
// usePageContext can be used anywhere to access data for the current page fetched via GraphQL.
let {
contentNode: { readmoreLinks },
} = usePageContext();
// We use optional chaining because the field value can be `null`.
let items = readmoreLinks?.links?.filter((item) => item?.link?.url);
// We don’t render any section at all if there are no links.
if (!items?.length) {
return null;
}
return (
<section>
<H>{t("readMore")}</H>
<ul>
{items.map(({ link }) => {
return (
<li>
<Link to={link.url} target={link.target}>
{link.title || link.url}
</Link>
</li>
);
})}
</ul>
</section>
);
}