Giter Site home page Giter Site logo

wpengine / wp-graphql-content-blocks Goto Github PK

View Code? Open in Web Editor NEW
97.0 41.0 12.0 1.59 MB

Plugin that extends WPGraphQL to support querying (Gutenberg) Blocks as data

Home Page: https://faustjs.org/docs/gutenberg/wp-graphql-content-blocks

License: GNU General Public License v2.0

PHP 86.93% JavaScript 4.40% Dockerfile 0.87% Shell 7.81%
graphql wordpress wpgraphql wordpress-plugin wp-plugin

wp-graphql-content-blocks's Introduction

WPGraphQL Content Blocks

End-to-End Tests

Download Latest Version

WordPress plugin that extends WPGraphQL to support querying (Gutenberg) Blocks as data.

How to Install

This plugin is an extension of wp-graphql, so make sure you have it installed first.

  1. Download the latest .zip version of the plugin
  2. Upload the plugin .zip to your WordPress site
  3. Activate the plugin within WordPress plugins page.

There is no other configuration needed once you install the plugin.

Getting started

Once the plugin is installed, head over to the GraphiQL IDE and you should be able to perform queries for the block data (This plugin is an extension of wp-graphql, so make sure you have it installed first.). There is a new field added in the Post and Page models called editorBlocks. This represents a list of available blocks for that content type:

{
  posts {
    nodes {
      # editorBlocks field represents array of Block data
      editorBlocks(flat: false) {
        # fields from the interface
        renderedHtml
        __typename
        # expand the Paragraph block attributes
        ... on CoreParagraph {
          attributes {
            content
          }
        }
        # expand a Custom block attributes
        ... on CreateBlockMyFirstBlock {
          attributes {
            title
          }
        }
      }
    }
  }
}

How do I query block data?

To query specific block data you need to define that data in the editorBlocks as the appropriate type. For example, to use CoreParagraph attributes you need to use the following query:

{
  posts {
    nodes {
      editorBlocks(flat: false) {
        __typename
        name
        ... on CoreParagraph {
          attributes {
            content
            className
          }
        }
      }
    }
  }
}

If the resolved block has values for those fields, it will return them, otherwise it will return null.

{
  "__typename": "CoreParagraph",
  "name": "core/paragraph",
  "attributes": {
    "content": "Hello world",
    "className": null
  }
}

What about innerBlocks?

In order to facilitate querying innerBlocks fields more efficiently you want to use editorBlocks(flat: true) instead of editorBlocks. By passing this argument, all the blocks available (both blocks and innerBlocks) will be returned all flattened in the same list.

For example, given the following HTML Content:

<columns>
  <column>
    <p>Example paragraph in Column</p>
    <p></p
  ></column>

  <column></column
></columns>

It will return the following blocks:

[
  {
    "__typename": "CoreColumns",
    "name": "core/columns",
    "id": "63dbec9abcf9d",
    "parentClientId": null
  },
  {
    "__typename": "CoreColumn",
    "name": "core/column",
    "id": "63dbec9abcfa6",
    "parentClientId": "63dbec9abcf9d"
  },
  {
    "__typename": "CoreParagraph",
    "name": "core/paragraph",
    "id": "63dbec9abcfa9",
    "parentClientId": "63dbec9abcfa6",
    "attributes": {
      "content": "Example paragraph in Column 1",
      "className": null
    }
  }
]

The CoreColumns contains one or more CoreColumn block, and each CoreColumn contains a CoreParagraph.

Given the flattened list of blocks though, how can you put it back? Well that's where you use the `` and parentId fields to assign temporary unique ids for each block.

The clientId field assigns a temporary unique id for a specific block and the parentClientId will be assigned only if the current block has a parent. If the current block does have a parent, it will get the parent's clientId value.

So in order to put everything back in the Headless site, you want to use the flatListToHierarchical function as mentioned in the WPGraphQL docs.

Note

Currently the clientId field is only unique per request and is not persisted anywhere. If you perform another request each block will be assigned a new clientId each time.

Contributor License Agreement

All external contributors to WP Engine products must have a signed Contributor License Agreement (CLA) in place before the contribution may be accepted into any WP Engine codebase.

  1. Submit your name and email
  2. ๐Ÿ“ Sign the CLA emailed to you
  3. ๐Ÿ“ฅ Receive copy of signed CLA

โค๏ธ Thank you for helping us fulfill our legal obligations in order to continue empowering builders through headless WordPress.

wp-graphql-content-blocks's People

Contributors

blakewilson avatar chriswiegman avatar dependabot[bot] avatar github-actions[bot] avatar jasonbahl avatar josephfusco avatar justlevine avatar kidunot89 avatar matthewguywright avatar mindctrl avatar teresagobble avatar theodesp avatar williamjulianvicary avatar wpe-jenkins-github-admin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

wp-graphql-content-blocks's Issues

Add support for extracting array type query data from blocks

Description

Certain types of blocks have an array type of attribute which means that there is a nested object types that we need to extract.
Currently we do not support those so we want to provide this functionality as well.

For example:

Technical notes

  • The code to detect the attribute type here should also handle the array type.
  • This should be a recursive step since it can have many levels of inner attributes.

image

Full size CoreImageAttributes.width & .height are null

When I try to get the width and height of an full-sized image block, WPGraphQL returns null.

However, when I manually specify a size other than the predetermined ones (thumbnail, medium, large, fullsize), the appropriate value is returned.

I'm using the last version of WPGraphQL (1.14.10) and WPGraphQL Content Blocks (1.1.3) on an fresh WordPress installation.

Support for Reusable blocks

I noticed that #54 was closed as resolved for the Classic Editor component of the issue but the reusable block item is still problematic.

I can see we can query like so:

query GetPageData {
  pageBy(uri: "/test-uri") {
    editorBlocks(flat: true) {
      blockEditorCategoryName
      name
      ... on CoreBlock {
        attributes {
          ref
        }
      }
    }
  }
}

This identifies the block type as reusable, and we're able to retrieve the ref attribute associated with the reusable block, however as far as I can tell it's not then possible to query for the reusable block directly. We're also using Faust, which means ideally we'll be wanting to pass down the complete response rather than having to resolve the appropriate block within the CoreBlock.

