Skip to content

Conversation

@justlevine
Copy link
Collaborator

@justlevine justlevine commented Feb 8, 2025

What does this implement/fix? Explain your changes.

This PR submits the following plugins for consideration to the WPGraphQL Extension Library

Did these in a single PR to avoid the merge conflicts, but if it's better for documentation purposes, I can split them up.

@qlty-cloud-legacy
Copy link

Code Climate has analyzed commit f2b15ed and detected 1 issue on this pull request.

Here's the issue category breakdown:

Category Count
Complexity 1

View more on Code Climate.

@coveralls
Copy link

Coverage Status

coverage: 83.018% (-0.2%) from 83.194%
when pulling f2b15ed on justlevine:extensions/gf-rm-login
into eb824fd on wp-graphql:develop.

@justlevine justlevine requested a review from jasonbahl February 8, 2025 19:22
@jasonbahl jasonbahl changed the title extensions: submit GF, Rank Math, and Headless Login plugins chore: submit GF, Rank Math, and Headless Login plugins Feb 10, 2025
@jasonbahl
Copy link
Collaborator

jasonbahl commented Feb 11, 2025

Did these in a single PR to avoid the merge conflicts, but if it's better for documentation purposes, I can split them up.

@justlevine I'm fine with this. Thanks for maintaining quality extensions and submitting them to the extensions page 🙌🏻

As part of the process I'll do some smoke tests and provide some documentation for each extension on this PR shortly.

@jasonbahl
Copy link
Collaborator

WPGraphQL for Gravity Forms

I performed some basic tests to ensure that WPGraphQL for Gravity Forms works as intended.

For these tests I installed the following plugins on a fresh WordPress install:

  • WordPress v6.7.1
  • WPGraphQL v1.31.1
  • Gravity Forms v2.9.2.3
  • WPGraphQL for Gravity Forms v0.13.0.1
  • WPGraphQL IDE v4.0.3 (not required for these tests, but listing as I had it in my test environment)

CleanShot 2025-02-10 at 18 20 54

Create a new form

I created a new Gravity Form starting with a blank template:

CleanShot 2025-02-10 at 18 23 34

I added a text field and a dropdown field (with 3 choices) and saved the form:

Preview of the Gravity Form with 2 fields

I was then able to see various new Types in the GraphQL Schema. For example, searching the Schema for Gf I see several new Types in the Schema:

  • GfEntry
  • GfForm
  • GfFormToGfEntryConnectionWhereArgs
  • GfFormToGfEntryConnection
  • GfEntryConnection
  • GfEntryConnectionEdge
  • GfEntryConnectionPageInfo
  • GfFormToGfEntryConnectionEdge
  • GfFormToGfEntryConnectionPageInfo

CleanShot 2025-02-10 at 18 35 37

Through exploring the Schema I was able to quickly identify how to query for a list of forms using the gfForms field which returns a RootQueryToGfFormConnection.

I was then able to determine that on each Form I could ask for properties of the form as well as a list of formFields.

The formFields connection returns a list of FormField interface, and I was able to see all the Types in the Schema that implement the FormField Interface:

Screenshot of the FormField Interface in the GraphiQL IDE Schema Explorer

With this information, I crafted a pretty simple query:

query getForms {
  gfForms {
    nodes {
      ...FormData
    }
  }
}

fragment FormData on GfForm {
  id
  title
  formFields {
    nodes {
      ...FormField
    }
  }
}

fragment FormField on FormField {
  __typename
  type
  id
  inputType
  pageNumber
  visibility
  ...TextField
  ...SelectField
}

fragment TextField on TextField {
  adminLabel
  autocompleteAttribute
  canPrepopulate
  cssClass
  databaseId
  defaultValue
  description
  errorMessage
  hasInputMask
  id
  inputName
  inputType
}

fragment SelectField on SelectField {
  adminLabel
  autocompleteAttribute
  canPrepopulate
  cssClass
  databaseId
  defaultValue
  description
  errorMessage
  choices {
    __typename
    isSelected
    text
    value
  }
}

And in response I got the following payload:

