diff --git a/.circleci/config.yml b/.circleci/config.yml index baba5b746..27d116c1d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,6 +6,7 @@ version: 2.1 orbs: wp-product-orb: wpengine/wp-product-orb@1.3.0 php: circleci/php@1.1.0 + node: circleci/node@4.1.0 jobs: checkout: @@ -17,11 +18,16 @@ jobs: paths: - . - plugin-wpe-headless-checkout: + plugin-checkout: executor: wp-product-orb/default - working_directory: ~/project/plugins/wpe-headless + working_directory: ~/project/plugins/<> environment: WPE_SESSION_DIR: ./.wpe + parameters: + slug: + type: string + filename: + type: string steps: - attach_workspace: at: ~/project @@ -30,7 +36,7 @@ jobs: command: | [ ! -d "build" ] && mkdir build &>/dev/null - wp-product-orb/get-version-from-php: - filename: wpe-headless.php + filename: <> return_var: BUILD_VERSION - wp-product-orb/variable: var: BUILD_VERSION @@ -38,11 +44,14 @@ jobs: - persist_to_workspace: root: ~/project paths: - - plugins/wpe-headless + - plugins/<> - plugin-wpe-headless-composer: + plugin-composer: executor: php/default - working_directory: ~/project/plugins/wpe-headless + working_directory: ~/project/plugins/<> + parameters: + slug: + type: string steps: - attach_workspace: at: ~/project @@ -53,22 +62,26 @@ jobs: name: Composer PHP lint and code sniffer command: | /usr/local/bin/composer lint && /usr/local/bin/composer phpcs + rm -v composer-setup.php working_directory: . - persist_to_workspace: root: ~/project paths: - - plugins/wpe-headless + - plugins/<> - plugin-wpe-headless-test: - working_directory: ~/project/plugins/wpe-headless + plugin-test: + working_directory: ~/project/plugins/<> docker: - image: cimg/php:7.4 - image: circleci/mysql:5.7 environment: - MYSQL_DATABASE: wpe_headless_tests - MYSQL_USER: wpe_headless_user - MYSQL_PASSWORD: wpe_headless_pass + MYSQL_DATABASE: wp_database + MYSQL_USER: wp_user + MYSQL_PASSWORD: wp_pass MYSQL_ROOT_PASSWORD: password + parameters: + slug: + type: string steps: - attach_workspace: at: ~/project @@ -83,7 +96,7 @@ jobs: - run: name: Setup WordPress testing framework command: | - /bin/bash tests/install-wp-tests.sh wpe_headless_tests wpe_headless_user wpe_headless_pass 127.0.0.1 latest true + /bin/bash tests/install-wp-tests.sh wp_database wp_user wp_pass 127.0.0.1 latest true working_directory: . - run: name: Run testing suite @@ -91,11 +104,14 @@ jobs: composer test working_directory: . - plugin-wpe-headless-bundle-zip: + plugin-bundle-zip: executor: wp-product-orb/default - working_directory: ~/project/plugins/wpe-headless + working_directory: ~/project/plugins/<> environment: WPE_SESSION_DIR: ./.wpe + parameters: + slug: + type: string steps: - attach_workspace: at: ~/project @@ -104,36 +120,39 @@ jobs: name: "Bundle plugin files into a zip" command: | cd .. - zip --verbose -x@wpe-headless/.zipignore -x *.wpe/* */build/ -r "wpe-headless/build/wpe-headless.$BUILD_VERSION.zip" wpe-headless + zip --verbose -x@<>/.zipignore -x *.wpe/* */build/ -r "<>/build/<>.$BUILD_VERSION.zip" <> - store_artifacts: path: build - persist_to_workspace: root: ~/project paths: - - plugins/wpe-headless/build + - plugins/<>/build - plugin-wpe-headless-bundle-json: + plugin-bundle-json: executor: wp-product-orb/parser - working_directory: ~/project/plugins/wpe-headless + working_directory: ~/project/plugins/<> environment: WPE_SESSION_DIR: ./.wpe + parameters: + slug: + type: string steps: - attach_workspace: at: ~/project - wp-product-orb/variable-load - wp-product-orb/parse-wp-readme: infile: readme.txt - outfile: build/wpe-headless.$BUILD_VERSION.json + outfile: build/<>.$BUILD_VERSION.json - store_artifacts: path: build - persist_to_workspace: root: ~/project paths: - - plugins/wpe-headless/build + - plugins/<>/build - plugin-wpe-headless-deploy: + plugin-deploy: executor: wp-product-orb/authenticate - working_directory: ~/project/plugins/wpe-headless + working_directory: ~/project/plugins/<> environment: WPE_SESSION_DIR: ./.wpe parameters: @@ -141,6 +160,8 @@ jobs: type: string upload_url: type: string + slug: + type: string steps: - attach_workspace: at: ~/project @@ -148,17 +169,15 @@ jobs: - wp-product-orb/authenticate: user: WPE_LDAP_USER pass: WPE_LDAP_PASS - url: << parameters.auth_url >> + url: <> - wp-product-orb/post-zip: - url: << parameters.upload_url >>/wpe-headless - zip: build/wpe-headless.$BUILD_VERSION.zip - json: build/wpe-headless.$BUILD_VERSION.json + url: <>/<> + zip: build/<>.$BUILD_VERSION.zip + json: build/<>.$BUILD_VERSION.json version: $BUILD_VERSION workflows: - version: 2 - - # Workflows defined for each package. + # Workflows defined for each package and plugin. # tag example for deploying an update for wpe-headless: plugin/wpe-headless/1.0.0 plugin-wpe-headless: jobs: @@ -166,46 +185,58 @@ workflows: filters: tags: only: /.*/ - - plugin-wpe-headless-checkout: + - plugin-checkout: + name: plugin-checkout-wpe-headless + slug: wpe-headless + filename: wpe-headless.php requires: - checkout # Enable running this job when a tag is published filters: tags: only: /.*/ - - plugin-wpe-headless-composer: + - plugin-composer: + name: plugin-composer-wpe-headless + slug: wpe-headless requires: - - plugin-wpe-headless-checkout + - plugin-checkout-wpe-headless # Enable running this job when a tag is published filters: tags: only: /.*/ - - plugin-wpe-headless-test: + - plugin-test: + name: plugin-test-wpe-headless + slug: wpe-headless requires: - - plugin-wpe-headless-composer + - plugin-composer-wpe-headless # Enable running this job when a tag is published filters: tags: only: /.*/ - - plugin-wpe-headless-bundle-zip: + - plugin-bundle-zip: + name: plugin-bundle-zip-wpe-headless + slug: wpe-headless requires: - - plugin-wpe-headless-test + - plugin-test-wpe-headless # Run this job on every commit/PR so the plugin is available as a build artifact filters: tags: only: /.*/ - - plugin-wpe-headless-bundle-json: + - plugin-bundle-json: + name: plugin-bundle-json-wpe-headless + slug: wpe-headless requires: - - plugin-wpe-headless-test + - plugin-test-wpe-headless # Run this job on every commit/PR to make sure it's in working order prior to deploying filters: tags: only: /.*/ - - plugin-wpe-headless-deploy: - name: "Deploy zip to api (staging)" + - plugin-deploy: + name: "Deploy zip to api (staging) wpe-headless" + slug: wpe-headless requires: - - plugin-wpe-headless-bundle-zip - - plugin-wpe-headless-bundle-json + - plugin-bundle-zip-wpe-headless + - plugin-bundle-json-wpe-headless filters: branches: only: @@ -216,11 +247,11 @@ workflows: context: wpe-ldap-creds auth_url: https://auth-staging.wpengine.io/v1/tokens upload_url: https://wp-product-info-staging.wpesvc.net/v1/plugins - - plugin-wpe-headless-deploy: - name: "Deploy zip to api (production)" + - plugin-deploy: + name: "Deploy zip to api (production) wpe-headless" + slug: wpe-headless requires: - - plugin-wpe-headless-bundle-zip - - plugin-wpe-headless-bundle-json + - "Deploy zip to api (staging) wpe-headless" filters: branches: ignore: /.*/ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3e8174f01..ceae6d5c7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,4 @@ -* @wpengine/blockheads +* @wpengine/developer-relations @wpengine/blockheads /docs/ @wpengine/developer-relations @wpengine/blockheads /examples/ @wpengine/developer-relations @wpengine/blockheads /README.md @wpengine/developer-relations @wpengine/blockheads diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index d4f704362..cddfb9348 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -1,21 +1,15 @@ -name: ESLint +name: lint on: pull_request jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Install Node Dependencies - run: yarn install - working-directory: packages/headless - env: - CI: TRUE - - name: Test Code Linting - run: yarn run eslint --output-file eslint_report.json --format json . --ext .js,.jsx,.ts,.tsx - working-directory: packages/headless - continue-on-error: true - - name: Annotate Code Linting Results - uses: ataylorme/eslint-annotate-action@1.1.2 + - uses: actions/setup-node@v2 with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" - report-json: "packages/headless/eslint_report.json" + node-version: '15' + - run: npm i -g npm@7 --registry=https://registry.npmjs.org + - run: npm install + - run: npm run build + - run: npm run lint + continue-on-error: FALSE diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..d2ffd266c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,15 @@ +name: test +on: pull_request +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: '15' + - run: npm i -g npm@7 --registry=https://registry.npmjs.org + - run: npm install + - run: npm run build + - run: npm test + continue-on-error: FALSE diff --git a/.gitignore b/.gitignore index ab3512d99..7c8c29d99 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist .idea .DS_Store process.yml +.vscode diff --git a/.graphqlconfig b/.graphqlconfig deleted file mode 100644 index 83a3d7918..000000000 --- a/.graphqlconfig +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "WP GraphQL + WP Engine Headless Schema", - "schemaPath": "wpgraphql-schema.graphql" -} diff --git a/.wp-env.json b/.wp-env.json new file mode 100644 index 000000000..9d1a7873a --- /dev/null +++ b/.wp-env.json @@ -0,0 +1,8 @@ +{ + "core": "WordPress/WordPress#5.6", + "phpVersion": "7.4", + "plugins": [ + "https://downloads.wordpress.org/plugin/wp-graphql.1.1.3.zip", + "./plugins/wpe-headless" + ] +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..e1cdc7bca --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,66 @@ +# Instructions For Logging Issues + +## 1. Search For Duplicates + +[Search the existing issues](https://github.com/wpengine/headless-framework/search?type=Issues) before logging a new one. + +Some search tips: +* *Don't* restrict your search to only open issues. An issue with a title similar to yours may have been closed as a duplicate of one with a less-findable title. +* Search for the title of the issue you're about to log. This sounds obvious but 80% of the time this is sufficient to find a duplicate when one exists. +* Read more than the first page of results. Many bugs here use the same words so relevancy sorting is not particularly strong. +* If you have a crash, search for the first few topmost function names shown in the call stack. + +## 2. Did You Find A Bug? + +When logging a bug, please be sure to include the following: + +* What version of the package/plugin are you using +* If at all possible, an *isolated* way to reproduce the behavior +* The behavior you expect to see, and the actual behavior + +## 3. Do You Have A Suggestion? + +We also accept suggestions in the issue tracker. Be sure to [search](https://github.com/wpengine/headless-framework/search?type=Issues) first. + + +In general, things we find useful when reviewing suggestions are: +* A description of the problem you're trying to solve +* An overview of the suggested solution +* Examples of how the suggestion would work in various places + * Code examples showing e.g. "this would be an error, this wouldn't" + * Code examples showing usage (if possible) +* If relevant, precedent in other frameworks or libraries can be useful for establishing context and expected behavior + +# Instructions For Contributing Code + +## What You'll Need + +0. [A bug or feature you want to work on](https://github.com/wpengine/headless-framework/labels/help%20wanted)! +1. [A GitHub account](https://github.com/join). +2. A working copy of the code. See [DEVELOPMENT](/docs/DEVELOPMENT.md) + +## Housekeeping + +Your pull request should: + +* Include a description of what your change intends to do +* Be based on reasonably recent commit in the **canary** branch +* Include adequate tests + * At least one test should fail in the absence of your non-test code changes. If your PR does not match this criteria, please specify why + * Tests should include reasonable permutations of the target fix/change + * Include baseline changes with your change +* Contain proper [semantic commit messages](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716#gistcomment-3711094) as follows: + + ``` + []: () + │ │ | │ + | | | └─> Summary in present tense. Not capitalized. No period at the end. + | | | + │ │ └─> Issue # (optional): Issue number if related to bug database. + │ │ + │ └─> Scope (optional): eg. common, compiler, authentication, core + │ + └─> Type: chore, docs, feat, fix, refactor, style, or test. + ``` + +* To avoid line ending issues, set `autocrlf = input` and `whitespace = cr-at-eol` in your git configuration diff --git a/README.md b/README.md index 17dc3e85c..70a29ead6 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,75 @@ -# WordPress Headless Framework (PREVIEW/ALPHA) +# Headless WordPress Framework -🚧 **Note:** This project is in the early stages of development, but it does contain useful functionality for headless WordPress sites like plugins and npm packages that assist in authentication and previews. +## Introduction + +WP Engine's Headless WordPress Framework provides a set of tools to make building front-end applications with WordPress as the headless CMS a pleasant experience for both developers and publishers. This framework consists of a WordPress plugin, a set of npm packages, and guides to get you started building headless WordPress sites in [Next.js](https://nextjs.org/). + +_🚧 **Note:** This project is in the early stages of development_ ## Quick Start -Eager to try out the Headless Framework? Here's how you can get started with our Preview example: +Eager to try out the Headless Framework? Here's how you can get started: + +### Create a front-end app + +1. Create a new Next.js app from our [getting-started project](https://github.com/wpengine/headless-framework/tree/canary/examples/getting-started): `npx create-next-app -e https://github.com/wpengine/headless-framework/tree/canary --example-path examples/getting-started --use-npm` +2. `cd my-app && cp .env.local.sample .env.local` to create a file that contains your environment variables. +3. `npm run dev` to start the development server. +4. See your site at http://localhost:3000. + +### Point the app to your own WordPress site + +The sample app loads WordPress content from our demo site at https://headlessfw.wpengine.com. + +Point it to your own WordPress site instead: -1. Create a WordPress site if you haven't already. We recommend using [Local](https://localwp.com/)! -2. Download, upload, and activate the `wpe-headless` plugin in this repository. [(Plugin Download)](https://wp-product-info.wpesvc.net/v1/plugins/wpe-headless?download) -3. Install [WP GraphQL](https://wordpress.org/plugins/wp-graphql/) on the WordPress site if it's not already installed -4. [Clone this repository](https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/cloning-a-repository) to a directory of your choice -5. Navigate to `examples/preview` in the cloned repository -6. `cp .env.local.sample .env.local` -7. Populate `WORDPRESS_URL` (or `NEXT_PUBLIC_WORDPRESS_URL`) and `WPE_HEADLESS_SECRET` accordingly in `.env.local` -8. `npm install && npm run dev` +1. Create a WordPress site if you haven't already. We recommend [Local](https://localwp.com/) to try things out locally, or you can use a live WordPress site. +2. Download, upload, and activate the `wpe-headless` plugin. [(Plugin Download)](https://wp-product-info.wpesvc.net/v1/plugins/wpe-headless?download) +3. Install [WP GraphQL](https://wordpress.org/plugins/wp-graphql/) on the WordPress site if it's not already installed. + +Then, in your front-end app directory: + +4. Change `NEXT_PUBLIC_WORDPRESS_URL` in `.env.local` to the full URL to your WordPress site, including the `http://` or `https://` prefix. +5. Change `WP_HEADLESS_SECRET` in `.env.local` to the secret key found at Settings → Headless in your WordPress admin area. +6. `npm run dev` (kill and restart npm if it was already running) + +You'll see the same site with your WordPress posts instead of ours. + +To enable post previews, set your front-end app URL on the WordPress Settings → Headless page (for example, `http://localhost:3000` when testing locally). + +➡️ [Learn more about getting started](/docs/getting-started/) ## Framework Features -- Headless Auth Flows - - OAuth token authentication for users - - Auth handler for Express/Next that exchanges a code for an access token. The access token can be used to make authenticated calls to WordPress via WPGraphQL or REST. -- [Previews](./docs/previews/README.md) - - Rewrite preview and draft links in WP Admin to redirect to the frontend. +### Plugin Features + +* **[Headless post previewing](/docs/previews/README.md)** + * OAuth token authentication creation + * Preview and draft link rewrites in WP Admin to redirect to the front-end +* **Smart content redirects** + * Automatically redirects content from the WP site to the front-end site to minimize site visitors’ confusion and avoid SEO penalties for duplicate content + * Redirects hyperlinks inserted into posts’ content to the front-end site +* **Disable WP theme admin pages** + * Prevents access to admin pages that have no effect on the headless front-end appearance, such as Appearance → Themes. +* **Ability to define custom menus in a GUI** +* **Additional data exposed through WPGraphQL** + * Block stylesheets + +### npm Package Features + +- [Post previewing integration](/docs/previews/README.md) + - Auth handler that exchanges a code for an access token +- A `HeadlessProvider` component to ease communication with WordPress via [Apollo](https://www.apollographql.com/) and [WPGraphQL](https://www.wpgraphql.com/). +- A [TemplateLoader](/docs/templating/README.md) component that optionally allows you to follow the WordPress [template hierarchy](https://developer.wordpress.org/themes/basics/template-hierarchy/) pattern in Next.js + - Load page templates based on the current URL path and page type + - Utilize functions like `getPropsMiddleware` for adding to/manipulating data depending on the template +- Display WordPress menus with our `Menu` component +- React hooks for pulling data from WordPress +- “Sensible defaults” for Next.js props and paths ## Download & Installation -There are two key parts of the WordPress Headless Framework. To take full advantage, you will need to install the plugin -in addition to the npm package. +There are two key parts of the WordPress Headless Framework. To take full advantage, you will need to install the plugin in addition to the npm package. ### WordPress Plugin @@ -52,14 +95,20 @@ npm install --save @wpengine/headless ## Guides -* [Creating a Next.js application from scratch and integrating `@wpengine/headless` to enable post previewing](./docs/previews/README.md) +* [Getting started with the Headless Framework](/docs/getting-started/README.md) +* [Enabling post previews in Next.js](/docs/previews/README.md) +* [Using the WordPress template hieararchy in Next.js](/docs/previews/README.md) + +## Contribute -## Contributing +There are many ways to [contribute](/CONTRIBUTING.md) to this project. -Since we're in the early stages of development, we are not currently accepting outside contributions; although, we are -interested in any problems that you encounter while using the framework. +* [Discuss open issues](/issues) to help define the future of the project. +* [Submit bugs](/issues) and help us verify fixes as they are checked in. +* Review and discuss the [source code changes](pulls). +* [Contribute bug fixes](/CONTRIBUTING.md) -### [Development Guide](./docs/DEVELOPMENT.md) +### [Development Guide](/docs/DEVELOPMENT.md) As this repository contains a WordPress plugin as well as npm packages, we have a few recommendations to help streamline your development process. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 2d328275f..b243d1c57 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1,8 +1,11 @@ # Contributing -Since we're in the early stages of development, we are not currently accepting outside contributions; although, we are interested in any problems that you encounter while using the framework. +There are many ways to [contribute](/CONTRIBUTING.md) to this project. -Create an issue in this repository to report bugs or feature requests. +* [Discuss open issues](/issues) to help define the future of the project. +* [Submit bugs](/issues) and help us verify fixes as they are checked in. +* Review and discuss the [source code changes](pulls). +* [Contribute bug fixes](/CONTRIBUTING.md) ## Project Structure @@ -12,14 +15,14 @@ Create an issue in this repository to report bugs or feature requests. ### NPM Packages -When working on the NPM packages in this repository, we recommend utilizing our Lerna setup in the root of the repository. +When working on the npm packages in this repository, use our Lerna setup from the project root: -To get going, you can run the following: - -1. Ensure that `.env.local` is properly configured in `examples/preview` +1. Ensure that `.env.local` exists and is correctly configured in `examples/getting-started` and `examples/preview`. 2. `npm run bootstrap` 3. `npm run dev` +When switching git branch, run `npm run clean` from the root and then re-run `npm run bootstrap`. + ### Plugins As this is a monorepo, you will not be able to check out this repository into `wp-content/themes` or `wp-content/plugins`. @@ -49,18 +52,20 @@ Run the syntax check. composer phpcs ``` -Some syntax errors can be fixed by phpcs. +Use `phpcs` to fix some syntax errors: + ``` composer phpcs:fix ``` **WordPress Unit Tests** -In order to run WordPress unit tests, the test framework needs to be set up. +To run WordPress unit tests, set up the test framework: + ``` /bin/bash /path/to/headless-framework/plugins/wpe-headless/tests/install-wp-tests.sh wpe_headless_tests db_name db_password ``` -If you connect to mysql via a sock connection, you can run the following. +If you connect to MySQL via a sock connection, you can run the following. ``` /bin/bash /path/to/headless-framework/plugins/wpe-headless/tests/install-wp-tests.sh wpe_headless_tests db_name db_password localhost:/path/to/mysql/mysqld.sock ``` @@ -70,7 +75,8 @@ Install the composer packages from within `wpe-headless` directory if you haven' composer install ``` -Within the `wpe-headless` directory, run `phpunit` either directly or as a composer command +Within the `wpe-headless` directory, run `phpunit` either directly or as a composer command: + ``` vendor/bin/phpunit ``` @@ -80,3 +86,80 @@ or ``` composer test ``` + +## End-2-End Testing + +Use [Codeception](https://codeception.com/) for running end-2-end tests in the browser. + +### 1. Environment Setup + +1. Install [Composer](https://getcomposer.org/). + - Within the `plugins/wpe-headless` directory, run `composer install`. +1. Install [Google Chrome](https://www.google.com/chrome/). +1. Install [Chromedriver](https://chromedriver.chromium.org/downloads) + - The major version will need to match your Google Chrome [version](https://www.whatismybrowser.com/detect/what-version-of-chrome-do-i-have). See [Chromedriver Version Selection](https://chromedriver.chromium.org/downloads/version-selection). + - Unzip the chromedriver zip file and move `chromedriver` application into the `/usr/local/bin` directory. + `mv chromedriver /usr/local/bin` + - In shell, run `chromedriver --version`. _Note: If you are using OS X, it may prevent this program from opening. Open "Security & Privacy" and allow chromedriver_. + - Run `chromedriver --version` again. _Note: On OS X, you may be prompted for a final time, click "Open"_. When you can see the version, chromedriver is ready. + +### 2. Headless Site Setup +1. From within the headless site `examples/getting-started` copy `.env.test.sample` to `.env.test`. + - If you are using the provided Docker build, you will not need to adjust any variables in the `.env.testing` file; else, you can adjust the environment variables as needed. + +### 3. WPE Headless Setup +1. Move into the WPE Headless plugin directory `plugins/wpe-headless`. +1. Prepare a test WordPress site. + - We have provided a Docker build to reduce the setup needed. You are welcome to set up your own WordPress end-2-end testing site. + 1. Install [Docker](https://www.docker.com/get-started). + 1. Run `docker-compose up -d --build`. If building for the first time, it could take some time to download and build the images. + 1. Run `docker-compose exec --workdir=/var/www/html/wp-content/plugins/wpe-headless --user=www-data wordpress wp plugin install wp-graphql --activate` + 1. Run `docker-compose exec --workdir=/var/www/html/wp-content/plugins/wpe-headless --user=www-data wordpress wp db export tests/_data/dump.sql` +1. Copy `.env.testing.example` to `.env.testing`. + - If you are using the provided Docker build, you will not need to adjust any variables in the `.env.testing` file. + - If you are not using the provided Docker build, edit the `.env.testing` file with your test WordPress site information. +1. Run `vendor/bin/codecept run acceptance` to start the end-2-end tests. + +### Browser testing documentation +- [Codeception Acceptance Tests](https://codeception.com/docs/03-AcceptanceTests) + - Base framework for browser testing in php. +- [WPBrowser](https://wpbrowser.wptestkit.dev/) + - WordPress framework wrapping Codeception for browser testing WordPress. + +## Deployment + +Developers with full GitHub repository access can create public releases: + +### Release the wpe-headless plugin + +1. Update the `Version` in the file header at `plugins/wpe-headless/wpe-headless.php`. +2. Update the changelog and 'stable tag' in `plugins/wpe-headless/readme.txt`. +3. Commit and push your changes for review. +4. Tag the approved commit with `plugin/wpe-headless/[version]`, for example: `git tag plugin/wpe-headless/0.3.5` and push the tag (`git push --tags`). Or use GitHub to [create a new release](https://github.com/wpengine/headless-framework/releases/new) with that tag. + +CircleCI will build and deploy the plugin zip. The latest version will be available here: + +`https://wp-product-info.wpesvc.net/v1/plugins/wpe-headless?download` + +### Release the @wpengine/headless package + +1. Update the `version` in `packages/headless/package.json`. +2. Update the changelog at `packages/headless/CHANGELOG.md`. +3. Commit and push your changes for review. +4. Tag the approved commit with `package/headless/[version]`, for example: `git tag package/headless/0.6.1` and push the tag (`git push --tags`). Or use GitHub to [create a new release](https://github.com/wpengine/headless-framework/releases/new) with that tag. + +CircleCI will build and deploy the package to npm. The updated package will be visible here: + +`https://www.npmjs.com/package/@wpengine/headless` + +To publish the package and plugin together, tag the commit you want to publish with two different tags in the above formats, then push the tags: + +``` +git checkout canary +git pull +git tag plugin/wpe-headless/1.0.0 +git tag package/headless/1.0.0 +git push --tags +``` + +This triggers the CircleCI jobs to publish the package and plugin. diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 18215275e..000000000 --- a/docs/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Headless Framework Documentation - -- [Previews](./previews/README.md) diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md new file mode 100644 index 000000000..6e52e0721 --- /dev/null +++ b/docs/getting-started/README.md @@ -0,0 +1,87 @@ +# Getting started + +This guide will get you up and running with our Headless Framework and help you understand what it offers. + +Make sure to install the latest version of [Node.js](https://nodejs.org/en/download/) before using this guide. + +## Create a Next.js app + +Our headless framework is built on top of [Next.js](https://nextjs.org/). You get all the fantastic features that Next.js provides, plus an easy way to use it with WordPress! + +We’ll start with an [example project](https://github.com/wpengine/headless-framework/tree/canary/examples/getting-started) to quickly see the power of our framework. To pull it down, use `npx` (which comes with Node.js) with the URL to our example project: + +```npx create-next-app -e https://github.com/wpengine/headless-framework/tree/canary --example-path examples/preview --use-npm``` + +`create-next-app` prompts you to provide a name for your project. Once you do that and the dependencies are installed, cd into the new project: + +```cd your-app-name``` + +Create a file to hold your environment variables, such as your WordPress site URL: + +``` +cp .env.local.sample .env.local +``` + +You don't need to edit `.env.local` just yet. + +Then `npm run dev` and visit your site at http://localhost:3000. + +## Connect the app to your WordPress site + +The sample app loads WordPress content from our demo site at https://headlessfw.wpengine.com. + +To point it to a different WordPress site: + +1. Create a WordPress site if you haven't already. We recommend [Local](https://localwp.com/) to try things out locally, or you can use a live WordPress site. +2. Download, upload, and activate the `wpe-headless` plugin. [(Plugin Download)](https://wp-product-info.wpesvc.net/v1/plugins/wpe-headless?download) +3. Install [WP GraphQL](https://wordpress.org/plugins/wp-graphql/) on the WordPress site. + +Next, go to your frontend app directory and: + +4. Change `NEXT_PUBLIC_WORDPRESS_URL` in `.env.local` to the full URL to your WordPress site, including the `http://` or `https://` prefix. +5. Change `WP_HEADLESS_SECRET` in `.env.local` to the secret key found at Settings → Headless in your WordPress admin area. +6. `npm run dev` (kill and restart npm if it was already running) + +Open or refresh http://localhost:3000. You should see a list of posts from your WordPress site at the bottom of the front page and view a single post. + +## Set up the WordPress plugin + +Install our [Headless WordPress plugin](https://github.com/wpengine/headless-framework#wordpress-plugin) to get the full benefits of the framework. We recommend [Local](https://localwp.com/) to spin up a local WordPress site quickly. After you have a WordPress site up and running, [download the plugin](https://wp-product-info.wpesvc.net/v1/plugins/wpe-headless?download) and upload and activate it [through the WordPress Admin](https://wordpress.org/support/article/managing-plugins/#manual-upload-via-wordpress-admin). + +After activation, WordPress redirects to the Headless settings page. The plugin has a dependency on WPGraphQL, so you’ll see a button on the right under the “Get Started with Headless” box to install and activate the WPGraphQL plugin if it’s not already active. + +At this point, if you know what the URL to your frontend site is (or is going to be), you can enter that now in the Front-end Site URL field. That’s all you need to do for now through WordPress. + +### What is the WordPress plugin doing? + +The plugin ensures that your WordPress site runs smoothly as a headless CMS. From smart content redirects to enabling post previewing to providing the right data in WPGraphQL, installing the plugin gives you the things you need to run WordPress as a headless CMS. Find a [full list of plugin features here](https://github.com/wpengine/headless-framework#plugin-features). + + +## Breaking down the example project + +The example project is set up with most of the features our [@wpengine/headless](https://npmjs.org/package/@wpengine/headless) npm package provides, along with a way to structure your app. + +### pages/_app.tsx + +Currently, we’re using a Next.js [Custom App](https://nextjs.org/docs/advanced-features/custom-app) to override the default `App` to inject our `` wrapper component. `HeadlessProvider` sets up an Apollo client connected to WordPress via the `NEXT_PUBLIC_WORDPRESS_URL` environment variable. Our Apollo client is all set up to support both server-side and client-side rendering out of the box. + +### pages/[[..page]].tsx + +Next.js uses pages for both rendering and routing purposes. For instance, an `about.tsx` file automatically renders and outputs in the `/about` directory. In our example, we’re using an [optional catch-all route](https://nextjs.org/docs/routing/dynamic-routes#optional-catch-all-routes) - `[[..page]].tsx`, which Next.js will hit any time a request is made. This component returns a `` component. `TemplateLoader` is imported from the [@wpengine/headless](https://npmjs.org/package/@wpengine/headless) npm package and is responsible for determining what template to render based on the requested URL’s content type in WordPress. This allows you to mimic the [WordPress template hierarchy](https://developer.wordpress.org/themes/basics/template-hierarchy/) inside your Next.js app. + +### /wp-templates/ + +The `wp-templates/` folder contains templates that are responsible for the final rendering of a page. Templates from the `` (`` if not using Next.js) in the catch-all route page mentioned above, similar to how WordPress PHP theme files are loaded according to the template hierarchy. Template injection is accomplished by the `TemplateLoader` picking up WordPress content from GraphQL. The naming convention of files in the `wp-templates/` directory follows 1:1 with the WordPress template hierarchy. For example, `single.tsx` is the template rendered for single posts, `page.tsx` is the template for pages, `category.tsx` is for categories, and so on. + +Our example includes an `index.tsx` and `single.tsx` template. To handle other content types like pages or custom types, you can create a new `.tsx` file with the desired template's name. + +[More information about how the `TemplateLoader` works can be found here](/docs/templating/). + +### Hooks + +The [@wpengine/headless](https://npmjs.org/package/@wpengine/headless) npm package contains several custom React hooks for interacting with WPGraphQL. In the example project’s `index.tsx` template, the `usePosts` hook gets a list of all posts. Similarly, `usePost` is used in `single.tsx` to get the single post data. The hooks permit specific parameters, which allow you to refine the data returned (passing in an `id` to `usePost`, for example, to get a particular post). + +## Learn more + +- [Enabling WordPress post previews](/docs/previews/) +- [Using the WordPress template hierarchy in Next.js](/docs/templating/) diff --git a/docs/previews/README.md b/docs/previews/README.md index 481e45676..d6686ae85 100644 --- a/docs/previews/README.md +++ b/docs/previews/README.md @@ -4,7 +4,7 @@ In this guide, we'll walk through how to configure a Next.js site for previews. -We're going to use Next with TypeScript for this example. +We're going to use Next.js with TypeScript for this example tutorial. ```bash npx create-next-app previews @@ -14,28 +14,27 @@ npm i @wpengine/headless @apollo/client graphql npm i typescript @types/react @types/react-dom @types/node -D ``` -TL;DR -Checkout the [example project](../../examples/preview) to see how it works. +**TL;DR:** Check out the [example project](/examples/preview) to see how it works. ## WPE Headless Plugin -In order to enable previews in WordPress, you'll first need to install the [wpe-headless plugin](https://github.com/wpengine/headless-framework/releases/download/v0.0.1-alpha.1/wpe-headless-plugin.zip). You also need to install [WPGraphQL](https://wordpress.org/plugins/wp-graphql/). +To enable previews in WordPress, you'll first need to install the [wpe-headless plugin](https://wp-product-info.wpesvc.net/v1/plugins/wpe-headless?download). You also need to install [WPGraphQL](https://wordpress.org/plugins/wp-graphql/). -The plugin enables an OAuth flow for users to authenticate with WordPress and receive an access token which is used for subsequent API calls (i.e. GQL/REST). +The plugin enables an OAuth flow for users to authenticate with WordPress and receive an access token for subsequent API calls (i.e., GQL/REST). -In addition, the plugin will rewrite URLs in WordPress so that when a user clicks view/preview on a post, they will be taken to the frontend rather than WP. +Also, the plugin will rewrite URLs in WordPress so that when a user clicks view/preview on a post, the URL goes to the frontend rather than WordPress. ### Plugin Settings -Go to Settings->Headless to view the plugin's settings page: +Go to **Settings->Headless** to view the plugin's settings page: -![Headless Plugin Menu](./headless-settings.jpg) +![Headless Plugin Menu](/docs/previews/headless-settings.jpg) -There are 2 settings that assist in previews. The first setting is the location of your frontend. You'll need to enter a `Front-end site URL`, which will be `http://localhost:3000` for this example. +Two settings assist in previews. The first setting is the location of your frontend. You'll need to enter a `Frontend site URL`, which will be `http://localhost:3000` for this example. The second one is read-only. It gives you an API secret key that you need to use on your backend for your frontend. -![Headless Plugin Auth Settings](./headless-settings-auth.jpg) +![Headless Plugin Auth Settings](/docs/previews/headless-settings-auth.jpg) ## Headless Framework (@wpengine/headless) @@ -47,38 +46,38 @@ Install the npm package via: npm i @wpengine/headless ``` -The package contains an auth handler to get an access token for a user when trying to view a preview/draft post as well as React hooks to pull post(s). +The package contains an auth handler to get an access token for a user when trying to view a preview/draft post and React hooks to pull post(s). ### Authorization Flow -In order to submit secure requests to WordPress, we need to be able to verify that a user has access to the content that is being requested. The plugin exposes routes that allow us to create access codes and exchange them for access tokens. +To submit secure requests to WordPress, we need to verify that a user has access to the content requested. The plugin exposes routes that allow us to create access codes and exchange them for access tokens. The flow looks like this: -- User makes a request to a secure route (i.e. draft post) +- User requests a secure route (i.e., draft post) - User is redirected to WordPress to login -- WordPress redirects back to frontend with a temporary code +- WordPress turns back to frontend with a temporary code - The frontend server exchanges the code for an access token - The access token is stored in a cookie - The user is finally redirected back to the original URL and uses the access token in the cookie to make the authenticated request The framework provides a Node.js auth handler to do the exchange for you. -### Auth Hander +### Auth Handler -In order to support the exchange of the access code for an access token, the framework provides a Node authorization handler: +To support the exchange of the access code for an access token, the framework provides a Node authorization handler: -```typescript +```ts import { authorizeHandler } from '@wpengine/headless'; ``` -`authorizeHandler` accepts a Node request (IncomingMessage) and response (ServerResponse) which are compatible with ExpressJS, Next APIs, etc. +`authorizeHandler` accepts a Node request (IncomingMessage) and response (ServerResponse) and will work with any Node-based server library. -In order to enable the handler in Next, create a new API route: +To enable the handler in Next, create a new API route: -`/pages/api/authorize.ts` +`/pages/api/auth/wpe-headless.ts` -```typescript +```ts import { authorizeHandler } from '@wpengine/headless'; export default authorizeHandler; @@ -86,16 +85,16 @@ export default authorizeHandler; ### Next Integration -The framework provides a provider and hooks that assist in routing and server side rendering. +The framework provides a provider and hooks that assist in routing and server-side rendering. #### HeadlessProvider The provider is the glue that allows the framework to communicate with WordPress. To set it up, create `/pages/_app.tsx`. -```jsx +```tsx import React from 'react'; import { AppContext, AppInitialProps } from 'next/app'; -import { HeadlessProvider } from '@wpengine/headless'; +import { HeadlessProvider } from '@wpengine/headless/react'; export default function App({ Component, @@ -111,20 +110,21 @@ export default function App({ #### Routes and Hooks -For this example, we're only going to need one page to handle all of our routes `/pages/[[...page]].tsx`. This is a catch-all route in Next. We'll use hooks provided by the framework to load the right content for each URL. +For this example, we're only going to need two pages. One page will handle all of our public routes (`/pages/[[...page]].tsx`) and another will handle our preview routes (`/pages/preview/[[...page]].tsx`). `[[...page]].tsx` is a catch-all route in Next. We'll use hooks provided by the framework to load the right content for each URL. Below is what will go in `/pages/[[...page]].tsx`: -```jsx +```tsx import React from 'react'; import { - useNextUriInfo, - initializeNextServerSideProps, -} from '@wpengine/headless'; -import { GetServerSidePropsContext } from 'next'; + useUriInfo, + getNextStaticPaths, + getNextStaticProps, +} from '@wpengine/headless/next'; +import { GetStaticPropsContext } from 'next'; import Posts from '../lib/components/Posts'; import Post from '../lib/components/Post'; export default function Page() { - const pageInfo = useNextUriInfo(); + const pageInfo = useUriInfo(); if (!pageInfo) { return <>; @@ -137,20 +137,25 @@ export default function Page() { return ; } -export function getServerSideProps(context: GetServerSidePropsContext) { - return initializeNextServerSideProps(context); +export function getStaticPaths() { + return getNextStaticPaths(); +} + +export function getStaticProps(context: GetStaticPropsContext) { + return getNextStaticProps(context); } ``` -`useNextUriInfo` gets the URL from the Next Router and queries WP to get information about the route. If the route has a list of posts, we'll show one component. If it has a single post, we'll show another. Let's add those components to `/lib/components`. +`getNextStaticProps` is used to allow for Static Site Generation. It knows how to get URL information on the server to query WP and pull the right `pageInfo` on the initial request. This is critical for SEO. We want to return the rendered page on the first request so that search engines can index our content. + +`useUriInfo` gets the URL from the Next Router and queries WP to get information about the route. If the route has a list of posts, we'll show one component. If it has a single post, we'll show another. Let's add those components to `/lib/components`. -`initializeNextServerSideProps` is used to allow for Server Side Rendering. It knows how to get URL information on the server so that we can query WP and pull the right `pageInfo` on the initial request. This is critical for SEO. We want to return the rendered page on the first request so that search engines can index our content. `/lib/components/Post.tsx` -```jsx +```tsx import React from 'react'; -import { usePost } from '@wpengine/headless'; +import { usePost } from '@wpengine/headless/next'; export default function Post() { const post = usePost(); @@ -161,7 +166,7 @@ export default function Post() {
{post.title}
-