We can get the innerHtml but that doesn't allow us to then handle the attribute, and I see that blockJSON was removed with the release of wp-graphql-content-blocks which would have been the other option, possibly.

{
    "pageBy": {
      "editorBlocks": [
        {
          "blockEditorCategoryName": "reusable",
          "name": "core/block",
          "attributes": {
            "ref": 2575,
            "lock": null
          }
        }
      ]
    }
  }

I guess ideally these blocks would be resolved to the underlying blocks (as flattened innerBlocks?) within the response, at which point they'd then be usable as if they were any other block.

This is a bit of a blocker for us using Faust long term for clients, the utility of reusable blocks is unsurpassed for repetitive content that needs to be centrally maintained and the alternative is either - produce static non-editable blocks or ask our clients to put up with copy + paste across all of their Gutenberg pages - neither a great option!

Is this on the roadmap?

Making object and array attribute types available

Currently this plugin only supports the following attribute types:

  • boolean
  • number
  • integer
  • string

We frequently use array and objects to store data in blocks rather than having to save data across multiple attributes. Is this something you guys are in the process of adding? Alternatively can you point me in the direction of how we can add these temporarily if/when the feature is added?

All Block Attributes

${PostType}EditorBlock Interfaces are being registered and implemented with different names

Using v1.2.0 and 2.0.0 I'm seeing the following GRAPHQL_DEBUG message:

      {
        "type": "GRAPHQL_DEBUG",
        "message": "\"TestEventEditorBlock\" is not a valid Interface Type and cannot be implemented as an Interface on the \"CoreLegacyWidget\" Type",
        "stack": [
          "/wp-graphql/src/Type/WPInterfaceTrait.php:82",
          ...
        ]
      }