{
  "data": {
    "gfForms": {
      "nodes": [
        {
          "id": "Z2ZfZm9ybTox",
          "title": "Test Form",
          "formFields": {
            "nodes": [
              {
                "__typename": "TextField",
                "type": "TEXT",
                "id": "Z2ZfZm9ybV9maWVsZDoxOjE=",
                "inputType": "TEXT",
                "pageNumber": 1,
                "visibility": "VISIBLE",
                "adminLabel": null,
                "autocompleteAttribute": null,
                "canPrepopulate": false,
                "cssClass": null,
                "databaseId": 1,
                "defaultValue": null,
                "description": "Example of a text field",
                "errorMessage": null,
                "hasInputMask": false,
                "inputName": null
              },
              {
                "__typename": "SelectField",
                "type": "SELECT",
                "id": "Z2ZfZm9ybV9maWVsZDoxOjM=",
                "inputType": "SELECT",
                "pageNumber": 1,
                "visibility": "VISIBLE",
                "adminLabel": null,
                "autocompleteAttribute": null,
                "canPrepopulate": false,
                "cssClass": null,
                "databaseId": 3,
                "defaultValue": null,
                "description": "This is a dropdown field",
                "errorMessage": null,
                "choices": [
                  {
                    "__typename": "SelectFieldChoice",
                    "isSelected": false,
                    "text": "Choice One",
                    "value": "Choice One"
                  },
                  {
                    "__typename": "SelectFieldChoice",
                    "isSelected": true,
                    "text": "Choice Two",
                    "value": "Choice Two"
                  },
                  {
                    "__typename": "SelectFieldChoice",
                    "isSelected": false,
                    "text": "Third Choice",
                    "value": "Third Choice"
                  }
                ]
              }
            ]
          }
        }
      ]
    }
  }
}

I was able to use GraphQL to fetch data about the forms (in my case only 1 form) and the fields of the form.

Using this data, I believe I could create a UI to render the form and its various field types using a component library such as shadcn/ui, Ant Design, Material UI or something else.

Final Thoughts

While there's a LOT more features to explore, the plugin is very clearly well built, follows WPGraphQL / GraphQL best practices, has good documentation and support.

This is a great example of a high quality WPGraphQL Extension. 🙌🏻

@jasonbahl
Copy link
Collaborator

jasonbahl commented Feb 11, 2025

Headless Login for WPGraphQL

To test the Headless Login for WPGraphQL I started with the following environment:

  • WordPress v6.7.1
  • WPGraphQL v1.31.1
  • Headless Login for WPGraphQL v0.4.0
  • WPGraphQL IDE v4.0.3 (not required, but listing as I had it active for my testing)

Settings

The first thing I did was head over to the Headless Login for WPGraphQL Settings page, which is under WPGraphQL > Settings > Headless Login

There are several tabs of settings:

  • Providers
  • Access Control
  • Cookies
  • Misc
  • Docs

I started on the "Providers" page and enabled the most basic authentication provider: "Password"

CleanShot 2025-02-11 at 10 27 22

Login with Password

With the "password" provider enabled, I should be able to execute a login mutation using my WordPress username and password.

To try this, I opened up the GraphQL IDE and tested the login mutation:

mutation Login($loginInput: LoginInput!) {
  login(input: $loginInput) {
    user {
      name
    }
  }
}

With the following variables:

{
  "loginInput": {
    "provider": "PASSWORD",
    "credentials": {
      "username": "jasonbahl",
      "password": "••••••••••"
    }
  }
}

This returned me an error: You are already logged in.

CleanShot 2025-02-11 at 10 30 24

This is because the WPGraphQL IDE is already authenticated as the user I'm logged into the WordPress dashboard as.

However, I can toggle the IDE to execute as a public user by clicking the Avatar below the play button. So I did that:

CleanShot 2025-02-11 at 10 28 43

Now that the IDE is executing as a public user, I could test the login mutation as a public user:

CleanShot 2025-02-11 at 10 33 40

I was successfully able to login with my username and password. Hooray! 🥳

I was then able to take my authToken and use it in another client. I used Postman to test.

In Postman, I executed a GET request for the current viewer ({viewer{name}}) which returns null for non-authenticated requests.

As expected, without a valid token in the Authorization headers, I got a null response.

CleanShot 2025-02-11 at 10 37 29

With the authToken I received from my login mutation added to the Authorization Bearer Token header in Postman, I was able to execute the viewer query and get my username back in response:

CleanShot 2025-02-11 at 10 39 40

Logging in with username and password works as expected! 🙌🏻

Now, if you do chose to use this method, be sure you ONLY use this method over HTTPS.

Github oAuth Provider

Next, I wanted to try the Github oAuth Provider.

I followed the docs and to be transparent I did have some issues and had to reach out to @justlevine for some assistance, but I was ultimately able to get things to work and I'll share what I did to get things to work.

Enable the Provider

Under the WPGraphQL > Settings > Headless Login > Providers settings page, I enabled the "Github" provider.

This has several fields to fill out:

  • Client Label
  • Client ID
  • Client Secret
  • Redirect URI
  • Scope
  • Login Existing Users
  • Create New Users
  • Set Authentication Cookie

