{"id":18334568,"url":"https://github.com/jeverhart383/first-headless-project","last_synced_at":"2025-04-06T04:33:25.832Z","repository":{"id":64574930,"uuid":"576745057","full_name":"JEverhart383/first-headless-project","owner":"JEverhart383","description":"Created with CodeSandbox","archived":false,"fork":false,"pushed_at":"2022-12-13T16:16:13.000Z","size":536,"stargazers_count":41,"open_issues_count":0,"forks_count":17,"subscribers_count":6,"default_branch":"main","last_synced_at":"2025-03-21T16:59:42.218Z","etag":null,"topics":["apollo","graphql","headless-cms","react","wordpress"],"latest_commit_sha":null,"homepage":"https://codesandbox.io/s/first-headless-wordpress-project-acf-wpgraphql-xr5ebt","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/JEverhart383.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-12-10T20:56:26.000Z","updated_at":"2025-01-16T10:05:22.000Z","dependencies_parsed_at":"2022-12-13T17:32:39.185Z","dependency_job_id":null,"html_url":"https://github.com/JEverhart383/first-headless-project","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JEverhart383%2Ffirst-headless-project","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JEverhart383%2Ffirst-headless-project/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JEverhart383%2Ffirst-headless-project/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JEverhart383%2Ffirst-headless-project/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JEverhart383","download_url":"https://codeload.github.com/JEverhart383/first-headless-project/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247435043,"owners_count":20938530,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["apollo","graphql","headless-cms","react","wordpress"],"created_at":"2024-11-05T19:49:42.652Z","updated_at":"2025-04-06T04:33:25.749Z","avatar_url":"https://github.com/JEverhart383.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Your First Headless WordPress Project with ACF and WPGraphQL\n\nThis starter project is a basic React SPA using React Router that consumes data from your WordPress WPGraphQL API. This repo is a teaching tool for the WP Engine Developer Relations team, or anyone interested in teaching or learning more about headless WordPress patterns.  \n\nThis project assumes that you have the following requirements met:\n- A WordPress site (Check out [Local](https://localwp.com/) to create a local WP site)\n- [WPGraphQL](https://www.wpgraphql.com/) Plugin\n- [Advanced Custom Fields](https://www.advancedcustomfields.com/) and [WPGraphQL for ACF](https://www.wpgraphql.com/acf)(optional)\n- [WPGraphQL Smart Cache (beta)](https://github.com/wp-graphql/wp-graphql-smart-cache) (optional)\n\nThe `main` branch of this project is considered the complete version, but since this is a teaching tool, you can look at the different branches of this repository to see how we incorporate data from WordPress step-by-step across the application.\n\nThis tutorial is heavily based on this [crash course on getting started with React and headless WP](https://developers.wpengine.com/blog/build-a-simple-headless-wordpress-app-with-react-wpgraphql).\n\nTo get started, do one of the following: \n- Open [the CodeSandbox for this workshop](https://codesandbox.io/s/first-headless-wordpress-project-acf-wpgraphql-xr5ebt). It's a good idea to create a fork of this project so you can come back to the starting point if needed.\n- Fork this repository or clone locally using `git clone https://github.com/JEverhart383/first-headless-project.git`\n\nFor this workshop, you can use the following endpoints:\n- REST API Base URL: https://acfheadless.wpengine.com/wp-json/wp/v2/posts\n- WPGraphQL URL: https://acfheadless.wpengine.com/graphql\n- WPGraphQL Backup URL: https://api.headlesswp.info/graphql\n\nPlease be a good community member and treat these resource nicely so that everyone can learn 🥳\n\n\n## Step 0: Starting Point\nYou can access the starting point for this tutorial through the `waypoint/start` branch by running `git checkout waypoint/start` in your terminal. At this point, all of our application's data is being sourced from the `dummy-data` directory. \n\n## Step 1: Adding Apollo for Data Fetching\n\nYou can access this waypoint by running `git checkout waypoint/step-one` in your terminal.\n\nDuring this step, we configure an Apollo client instance to pull data from our WordPress site by creating a file in `lib/apollo.js` with the following contents:\n\n```\nimport {\n  ApolloClient,\n  ApolloLink,\n  HttpLink,\n  InMemoryCache\n} from \"@apollo/client\";\n\nconst link = ApolloLink.from([\n  new HttpLink({\n    uri: `https://acfheadless.wpengine.com/graphql`,\n    useGETForQueries: true\n  })\n]);\n\nconst client = new ApolloClient({\n  link,\n  cache: new InMemoryCache()\n});\n\nexport default client;\n\n```\n\nWith the client created, we then use the `ApolloProvider` component to make that data available throughout our component tree using hooks. Replace the contents of `App.js` with the following code:\n\n```\nimport React from \"react\";\nimport { Route, Switch } from \"react-router-dom\";\nimport HomePage from \"./pages/HomePage\";\nimport PostPage from \"./pages/PostPage\";\nimport \"./styles.css\";\n\nimport { ApolloProvider } from \"@apollo/client/react\";\nimport client from \"./lib/apollo\";\n\nexport default function App() {\n  return (\n    \u003cApolloProvider client={client}\u003e\n      \u003cSwitch\u003e\n        \u003cRoute exact path=\"/\" component={HomePage} /\u003e\n        \u003cRoute path=\"/blog/:slug\" component={PostPage} /\u003e\n      \u003c/Switch\u003e\n    \u003c/ApolloProvider\u003e\n  );\n}\n\n```\n\nYou can read more about the React Router [Switch](https://v5.reactrouter.com/web/api/Switch) and [Route](https://v5.reactrouter.com/web/api/Route) components to get a understanding of how to include additional routes.\n\n## Step 2: Query for Data on Home Page\n\nYou can access this waypoint by running `git checkout waypoint/step-two` in your terminal.\n\nThe data displayed on in the `HomePage` component is actually sourced and rendered inside of the `components/PostList.js` component. In this file, you will need to update the imports to include `gql` and `useQuery` from the `@apollo/client` package. From there, we format our query using `gql` and fetch the data using `useQuery` before rendering our `posts` using `PostCard` components.\n\nUpdate your `components/PostList.js` to the following code:\n\n```\nimport React from \"react\";\nimport PostCard from \"../components/PostCard\";\nimport { gql, useQuery } from \"@apollo/client\";\n\nconst GET_ALL_POSTS = gql`\n  query getAllPosts {\n    posts {\n      nodes {\n        databaseId\n        title\n        date\n        slug\n        author {\n          node {\n            name\n          }\n        }\n        featuredImage {\n          node {\n            altText\n            sourceUrl\n          }\n        }\n      }\n    }\n  }\n`;\n\nexport default function PostsList() {\n const { loading, error, data } = useQuery(GET_ALL_POSTS);\n\n if (loading) return \u003cp\u003eLoading posts…\u003c/p\u003e;\n if (error) return \u003cp\u003eError :(\u003c/p\u003e;\n\n const postsFound = Boolean(data?.posts.nodes.length);\n if (!postsFound) {\n   return \u003cp\u003eNo matching posts found.\u003c/p\u003e;\n }\n\n return (\n   \u003cdiv className=\"posts-list\"\u003e\n     {data.posts.nodes.map((post) =\u003e (\n       \u003cPostCard key={post.databaseId} post={post} /\u003e\n     ))}\n   \u003c/div\u003e\n );\n}\n\n```\n\n## Step 3: Query for Data on Post Details Page\n\nYou can access this waypoint by running `git checkout waypoint/step-three` in your terminal.\n\nNow we should have a functioning home page, but if you click into any of the actual posts they all display the same data. To get this working, we need to use out `slug` route parameter to query our WordPress install.\n\n```\nimport React from \"react\";\nimport { Link } from \"react-router-dom\";\nimport PostPageContent from \"../components/PostPageContent\";\nimport { gql, useQuery } from \"@apollo/client\";\n\nconst GET_POST_BY_SLUG = gql`\n  query getPostBySlug($id: ID!) {\n    post(id: $id, idType: SLUG) {\n      title\n      date\n      content\n      categories {\n        nodes {\n          slug\n          name\n        }\n      }\n      author {\n        node {\n          name\n        }\n      }\n      postResources{\n        blogPosts\n        \t{\n            title\n            url\n          }\n        videos {\n          title\n          url\n        }\n      }\n    }\n  }\n`;\n\nexport default function PostPage(props) {\n const { loading, error, data } = useQuery(GET_POST_BY_SLUG, {\n\tvariables: {\n\t\tid: props.match.params.slug\n\t}\n});\n\n const postFound = Boolean(data?.post);\n\n return (\n   \u003cdiv className=\"page-container\"\u003e\n     \u003cLink to=\"/\"\u003e← Home\u003c/Link\u003e\n     {loading ? (\n       \u003cp\u003eLoading…\u003c/p\u003e\n     ) : error ? (\n       \u003cp\u003eError: {error.message}\u003c/p\u003e\n     ) : !postFound ? (\n       \u003cp\u003ePost could not be found.\u003c/p\u003e\n     ) : (\n       \u003cPostPageContent post={data.post} /\u003e\n     )}\n   \u003c/div\u003e\n );\n}\n\n```\n\n\n\n## Step 4: Show ACF Fields in Post Page Content\n\nYou can access this waypoint by running `git checkout waypoint/step-four` in your terminal.\n\nNow that we have our basic post details page wired up, it's time to show our ACF data on those pages. We can create two variables called `haveResourcePosts` and `haveResourceVideos` to check whether or not we have ACF resources for a given post, and then we can render those items in a unified list. To complete this step, copy the code below into your `components/PostPageContent.js` file.\n\n```\nimport React from \"react\";\nimport { Link } from \"react-router-dom\";\n\nconst formatDate = (date) =\u003e new Date(date).toLocaleDateString();\n\nexport default function PostPageContent({ post }) {\n  const { date, title, content, author, categories, postResources } = post;\n  const haveCategories = Boolean(categories?.nodes?.length);\n  const haveResourcePosts = Boolean(postResources?.blogPosts?.length)\n  const haveResourceVideos = Boolean(postResources?.videos?.length)\n\n  return (\n    \u003carticle\u003e\n      \u003ch1\u003e{title}\u003c/h1\u003e\n      \u003cp className=\"post-meta\"\u003e\n        \u003cspan role=\"img\" aria-label=\"writing hand\"\u003e\n          ✍️\n        \u003c/span\u003e{\" \"}\n        {author.node.name} on {formatDate(date)}\n      \u003c/p\u003e\n      \u003cdiv\n        className=\"post-content\"\n        dangerouslySetInnerHTML={{ __html: content }}\n      /\u003e\n      { (haveResourcePosts || haveResourceVideos) ? (\n        \u003cdiv className=\"categories-list\"\u003e\n          \u003ch2\u003ePost Resources\u003c/h2\u003e\n          \u003cul\u003e\n            { haveResourcePosts ? (postResources.blogPosts.map((post)=\u003e{\n              return ( \n               \u003cli\u003e📄 \u003ca href={post.url} key={post.title}\u003e{post.title}\u003c/a\u003e\u003c/li\u003e\n               )\n            })) : null }\n\n            { haveResourceVideos ? (postResources.videos.map((video)=\u003e{\n              return ( \n               \u003cli\u003e🎥 \u003ca href={video.url} key={video.title}\u003e{video.title}\u003c/a\u003e\u003c/li\u003e\n               )\n            })) : null }\n          \u003c/ul\u003e\n        \u003c/div\u003e\n      ) : null}\n      {haveCategories ? (\n        \u003cdiv className=\"categories-list\"\u003e\n          \u003ch2\u003eCategorized As\u003c/h2\u003e\n          \u003cul\u003e\n            {categories.nodes.map((category) =\u003e {\n              const { slug, name } = category;\n              return (\n                \u003cLink to={`/category/${slug}`} key={slug}\u003e\n                  \u003cli key={slug}\u003e{name}\u003c/li\u003e\n                \u003c/Link\u003e\n              );\n            })}\n          \u003c/ul\u003e\n        \u003c/div\u003e\n      ) : null}\n    \u003c/article\u003e\n  );\n}\n\n```\n## Deploy\n\nYou can access this waypoint by running `git checkout deploy` in your terminal.\n\n[Atlas](https://wpengine.com/atlas/) is WP Engine's headless WordPress hosting platform, where an app consists of a WordPress install and a Node.js hosting container, powered by modern JAMstack developer workflows. \n\nYou can sign up for an [Atlas Sandbox Account](https://my.wpengine.com/signup?plan=headless-eval) to deploy your app. The sign up process asks for a credit card, but this does not get charged. It's only for fraud prevention purposes.\n\nThis branch is ready to be deployed. To serve our React SPA in a node container we installed the `express` package, and modified the `npm run start` command to run `node server.js`, which should start our express server. \n\n```\n\nconst express = require('express');\nconst path = require('path');\nconst app = express();\n\napp.use(express.static(path.join(__dirname, 'build')));\n\napp.get('/*', function (req, res) {\n  res.sendFile(path.join(__dirname, 'build', 'index.html'));\n});\n\napp.listen(8080);\n\n```\n\nIdeally, you would swap out the URL here to the WPGraphQL endpoint you create. You can follow our [getting started guide on deploying from your own repository](https://developers.wpengine.com/docs/atlas/getting-started/deploy-from-existing-repo).\n\n## Want to Learn More?\nTo get more content from the WP Engine developer relations team, you can [read tutorials on our website](https://developers.wpengine.com/) or [watch on our YouTube channel](https://www.youtube.com/channel/UCh1WuL54XFb9ZI6m6goFv1g). Our [Headless WordPress Developer Roadmap](https://developers.wpengine.com/roadmap) builds on the concepts you learned here today and fills in some background on a few key technologies like React and GraphQL.\n\nIf you're on Discord, join the 700+ developers in [the headless WordPress Discord community](https://developers.wpengine.com/discord). This is a great place to ask questions, and stay updated on community events like this one.\n\n## Credits\n\n- Thanks to Kellen Mace and Grace Erixon for creating the initial version of this app\n- Thanks to [Tania Rascia](https://www.taniarascia.com) for the [starting point using React Router](https://www.taniarascia.com/using-react-router-spa).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjeverhart383%2Ffirst-headless-project","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjeverhart383%2Ffirst-headless-project","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjeverhart383%2Ffirst-headless-project/lists"}