{posts && - posts.map((post) => ( + posts.nodes.map((post) => (

@@ -195,7 +200,7 @@ export default function Posts() { {post.title} -

; +} +``` + +> **NOTE**: Our preview component does not export `getStaticProps` or `getStaticPaths`. This is because the logic for getting a preview post is dynamic and will be handled client-side. + #### Configuration We need to let the frontend know about our WordPress instance. The framework expects a few environment variables with this information. Create a file in the root of the project `/.env.local`. ```bash # Base URL for WordPress -NEXT_PUBLIC_WORDPRESS_URL=http://yourwp.com +NEXT_PUBLIC_WORDPRESS_URL=http://yourwpsite.com # Plugin secret found in WordPress Settings->Headless -WPE_HEADLESS_SECRET=YOUR_PLUGIN_SECRET - -# Location of the auth handler endpoint -NEXT_PUBLIC_AUTHORIZATION_URL=/api/authorize +WP_HEADLESS_SECRET=YOUR_PLUGIN_SECRET ``` ## Try it out! @@ -233,8 +248,9 @@ npm run dev The server will start on port 3000 by default: [http://localhost:3000](http://localhost:3000) -You should see a list of posts on the home page and be able to view a single post. +You should see a list of posts on the home page and view a single post. For previews, go to WP and create a new post, but don't publish it. Click preview, and you'll be sent to the frontend through the authorization flow. -NOTE: If you open the preview link in a private window, you'll be prompted to login to WP before being redirected back to the frontend. +**NOTE:** If you open the preview link in a private window, you'll be prompted to log in to WP before redirecting back to the frontend. + diff --git a/docs/previews/headless-settings-auth.jpg b/docs/previews/headless-settings-auth.jpg index c008b30ae..50d000451 100644 Binary files a/docs/previews/headless-settings-auth.jpg and b/docs/previews/headless-settings-auth.jpg differ diff --git a/docs/queries/README.md b/docs/queries/README.md new file mode 100644 index 000000000..c22b0b12b --- /dev/null +++ b/docs/queries/README.md @@ -0,0 +1,263 @@ +# Fetching WordPress Data With @wpengine/headless + +**NOTE: This is prerelease software. As we work towards our first release, we will 💯 introduce breaking changes.** + +In this guide, we will talk about customizing GQL queries in your app. This app will go over how to do this in client-side queries and server-side queries if you are building an app with Next.js. + +This guide assumes you have set up an initial application using `@wpengine/headless`. Please refer to the [Getting Started](/docs/getting-started/) guide if you need help setting up a project. + +## Typical Queries + +There are multiple ways to query your WordPress GraphQL API, and the framework provides you with some simple helper functions to get you started. The framework has built-in functions to support the following queries: + + 1. Getting a list of posts + 1. Getting a single post or page + 1. Getting information about a URI + 1. Getting general WordPress settings + +For this guide, let's focus on the first two types of queries. + +### Getting A List Of Posts + +One of the most common scenarios when building a headless WordPress site is displaying a list of your posts. The framework provides an abstraction around this API request, with some sensible defaults. Depending upon the libraries and tools you are using to build your app, you can take advantage of one of the following: + +#### getPosts + +```tsx +import { getApolloClient, getPosts } from '@wpengine/headless'; + +// ... + +const client = getApolloClient(); +const postsData = await getPosts(client); + +console.log(postsData); +``` + +The code above is the simplest example of using `getPosts` to make a query. This query works with any frontend toolset and uses `@apollo/client` to make a GraphQL request your WordPress GraphQL API. When you make the query without configuration, it will request a default set of fields returned from the query. You can find these fields in `/packages/headless/src/api/queries/LIST_POST_DATA_FRAGMENTS.ts`, and you can find the query in `/packages/headless/src/api/queries/getPosts.ts`. [Custom queries](#custom-queries) will be described later in this guide. The call to `getPosts` returns a type `WPGraphQL.RootQueryToPostConnection`, which can be found in `/packages/headless/src/types/wpgraphql.d.ts`. Assuming you are using TypeScript and a supported IDE, you will get IntelliSense for the response. + +> **NOTE:** In the code above, the call to `getApolloClient` expects an optional `context` value. You can ignore this if you are working strictly client-side. If you are making server-side requests, you will want to use an Apollo client that persists for each request's duration. In this case, `context` should be your persistent object. In Next.js this will be the `context` value that is passed in `getStaticProps` or `getServerSideProps`. However, if you are using some other framework or vanilla Node, you could also send in the `request` object. + +#### usePosts + +```tsx +import { usePosts } from '@wpengine/headless/react'; + +// ... +export default function Component() { + const postsData = usePosts(); + + console.log(postsData); +} +``` + +The code above works best in a React app. The framework provides a `usePosts` hook that acts exactly like the `getPosts` function but works as a React hook. `usePosts` is the recommended way to make requests for posts when using React. + +#### getContentNode + +```tsx +import { getApolloClient, getContentNode } from '@wpengine/headless'; + +// ... + +const client = getApolloClient(); +const postData = await getContentNode(client, { + variables: { + id: '/hello-world', + }, +}); + +console.log(postData); +``` + +The code above is the simplest example of using `getContentNode` to make a query. You must pass in at least the `id` variable. The variables you can pass on `WPGraphQL.RootQueryContentNodeArgs` can be found in `/packages/headless/src/types/wpgraphql.d.ts`. `getContentNode` works for WordPress pages and posts and will return a default set of fields for each. As with [`getPosts`](#getPosts), `getContentNode` expects the Apollo client in order to make queries. + +#### usePost + +```tsx +import { usePost } from '@wpengine/headless/react'; + +// ... +export default function Component() { + const postData = usePost(); + + console.log(postData); +} +``` + +The code above is intended to be used in a React app. The framework provides a `usePost` hook that acts exactly like the `getContentNode` function but works as a React hook. `usePost` is the recommended way to make requests for posts when using React. With `usePost`, you do not need to specify an `id` variable. If you leave out the options `usePost`, you will use the current URL to determine what post to fetch. + +**If you are using Next.js** you should take advantage of the Next-specific `usePost` hook as follows: + +```tsx +import { usePost } from '@wpengine/headless/next'; + +// ... +export default function Component() { + const postData = usePost(); + + console.log(postData); +} +``` + +The advantage of using the Next-specific `usePost` query is that it will work with Next's functionality for previewing posts. + +## Custom Queries + +Now that you are familiar with some of the basic queries the framework offers let's look at how you might customize them. You can customize the fields returned from the queries as well as the variables passed through. The framework chooses a default set of fields and some default arguments. Override these by passing options into the corresponding functions. + +### Customizing getPosts and usePosts + +```tsx +import { getApolloClient, getPosts } from '@wpengine/headless'; +import { gql } from '@apollo/client'; + +// ... + +const client = getApolloClient(); +const postsData = await getPosts(client, { + fragments: { + listPostData: gql` + fragment listPostData on Post { + title + excerpt + uri + } + ` + }, + variables: { + first: 3, + } +}); + +console.log(postsData); +``` + +The above code demonstrates how you can call `getPosts` and configure both the fields to return each post and configure the number of posts returned. For variables, you can send in every variable accepted on the WPGraphQL `posts` query. For a look at what those are, look at `WPGraphQL.RootQueryPostsArgs` in `/packages/headless/src/types/wpgraphql.d.ts`. + +The above code specifies the `listPostData` fragment, used to retrieve the selected fields in the GQL query. If you specify a fragment it will override the default fragment. Your fragment **must be named listPostData** for it to work correctly. + +> **NOTE:** If you are using React/Next.js, `usePosts` takes the same arguments. + +### Customizing getContentNode and usePost + +```tsx +import { getApolloClient, getContentNode } from '@wpengine/headless'; +import { gql } from '@apollo/client'; + +// ... + +const client = getApolloClient(); +const postData = await getContentNode(client, { + fragments: { + postData: gql` + fragment postData on Post { + title + content + } + `, + pageData: gql` + fragment pageData on Page { + title + content + } + `, + }, + variables: { + id: '/hello-world', + idType: 'URI', + } +}); + +console.log(postData); +``` + +The above code demonstrates how you can call `getContentNode` and configure both the fields returns on the post or page and what post or page to fetch. For variables, you can send in every variable on the WPGraphQL `contentNode` query. For a look at what those are, look at `WPGraphQL.RootQueryContentNodeArgs` in `/packages/headless/src/types/wpgraphql.d.ts`. + +The `postData` fragment and `pageData` fragment, retrieves the specified fields in the GQL query. If you select a fragment it will override the default fragment. Your fragments **must be named postData or pageData** for them to work correctly. + +> **NOTE:** If you are using React/Next.js, `usePost` takes the same arguments. + +### Custom Queries With Apollo + +Note that while you can take advantage of some of the functions built into the framework for querying posts and pages, you can also use the Apollo client to make any other requests. When you call `getApolloClient`, the call provides you with an Apollo client bound to your configured WordPress instance. + +### Customizing Server-Side Fragments With Next.js + +If you are writing a Next.js app and want to take advantage of Server-Side Rendering (SSR) or Static Site Generation (SSG) you can still customize the queries you want to make getServerSideProps` or `getStaticProps`. The framework provides two functions for setting up SSR and SSG and configuring queries ahead of time. Those functions are `getNextStaticProps` and `getNextServerSideProps`. Both functions operate the same, but `getNextStaticProps` is used with `getStaticProps` and `getNextServerSideProps` is used with `getServerSideProps`. + +```tsx +import { + NextTemplateLoader, + getNextStaticPaths, + getNextStaticProps, +} from '@wpengine/headless/next'; +import { getApolloClient } from '@wpengine/headless'; +import { usePosts } from '@wpengine/headless/react'; +import { GetStaticPropsContext } from 'next'; +import { gql, useQuery } from '@apollo/client'; +import WPTemplates from '../wp-templates/_loader'; + +const menusQuery = gql` + { + menus { + edges { + node { + menuItems { + edges { + node { + url + title + label + } + } + } + } + } + } + } +`; + +export default function MyComponent() { + const posts = usePosts(); + const menus = useQuery(menusQuery) + + // ... +} + +export async function getStaticProps(context: GetStaticPropsContext) { + const client = getApolloClient(context); + await client.query({ + query: menusQuery + }); + + return getNextStaticProps(context, { + templates: WPTemplates, + queries: { + posts: { + fragments: { + listPostData: gql` + fragment listPostData on Post { + title + excerpt + uri + } + `, + }, + }, + }, + }); +} +``` + +The above code does a few things: +It gets a Next context-bound Apollo client. +It makes a query with the Apollo client. +It calls `getNextStaticProps` and configures the fragment it wants to use for the `posts` query. + +Note that **MyComponent** calls `usePosts`. The call to `usePosts` will get the posts with the Apollo client cache's configured fields. + + +You can configure `getNextStaticProps` and `getNextServerSideProps` exactly the same way. + +> **NOTE:** The order of operations matters in `getStaticProps` and `getServerSideProps`. In the code above, `getNextStaticProps` is the final call in the function. The `getNextStaticProps` handles caching the Apollo client and storing it on props (among other things). Ensuring the operation order makes a query with the Apollo client, and then make the same query from your component, and it won't have to make a client-side API call. diff --git a/docs/reference/api/README.md b/docs/reference/api/README.md new file mode 100644 index 000000000..c8ae368bc --- /dev/null +++ b/docs/reference/api/README.md @@ -0,0 +1,9 @@ +@wpengine/headless / [Exports](modules.md) + +# WordPress Headless Framework + +[![Version](https://img.shields.io/npm/v/@wpengine/headless.svg)](https://npmjs.org/package/@wpengine/headless) + +NOTE: This project is in the early stages of development, but it does contain useful functionality for headless WordPress sites like authentication and post previews. Be sure to install the [WordPress plugin](https://github.com/wpengine/headless-framework) that enables the functionality in this package. + +[Documentation](https://github.com/wpengine/headless-framework) diff --git a/docs/reference/api/interfaces/cookieoptions.md b/docs/reference/api/interfaces/cookieoptions.md new file mode 100644 index 000000000..173fa7c58 --- /dev/null +++ b/docs/reference/api/interfaces/cookieoptions.md @@ -0,0 +1,30 @@ +[@wpengine/headless](../README.md) / [Exports](../modules.md) / CookieOptions + +# Interface: CookieOptions + +## Hierarchy + +* **CookieOptions** + +## Table of contents + +### Properties + +- [cookies](cookieoptions.md#cookies) +- [request](cookieoptions.md#request) + +## Properties + +### cookies + +• `Optional` **cookies**: *undefined* \| *string* + +Defined in: [auth/cookie.ts:7](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/auth/cookie.ts#L7) + +___ + +### request + +• `Optional` **request**: *undefined* \| *IncomingMessage* + +Defined in: [auth/cookie.ts:6](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/auth/cookie.ts#L6) diff --git a/docs/reference/api/interfaces/headlessconfig.md b/docs/reference/api/interfaces/headlessconfig.md new file mode 100644 index 000000000..faeb1e25d --- /dev/null +++ b/docs/reference/api/interfaces/headlessconfig.md @@ -0,0 +1,34 @@ +[@wpengine/headless](../README.md) / [Exports](../modules.md) / HeadlessConfig + +# Interface: HeadlessConfig + +The configuration for your headless site + +**`export`** + +**`interface`** HeadlessConfig + +## Hierarchy + +* **HeadlessConfig** + +## Table of contents + +### Properties + +- [uriPrefix](headlessconfig.md#uriprefix) + +## Properties + +### uriPrefix + +• `Optional` **uriPrefix**: *undefined* \| *string* + +This is a prefix URI path that we will use as the base URL for your WordPress posts. +By default we will assume that your site is configured with no blog-specific URI. + +**`example`** /blog + +**`memberof`** WPEHeadlessConfig + +Defined in: [types.ts:17](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/types.ts#L17) diff --git a/docs/reference/api/interfaces/menuitem.md b/docs/reference/api/interfaces/menuitem.md new file mode 100644 index 000000000..3dbc3101b --- /dev/null +++ b/docs/reference/api/interfaces/menuitem.md @@ -0,0 +1,32 @@ +[@wpengine/headless](../README.md) / [Exports](../modules.md) / MenuItem + +# Interface: MenuItem + +## Hierarchy + +* *DetailedHTMLProps*<*React.AnchorHTMLAttributes*, HTMLAnchorElement\> + + ↳ **MenuItem** + +## Table of contents + +### Properties + +- [children](menuitem.md#children) +- [title](menuitem.md#title) + +## Properties + +### children + +• `Optional` **children**: *undefined* \| [*MenuItem*](menuitem.md)[] + +Defined in: [components/menu/MenuItemInterface.ts:9](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/components/menu/MenuItemInterface.ts#L9) + +___ + +### title + +• **title**: *string* + +Defined in: [components/menu/MenuItemInterface.ts:8](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/components/menu/MenuItemInterface.ts#L8) diff --git a/docs/reference/api/interfaces/parsedurlinfo.md b/docs/reference/api/interfaces/parsedurlinfo.md new file mode 100644 index 000000000..4b7505539 --- /dev/null +++ b/docs/reference/api/interfaces/parsedurlinfo.md @@ -0,0 +1,81 @@ +[@wpengine/headless](../README.md) / [Exports](../modules.md) / ParsedUrlInfo + +# Interface: ParsedUrlInfo + +The result of parsing a URL into its parts + +**`export`** + +**`interface`** ParsedUrlInfo + +## Hierarchy + +* **ParsedUrlInfo** + +## Table of contents + +### Properties + +- [baseUrl](parsedurlinfo.md#baseurl) +- [hash](parsedurlinfo.md#hash) +- [host](parsedurlinfo.md#host) +- [href](parsedurlinfo.md#href) +- [pathname](parsedurlinfo.md#pathname) +- [protocol](parsedurlinfo.md#protocol) +- [search](parsedurlinfo.md#search) + +## Properties + +### baseUrl + +• **baseUrl**: *string* + +Defined in: [types.ts:29](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/types.ts#L29) + +___ + +### hash + +• **hash**: *string* + +Defined in: [types.ts:33](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/types.ts#L33) + +___ + +### host + +• **host**: *string* + +Defined in: [types.ts:30](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/types.ts#L30) + +___ + +### href + +• **href**: *string* + +Defined in: [types.ts:27](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/types.ts#L27) + +___ + +### pathname + +• **pathname**: *string* + +Defined in: [types.ts:31](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/types.ts#L31) + +___ + +### protocol + +• **protocol**: *string* + +Defined in: [types.ts:28](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/types.ts#L28) + +___ + +### search + +• **search**: *string* + +Defined in: [types.ts:32](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/types.ts#L32) diff --git a/docs/reference/api/interfaces/uriinfo.md b/docs/reference/api/interfaces/uriinfo.md new file mode 100644 index 000000000..70401267f --- /dev/null +++ b/docs/reference/api/interfaces/uriinfo.md @@ -0,0 +1,108 @@ +[@wpengine/headless](../README.md) / [Exports](../modules.md) / UriInfo + +# Interface: UriInfo + +WordPress URI information + +**`export`** + +**`interface`** UriInfo + +## Hierarchy + +* **UriInfo** + +## Table of contents + +### Properties + +- [id](uriinfo.md#id) +- [idType](uriinfo.md#idtype) +- [is404](uriinfo.md#is404) +- [isArchive](uriinfo.md#isarchive) +- [isFrontPage](uriinfo.md#isfrontpage) +- [isPostsPage](uriinfo.md#ispostspage) +- [isPreview](uriinfo.md#ispreview) +- [isSingular](uriinfo.md#issingular) +- [templates](uriinfo.md#templates) +- [uriPath](uriinfo.md#uripath) + +## Properties + +### id + +• `Optional` **id**: *undefined* \| *string* + +Defined in: [types.ts:43](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/types.ts#L43) + +___ + +### idType + +• `Optional` **idType**: *undefined* \| *DATABASE_ID* \| *ID* \| *URI* + +Defined in: [types.ts:44](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/types.ts#L44) + +___ + +### is404 + +• `Optional` **is404**: *undefined* \| *boolean* + +Defined in: [types.ts:50](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/types.ts#L50) + +___ + +### isArchive + +• `Optional` **isArchive**: *undefined* \| *boolean* + +Defined in: [types.ts:48](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/types.ts#L48) + +___ + +### isFrontPage + +• `Optional` **isFrontPage**: *undefined* \| *boolean* + +Defined in: [types.ts:46](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/types.ts#L46) + +___ + +### isPostsPage + +• `Optional` **isPostsPage**: *undefined* \| *boolean* + +Defined in: [types.ts:45](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/types.ts#L45) + +___ + +### isPreview + +• `Optional` **isPreview**: *undefined* \| *boolean* + +Defined in: [types.ts:47](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/types.ts#L47) + +___ + +### isSingular + +• `Optional` **isSingular**: *undefined* \| *boolean* + +Defined in: [types.ts:49](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/types.ts#L49) + +___ + +### templates + +• `Optional` **templates**: *undefined* \| *string*[] + +Defined in: [types.ts:52](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/types.ts#L52) + +___ + +### uriPath + +• **uriPath**: *string* + +Defined in: [types.ts:51](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/types.ts#L51) diff --git a/docs/reference/api/modules.md b/docs/reference/api/modules.md new file mode 100644 index 000000000..a5cef96c5 --- /dev/null +++ b/docs/reference/api/modules.md @@ -0,0 +1,824 @@ +[@wpengine/headless](README.md) / Exports + +# @wpengine/headless + +## Table of contents + +### Interfaces + +- [CookieOptions](interfaces/cookieoptions.md) +- [HeadlessConfig](interfaces/headlessconfig.md) +- [MenuItem](interfaces/menuitem.md) +- [ParsedUrlInfo](interfaces/parsedurlinfo.md) +- [UriInfo](interfaces/uriinfo.md) + +### Variables + +- [APOLLO\_STATE\_PROP\_NAME](modules.md#apollo_state_prop_name) +- [COOKIE\_KEY](modules.md#cookie_key) + +### Functions + +- [HeadlessProvider](modules.md#headlessprovider) +- [Menu](modules.md#menu) +- [NextTemplateLoader](modules.md#nexttemplateloader) +- [TemplateLoader](modules.md#templateloader) +- [WPHead](modules.md#wphead) +- [addApolloState](modules.md#addapollostate) +- [authorize](modules.md#authorize) +- [ensureAuthorization](modules.md#ensureauthorization) +- [getAccessToken](modules.md#getaccesstoken) +- [getAccessTokenAsCookie](modules.md#getaccesstokenascookie) +- [getContentNode](modules.md#getcontentnode) +- [getGeneralSettings](modules.md#getgeneralsettings) +- [getPosts](modules.md#getposts) +- [getUriInfo](modules.md#geturiinfo) +- [headlessConfig](modules.md#headlessconfig) +- [initializeApollo](modules.md#initializeapollo) +- [initializeCookies](modules.md#initializecookies) +- [initializeNextServerSideProps](modules.md#initializenextserversideprops) +- [initializeNextStaticPaths](modules.md#initializenextstaticpaths) +- [initializeNextStaticProps](modules.md#initializenextstaticprops) +- [nextAuthorizeHandler](modules.md#nextauthorizehandler) +- [storeAccessToken](modules.md#storeaccesstoken) +- [useApollo](modules.md#useapollo) +- [useGeneralSettings](modules.md#usegeneralsettings) +- [useNextUriInfo](modules.md#usenexturiinfo) +- [usePost](modules.md#usepost) +- [usePosts](modules.md#useposts) +- [useUriInfo](modules.md#useuriinfo) + +## Variables + +### APOLLO\_STATE\_PROP\_NAME + +• `Const` **APOLLO\_STATE\_PROP\_NAME**: *__APOLLO_STATE__*= '\_\_APOLLO\_STATE\_\_' + +Defined in: [provider/apolloClient.ts:27](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/provider/apolloClient.ts#L27) + +___ + +### COOKIE\_KEY + +• `Const` **COOKIE\_KEY**: *string* + +Defined in: [auth/cookie.ts:13](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/auth/cookie.ts#L13) + +## Functions + +### HeadlessProvider + +▸ **HeadlessProvider**(`__namedParameters`: *React.PropsWithChildren*): JSX.Element + +Provider component to be used in your Next.js Custom `App` component (pages/_app.js) + +**`see`** https://nextjs.org/docs/advanced-features/custom-app + +**`example`** +```ts +import {WPGraphQLProvider} from '@wpengine/headless/graphql' + +function MyApp({Component, pageProps}) { + return ( + + + + ) +} + +export default MyApp +``` + +#### Parameters: + +• **__namedParameters**: *React.PropsWithChildren* + +**Returns:** JSX.Element + +Defined in: [provider/HeadlessProvider.tsx:32](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/provider/HeadlessProvider.tsx#L32) + +___ + +### Menu + +▸ **Menu**(`__namedParameters`: Props): JSX.Element \| *null* + +Menu component to display menu items. + +**`example`** +```ts +import { Menu, MenuItem } from '@wpengine/headless/components' + +function MyApp() { + const items = [ + { title: "Home", href: "/" }, + { + title: "About", + href: "/about", + children: [{ title: "Careers", href: "/careers" }], + }, + ]; + + // Alter link output if required. Remember to import `Link` components. + const nextLink = (item: MenuItem): React.ReactNode => ( + + {item.title} + + ); + const reactRouterLink = (item: MenuItem): React.ReactNode => ( + {item.title} + ); + + return ( + <> +

+ + + + + ); +} + +export default MyApp +``` + +#### Parameters: + +• **__namedParameters**: Props + +**Returns:** JSX.Element \| *null* + +Defined in: [components/menu/Menu.tsx:54](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/components/menu/Menu.tsx#L54) + +___ + +### NextTemplateLoader + +▸ **NextTemplateLoader**(`__namedParameters`: { `templates`: WPTemplates }): JSX.Element \| *null* + +#### Parameters: + +• **__namedParameters**: *object* + +Name | Type | +------ | ------ | +`templates` | WPTemplates | + +**Returns:** JSX.Element \| *null* + +Defined in: [components/NextTemplateLoader.tsx:6](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/components/NextTemplateLoader.tsx#L6) + +___ + +### TemplateLoader + +▸ **TemplateLoader**(`__namedParameters`: { `dynamicLoader`: (`loader`: () => *Promise*) => React.ComponentType ; `templates`: WPTemplates ; `uriInfo`: [*UriInfo*](interfaces/uriinfo.md) \| *undefined* }): JSX.Element \| *null* + +#### Parameters: + +• **__namedParameters**: *object* + +Name | Type | +------ | ------ | +`dynamicLoader` | (`loader`: () => *Promise*) => React.ComponentType | +`templates` | WPTemplates | +`uriInfo` | [*UriInfo*](interfaces/uriinfo.md) \| *undefined* | + +**Returns:** JSX.Element \| *null* + +Defined in: [components/TemplateLoader.tsx:22](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/components/TemplateLoader.tsx#L22) + +___ + +### WPHead + +▸ **WPHead**(): JSX.Element + +**Returns:** JSX.Element + +Defined in: [components/WPHead.tsx:5](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/components/WPHead.tsx#L5) + +___ + +### addApolloState + +▸ **addApolloState**(`client`: *ApolloClient*, `pageProps`: *GetServerSidePropsResult*<*unknown*\> \| *GetStaticPropsResult*<*unknown*\>): {} \| {} \| {} \| {} \| {} \| {} + +Merges the Apollo state with the page props passed through the various Next.js Data Fetching +functions such as getStaticProps, getServerSideProps, etc. + +**`example`** +```ts +export async function getStaticProps({preview = false}) { + const apolloClient = initializeApollo() + + await apolloClient.query({query: YOUR_QUERY}) + + return addApolloState(apolloClient, { + props: {preview}, + revalidate: 1 + }) +} +``` + +#### Parameters: + +Name | Type | +------ | ------ | +`client` | *ApolloClient* | +`pageProps` | *GetServerSidePropsResult*<*unknown*\> \| *GetStaticPropsResult*<*unknown*\> | + +**Returns:** {} \| {} \| {} \| {} \| {} \| {} + +Defined in: [provider/apolloClient.ts:166](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/provider/apolloClient.ts#L166) + +___ + +### authorize + +▸ **authorize**(`code`: *string*): *Promise*<{ `access_token?`: *string* }\> + +Exchanges an Authorization Code for an Access Token that you can use to make authenticated requests to +the WordPress API + +**`async`** + +**`export`** + +#### Parameters: + +Name | Type | +------ | ------ | +`code` | *string* | + +**Returns:** *Promise*<{ `access_token?`: *string* }\> + +>} + +Defined in: [auth/authorize.ts:35](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/auth/authorize.ts#L35) + +___ + +### ensureAuthorization + +▸ **ensureAuthorization**(`redirectUri`: *string*, `options?`: [*CookieOptions*](interfaces/cookieoptions.md)): *string* \| { `redirect`: *string* } \| *undefined* + +Checks for an existing Access Token and returns one if it exists. Otherwise returns +an object containing a redirect URI to send the client to for authorization. + +**`export`** + +#### Parameters: + +Name | Type | +------ | ------ | +`redirectUri` | *string* | +`options?` | [*CookieOptions*](interfaces/cookieoptions.md) | + +**Returns:** *string* \| { `redirect`: *string* } \| *undefined* + +)} + +Defined in: [auth/authorize.ts:74](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/auth/authorize.ts#L74) + +___ + +### getAccessToken + +▸ **getAccessToken**(`options?`: [*CookieOptions*](interfaces/cookieoptions.md)): *string* \| *undefined* + +Gets an Access Token from the cookie, if it exists + +**`export`** + +#### Parameters: + +Name | Type | +------ | ------ | +`options?` | [*CookieOptions*](interfaces/cookieoptions.md) | + +**Returns:** *string* \| *undefined* + +Defined in: [auth/cookie.ts:37](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/auth/cookie.ts#L37) + +___ + +### getAccessTokenAsCookie + +▸ **getAccessTokenAsCookie**(`options?`: [*CookieOptions*](interfaces/cookieoptions.md)): *string* \| *undefined* + +Gets an Access Token from the cookie and formats it as a cookie pair + +**`export`** + +#### Parameters: + +Name | Type | +------ | ------ | +`options?` | [*CookieOptions*](interfaces/cookieoptions.md) | + +**Returns:** *string* \| *undefined* + +Defined in: [auth/cookie.ts:56](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/auth/cookie.ts#L56) + +___ + +### getContentNode + +▸ **getContentNode**(`client`: *ApolloClient*, `id`: *string*, `idType?`: WPGraphQL.ContentNodeIdTypeEnum, `asPreview?`: *boolean*): *Promise* + +Gets an individual Post or Page from WordPress + +**`async`** + +**`export`** + +#### Parameters: + +Name | Type | Default value | Description | +------ | ------ | ------ | ------ | +`client` | *ApolloClient* | - | | +`id` | *string* | - | The identifier for the Post or Page | +`idType` | WPGraphQL.ContentNodeIdTypeEnum | 'URI' | - | +`asPreview` | *boolean* | false | - | + +**Returns:** *Promise* + +Defined in: [api/services.ts:42](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/api/services.ts#L42) + +___ + +### getGeneralSettings + +▸ **getGeneralSettings**(`client`: *ApolloClient*): *Promise* + +Gets the General Settings from WordPress + +**`async`** + +**`export`** + +#### Parameters: + +Name | Type | +------ | ------ | +`client` | *ApolloClient* | + +**Returns:** *Promise* + +Defined in: [api/services.ts:87](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/api/services.ts#L87) + +___ + +### getPosts + +▸ **getPosts**(`client`: *ApolloClient*): *Promise* + +Gets all posts from WordPress + +**`async`** + +**`export`** + +#### Parameters: + +Name | Type | +------ | ------ | +`client` | *ApolloClient* | + +**Returns:** *Promise* + +Defined in: [api/services.ts:21](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/api/services.ts#L21) + +___ + +### getUriInfo + +▸ **getUriInfo**(`client`: *ApolloClient*, `uriPath`: *string*, `isPreview?`: *boolean*): *Promise*<[*UriInfo*](interfaces/uriinfo.md) \| *void*\> + +Gets information about the URI from WordPress + +**`async`** + +**`export`** + +#### Parameters: + +Name | Type | Description | +------ | ------ | ------ | +`client` | *ApolloClient* | | +`uriPath` | *string* | The path for the URI (e.g. '/hello-world') | +`isPreview?` | *boolean* | - | + +**Returns:** *Promise*<[*UriInfo*](interfaces/uriinfo.md) \| *void*\> + +Defined in: [api/services.ts:108](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/api/services.ts#L108) + +___ + +### headlessConfig + +▸ **headlessConfig**(`config?`: [*HeadlessConfig*](interfaces/headlessconfig.md)): [*HeadlessConfig*](interfaces/headlessconfig.md) + +A setter/getter for the HeadlessConfig + +**`export`** + +#### Parameters: + +Name | Type | +------ | ------ | +`config?` | [*HeadlessConfig*](interfaces/headlessconfig.md) | + +**Returns:** [*HeadlessConfig*](interfaces/headlessconfig.md) + +Defined in: [config/config.ts:13](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/config/config.ts#L13) + +___ + +### initializeApollo + +▸ **initializeApollo**(`context?`: NextPageContext \| GetStaticPropsContext \| GetServerSidePropsContext, `initialState?`: *null*): *ApolloClient* + +Creates the Apollo Client instance if it doesn't already exist. This works on both the client side and server side. + +If client side, it will hydrate the cache using initial state passed through Next.js' Data Fetching functions. + +**`example`** +```ts +// Client-side +// For client-side, it's recommended that you use useApollo() instead initializeApollo() directly. +``` + +**`example`** +```ts +// Server-side +export async function getStaticProps() { + const apolloClient = initializeApollo() + + await apolloClient.query({ + query: ALL_POSTS_QUERY, + variables: allPostsQueryVars, + }) + + return addApolloState(apolloClient, { + props: {}, + revalidate: 1, + }) +} +``` + +#### Parameters: + +Name | Type | Default value | +------ | ------ | ------ | +`context?` | NextPageContext \| GetStaticPropsContext \| GetServerSidePropsContext | - | +`initialState` | *null* | null | + +**Returns:** *ApolloClient* + +Defined in: [provider/apolloClient.ts:105](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/provider/apolloClient.ts#L105) + +___ + +### initializeCookies + +▸ **initializeCookies**(`__namedParameters?`: [*CookieOptions*](interfaces/cookieoptions.md)): Cookies + +#### Parameters: + +• **__namedParameters**: [*CookieOptions*](interfaces/cookieoptions.md) + +**Returns:** Cookies + +Defined in: [auth/cookie.ts:15](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/auth/cookie.ts#L15) + +___ + +### initializeNextServerSideProps + +▸ **initializeNextServerSideProps**(`context`: GetServerSidePropsContext, `templates?`: WPTemplates): *Promise*<*GetServerSidePropsResult*<*unknown*\>\> + +Must be called from getServerSideProps within a Next app in order to support SSR. It will +initialized cookies and prefetch/cache the page content and bundle it with the page for +rehydration on the frontend. + +#### Parameters: + +Name | Type | Description | +------ | ------ | ------ | +`context` | GetServerSidePropsContext | The Next SSR context | +`templates?` | WPTemplates | to be made available to the template loader | + +**Returns:** *Promise*<*GetServerSidePropsResult*<*unknown*\>\> + +Defined in: [api/initializeNextServerSideProps.ts:19](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/api/initializeNextServerSideProps.ts#L19) + +___ + +### initializeNextStaticPaths + +▸ **initializeNextStaticPaths**(`override?`: GetStaticPathsResult): GetStaticPathsResult + +#### Parameters: + +Name | Type | +------ | ------ | +`override?` | GetStaticPathsResult | + +**Returns:** GetStaticPathsResult + +Defined in: [api/initializeNextStaticPaths.ts:9](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/api/initializeNextStaticPaths.ts#L9) + +___ + +### initializeNextStaticProps + +▸ **initializeNextStaticProps**(`context`: GetStaticPropsContext, `templates?`: WPTemplates): *Promise*<*GetServerSidePropsResult*<*unknown*\>\> + +Must be called from getServerSideProps within a Next app in order to support SSR. It will +initialized cookies and prefetch/cache the page content and bundle it with the page for +rehydration on the frontend. + +#### Parameters: + +Name | Type | Description | +------ | ------ | ------ | +`context` | GetStaticPropsContext | The Next SSR context | +`templates?` | WPTemplates | to be made available to the template loader | + +**Returns:** *Promise*<*GetServerSidePropsResult*<*unknown*\>\> + +Defined in: [api/initializeNextStaticProps.ts:19](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/api/initializeNextStaticProps.ts#L19) + +___ + +### nextAuthorizeHandler + +▸ **nextAuthorizeHandler**(`req`: NextApiRequest, `res`: NextApiResponse): *Promise*<*void*\> + +A Node handler for processing incomming requests to exchange an Authorization Code +for an Access Token using the WordPress API. Once the code is exchanged, this +handler stores the Access Token on the cookie and redirects to the frontend. + +#### Parameters: + +Name | Type | +------ | ------ | +`req` | NextApiRequest | +`res` | NextApiResponse | + +**Returns:** *Promise*<*void*\> + +Defined in: [auth/middleware.ts:10](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/auth/middleware.ts#L10) + +___ + +### storeAccessToken + +▸ **storeAccessToken**(`token`: *string* \| *undefined*, `res`: ServerResponse, `options`: [*CookieOptions*](interfaces/cookieoptions.md)): *void* + +Stores an Access Token on the cookie + +**`export`** + +#### Parameters: + +Name | Type | Description | +------ | ------ | ------ | +`token` | *string* \| *undefined* | | +`res` | ServerResponse | | +`options` | [*CookieOptions*](interfaces/cookieoptions.md) | - | + +**Returns:** *void* + +Defined in: [auth/cookie.ts:77](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/auth/cookie.ts#L77) + +___ + +### useApollo + +▸ **useApollo**(`ctx`: NextPageContext, `pageProps`: *Record*<*string*, *any*\>): *ApolloClient* + +React Hook to use the Apollo client. This is used by + +**`see`** WPGraphQLProvider + +#### Parameters: + +Name | Type | +------ | ------ | +`ctx` | NextPageContext | +`pageProps` | *Record*<*string*, *any*\> | + +**Returns:** *ApolloClient* + +Defined in: [provider/apolloClient.ts:185](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/provider/apolloClient.ts#L185) + +___ + +### useGeneralSettings + +▸ **useGeneralSettings**(): WPGraphQL.GeneralSettings \| *undefined* + +React Hook for retrieving the general settings (title, description) from your WordPress site + +**`example`** +```tsx +import { useGeneralSettings } from '@wpengine/headless'; + +export function Header() { + const settings = useGeneralSettings(); + + if (!settings) { + return <>; + } + + return ( +
+

{settings.title}

+

{settings.description}

+
+ ); +} +``` + +**`export`** + +**Returns:** WPGraphQL.GeneralSettings \| *undefined* + +Defined in: [api/hooks.ts:82](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/api/hooks.ts#L82) + +___ + +### useNextUriInfo + +▸ **useNextUriInfo**(): [*UriInfo*](interfaces/uriinfo.md) \| *undefined* + +React Hook for retrieving information about the current URI within a Next app. + +**`see`** useUriInfo For similar functionality outside of Next apps. + +**`example`** +```tsx +import { useNextUriInfo } from '@wpengine/headless'; + +export function Screen() { + const uriInfo = useNextUriInfo(); + + console.log(uriInfo); +} +``` + +**`export`** + +**Returns:** [*UriInfo*](interfaces/uriinfo.md) \| *undefined* + +Defined in: [api/hooks.ts:228](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/api/hooks.ts#L228) + +___ + +### usePost + +▸ **usePost**(): WPGraphQL.GetContentNodeQuery[*contentNode*] \| *undefined* + +React Hook for retrieving the post based on the current URI. Uses window.location if necessary + +**`example`** +```tsx +import { usePost } from '@wpengine/headless'; + +export default function Post() { + const post = usePost(); + + return ( +
+ {post && ( +
+
+
{post.title}
+

+

+
+ )} +
+ ); +} +``` + +**`export`** + +**Returns:** WPGraphQL.GetContentNodeQuery[*contentNode*] \| *undefined* + +Defined in: [api/hooks.ts:267](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/api/hooks.ts#L267) + +▸ **usePost**(`id`: *string*, `idType`: WPGraphQL.ContentNodeIdTypeEnum): WPGraphQL.GetContentNodeQuery[*contentNode*] + +React Hook for retrieving the post based on the passed-in id and idType. + +**`see`** ContentNodeIdType For the different types of identifiers you can pass in + +**`example`** +```tsx +import { usePost, ContentNodeIdType } from '@wpengine/headless'; + +export default function Post({ slug }: { slug: string; }) { + const post = usePost(slug, ContentNodeIdType.SLUG); + + return ( +
+ {post && ( +
+
+
{post.title}
+

+

+
+ )} +
+ ); +} +``` + +**`export`** + +#### Parameters: + +Name | Type | Description | +------ | ------ | ------ | +`id` | *string* | The identifier for the post based on ContentNodeIdType | +`idType` | WPGraphQL.ContentNodeIdTypeEnum | The description of the type of id passed in | + +**Returns:** WPGraphQL.GetContentNodeQuery[*contentNode*] + +Defined in: [api/hooks.ts:301](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/api/hooks.ts#L301) + +___ + +### usePosts + +▸ **usePosts**(): WPGraphQL.GetPostsQuery[*posts*][*nodes*] \| *undefined* + +React Hook for retrieving a list of posts from your WordPress site + +**`example`** +```tsx +import { usePosts } from '@wpengine/headless'; + +export function ListPosts() { + const posts = usePosts(); + + if (!posts) { + return <>; + } + + return ( + <> + {posts.map((post) => ( +
+ ))} + + ); +} +``` + +**`export`** + +**Returns:** WPGraphQL.GetPostsQuery[*posts*][*nodes*] \| *undefined* + +Defined in: [api/hooks.ts:49](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/api/hooks.ts#L49) + +___ + +### useUriInfo + +▸ **useUriInfo**(`uri?`: *string*, `resolvedUri?`: *string*): [*UriInfo*](interfaces/uriinfo.md) \| *undefined* + +React Hook for retrieving information about the current URI. Expects you to +either pass in a URI, otherwise it will use window.location + +**`see`** useNextUriInfo For similar functionality inside Next apps. + +**`example`** +```tsx +import { useUriInfo } from '@wpengine/headless'; + +export function Screen() { + const uriInfo = useUriInfo(); + + console.log(uriInfo); +} +``` + +**`export`** + +#### Parameters: + +Name | Type | +------ | ------ | +`uri?` | *string* | +`resolvedUri?` | *string* | + +**Returns:** [*UriInfo*](interfaces/uriinfo.md) \| *undefined* + +Defined in: [api/hooks.ts:107](https://github.com/wpengine/headless-framework/blob/9e3ac37/packages/headless/src/api/hooks.ts#L107) diff --git a/docs/reference/components/wphead/README.md b/docs/reference/components/wphead/README.md new file mode 100644 index 000000000..b723c24d4 --- /dev/null +++ b/docs/reference/components/wphead/README.md @@ -0,0 +1,101 @@ +# WPHead + +The `` component injects a title element and stylesheets needed by the current WordPress page. + +WPHead requires [Next.js](https://nextjs.org/) with the `usePost` and `useGeneralSettings` custom hooks from the `@wpengine/headless` package. These hooks assume the connected WordPress site is using GraphQL and your JavaScript app is using the `HeadlessProvider` component. + +## Usage + +Add the `` component to your site's header: + +```tsx +import React from 'react'; +import { WPHead } from '@wpengine/headless/nest'; + +function Header(): JSX.Element { + return ( + <> + +
+ {/* Your site title and menu items here*/} +
+ + ); +} + +export default Header; +``` + +Your site's head will contain the page title and stylesheets required by WordPress, such as Gutenberg block CSS. + +## Add extra content to head + +Use the [Next.js `Head` component](https://nextjs.org/docs/api-reference/next/head) before `WPHead` to add additional elements to the site head: + +```tsx +import React from 'react'; +import { WPHead } from '@wpengine/headless/next'; +import Head from 'next/head'; + +function Header(): JSX.Element { + return ( + <> + + {/* Title is required here but replaced by WPHead. */} + {/* Add extra elements to here, such as web font links: */} + + + + + {/* Next.js combines elements in the Head component above with those in WPHead. */} + +
+ {/* Your site title and menu items here*/} +
+ + ); +} + +export default Header; +``` + +## Override the WPHead page title + +Place a Next.js `Head` component _after_ `WPHead` to override its title: + +```tsx +import React from 'react'; +import { WPHead } from '@wpengine/headless/next'; +import Head from 'next/head'; + +function Header(): JSX.Element { + return ( + <> + + + Your custom page title here + +
+ {/* Your site title and menu items here*/} +
+ + ); +} + +export default Header; +``` + +A page can contain multiple Next.js `` components. Next.js will combine them into one. diff --git a/docs/reference/hooks.md b/docs/reference/hooks.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/reference/plugin.md b/docs/reference/plugin.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/templating/README.md b/docs/templating/README.md new file mode 100644 index 000000000..f13e53f4e --- /dev/null +++ b/docs/templating/README.md @@ -0,0 +1,115 @@ +# Template Hierarchy + +Arguably, one of the most powerful systems in WordPress Theme API is its [Template Hierarchy](https://developer.wordpress.org/themes/basics/template-hierarchy/). Template Hierarchy is WordPress's way of allowing customizations of various page types on highly dynamic websites while also allowing fine granularity to make templating exceptions. + +### Template Hierarchy Visual Overview + +In the overview below, if you start with a page on the left and trace it to the right, you'll see what template files WordPress will attempt to load. If there are no templates found, it will fallback to `index.php`. + +![WordPress Template Hierarchy Diagram](https://developer.wordpress.org/files/2014/10/Screenshot-2019-01-23-00.20.04.png) +(Credit: https://developer.wordpress.org/themes/basics/template-hierarchy/#visual-overview) + +## Template Hierarchy in Next.js + +While Next.js has a robust [routing layer](https://nextjs.org/docs/routing/introduction), it is not always ideal for WordPress-based sites where routes are built around directory and file structures rather than content types. + +As an example, you can customize pages under an `/account` path by adding files to `pages/account`. While this is powerful, it doesn't immediately offer ways to customize specific content types like WordPress's Template Hierarchy allows for by adding files such as `single-$posttype.php` or `category.php`. + +Luckily, the WP Engine Headless Framework brings WordPress Template Hierarchy to Next.js! 🚀 + +### Enabling Template Hierarchy in Next.js + +The steps below assume that you have created a `pages/_app.tsx` file that uses ``. + +#### 1. Embed `` + +The first step in enabling Template Hierarchy in your Next.js project using this framework is to create an [optional catch-all route](https://nextjs.org/docs/routing/dynamic-routes#optional-catch-all-routes) in the `pages` directory named `[[...page]].tsx`. + +This optional catch-all route will act as a fallback for all pages and route all requests to the Headless Framework's Template Loader. + +The component in this page should simply return `` with a `templates` prop. Example: + +```tsx +import React from 'react'; +import { + NextTemplateLoader, + getNextStaticPaths, + getNextStaticProps, +} from '@wpengine/headless/next'; + +import WPTemplates from '../wp-templates/_loader'; + +export default function Page() { + return ; +} + +export function getStaticProps(context: any) { + return getNextStaticProps(context, WPTemplates); +} + +export function getStaticPaths() { + return getNextStaticPaths(); +} +``` + +#### 2. Add Templates + +After setting up the Template Loader, the next step is to add templates and a loader file that exports all of the templates. We recommend adding templates to a directory named `wp-templates` adjacent to the `pages` folder in a Next.js project. + +The template's name should follow WordPress's template hierarchy above, but the files should be JSX/TSX instead of PHP. + +**Examples:** + +* `wp-templates/index.tsx` +* `wp-templates/single.tsx` +* `wp-templates/page.tsx` +* `wp-templates/page-example-slug.tsx` + +#### 2b. Export templates + +After adding the templates, you will want to export them in a file named `wp-templates/_loader.ts`. + +```typescript +const templates = { + index: import('./index'), + page: import('./page'), + 'page-example-slug': import('./page-example-slug'), + single: import('./single'), +}; + +export default templates; +``` + +## Anatomy of a Template + +### React Component + +Much like a Next.js page, templates need to export a React component at a minimum. + +### `getServerSideProps` and `getStaticProps` + +Optionally, you can export named functions in the template called `getServerSideProps` and `getStaticProps` with the following signatures: + +```tsx +export async function getStaticProps( + context: GetStaticPropsContext, +): Promise; + +export async function getServerSideProps( + context: GetServerSidePropsContext, +): Promise; +``` + +Use these functions to make additional requests using the Apollo Client, which executes with Next.js [Data Fetchers](https://nextjs.org/docs/basic-features/data-fetching) run. + +You may find this helpful when you need additional data from the WordPress backend that is not present by default. + +In an average Next.js page, you would only implement one of these functions. Since theme components are not Next.js pages, these functions call on the framework, not Next.js. If you are shipping a reusable Theme component you will want to export both functions. The framework will call on the function that corresponds to what the Next.js page uses. So if the Next.js page is using `getStaticProps`, the framework will call the `getStaticProps` function on your theme component. + +### Example + +See [`single.tsx`](https://github.com/wpengine/headless-framework/blob/canary/examples/preview/theme/single.tsx) in our preview example. This template exports the component `getStaticProps` and `getServerSideProps` to modify the default data requested in the Next.js Data Fetchers. + +# CMS-Based Routing (Template Hierarchy) Flow + +![CMS-Based Routing](/docs/templating/cms-based-routing.jpg) diff --git a/docs/templating/cms-based-routing.jpg b/docs/templating/cms-based-routing.jpg new file mode 100644 index 000000000..f8306bfdc Binary files /dev/null and b/docs/templating/cms-based-routing.jpg differ diff --git a/examples/getting-started/.env.local.sample b/examples/getting-started/.env.local.sample new file mode 100644 index 000000000..ea9e9d1cf --- /dev/null +++ b/examples/getting-started/.env.local.sample @@ -0,0 +1,15 @@ +# Either WORDPRESS_URL or NEXT_PUBLIC_WORDPRESS_URL need to be populated. Not both! +# +# Setting WORDPRESS_URL instead of NEXT_PUBLIC_WORDPRESS_URL will limit requests to the WordPress backend +# to only come from the Node.js server. +# +# Setting NEXT_PUBLIC_WORDPRESS_URL instead of WORDPRESS_URL will allow requests to come from the client-side which may +# reduce site performance and put extra load on the WordPress backend. +# +# NOTE: In order for previews to work you must use NEXT_PUBLIC_WORDPRESS_URL + +NEXT_PUBLIC_WORDPRESS_URL=https://headlessfw.wpengine.com +# WORDPRESS_URL=https://headlessfw.wpengine.com + +# Plugin secret found in WordPress Settings->Headless +WP_HEADLESS_SECRET=YOUR_PLUGIN_SECRET diff --git a/examples/getting-started/.env.test.sample b/examples/getting-started/.env.test.sample new file mode 100644 index 000000000..7735a1d1d --- /dev/null +++ b/examples/getting-started/.env.test.sample @@ -0,0 +1,13 @@ +# Either WORDPRESS_URL or NEXT_PUBLIC_WORDPRESS_URL need to be populated. Not both! +# +# (Recommended) Setting WORDPRESS_URL instead of NEXT_PUBLIC_WORDPRESS_URL will limit requests to the WordPress backend +# to only come from the Node.js server. +# +# Setting NEXT_PUBLIC_WORDPRESS_URL instead of WORDPRESS_URL will allow requests to come from the client-side which may +# reduce site performance and put extra load on the WordPress backend. + +NEXT_PUBLIC_WORDPRESS_URL=http://localhost:8080 +# WORDPRESS_URL=http://localhost:8080 + +# Plugin secret found in WordPress Settings->Headless +WP_HEADLESS_SECRET=00000000-0000-0000-0000-000000000001 diff --git a/examples/getting-started/.eslintrc.js b/examples/getting-started/.eslintrc.js new file mode 100644 index 000000000..961f04c2a --- /dev/null +++ b/examples/getting-started/.eslintrc.js @@ -0,0 +1,55 @@ +module.exports = { + env: { + browser: true, + es6: true, + node: true, + amd: true, + }, + parser: '@typescript-eslint/parser', // Specifies the ESLint parser + extends: [ + 'airbnb-typescript', + 'airbnb/hooks', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-requiring-type-checking', + 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier + 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. + ], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json', + ecmaFeatures: { + ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features + sourceType: 'module', // Allows for the use of imports + jsx: true, + }, + }, + plugins: ['react', 'react-hooks', 'simple-import-sort'], + rules: { + // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs + // e.g. "@typescript-eslint/explicit-function-return-type": "off", + 'import/no-extraneous-dependencies': 0, + '@typescript-eslint/unbound-method': 0, + 'no-void': 0, + 'import/named': 0, + 'import/prefer-default-export': 0, + 'react/jsx-closing-bracket-location': 0, + 'react/jsx-wrap-multilines': [ + 'error', + { declaration: false, assignment: false }, + ], + 'react/require-default-props': 0, + 'jsx-a11y/anchor-is-valid': 0, + }, + settings: { + react: { + createClass: 'createReactClass', // Regex for Component Factory to use, + // default to "createReactClass" + pragma: 'React', // Pragma to use, default to "React" + version: '17.0', // React version. "detect" automatically picks the version you have installed. + // You can also use `16.0`, `16.3`, etc, if you want to override the detected value. + // default to latest and warns if missing + // It will default to "detect" in the future + }, + }, +}; diff --git a/examples/getting-started/.gitattributes b/examples/getting-started/.gitattributes new file mode 100644 index 000000000..191e79e52 --- /dev/null +++ b/examples/getting-started/.gitattributes @@ -0,0 +1,65 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto +Dockerfile eol=lf +*.sh eol=lf + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/examples/getting-started/.gitignore b/examples/getting-started/.gitignore new file mode 100644 index 000000000..cbb8e80a2 --- /dev/null +++ b/examples/getting-started/.gitignore @@ -0,0 +1,8 @@ +node_modules + +.next +.env +.env.* +!.env*.sample + +wpe.json diff --git a/examples/getting-started/.prettierrc.js b/examples/getting-started/.prettierrc.js new file mode 100644 index 000000000..0f198db28 --- /dev/null +++ b/examples/getting-started/.prettierrc.js @@ -0,0 +1,10 @@ +module.exports = { + arrowParens: 'always', + jsxBracketSameLine: true, + singleQuote: true, + tabWidth: 2, + semi: true, + trailingComma: 'all', + endOfLine: 'lf', + useTabs: false, +}; diff --git a/examples/getting-started/README.md b/examples/getting-started/README.md new file mode 100644 index 000000000..f9bd4188f --- /dev/null +++ b/examples/getting-started/README.md @@ -0,0 +1,14 @@ +# Headless WordPress Getting Started Example + +## Setup + +See the [setup steps](https://github.com/wpengine/headless-framework#quick-start). + +## Run it + +```bash +npm install +npm run dev +``` + +[http://localhost:3000]() diff --git a/examples/getting-started/assets/logos/mark.svg b/examples/getting-started/assets/logos/mark.svg new file mode 100644 index 000000000..bbf173410 --- /dev/null +++ b/examples/getting-started/assets/logos/mark.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/examples/getting-started/assets/logos/text.svg b/examples/getting-started/assets/logos/text.svg new file mode 100644 index 000000000..fd3d57a57 --- /dev/null +++ b/examples/getting-started/assets/logos/text.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/examples/getting-started/components/CTA.tsx b/examples/getting-started/components/CTA.tsx new file mode 100644 index 000000000..039cdc088 --- /dev/null +++ b/examples/getting-started/components/CTA.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import styles from 'scss/components/CTA.module.scss'; +import Heading, { HeadingProps } from './Heading'; + +interface Props { + title: string; + buttonText?: string; + buttonURL?: string; + children?: React.ReactNode; + headingLevel?: HeadingProps['level']; +} + +function CTA({ + title = 'Get in touch', + buttonText, + buttonURL, + children, + headingLevel = 'h1', +}: Props): JSX.Element { + return ( +
+
+ + {title} + +
+
{children}
+ {buttonText && buttonURL && ( + + )} +
+
+
+ ); +} + +export default CTA; diff --git a/examples/getting-started/components/Footer.tsx b/examples/getting-started/components/Footer.tsx new file mode 100644 index 000000000..f3e35d913 --- /dev/null +++ b/examples/getting-started/components/Footer.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import styles from 'scss/components/Footer.module.scss'; + +interface Props { + copyrightHolder?: string; +} + +function Footer({ copyrightHolder = 'Company Name' }: Props): JSX.Element { + const year = new Date().getFullYear(); + + return ( +
+
+

{`© ${year} ${copyrightHolder}. All rights reserved.`}

+
+
+ ); +} + +export default Footer; diff --git a/examples/getting-started/components/Header.tsx b/examples/getting-started/components/Header.tsx new file mode 100644 index 000000000..c5a141955 --- /dev/null +++ b/examples/getting-started/components/Header.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { WPHead } from '@wpengine/headless/next'; +import styles from 'scss/components/Header.module.scss'; +import Link from 'next/link'; +import Head from 'next/head'; + +interface Props { + title?: string; + description?: string; +} + +function Header({ + title = 'Headless by WP Engine', + description, +}: Props): JSX.Element { + // TODO: accept a `menuItems` prop to receive menu items from WordPress. + const menuItems = [ + { title: 'Home', href: '/' }, + { title: 'About', href: '/about' }, + { title: 'Posts', href: '/category/uncategorized' }, + { + title: 'GitHub', + href: 'https://github.com/wpengine/headless-framework', + class: 'button', + }, + ]; + + return ( + <> + + {/* Title is required here but replaced by WPHead. */} + {/* Add extra elements to here. */} + + + + + +
+
+
+

+ + {title} + +

+ {description &&

{description}

} +
+
+
    + {menuItems && + menuItems.map((item) => ( +
  • + + {item.title} + +
  • + ))} +
+
+
+
+ + ); +} + +export default Header; diff --git a/examples/getting-started/components/Heading.tsx b/examples/getting-started/components/Heading.tsx new file mode 100644 index 000000000..b408fee48 --- /dev/null +++ b/examples/getting-started/components/Heading.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +// HeadingProps constrains headings to levels h1-h6. +interface HeadingProps extends React.HTMLAttributes { + level: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; +} + +// Heading allows components to pass a heading level via props. +function Heading({ + level = 'h1', + children, + className, +}: HeadingProps): JSX.Element { + const H = ({ ...props }: React.HTMLAttributes) => + React.createElement(level, props, children); + + return {children}; +} + +export default Heading; +export type { HeadingProps }; diff --git a/examples/getting-started/components/Hero.tsx b/examples/getting-started/components/Hero.tsx new file mode 100644 index 000000000..a767ef47d --- /dev/null +++ b/examples/getting-started/components/Hero.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import styles from 'scss/components/Hero.module.scss'; + +interface Props { + title: string; + id?: string; + bgImage?: string; + buttonText?: string; + buttonURL?: string; + button2Text?: string; + button2URL?: string; + children?: React.ReactNode; +} + +function Hero({ + title = 'Hero Title', + id, + bgImage, + buttonText, + buttonURL, + button2Text, + button2URL, + children, +}: Props): JSX.Element { + return ( +
+
+

{title}

+
+
{children}
+ {buttonText && buttonURL && ( +

+ + {buttonText} + +

+ )} + {button2Text && button2URL && ( +

+ + {button2Text} + +

+ )} +
+
+
+ ); +} + +export default Hero; diff --git a/examples/getting-started/components/Posts.tsx b/examples/getting-started/components/Posts.tsx new file mode 100644 index 000000000..6fe670322 --- /dev/null +++ b/examples/getting-started/components/Posts.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import Link from 'next/link'; +import styles from 'scss/components/Posts.module.scss'; +import Heading, { HeadingProps } from './Heading'; + +interface Props { + posts: WPGraphQL.Post[] | undefined; + intro?: string; + id?: string; + heading?: string; + headingLevel?: HeadingProps['level']; + postTitleLevel?: HeadingProps['level']; + readMoreText?: string; +} + +function Posts({ + posts, + intro, + heading, + id, + headingLevel = 'h1', + postTitleLevel = 'h2', + readMoreText = 'Read more', +}: Props): JSX.Element { + return ( + // eslint-disable-next-line react/jsx-props-no-spreading +
+
+ {heading && ( + + {heading} + + )} + {intro &&

{intro}

} +
+ {posts && + posts.map((post) => ( +
+
+ + + {post.title} + + + +
+ ))} + {posts && posts?.length < 1 &&

No posts found.

} +
+
+
+ ); +} + +export default Posts; diff --git a/examples/getting-started/components/index.ts b/examples/getting-started/components/index.ts new file mode 100644 index 000000000..19c01ab77 --- /dev/null +++ b/examples/getting-started/components/index.ts @@ -0,0 +1,7 @@ +import CTA from './CTA'; +import Footer from './Footer'; +import Header from './Header'; +import Hero from './Hero'; +import Posts from './Posts'; + +export { CTA, Footer, Header, Hero, Posts }; diff --git a/examples/getting-started/next-env.d.ts b/examples/getting-started/next-env.d.ts new file mode 100644 index 000000000..7b7aa2c77 --- /dev/null +++ b/examples/getting-started/next-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/examples/getting-started/next.config.js b/examples/getting-started/next.config.js new file mode 100644 index 000000000..8241797a8 --- /dev/null +++ b/examples/getting-started/next.config.js @@ -0,0 +1,3 @@ +const withWPEHeadless = require('@wpengine/headless/nextConfig'); + +module.exports = withWPEHeadless(); diff --git a/examples/getting-started/package.json b/examples/getting-started/package.json new file mode 100644 index 000000000..86729c265 --- /dev/null +++ b/examples/getting-started/package.json @@ -0,0 +1,64 @@ +{ + "name": "getting-started", + "version": "1.0.0", + "private": true, + "description": "WordPress Headless Getting Started Template", + "main": "index.js", + "scripts": { + "build": "next build", + "clean": "rimraf .next node_modules", + "dev": "next dev", + "dev:test": "NODE_ENV=test next dev", + "dev-lerna": "wait-on ./node_modules/@wpengine/headless/dist/index.js && npm run dev", + "open": "run-s open:local", + "open:local": "open-cli http://localhost:3000/", + "lint": "tsc --noEmit -p . && eslint **/*.{ts,tsx} --parser-options=project:tsconfig.json --quiet --fix", + "start": "next start -p 8080", + "test": "echo \"Error: no test specified\" && exit 1", + "wpe-build": "next build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wpengine/headless-framework/tree/main.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/wpengine/headless-framework/tree/main/issues" + }, + "homepage": "https://github.com/wpengine/headless-framework/tree/main/examples/getting-started#readme", + "dependencies": { + "@apollo/client": "^3.3.4", + "@wpengine/headless": "^0.6.3", + "graphql": "^15.4.0", + "next": "^10.0.9", + "normalize.css": "^8.0.1", + "npm-run-all": "^4.1.5", + "react": "^17.0.1", + "react-dom": "^17.0.1", + "sass": "^1.32.5" + }, + "devDependencies": { + "@svgr/webpack": "^5.5.0", + "@types/node": "^14.14.11", + "@types/react": "^17.0.0", + "@typescript-eslint/eslint-plugin": "^4.9.1", + "@typescript-eslint/parser": "^4.9.1", + "eslint": "^7.15.0", + "eslint-config-airbnb-typescript": "^12.0.0", + "eslint-config-prettier": "^7.0.0", + "eslint-plugin-import": "^2.22.1", + "eslint-plugin-jsx-a11y": "^6.4.1", + "eslint-plugin-prettier": "^3.2.0", + "eslint-plugin-react": "^7.21.5", + "eslint-plugin-react-hooks": "^4.2.0", + "eslint-plugin-simple-import-sort": "^6.0.1", + "husky": "^4.3.5", + "open-cli": "^6.0.1", + "prettier": "^2.2.1", + "rimraf": "^3.0.2", + "typescript": "^4.1.2", + "wait-on": "^5.2.1" + } +} diff --git a/examples/getting-started/pages/[[...page]].tsx b/examples/getting-started/pages/[[...page]].tsx new file mode 100644 index 000000000..a250c77bf --- /dev/null +++ b/examples/getting-started/pages/[[...page]].tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { + NextTemplateLoader, + getNextStaticPaths, + getNextStaticProps, +} from '@wpengine/headless/next'; + +import WPTemplates from '../wp-templates/_loader'; +import { GetStaticPropsContext } from 'next'; + +/** + * @todo make conditionalTags available + */ +export default function Page() { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + return ; +} + +export async function getStaticProps(context: GetStaticPropsContext) { + return getNextStaticProps(context, { + templates: WPTemplates, + }); +} + +export function getStaticPaths() { + return getNextStaticPaths(); +} diff --git a/examples/getting-started/pages/_app.tsx b/examples/getting-started/pages/_app.tsx new file mode 100644 index 000000000..9e56c0193 --- /dev/null +++ b/examples/getting-started/pages/_app.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { AppContext, AppInitialProps } from 'next/app'; +import { HeadlessProvider } from '@wpengine/headless/react'; +import 'normalize.css/normalize.css'; +import 'scss/main.scss'; + +/* eslint-disable react/jsx-props-no-spreading */ +export default function App({ + Component, + pageProps, +}: AppContext & AppInitialProps) { + return ( + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + + + + ); +} diff --git a/examples/getting-started/pages/api/auth/wpe-headless.ts b/examples/getting-started/pages/api/auth/wpe-headless.ts new file mode 100644 index 000000000..03e4dfdd1 --- /dev/null +++ b/examples/getting-started/pages/api/auth/wpe-headless.ts @@ -0,0 +1,3 @@ +import { authorizeHandler } from '@wpengine/headless'; + +export default authorizeHandler; diff --git a/examples/getting-started/pages/preview/[[...page]].tsx b/examples/getting-started/pages/preview/[[...page]].tsx new file mode 100644 index 000000000..f132cbf2b --- /dev/null +++ b/examples/getting-started/pages/preview/[[...page]].tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { + NextTemplateLoader, +} from '@wpengine/headless/next'; + +import WPTemplates from '../../wp-templates/_loader'; + +/** + * @todo make conditionalTags available + */ +export default function Page() { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + return ; +} diff --git a/examples/getting-started/public/favicon.ico b/examples/getting-started/public/favicon.ico new file mode 100644 index 000000000..851e88c43 Binary files /dev/null and b/examples/getting-started/public/favicon.ico differ diff --git a/examples/getting-started/public/images/headless_hero_background.jpg b/examples/getting-started/public/images/headless_hero_background.jpg new file mode 100644 index 000000000..4248eb22a Binary files /dev/null and b/examples/getting-started/public/images/headless_hero_background.jpg differ diff --git a/examples/getting-started/scss/_typography.scss b/examples/getting-started/scss/_typography.scss new file mode 100644 index 000000000..5e79618e7 --- /dev/null +++ b/examples/getting-started/scss/_typography.scss @@ -0,0 +1,50 @@ +@import "variables"; + +body { + font-family: $font-family; + font-weight: $font-weight-normal; +} + +h1, h2, h3, h4, h5, h6 { + font-family: $font-family; + font-weight: $font-weight-bold; + line-height: 1.2; + margin-bottom: 0.5em; +} + +h1 { + font-size: 3.5em; +} + +h2 { + font-size: 2.5em; +} + +h3 { + font-size: 1.4em; +} + +h4 { + font-size: 1.2em; +} + +h5 { + font-size: 1.1em; +} + +h6 { + font-size: 1em; +} + +p { + margin-bottom: 1.2em; +} + +a { + color: $color-primary; +} + +a:focus, +a:hover { + color: lighten($color-primary, 15); +} diff --git a/examples/getting-started/scss/_variables.scss b/examples/getting-started/scss/_variables.scss new file mode 100644 index 000000000..0bcda7712 --- /dev/null +++ b/examples/getting-started/scss/_variables.scss @@ -0,0 +1,16 @@ +$color-primary: #0070f3; +$color-black: #000; +$color-white: #fff; +$color-dark-gray: #1f1f1f; +$color-light-gray: #f2f2f2; +$color-mid-gray: #606060; + +$font-family: "Public Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; +$font-weight-normal: 300; +$font-weight-bold: 600; + +$content-width: 1200px; +$content-width-extended: 1400px; + +$breakpoint-small: 600px; +$breakpoint-medium: 1000px; diff --git a/examples/getting-started/scss/components/CTA.module.scss b/examples/getting-started/scss/components/CTA.module.scss new file mode 100644 index 000000000..0b73a5949 --- /dev/null +++ b/examples/getting-started/scss/components/CTA.module.scss @@ -0,0 +1,43 @@ +// CSS for the CTA component in components/CTA.tsx. + +@import "scss/variables"; + +.cta { + background-color: $color-dark-gray; + color: $color-white; + padding: 10em 0 12em; +} + +.cta a { + color: $color-white; +} + +.wrap { + margin: 0 auto; + max-width: $content-width; + text-align: center; +} + +.children a:focus, +.children a:hover { + color: darken($color-white, 25); +} + +.title { + margin: 0 auto; + max-width: 80%; +} + +.intro { + max-width: 80%; + margin: 0 auto; +} + +.button-wrap { + display: block; + text-align: center; +} + +.button-wrap :global(.button) { + float: none; +} diff --git a/examples/getting-started/scss/components/Footer.module.scss b/examples/getting-started/scss/components/Footer.module.scss new file mode 100644 index 000000000..bc990d03f --- /dev/null +++ b/examples/getting-started/scss/components/Footer.module.scss @@ -0,0 +1,26 @@ +// CSS for the Footer component in components/Footer.tsx. + +@import "scss/variables"; + +.wrap { + margin: 0 auto; + max-width: $content-width-extended; + padding: 0 10px; +} + +@media screen and (min-width: $content-width-extended) { + .wrap { + padding: 0; + } +} + +footer.main { + background: $color-light-gray; + height: 100px; +} + +footer.main p { + color: $color-dark-gray; + line-height: 100px; + margin: 0; +} diff --git a/examples/getting-started/scss/components/Header.module.scss b/examples/getting-started/scss/components/Header.module.scss new file mode 100644 index 000000000..4ee4ed3d9 --- /dev/null +++ b/examples/getting-started/scss/components/Header.module.scss @@ -0,0 +1,104 @@ +// CSS for the Header component in components/Header.tsx. + +@import "scss/variables"; + +.site-title { + margin-bottom: 0; +} + +.site-title a { + color: $color-black; + font-family: $font-family; + font-weight: $font-weight-bold; + font-size: 1.2em; + text-decoration: none; +} + +.title-wrap { + padding: 20px 0 0; +} + +@media screen and (min-width: $breakpoint-small) { + .title-wrap { + padding: 20px 0; + } +} + +.title-wrap .description { + color: $color-mid-gray; + font-family: $font-family; + font-size: 0.8em; + margin-top: 0.1em; +} + +.wrap { + padding: 0 10px; +} + +@media screen and (min-width: $breakpoint-small) { + .wrap { + align-items: center; + display: flex; + flex-wrap: inherit; + justify-content: space-between; + margin: 0 auto; + max-width: $content-width-extended; + } + + .menu { + max-width: 70%; + } +} + +@media screen and (min-width: $content-width-extended) { + .wrap { + padding: 0; + } +} + +.menu ul { + padding-left: 0 +} + +.menu li { + display: inline-block; + list-style-type: none; + padding: 0 1em; +} + + +.menu li:first-of-type { + padding-left: 0; +} + +@media screen and (min-width: $breakpoint-small) { + .menu li:first-of-type { + padding-left: 1em; + } +} + +.menu a { + color: $color-black; + font-family: $font-family; + font-weight: $font-weight-bold; + text-decoration: none; +} + +.menu a:focus, +.menu a:hover { + color: $color-primary; +} + +.menu a:global(.button) { + background-color: $color-primary; + display: inline-block; + color: $color-white; + padding: 6px 16px; + border-radius: 4px; +} + +.menu a:global(.button):focus, +.menu a:global(.button):hover { + background-color: lighten($color-primary, 10); + color: $color-white; +} diff --git a/examples/getting-started/scss/components/Hero.module.scss b/examples/getting-started/scss/components/Hero.module.scss new file mode 100644 index 000000000..e2cb9eb79 --- /dev/null +++ b/examples/getting-started/scss/components/Hero.module.scss @@ -0,0 +1,60 @@ +// CSS for the Hero component in components/Hero.tsx. + +@import "scss/variables"; + +.hero { + background-color: $color-dark-gray; + background-position: 48% 72%; + background-size: cover; + background-repeat: no-repeat; + color: $color-white; + padding: 7em 0 9em; + position: relative; +} + +.wrap { + margin: 0 auto; + max-width: $content-width; + overflow: auto; + padding: 0 10px; + position: relative; + z-index: 1; +} + +@media screen and (min-width: $content-width) { + .wrap { + padding: 0; + } +} + +.hero::before { + content: ""; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background-color: inherit; + opacity: 0.8; + z-index: 1; +} + +.hero a { + color: $color-white; +} + +.children a:focus, +.children a:hover { + color: darken($color-white, 25); +} + +@media screen and (min-width: $breakpoint-medium) { + .hero h1 { + max-width: 80%; + } + + .intro { + margin-bottom: 2%; + max-width: 50%; + } +} diff --git a/examples/getting-started/scss/components/Posts.module.scss b/examples/getting-started/scss/components/Posts.module.scss new file mode 100644 index 000000000..e6d7c55e4 --- /dev/null +++ b/examples/getting-started/scss/components/Posts.module.scss @@ -0,0 +1,23 @@ +// CSS for the Posts component in components/Posts.tsx. + +@import "scss/variables"; + +.title a { + color: $color-black; + text-decoration: none; +} + +.title a:focus, +.title a:hover { + color: $color-primary; +} + +.single { + margin-bottom: 2.5em; +} + +@media screen and (min-width: $content-width) { + .single { + margin-bottom: 1em; + } +} diff --git a/examples/getting-started/scss/main.scss b/examples/getting-started/scss/main.scss new file mode 100644 index 000000000..bbc70aac2 --- /dev/null +++ b/examples/getting-started/scss/main.scss @@ -0,0 +1,80 @@ +// CSS for use on every page. Added with normalize.css in pages/_app.tsx. + +@import "variables"; +@import "typography"; + +body { + color: $color-black; + font-size: 18px; + line-height: 1.6; +} + +.wrap { + margin: 0 auto; + max-width: $content-width; + overflow: auto; + padding: 0 10px; +} + +.content-page .wrap, +.content-single .wrap, +.content-index .wrap { + padding: 55px 10px; +} + +@media screen and (min-width: $content-width) { + .wrap { + padding: 0; + } + + .content-page .wrap, + .content-single .wrap, + .content-index .wrap { + padding-left: 0; + padding-right: 0; + } +} + +.content .button { + background-color: $color-primary; + border-radius: 4px; + color: #ffffff; + cursor: pointer; + display: inline-block; + float: left; + font-family: $font-family; + font-weight: $font-weight-bold; + padding: 12px 24px; + text-align: center; + text-decoration: none; + margin-right: 8px; +} + +.content .button:focus, +.content .button:hover { + background-color: lighten($color-primary, 10); +} + +.content .button-secondary { + background-color: $color-white; + color: $color-black; +} + +.content .button-secondary:focus, +.content .button-secondary:hover { + background-color: darken($color-white, 10); +} + +.pagination ul { + list-style-type: none; + margin: 0; + padding: 0; +} + +.pagination.has-next.has-previous ul { + column-count: 2; +} + +.pagination-next { + text-align: right; +} diff --git a/examples/getting-started/scss/wp-templates/front-page.module.scss b/examples/getting-started/scss/wp-templates/front-page.module.scss new file mode 100644 index 000000000..6ec8ecc6e --- /dev/null +++ b/examples/getting-started/scss/wp-templates/front-page.module.scss @@ -0,0 +1,30 @@ +// CSS for the themes/front-page.tsx template. + +@import "scss/variables"; + +.explore { + margin: 5em 0 5em; +} + +#post_list { + padding: 5em 0 6em; + background-color: $color-light-gray; +} + +@media screen and (min-width: $breakpoint-medium) { + #home_hero h1 { + max-width: 40%; // Reduced from 80% on the front-page only to wrap the headline. + } + + .features { + column-gap: 20px; + display: grid; + grid-template-columns: 1fr 1fr; + } + + #post_list :global(.posts) { + display: grid; + column-gap: 10px; + grid-template-columns: 1fr 1fr 1fr; + } +} diff --git a/examples/getting-started/tsconfig.json b/examples/getting-started/tsconfig.json new file mode 100644 index 000000000..25ee69727 --- /dev/null +++ b/examples/getting-started/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "baseUrl": "." + }, + "include": ["next-env.d.ts", "typings/*.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/examples/getting-started/typings/global.d.ts b/examples/getting-started/typings/global.d.ts new file mode 100644 index 000000000..bff94710c --- /dev/null +++ b/examples/getting-started/typings/global.d.ts @@ -0,0 +1 @@ +declare module '*.svg'; diff --git a/examples/getting-started/wp-templates/404.tsx b/examples/getting-started/wp-templates/404.tsx new file mode 100644 index 000000000..1eb7d1af3 --- /dev/null +++ b/examples/getting-started/wp-templates/404.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { useGeneralSettings } from '@wpengine/headless/react'; +import { Header, Hero, Footer } from '../components'; + +export default function Page(): JSX.Element { + const settings = useGeneralSettings(); + + return ( + <> +
+
+ +
+
+
+

+ The page you were looking for does not exist or is no longer + available. +

+
+
+
+
+