In order to have all the values to fill in the GitHub Provider settings, we need to first create a GitHub oAuth App.

I created a GitHub oAuth App with the following fields:

CleanShot 2025-02-11 at 10 51 02

Creating the GitHub oAuth application provides you with a Client ID and Client Secret which you can then use in your GitHub Provider Settings:

CleanShot 2025-02-11 at 10 43 05

For the Redirect URI setting, I wasn't quite sure what value to put in here and I couldn't really find much information in the Headless Login for WPGraphQL Docs about what to put in this field. Ulitmately, after reaching out to @justlevine, I found out that this field needs to have a URL that will be responsible for receiving the "code" and "status" from GitHub via HTTP GET params after the user grants access to WordPress, and that url needs to use the "code" and "status" to execute the login mutation.

Typically, this would likely be a url of the headless application (i.e. a NEXT JS API route or similar).

For my local testing sake, I decided to just use a url in my local WordPress site, and I used some PHP code to handle the mutation.

(the php code I used)
add_action( 'template_redirect', function() {

	// if the url being visited is /auth-callback/confirmation then I want to do something custom with the GET params
	$current_path = trim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');
	
	// check if the url is /auth-callback/confirmation
	if ( $current_path !== 'auth-callback' ) {
		return;
	}

	$code = isset( $_GET['code'] ) ? sanitize_text_field( $_GET['code'] ) : '';
	$state = isset( $_GET['state'] ) ? sanitize_text_field( $_GET['state'] ) : '';

	$mutation = '
	mutation Login($loginInput: LoginInput!) {
	  login(input:$loginInput) {
	    user {
	      name
	    }
	  }
	}
	';

	$variables = [
		'loginInput' => [
			'provider' => 'GITHUB',
		    'oauthResponse' => [
				'code' => $code,
		        'state' => $state
		    ]
		]
    ];

	$response = graphql([
		'query' => $mutation,
		'variables' => $variables
	]);

	// if the response has errors, show them via wp_send_json
	// else, redirect to the home page
	if ( ! empty( $response['errors'] ) ) {
		wp_send_json( [
			'$mutation' => $mutation,
			'$variables' => $variables,
			'$code' => $code,
			'$state' => $state,
			'$response' => $response
		]);
	}

	wp_redirect( home_url() );
	exit;

} );

So, I entered the following value for the Redirect URI: http://wpgraphql.local/auth-callback

This is not actually a page on my local WordPress site, so I wrote some basic code that:

  • only executes if the path is /auth-callback
  • gets the code and state from the GET params
  • uses the code and state to execute a login mutation
  • if there are errors, return the errors in a json payload, else redirect the user to the home url

With this in place, I can now:

  • use WPGraphQL to query for loginClients
  • visit the client's authorizationUrl
  • grant the client access to WordPress
  • get redirected to WordPress and already be logged in 🙌🏻

Let's take a look at this step-by-step:

Query for Login Providers

Headless Login for WPGraphQL provides a loginClients query that allows fetching a list of activated clients.

query GetLoginClients {
  loginClients {
    name
    authorizationUrl
  }
}

As I have 2 providers active (GitHub and Password) I get the following response:

{
  "data": {
    "loginClients": [
      {
        "name": "Github Client",
        "authorizationUrl": "https://github.com/login/oauth/authorize?scope=user%2Cread%3Auser%2Cuser%3Aemail%2Cuser%3Afollow&redirect_uri=http%3A%2F%2Fwpgraphql.local%2Fauth-callback&state=*****&response_type=code&approval_prompt=auto&client_id=*****"
      },
      {
        "name": "Password",
        "authorizationUrl": null
      }
    ]
  },
}

If I visit the authorizationUrl in an incognito window (or a browser where I'm not already logged into the WordPress install), then I will be redirected to Github to login and grant access to WordPress.

CleanShot 2025-02-11 at 11 15 10

Once I authenticate to GitHub and grant access to the "WPGraphQL Headless Login" app, then I am redirected back to WordPress as a logged in user.

CleanShot 2025-02-11 at 11 16 10

Pretty neat!

Now, typically if you are using WPGraphQL to build decoupled applications, you wouldn't redirect to a WordPress page executing a WPGraphQL query to authenticate you, but it was good to test it this way and validate that the plugin works as promised.

The documentation has a "recipe" for Client Side Auth with NextJS that might help you implement a workflow for a truly decoupled application: https://github.com/AxeWP/wp-graphql-headless-login/blob/develop/docs/recipes/client-side-auth-nextauth.md

@jasonbahl
Copy link
Collaborator

WPGraphQL for Rank Math SEO

I performed some basic tests to ensure that WPGraphQL for Rank Math SEO works as intended.

For these tests I installed the following plugins on a fresh WordPress install:

WordPress v6.7.1
WPGraphQL v1.31.1
WPGraphQL IDE v4.0.3 (not required for these tests, but listing as I had it in my test environment)
WPGraphQL for Rank Math SEO v0.3.2
Rank Math SEO v1.0.237

CleanShot 2025-02-11 at 11 57 05

Rank Math Setup

After activating the Rank Math plugin I was taken to a setup wizard, but in typical developer fashion I skipped the wizard.

In the Rank Math Settings page there are several "modules" that can be enabled or disabled. I'm left the default modules enabled (some of these seem unnecessary for smoke testing a WPGraphQL plugin, but they're default active so I let them be):

  • Content AI
  • Analytics
  • Instant Indexing
  • Link Counter
  • Schema (Structured Data)
  • SEO Analyzer
  • Sitemap