Steps to Reproduce

  1. register a post type that has Block Editor support and is set to show in graphql, (public, show_in_rest => true, show_in_graphql => true)
  2. Give that post type a graphql_single_name that is different than the post type name itself: (i.e. register_post_type( 'something', [ ... 'graphql_single_name' => 'SomethingElse' ] );
  3. Enable GraphQL Debug and execute a query
  4. See GraphQL Debug messages like the one above

I tracked this down to being a bug with how the ${PostType}EditorBlock Types are being registered vs. how they're being applied.

Registered

Implemented


The problem is that the Interfaces are registered to the schema using $post_type->name and the Interfaces are implemented using $post_type->graphql_single_name

Why was `WPGraphQL\ContentBlocks\Blocks\Block::register_fields()` removed.

Summary

I have been using this functionality for quite sometime now, and it was removed in v0.3.0 with no documentation or explanation I can find anywhere.

Here is a block I have that was broken when I updated to v1.0.0

/**
 * Product_And_Cart_Block class.
 */
class Product_And_Cart_Block extends Block {
	/**
	 * Registers GraphQL fields to the ProductAndCartAttributes type.
	 *
	 * @return void
	 */
	public function register_fields() {
		$this->block_registry->type_registry->register_fields(
			$this->type_name,
			[
				'selectedProduct' => [
					'type'        => 'Product',
					'description' => __( 'The alignment of the block', 'probable-doodle' ),
					'resolve'     => function( $block, $args, $context ) {
						$product_id = $block['attrs']['product'] ?? null;
						// Resolve product if ID found.
						if ( absint( $product_id ) ) {
							return $context->get_loader( 'wc_post' )->load( absint( $product_id ) );
						}
						return null;
					},
				],
			]
		);
	}
}

Is this functionality no longer supported. If not, what is the alternative to this functionality?

Fetching plugin blocks

Hi! Iโ€™m trying to fetch and render LearnDash blocks.
Iโ€™m doing the following query:

query GetPageData {
	page(id:โ€œcG9zdDo5โ€) {
    id
    editorBlocks(flat:true) {
      name
      __typename
    }
  }
}

And getting the following result:

{
  โ€œdataโ€: {
    โ€œpageโ€: {
      โ€œidโ€: โ€œcG9zdDo5โ€,
      โ€œeditorBlocksโ€: [
        {
          โ€œnameโ€: โ€œlearndash/ld-profileโ€,
          โ€œ__typenameโ€: โ€œLearndashLdProfileโ€
        }
      ]
    }
  }
}

Instead of getting the whole component flattened tree, I only get the top one. Iโ€™m not a WordPress expert, so Iโ€™m not sure whatโ€™s happening here or how I can customize LearnDash so I can query it with GraphQL.
Any input on how to do it helps, thanks!

Bug: `BlockWithSupportsAnchor` is causing `DUPLICATE_TYPE` error

When querying for editor blocks with GRAPHQL_DEBUG enabled, I get the following DUPLICATE_TYPE error:
You cannot register duplicate Types to the Schema. The Type 'BlockWithSupportsAnchor' already exists in the Schema. Make sure to give new Types a unique name.

This is due to the fact that that register_graphql_interface() is being called for every block type.

What should actually happen is that the BlockWithSupportsAnchor should be registered once in the Registry, and Block::register_block_supports_field() should only register_interface_to_type().

Example

image

Plugin breaks GraphQL schema if CPT slug contains dash (`-`)

Some interface type names are invalid.
For example, I have a case-study CPT registered. This causes wp-graphql-content-blocks to create an interface named NodeWithCase-studyEditorBlock, which is invalid in GraphQL and causes errors. GraphiQL doesn't load, it presents the error in the console: Uncaught (in promise) GraphQLError: Names must only contain [_a-zA-Z0-9] but "NodeWithCase-studyEditorBlocks" does not.
obraz

I believe the code responsible for this is in WPGraphQL\ContentBlocks\Type\InterfaceType\PostTypeBlockInterface class, here and here. This code uses ucfirst on $post_type, which is a post slug containing a dash. I think it should rather use Utils::format_type_name, like here in the Registry class.

Setup Content Block plugin updates through product info service

User Story

As a WPGraphQL Content Blocks user, I want to have auto updates so that I donโ€™t have to go back to GitHub each time.

We will serve product updates from WP Engine's wp-product-info service.

Acceptance Criteria

  • Plugin is added and configured in wp-product-info service
  • Repo and main plugin file are renamed (see first comment below)
  • Plugin code retrieves update information from wp-product-info and passes it to WP
  • Releases are automated and pushed to wp-product-info
  • Updates for major breaking versions (semver) require the user confirm they want to install the update

Technical Details

wp-product-info is documented here

The following plugins have good examples of integrating: Interview API, Atlas Headless Extension, ACM.

Notes

There might be a CircleCI requirement still in place for this but we donโ€™t believe it should apply to our public repo. We can use CircleCI if it is required for this step.

v1.1.1 throwing error: Class "WPGraphQL\ContentBlocks\Utilities\WPGraphQLHelpers" not found

This is the error message in my debug logs when I try consuming one of the blocks using graphql.

[18-Jul-2023 06:28:02 UTC] PHP Fatal error:  Uncaught Error: Class "WPGraphQL\ContentBlocks\Utilities\WPGraphQLHelpers" not found in /var/www/wordpress/web/app/mu-plugins/wp-graphql-content-blocks/includes/Registry/Registry.php:173

The problem:
The /includes/utilities does not follow the PSR-4 standard and there the composer dump-autoload may have not produced a rule to resolve the Utilities in the namespaces.

Solution:
As soon as I changed the utilities directory name to Utilities, made a autload dump, the error disappeared. I think you guys should look into that, just to you know check if there are any side-effects of this change. I'm surprised this issue has not been mentioned in the active set of issues.

Fatal error after installing the plugin

I am getting an error after installing your plugin while i am going to the GraphQL IDE. It keeps loading and never stop loading.

error: Uncaught Error: Class "WPGraphQLContentBlocksRegistryRegistry" not found in /var/www/html/wp-content/plugins/wp-graphql-content-blocks/includes/WPGraphQLContentBlocks.php:179

HTML anchors/IDs are not passed anywhere.

Values set under HTML Anchor under the advanced tabs are passed to the schema anywhere for any block type. I can't even find this value on the Block objects generated by Gutenberg. Below are screenshots of the input field I'm referring and a JSON rendering of the gutenberg generated block object for the corresponding block from the first screen.
image
image

Reusable block isn't resolved inside columns

When I try to make a query to resolve innerBlocks of Core/Block (reusable block), I only got renderedHtml, but it doesn't resolve innerBlocks or attributes, but when I use it inside root of Gutenberg, GraphQL resolve innerBlocks block to display them as root too.

SCR-20240205-jtol

Step to reproduce:

  1. Create a block (on my case : heading + paragraph and group them)
  2. Save it has reusable
  3. Add a columns block
  4. Insert reusable block inside one column

This is what it looks like used on the same page:
SCR-20240205-jwnv

GraphQL result
{
  "data": {
    "testPage": {
      "editorBlocks": [
        {
          "name": "core/group",
          "innerBlocks": [
            {
              "name": "core/heading",
              "innerBlocks": []
            },
            {
              "name": "core/paragraph",
              "innerBlocks": []
            },
            {
              "name": "core/buttons",
              "innerBlocks": [
                {
                  "name": "core/button",
                  "innerBlocks": []
                },
                {
                  "name": "core/button",
                  "innerBlocks": []
                },
                {
                  "name": "core/button",
                  "innerBlocks": []
                },
                {
                  "name": "core/button",
                  "innerBlocks": []
                }
              ]
            }
          ]
        },
        {
          "name": "core/columns",
          "innerBlocks": [
            {
              "name": "core/column",
              "innerBlocks": [
                {
                  "name": "core/paragraph",
                  "innerBlocks": []
                }
              ]
            },
            {
              "name": "core/column",
              "innerBlocks": [
                {
                  "name": "core/block",
                  "innerBlocks": []
                }
              ]
            }
          ]
        },
      ]
    }
  },
}

Version:

  • wp-graphql-content-blocks: 2.0.0
  • Faust.js: 1.2.1
  • WPGraphQL: 1.20.0

regression: slow schema load time, query execution time

After merging PR #47, the schema is slow to load and queries are slow to execute. Additionally, all post types editorBlocks have the return type [ EditorBlock ] instead of [ ${PostType}Block ] as was the intent of #47.

Steps to Reproduce:

  1. Setup a WordPress environment running WPGraphQL v.1.14.0 and the latest "main" branch of WPGraphQL Content Blocks with the above PR merged
  2. Load the Schema in GraphiQL and note the time it took.
  3. Add the following snippet to remove support for the "core/quote" block from Gutenberg
add_filter( 'allowed_block_types_all', 'test_remove_quote_block', 10, 2 );

function test_remove_quote_block( $allowed_blocks, WP_Block_Editor_Context $editor_context ) {

	if ( ! isset( $editor_context->post->post_type ) ) {
		return $allowed_blocks;
	}

	if ( $editor_context->post->post_type !== 'page' ) {
		$all_blocks = \WP_Block_Type_Registry::get_instance()->get_all_registered();
		unset( $all_blocks['core/quote'] );
		return array_keys( $all_blocks );
	}

	return $allowed_blocks;
}
  1. Load the schema again in GraphiQL and note that it's taking significantly longer. (For me it was 9s before adding the snippet above and 45s after).

Also, without this snippet above, I don't get the editorBlocks: [ PageBlock ], editorBlocks: [ PostBlock ] in the Schema. . . all post types have the editorBlocks: [ EditorBlock ] as the field return type. This isn't expected behavior, as discussed in the comments of #47. I would expect each Post Type (and other block context locations in the future) to have the blocks represented in the schema with a return type of [ $typeName . 'Block' ] so that we can more accurately understand what blocks are available on each type (the intention behind much of the comments in #15).

  1. Even queries such as {posts{nodes{id}}} are slower after adding the snippet above

Proposed Solution

I believe the culprit comes down to this section of code: https://github.com/wpengine/wp-graphql-content-blocks/pull/47/files#diff-ba0150e300f9b1241e52cdf70c23f1da495d595cf592712b91818a091093b806R76-R108

Most specifically line #103.

Right now there's an array_map within a foreach loop causing an N+1 problem. For each post type registered to show in GraphQL there will be an array_map passed over all blocks associated with that post type. I'm not sure that's a significant issue, but one that might be better handled once instead of N+1 times.

Line #103 attempts to register a lot of interfaces on a lot of Types for EVERY single GraphQL request. I don't think this is necessary.

Instead of adding Interfaces to the Block Types here, I think we can more efficiently add the interfaces at the time of registering the Block Types.

What I propose is the Registry collect (one time and memoize it) a list of Blocks supported on each post type.

Then, when the Block Type is being registered and interfaces are being applied to each Block Type, it can reference that list and apply the necessary interfaces at that time.

Even queries such as {posts{nodes{id}}} are slower after adding the snippet above

This should help only generate the Interfaces for GraphQL requests that need them to be fulfilled, vs generating them on every request.

Queries like that shouldn't be needing to generate Interfaces for PageBlocks, CarBlocks, HouseBlocks, etc. . .becuase they weren't queried for. But right now each GraphQL request is paying the price of the overhead of registering the interfaces on every request.

Submit plugin to WordPress.org

As WP GraphQL moves to a model where they're more comfortable with extension plugins, like this one, it is time we add it to WordPress.org to make it easier for users and developers to access.

Acceptance Criteria

  • Prepare a plugin version for WordPress.org submission (may require removing update code)
  • Submit to .org
  • Setup CI/CD for automatic deployment when the project is accepted
  • Push a new release to .org when we're ready

Notes

  • Watch the slug. We don't want to change that
  • After it's on .org we'll add it's download count to our own metrics

Custom block attributes not exposed

I might be missing something but only things I can export from custom block are className & lock. None of my attributes that I registerd in theme with registerBlockType are showing up.

Switch to new PHPCSStandards/PHP_CodeSniffer package

The Squizlabs/PHP_CodeSniffer package has been deprecated. It has been forked and will be maintained under a new package at PHPCSStandards/PHP_CodeSniffer.

Reference: The Future of PHP_CodeSniffer ยท Issue #3932 ยท squizlabs/PHP_CodeSniffer

Acceptance Criteria

  • Existing squizlabs package is removed and replaced with the new one.
  • Existing phpcs tests continue to pass. If failures happen after upgrading, the changes needed to make it pass should happen in the same PR.

CoreVideo returns false for controls regardless of admin setting

When fetching the CoreVideo settings, it returns false regardless of what has been set in the admin.

I have tried to save it when set to true, and false then true and it always comes through as false.

Removing and re-adding the component also doesn't help.

Weird behaviour in queries made via Apollo Client

Hey guys,

I'm running into a very specific edge case where I'd like to bring and move every block related setting in a fragment since I have so many post types and writing the editorBlocks property for each feels cumbersome. Like here's what I mean,

query GetNodeByURI( $uri: String! ) {
  nodeByUri( uri: $uri ) {
    ... on Post {
      editorBlocks(flat: true) {
        ... on CoreParagraph { ... }
        ... on CoreImage{ ... }
        ... on CoreTable{ ... }
      }
    }
    
    ... on Interview {
      editorBlocks(flat: true) {
        ... on CoreParagraph { ... }
        ... on CoreImage{ ... }
        ... on CoreTable{ ... }
      }
    }
    
    ... on Event {
      editorBlocks(flat: true) {
        ... on CoreParagraph { ... }
        ... on CoreImage{ ... }
        ... on CoreTable{ ... }
      }
    }
    
    ... on Movies {
      editorBlocks(flat: true) {
        ... on CoreParagraph { ... }
        ... on CoreImage{ ... }
        ... on CoreTable{ ... }
      }
    }
  }
}

You get the idea, lots of duplication of code. So what I was trying to do was something like this

query GetNodeByURI( $uri: String! ) {
  nodeByUri( uri: $uri ) {
    ... on Post {
      ...BlockContentData
    }
    
    ... on Interview {
      ... BlockContentData
    }
    
    ... on Event {
      ... BlockContentData
    }
    
    ... on Movies {
      ... BlockContentData
    }
  }
}

fragment BlockContentData on NodeWithEditorBlocks {
    blockList: editorBlocks( flat: true ) {
      cssClassNames
      name
      renderedHtml
      clientId
      parentClientId
   }
}

This, when i tried in the graphql playground, works fine, without any problems. I see the results. However, when I try to make it work with the apollo client query method, things don't turn out the way I expected. I'm not getting the blockList in the results when I use the fragment. But if I plug it directly in the Post, Interviews or Events, it works.

To be more elaborate,

  const results: QueryResults = await clients[ region ].query( {
      query: gql`
        query GetPost( $uri: String! )  {
          nodeByUri( uri: $uri ) {
            ...on Post {
              id
              title
              ...BlockContentData
            }
          }
        }

        fragment BlockContentData on NodeWithEditorBlocks {
          blockList: editorBlocks( flat: true ) {
            cssClassNames
            name
            renderedHtml
            clientId
            parentClientId
          }
        }
      `,
      variables: { uri },
      fetchPolicy: 'network-only',
    } );

    console.log(results);

logs the following object,

{
 data: {
   nodeByUri: {
     __typename: 'Post',
     id: 'cG9zdDoxMjM5MjM=',
     title: 'Tiempos inciertos, tiempos para los activos privados'
   }
 },
 loading: false,
 networkStatus: 7
}

But as I change the query to something like this,

  const results: QueryResults = await clients[ region ].query( {
      query: gql`
        query GetPost( $uri: String! )  {
          nodeByUri( uri: $uri ) {
            ...on Post {
              id
              title
              blockList: editorBlocks( flat: true ) {
                cssClassNames
                name
                renderedHtml
                clientId
                parentClientId
              }
            }
          }
        }
      `,
      variables: { uri },
      fetchPolicy: 'network-only',
    } );

    console.log(results);

I can see the blockList as an array of blocks.

{
  data: {
    nodeByUri: {
      __typename: 'Post',
      id: 'cG9zdDoxMjM5MjM=',
      title: 'Tiempos inciertos, tiempos para los activos privados',
      blockList: [Array]
    }
  },
  loading: false,
  networkStatus: 7
}

I want to clarify that this is not the first time I'm using the fragments in apollo client, so this is happening only when I do fragment on NodeWithEditorBlocks.

Just need to know whether it's something that I'm doing wrong or it is something that we need to change in the plugin itself.

CircleCI deploy fails due to restricted context

When the release deploy jobs run in CircleCI (introduced in #179), the job fails because the GitHub Actions bot does not have access to the restricted Contexts stored in CircleCI.

Example: https://app.circleci.com/pipelines/github/wpengine/wp-graphql-content-blocks/88/workflows/fea1e32f-40c9-41c0-ba94-5eba26a57d74

Re-running the job manually as a user with the appropriate team access in GitHub results in the job working as expected. We need to fix this so it's automated.

WPGraphQL Content Blocks Plugin Error

Hello,

I have installed the โ€œWPGraphQL Content Blocksโ€ plugin and it activated successfully. However, I am getting an error when I try to open GraphQL IDE. It is showing a loader continues on screen and getting an error on screen โ€œParse error: syntax error, unexpected โ€˜WP_Block_Typeโ€™ (T_STRING), expecting function (T_FUNCTION) or const (T_CONST) in /var/www/html/wp-content/plugins/wp-graphql-content-blocks/includes/Blocks/Block.php on line 30โ€.

Here are all the details about my website:
PHP: Version 7.3.17
WordPress Version 6.2
Theme: WordPress Twenty Twenty-Three Version 1.1
Plugin: WPGraphQL Version 1.14.3,
WPGraphQL Content Blocks Version 0.2.1

Could you please help me resolve this issue?

Support for ACF blocks

We plan to build a Faust/WordPress app with custom Gutenberg blocks created with Advanced Custom Fields. The app uses TypeScript (in strict mode) and we use graphql-codegen to generate types for GraphQL content based on the GraphQL schema exposed by WordPress backend and our frontend queries/mutations written in plain GraphQL (in .gql files).

The problem

ACF exposes block fields as a single data attribute. In PHP there are ACF functions allowing to get values of certain fields from this data object. This plugin however handles object attributes by exposing them as a JSON string in GraphQL. There are two problems with this approach:

  1. There are no types of ACF fields passed through GraphQL - on the frontend we only get a single data attribute of type String, so getting the real data would require manually parsing the JSON into an object and using TypeScript type guard functions with some schema validator (e.g. yup) to check if the parsed object actually contains all the fields we expect and cast it to manually defined type (this would need to be done for each custom block manually).
  2. Some complex ACF fields are stored as plain arrays, e.g. repeater field called reviews with two nested fields: title and content and containing two items defined inside Gutenberg block would have a structure like this (simplified):
{
    "reviews_0_title": "...",
    "reviews_0_content": "...",
    "reviews_1_title": "...",
    "reviews_1_content": "...",
    "_": "...there are more ACF internal items with ids pointing to field configuration etc."
}

While retrieving the same field with the PHP function would return an array of nested objects:

{
    "reviews": [
        {
            "title": "...",
            "content": "..."
        },
        {
            "title": "...",
            "content": "..."
        }
    ],
    "_": "...other fields"
}

So this would require us also to rebuild a proper data structure manually in Faust (while it could be done automatically by ACF itself on the WordPress side).

The solution

Ideally, the WPGraphQL Content Blocks plugin should handle object type attributes by exposing the fully typed data structured as it is. It would also be great to have some filters allowing to change the data structure (and its type definitions). This would allow hooking into these filters to replace the ACF data attribute stored inside the block with ACF-generated field values (same as the ones available in PHP with ACF functions).

Are there any plans to support nested object attributes?
Or maybe it's planned to add built-in support for ACF blocks, as it is a widely used WordPress plugin?

CorePostTerms doesn't expose any way to ask for the terms

When querying Blocks, if a block is of the CorePostTerms block type, there's no way on the block to fetch the terms.

In the Block Editor I can see the terms:

CleanShot 2024-01-24 at 15 55 40

However when querying for blocks, I cannot find any field in the Schema that would allow me to fetch these terms and make use of them.

I would like to be able to query the terms as a connection.

Something to the tune of:

query WysiwygField($uri: String! = "my-uri") {
  nodeByUri(uri: $uri) {
    id
    uri
    ...on NodeWithEditorBlocks {
      editorBlocks {
        __typename
        ...on CorePostTerms {
          ## There's no way to query the terms here ๐Ÿค”
          terms { ## Would be great to be able to query like this
            nodes {
              __typename
               id
               name
            }
          }
        }
      }
    }
  }
}

Failure running getIntrospectionQuery - GraphiQL IDE Schema not loading

I'm new to using this plugin and I'm trying to get it setup with Faust.

I've run into the following error:

Screenshot 2024-03-19 203923

Details:

  • Wordpress 6.4.3
  • WPGraphQL Content Blocks: 3.1.2
  • WPGraphQL: 1.22.1
  • Faust.js: 1.2.2
  • Roots Bedrock

I've checked permissions and the folder structure is owned by www-data:www-data. Everything is set to 644 other than the uploads, which is 775.

I can post to http://localhost:8080/wp/graphql with postman without getting 500.

Downloading the plugin as a composer package does not contain the vendor directory and autoloads

Hi

I recently upgraded my wp-graphql-content-blocks plugin from 1.1.1 to 2.0.0 and the first thing the graphql requests lead to is an exception saying that it can not find WPGraphQLHelpers.
image

I checked in the downloaded plugin and saw no vendor/ folder. I checked the composer.json file for PSR-4 autoloads and was able to get it resolved via generating the autoloads for the plugin.

However, when I checked the zip present in the release https://github.com/wpengine/wp-graphql-content-blocks/releases/tag/v2.0.0, there is a vendor folder. So this issue happens only when loading this plugin via composer.

Content Blocks: Add configuration for running tests locally

As maintainers of the Content Blocks plugin, it would be nice to be able to easily run the test suites locally, to get faster feedback and improve velocity. Having to wait for GitHub to run the tests is slower and potentially wastes computing resources.

Acceptance Criteria

  • A Docker config is created for running phpunit tests
  • A Makefile is created to run the phpunit tests within Docker
  • Documentation is written explaining how to run the tests locally (can go in README.md for now)
  • The npm run test-e2e command works properly, or is replaced with something that does. (It currently fails and appears to require additional steps)

Notes

  • We have a similar setup in the AtlasWP and ACM plugins.

`core/list-item` content attribute returns html of nested elements

I have created the following block structure in the editor for demo:
Bildschirmยญfoto 2023-08-07 um 07 24 43

My GraphQL query:

GraphQL
query ListPageByUri($uri: ID!) {
  page(id: $uri, idType: URI) {
    id
    title
    uri
    editorBlocks(flat: true) {
      ... on CoreList {
        name
        attributes {
          ordered
          start
        }
      }
      ... on CoreListItem {
        name
        attributes {
          content
        }
      }
      ... on CoreParagraph {
        name
        attributes {
          content
          className
        }
      }
    }
  }
}

Which returns this JSON:

JSON
{
  "data": {
    "page": {
      "id": "cG9zdDoxNDA=",
      "title": "Team",
      "uri": "/unternehmen/team",
      "editorBlocks": [
        {
          "name": "core/list",
          "attributes": {
            "ordered": false,
            "start": null
          }
        },
        {
          "name": "core/list-item",
          "attributes": {
            "content": "Interesting list item"
          }
        },
        {
          "name": "core/list-item",
          "attributes": {
            "content": "Another one\n<ol>\n<li>Nested ordered</li>\n\n\n\n<li>Another nested ordered\n<ul>\n<li>Deeply nested</li>\n</ul>\n</li>\n</ol>\nNested orderedAnother nested ordered\n<ul>\n<li>Deeply nested</li>\n</ul>\nDeeply nested"
          }
        },
        {
          "name": "core/list",
          "attributes": {
            "ordered": true,
            "start": null
          }
        },
        {
          "name": "core/list-item",
          "attributes": {
            "content": "Nested ordered"
          }
        },
        {
          "name": "core/list-item",
          "attributes": {
            "content": "Another nested ordered\n<ul>\n<li>Deeply nested</li>\n</ul>\nDeeply nested"
          }
        },
        {
          "name": "core/list",
          "attributes": {
            "ordered": false,
            "start": null
          }
        },
        {
          "name": "core/list-item",
          "attributes": {
            "content": "Deeply nested"
          }
        },
        {
          "name": "core/list-item",
          "attributes": {
            "content": "The funding"
          }
        },
        {
          "name": "core/list",
          "attributes": {
            "ordered": true,
            "start": null
          }
        },
        {
          "name": "core/list-item",
          "attributes": {
            "content": "Some numbers\n<ul>\n<li>nested</li>\n</ul>\nnested"
          }
        },
        {
          "name": "core/list",
          "attributes": {
            "ordered": false,
            "start": null
          }
        },
        {
          "name": "core/list-item",
          "attributes": {
            "content": "nested"
          }
        },
        {
          "name": "core/list-item",
          "attributes": {
            "content": "Again some numbers"
          }
        }
      ]
    }
  },
  "extensions": {
    "debug": [
      {
        "type": "DEBUG_LOGS_INACTIVE",
        "message": "GraphQL Debug logging is not active. To see debug logs, GRAPHQL_DEBUG must be enabled."
      }
    ]
  }
}

As you can see, it returns every nested item as a block, correctly. But on the attributes.content field, it includes the HTML for the nested elements.

The current workaround is to just strip the HTML off, but I'd consider this more of a bug.

Thanks for creating this plugin, and thanks in advance!

Revert CoreImageAttributes width field is of type String but it should be Float

Steps to Reproduce

Check CoreImageAttributes width type in GraphQLi. It is of type String.

##Issue
CoreImageAttributes.width is of type String.

##Expected Behavior
CoreImageAttributes.width should be of type Float.

##Additional Information
Note any other relevant details.

##Related ticket:
CoreImageAttributes.width is type String, but should be a Float ยท Issue #136 ยท wpengine/wp-graphql-content-blocks
You need to revert the following commit: Bug: CoreImage width attribute resolve throws error. (#130) ยท wpengine/wp-graphql-content-blocks@28fca4a
However we need to document the failing unit test case and provide a workaround for this error.
The workaround is to use an alias for the attributes field so that GrapgQL can resolve the types without any conflicts.

Image

Using the plugin as a submodule with GitHub Actions

I have failed to implement this plugin as a submodule in my deployment process. I use GitHub Actions to update files with a self-hosted runner that deploys my WordPress sites in production.

However, I have noticed that installing the plugin manually works perfectly. I will do this in the meantime, but it is unfortunately very cumbersome, as the plugin will get deleted everytime I push changes to my repository.

Is there a special process going on during the manual plugin install? Or am I doing something wrong?

Even when I try to install the plugin as a submodule after the manual installation (the version controlled files are the exact same as the ones being deleted and replaced), the plugin does not work properly. Accessing WpgraphQL IDE becomes impossible (the page tries to load forever).

I have been using the production ready files as well.

Thanks for your help!

Can't retrieve Core Navigation data

Thanks for your hardwork on this. I'm struggling to get data from CoreNavigation, CoreNavigationLink, CoreNavigationSubmenu. Please see code:

query NewQuery {
  pages {
    nodes {
      editorBlocks(flat: true) {
        __typename
        name
        ... on CoreNavigation {
          attributes {
            ref
          }
        }
        ... on CoreNavigationLink {
          attributes {
            label
            url
            title
          }
        }
        ... on CoreNavigationSubmenu {
          attributes {
            title
            url
            label
          }
        }
      }
    }
  }
}

This returns the following:

{
  "data": {
    "pages": {
      "nodes": [
        {
          "editorBlocks": []
        },
        {
          "editorBlocks": []
        },
        {
          "editorBlocks": [
            {
              "__typename": "CoreParagraph",
              "name": "core/paragraph"
            }
          ]
        },
        {
          "editorBlocks": []
        },
        {
          "editorBlocks": []
        },
        {
          "editorBlocks": []
        },
        {
          "editorBlocks": []
        },
        {
          "editorBlocks": [
            {
              "__typename": "CoreParagraph",
              "name": "core/paragraph"
            }
          ]
        },
        {
          "editorBlocks": [
            {
              "__typename": "CoreParagraph",
              "name": "core/paragraph"
            },
            {
              "__typename": "CoreParagraph",
              "name": "core/paragraph"
            },
            {
              "__typename": "CoreColumns",
              "name": "core/columns"
            },
            {
              "__typename": "CoreColumn",
              "name": "core/column"
            },
            {
              "__typename": "CoreParagraph",
              "name": "core/paragraph"
            },
            {
              "__typename": "CoreColumn",
              "name": "core/column"
            },
            {
              "__typename": "CoreParagraph",
              "name": "core/paragraph"
            },
            {
              "__typename": "CoreGroup",
              "name": "core/group"
            },
            {
              "__typename": "CoreImage",
              "name": "core/image"
            }
          ]
        }
      ]
    }
  },
  "extensions": {
    "debug": [
      {
        "type": "DEBUG_LOGS_INACTIVE",
        "message": "GraphQL Debug logging is not active. To see debug logs, GRAPHQL_DEBUG must be enabled."
      }
    ]
  }
}

Allow get_block_context_interfaces to be filtered

Problem

I'm using ACF PRO and ACF Extended PRO to create Block Types.

ACF Extended allows Block Types to be limited to specific post types.

i.e.:

CleanShot 2023-08-17 at 13 01 44

Under the hood, this translates to blocks being registered with a post_types property.

This is not a property of block registration that WordPress core respects (at least not yet ๐Ÿคท๐Ÿปโ€โ™‚๏ธ ).

ACF Extended uses this post_types property to limit the inclusion of the block in the editor interface, but WPGraphQL Content Blocks is applying the ${PostType}EditorBlock to blocks registered by ACF Extended.

For example, even though I've limited my block to 1 post type (see screenshot above), we can see that it has interfaces applied for all the post types:

CleanShot 2023-08-17 at 14 19 16

These interfaces are applied here: https://github.com/wpengine/wp-graphql-content-blocks/blob/main/includes/Registry/Registry.php#L87-L129

Proposal

We should add a filter that allows for external code to determine whether any given ${PostType}EditorBlock interface should not be implemented on a block.

Something like:

/**
 * Filter before applying per-post_type Interfaces to blocks. This allows 3rd parties to control
 * whether the interface(s) should or should not be applied based on custom logic.
 *
 * @param bool $should                                          Whether to apply the ${PostType}EditorBlock Interface. If the filter returns false, the default
 *                                                              logic will not execute and the ${PostType}EditorBlock will not be applied.
 * @param string.                                               The name of the block Interfaces will be applied to
 * @param \WP_Block_Editor_Context                              The context of the Block Editor
 * @param \WP_Post_Type $post_type                              The Post Type an Interface might be applied to the block for
 * @param array         $all_registered_blocks                  Array of all registered blocks
 * @param array         $supported_blocks_for_post_type_context Array of all supported blocks for the context
 * @param array         $block_and_graphql_enabled_post_types   Array of Post Types that have block editor and GraphQL support
 */
$should_apply_post_type_editor_block_interface = apply_filters( 'wpgraphql_content_blocks_should_apply_post_type_editor_blocks_interfaces', true, $block_name, $block_editor_context, $post_type, $all_registered_blocks, $supported_blocks_for_post_type_context, $block_and_graphql_enabled_post_types );

if ( true !== $should_apply_post_type_editor_block_interface ) {
  continue;
}

This would allow 3rd party code, such as ACF Extended (or WPGraphQL for ACF) to filter here and add support for respecting the "post_types" property of blocks.

A 3rd party codebase could filter like so:

add_filter( 'wpgraphql_content_blocks_should_apply_post_type_editor_blocks_interfaces', function( $should, $block_name, $block_editor_context, $post_type, $all_registered_blocks, $supported_blocks_for_post_type_context, $block_and_graphql_enabled_post_types ) {
	if ( ! empty( $all_registered_blocks[ $block_name ]->post_types ) && ! in_array( $post_type->name, $all_registered_blocks[ $block_name ]->post_types, true ) ) {
		return false;
	}
	return $should;
}, 10, 7 );

Thus resulting in the Block, limited to a singular post type, having only that Post Type's EditorBlock Interface implemented:

CleanShot 2023-08-17 at 14 31 29

Can I extend this to support GravityForms?

As part of my query, I have the following:

query PageByUri($uri: ID!) {
    page(id: $uri, idType: URI) {
        id
        title

        editorBlocks(flat: false) {
            ... on GravityformsForm {
                attributes {
                    formId
                }
            }
        }
    }
}

However, this just returns the formId. What I want to do, is use the gfForm query to actually fetch all the fields (example below):

    gfForm(id: $formId, idType: DATABASE_ID) {
      databaseId
      title
      description
      submitButton {
        text
      }

But there doesn't seem to be any support for this. Any ideas? Thank you in advance

General Feedback on Schema Design

This is some general feedback after initial tinkering with the plugin.

First off, good stuff! This is working similar to what I had in mind when I began work on WPGraphQL Block Editor. ๐Ÿ™Œ๐Ÿป

I have some feedback I think we should consider, ideally before we get too many users as they would be breaking changes.

Rename ContentBlock Interface

Not necessarily a hill I will die on, but I think Content usually has connotations with things like blog posts, but nav menus (in my mind) are not the same as "content".

I had selected "EditorBlock" as the name of the interface because each block is a block of the block editor. It might be a ContentBlock, or it might be a menu block or a layout block, etc.

I think we can do users (and ourselves) a service by being more clear with the naming.

Maybe something like:

  • EditorBlock: All blocks that can be used within the Block Editor
  • ContentBlock (implements EditorBlock): Blocks used to manage content
  • LayoutBlock (implements EditorBlock): Blocks used to manage layout
  • NavMenuBlock(?) (Implements EditorBlock): Blocks used by the block editor when managing nav menus

By having a generic (EditorBlock) Interface, and then more specific Interfaces (Content, Layout, etc) we can better use the Schema as documentation, and provide better experiences for our users.

We can even get more specific like:

  • PostContentBlock (Implements ContentBlock, EditorBlock): Blocks used to manage content on the "Post" type
  • PageContentBlock (Implements ContentBlock, EditorBlock): Blocks used to manage content on the "Page" type

etc.

This way, we can allow the Schema to better differentiate for consumers what blocks they can expect.

For example, if I register a custom block only on the Car post type, It shouldn't show up as a possible block on the Page and Post post types, because we know the block will never be available on those types.

So, if our schema for posts was:

# A block used by the block editor
Interface EditorBlock {
  name: Name of the block
  clientId: Non-persisted ID used to map blocks by the client application
  clientParentId: Non-persisted ID of the parent block used to map hierarchical relationships in the client application
}

# Editor Block used to manage content
Interface ContentBlock implements EditorBlock {
  name: Name of the block
  someFieldThatExistsForContentBlocksButNotOtherBlocksLikeNavMenuBlocks: Just an example
}


Interface ContentNode implements NodeWithContentBlocks {
  ...
  # List of blocks used to produce the content of the ContentNode (instead of returning _all_ EditorBlocks, it would return _just_ ContentBlocks, a subset of blocks used _just_ for Content)
  contentBlocks: [ContentBlock]
}

# A ContentBlock that is associated with the Post post type (i.e.https://www.designbombs.com/registering-gutenberg-blocks-for-custom-post-type/)
Interface PostContentBlock implements ContentBlock, EditorBlock {
  name: Name of the block
}

# We'd use register_graphql_interfaces_to_types() to accomplish this
type Post implements NodeWithPostContentBlocks {
  # the Post type will return a list of PostContentBlock (not a list of ContentBlock or EditorBlock) as this will most accurately describe in the schema what's actually possible to be returned. We _know_ that the post.contentBlocks will not return blocks registered only to the "Car" post type, so we should be able to properly express this in the Schema.
  contentBlocks: [PostContentBlock]
}

Rename nodeId field

Unfortunately, Blocks are not actually "nodes" as they cannot be accessed individually via node( id: "..." ) queries like other nodes.

I recommend we change this field name to clientId rather than nodId as these are not nodes and the field name could cause confusion.

Probably should also change parentId to clientParentId or parentClientId as well to clarify intent of the usage of these fields.

NOTE: If we're able to come up with a reliable solution for persisting block IDs (uniqueId persisted with blocks in the database, we might be able to make blocks actual nodes, which would be rad, but there's a lot of challenges to this we'd need to figure out still).

Add support for genesis-custom-blocks

Bug summary

When querying data with graphql , fields are coming empty. It should output the data of fields .

Steps to reproduce

  1. Create a genesis custom block .
  2. Add custom block to a page/post .
  3. Open grapgql IDE on wordpress .

Expected behavior

should output the data of fields present in the custom block

Actual behavior

The fields are coming empty also no errors.

Versions

  • WordPress version: 6.2.2
  • wpgraphql version: 1.14.3
  • WPGraphQL Content Blocks version: 0.2.1
  • Genesis Custom Blocks version: 1.5.1
    Screenshot (306)

Add support for Reusable & Classic blocks

Currently I cannot see a way to query reusable blocks or a classic block. I can see the typename available CoreBlock & CoreFreeform, however the blocks themselves don't actually appear in the page data.

For example, the below query does not return 'core/block' or 'core/freeform' on a page which has those blocks added.

editorBlocks {
      name
}

Upload Schema Artifact workflow fails to upload schema

In #192 we attempted to fix the Schema Upload Artifact workflow by reverting changes made previously which had resulted in the workflow not running. The attempted fix did not work. Source: https://github.com/wpengine/wp-graphql-content-blocks/actions/workflows/upload-schema-artifact.yml

The error is GitHub Releases requires a tag. It seems the context in which this runs does not have access to the tag data. cc @blakewilson for any additional context you may be able to add.

Every time we do a tagged release this workflow should run but it broke about 4 months ago and the schema linter job fails on every PR as a result. We've tried a change on this but it didn't fix it so we need to start over.

Acceptance Criteria

  • Ensure the schema linter can successfully fun on every PR and branch

Notes

Feature: Add support for Xpath selectors

Description

Due to this #185 some selectors are wrong and do not capture accurately the html contents of an attribute. In that case we can replace those selectors with Xpath.

Acceptance criteria

  • XPath selectors are supported
  • Documentation entry added in Faust.org

Technical details

  • Do a simple check if the selector starts with either (// or // to determine if its an Xpath selector.

Consider aligning PHPCS ruleset with WPGraphQL.

In order to make it easier for other developers in the ecosystem to contribute to this plugin,would it be possible to align with some of the ruleset options used by WPGraphQL core - or at least the ones that dont conflict too bad with the included WPEngine ruleset (I'm not particularly familar with what ya'll usually use internally)?

(I'd be particularly nice if in 2023 we could be all be using short array syntax).

Thanks for considering!

CoreImage give wrong caption when lightbox is enable

When I add caption on Image, if "lightbox" checkbox is checked, graphql show me the caption three times.

SCR-20240206-ixaf

Step to reproduce:

  1. Add a block "image" on your gutenberg and add a caption
  2. Duplicate it and enable "lightbox on click" checkbox
  3. Make the following graphql request :
{
  page(id: "your-page-slug", idType: URI) {
    editorBlocks(flat: false) {
      ... on CoreImage {
        attributes {
          caption
          lightbox
        }
      }
    }
  }
}

Version:

  • wp-graphql-content-blocks: 2.0.0
  • Faust.js: 1.2.1
  • WPGraphQL: 1.20.0

ACF Blocks data is not displaying

I am attempting to query ACF blocks, but it seems that the data from the blocks is not being displayed. Could you please assist me with this?
image

Getting 500 error when using this plugin with WPGraphQL

I just downloaded this plugin from github (version 2.0.0)
As soon as I activate this plugin, I cannot open my graphQL IDE in wordpress dashboard
It is giving 500 error in console:
image

Have anybody faced similar issue?
Or any help on how I can resolve this

Basically my project is using "acf_register_block" to register custom blocks and then using ACF in them, I need to expose those blocks with data in the graphQL.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.