Publish a Post

I then published a post and checked that at least some Rank Math data was filled in:

CleanShot 2025-02-11 at 12 03 40

Exploring the WPGraphQL Schema

With WPGraphQL for Rank Math SEO active, the GraphQL Schema should have new Types and Fields available to query.

I opened up the WPGraphQL IDE and started to explore the Schema.

The first thing I did was search SEO and I saw several results:

CleanShot 2025-02-11 at 12 05 54

The one that caught my eye the most was NodeWithRankMathSeo.

The NodeWith** pattern is a pattern in WPGraphQL for applying interfaces to Types. As WordPress is a pluggable CMS, some Types might support certain features that other types do not. For example, some Post Types support a title and author while other post types do not. Therefore the NodeWithTitle and NodeWithAuthor Interfaces exist and are applied to the appropriate post types that support them. Likewise, not all post types will have Rank Math enabled. Therefore the NodeWithRankMathSeo Interface will apply to all Types that do have SEO capabilities.

In the Schema, I can see all the Types that implement this Interface:

CleanShot 2025-02-11 at 12 18 04

I can also see that the Interface has the following fields:

  • id
  • seo

With this information, I know that I can query the seo field on any of the following Types:

  • Category
  • ContentType
  • User
  • MediaItem
  • Page
  • Post
  • PostFormat
  • Tag

Or, I could also query any node, and request that if it's a Type that has SEO support, give me the seo field(s).

I came up with the following query for any node, provided a uri for input:

query NodeWithSeoData {
  nodeByUri(uri:"/rank-math-post") {
    uri
    ...title
    ...content
    ...seo
  }
}

fragment title on NodeWithTitle {
  title
}

fragment content on NodeWithContentEditor {
  content
}

fragment seo on NodeWithRankMathSeo {
  seo {
    title
    focusKeywords
    canonicalUrl
    openGraph {
      siteName
      description
      url
    }
    robots
  }
}

I received the following response:

{
  "data": {
    "nodeByUri": {
      "uri": "/rank-math-post/",
      "title": "Rank Math Post",
      "content": "\n<p>This is a post I wrote to test the WPGraphQL for Rank Math SEO plugin. </p>\n",
      "seo": {
        "title": "Rank Math Post - wpgraphql",
        "focusKeywords": null,
        "canonicalUrl": "http://wpgraphql.local/rank-math-post/",
        "openGraph": {
          "siteName": "wpgraphql",
          "description": "This is a post I wrote to test the WPGraphQL for Rank Math SEO plugin.",
          "url": "http://wpgraphql.local/rank-math-post/"
        },
        "robots": [
          "index",
          "follow",
          "max-snippet:-1",
          "max-video-preview:-1",
          "max-image-preview:large"
        ]
      }
    }
  }
}

I would say that WPGraphQL for Rank Math SEO works as I would expect, providing good integration between the data managed by the Rank Math SEO plugin and exposing it to WPGraphQL.

It also follows best practices and patterns for GraphQL / WPGraphQL, including naming conventions and Interface implementation (ex: using the NodeWith* pattern for flexible querying of data across post types that might have wildly different Types).

There's a lot to this plugin that I did not explore, but I would have high confidence that it would work well for decoupled applications that need SEO related data.

The codebase is well maintained, documented and supported.

Copy link
Collaborator

@jasonbahl jasonbahl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@justlevine I smoke tested all 3 plugins and documented my findings in the PR.

These are solid plugins and I'm excited to have them listed on the extensions page for users! 🙏🏻

@jasonbahl jasonbahl merged commit b23019a into wp-graphql:develop Feb 11, 2025
37 of 39 checks passed
@justlevine justlevine deleted the extensions/gf-rm-login branch February 11, 2025 20:31
@jasonbahl jasonbahl mentioned this pull request Feb 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants