Closed nolros closed 5 years ago
So this would allow us to make a "combined" schema with one script tag right?
I'd like to keep the previous implementation working though, so I don't think we can simply move context. I think it's okay to duplicate it though.
Otherwise, I'd accept a PR that adds this :)
Purpose
Provide Schema::graph() @graph (concat) functionality. Current approach requires load each root level node schema is loaded individually e.g. Schema::organization() and . Schema::blog(). Although Google states this is accepted practice in reality there are a number of issues associated with that approach:
Solution:
The JSON-lD 1.1 spec provides for a @graph object defined as
A graph object represents a named graph represented as the value of a property within a node object. When expanded, a graph object MUST have an @graph member, and may also have @context, @id, and @index members. A simple graph object is a graph object which does not have an @id member. Note that node objects may have a @graph member, but are not considered graph objects if they include any other properties. A top-level object consisting of @graph is also not a graph object.
JSON 1-1 Specification:
4.15 Named Graphs - https://json-ld.org/spec/latest/json-ld/#named-graphs
At times, it is necessary to make statements about a graph itself, rather than just a single node. This can be done by grouping a set of nodes using the @graph keyword. A developer may also name data expressed using the @graph keyword by pairing it with an @id keyword as shown in the following example:
EXAMPLE 57: Using @graph to explicitly express the default graph
{
"@context": {...},
"@graph": [{
"@type": "WebSite",
"name": "My Website Name",
"url": "http://www.example.com"
}, {
"@type": "WebPage",
"name": "My Website Name",
"url": "http://www.example.com"
}, {
"@type": "Organization",
"name": "My Website Name",
"url": "http://www.example.com",
"sameAs": [
"http://www.facebook.com/example-com",
"http://www.instagram.com/example-com"
]
}]
}
Real world example:
{
"@context": "http://schema.org",
"@graph": [
{
"name": "Cybersecurity Software and Consulting services solutions targeting midmarket and enterprise sized companies in the US. We specialize in Distributed Denial of Service Prevention, Application Security, and Data Protection.",
"address": {
"postalCode": "84010",
"addressLocality": "Bountiful",
"addressRegion": "UT",
"addressCountry": "US",
"@type": "PostalAddress",
"streetAddress": "585 W 500 S #110"
},
"openingHours": "Mo,Tu,We,Th,Fr 08:00-17:00",
"@type": "LocalBusiness",
"image": [
"https:\/\/cdn.wallpapersafari.com\/71\/14\/Uk7QKa.jpg",
"https:\/\/media.istockphoto.com\/photos\/salt-lake-city-panoramic-picture-id518556450?k=6&m=518556450&s=612x612&w=0&h=OpX0jR-Mc4h9MDHfzawGEh5wHm-KjkvZocTUk_vwe2M=",
"http:\/\/www.ddos-911.com\/images\/social\/facebook\/solutions-facebook.png",
"http:\/\/www.roseman.edu\/wp-content\/uploads\/2014\/02\/1-salt-lake-city-utah-usa-utah-images.jpg",
"http:\/\/www.ddos-911.com\/images\/social\/facebook\/articles-facebook.png",
"http:\/\/www.ddos-911.com\/images\/social\/facebook\/company-facebook.png",
"http:\/\/www.ddos-911.com\/images\/social\/facebook\/home-facebook.png",
"http:\/\/www.ddos-911.com\/images\/social\/facebook\/blog-facebook.png"
],
"telephone": "+18012957555",
"geo": {
"@type": "GeoCoordinates",
"longitude": -111.894683,
"latitude": 40.887610000000002
},
"@id": "http:\/\/www.ddos-911.com\/",
"@context": "http:\/\/schema.org"
},
{
"url": "http:\/\/www.ddos-911.com\/",
"author": {
"@type": "Person",
"name": "Nolan"
},
"@type": "WebSite",
"publisher": "NCSi Inc.",
"description": "Cybersecurity Software and Consulting services solutions targeting midmarket and enterprise sized companies in the US. We specialize in Distributed Denial of Service Prevention, Application Security, and Data Protection.",
"name": "Cybersecurity Technology Solutions | Distributed Denial of Service Prevention | Application Security | Data Protection",
"@context": "http:\/\/schema.org"
}
]
}
Proposal
Extend the Schema library to include graphing functionality:
<?php
use Spatie\SchemaOrg\Schema;
use ExampleInterface; // public graph
class ExampleClass implements ExampleInterface
{
/**
* Example schema graph
*
* @return mixed
*/
public function graph()
{
/**
* Return graphed schema objects
*/
$this->graphExample = Schema::graph([
$this->orgExample(...),
$this->blogExample(...),
...
])
/**
* Maintain current parsing
*/
$this->graphExample->toScript();
$this->graphExample->toArray();
/**
* Potential additional features
*/
/**
* Return true if node child type exists on the graph parent
*/
$this->graphExample->has()->organization();
/**
* Return true the node child type on the graph parent
*/
$this->graphExample->get()->organization();
/**
* Ierative method on the collection. Example
*/
$this->graphExample->each()->toScript();
/**
* Bonus = iteractive callback on the collection. Example
*/
$this->graphExample->each(function($v,$k) {
return $v->isOrganization()->hasUrl()
});
}
/**
* Example org schema
*
* @return mixed
*/
protected function orgExample(...)
{
return $this->orgSchemaName = Schema::organization()
(...);
}
/**
* Example blog schema
*
* @return mixed
*/
protected function blogExample(...)
{
return Schema::blog()
(...);
}
}
Thanks for the detailed proposal!
If you're interested in contributing this, feel free to send a PR because I definitely want something like this. Not sure how I'd like to see it though, but that's gonna be easier to discuss over a PR :)
Wouldn't it suffice though to simple add a Schema::graph
method, that accepts an array of types? Or do you see other downsides there?
@sebastiandedeyne sorry been moving. I will go take a look to ensure I have this right, but so far all my @graph
is passing google snippet tests. All that needs to be done for a version 0.1 is as you said pass an array of unique schema objects which is then encapsulated in @graph
array so foreach() :
// Snippet 1
{
"@context": "http:\/\/schema.org",
"@type": "Organization",
// ... remove for brevity sake
}
// Snippet 2
{
"@context": "http:\/\/schema.org",
"@type": "Blog",
// ... remove for brevity sake
"blogPosts": [
{
"@type": "BlogPosting",
// ... remove for brevity sake
}
]
}
@graph
we are really just inserting x number of snippet objects (Schemas) into a@graph
array. So to answer your question, yes an array of unique schema types
{
"@context": "http:\/\/schema.org", //load single content for all @graph
"@graph": [
{
"@type": "Organization"
// ... remove for brevity sake
}, // end of Org
{
"@type": "Blog",
// ... remove for brevity sake
"blogPosts": [
{
"@type": "BlogPosting"
// ... remove for brevity sake
},
{
"@type": "BlogPosting"
// ... remove for brevity sake
}
]
} // end of Blog
] // end of @graph
} // encapsulated as an obj
@graph
example I have running on an actual live site. All snippets pass in google search console and google search parses out each snippet contained within. I should add that the site ranks #1 on google and bing with zero dollars spent to date on Adwords / PPC. The entire SEO backend runs on spatie/schema-org (with customization). The site is still undergoing a rewrite, but we have site gets a 90 - 95% performance grade on google ad GTMetrix i.e. you code works great!I should add I still @context on every schema versus at graph level but none of the search engines complain and read it just fine.
{"@graph":[{"@context":"http:\/\/schema.org","@type":"Organization","address":{"streetAddress":"585 W 500 S #110","postalCode":"84010","addressCountry":"USA","addressRegion":"Utah"},"logo":"http:\/\/ddos-911.com\/storage\/_images\/_brand\/ncsi-security-logo-2018.png","email":"sales@ncsi.us","legalName":"+1-801-295-7555","areaServed":"USA","telephone":"+1-801-295-7555","url":"http:\/\/ddos-911.com","image":"\/images\/social\/facebook\/home-facebook.png","description":"Cybersecurity Software and Consulting services solutions targeting midmarket and enterprise sized companies in the US. We specialize in Distributed Denial of Service Prevention, Application Security, and Data Protection.","name":"NCSi, Inc. Cyber Security Software & Consulting Solutions","sameAs":["https:\/\/twitter.com\/GoNCSi","https:\/\/www.facebook.com\/goncsi\/","https:\/\/www.youtube.com\/channel\/UCVmaC_U5vRj4wPE5eSsKCkg","https:\/\/www.linkedin.com\/company\/3793997\/"],"contactPoint":[{"@type":"ContactPoint","telephone":"+1-801-295-7555","availableLanguage":"us-EN, English","areaServed":"USA","hoursAvailable":{"dayOfWeek":["Monday","Tuesday","Wednesday","Thursday","Friday"],"opens":"08:00","closes":"17:00"},"contactType":"Customer service"},{"@type":"ContactPoint","telephone":"+1-801-295-7555","availableLanguage":"us-EN, English","areaServed":"USA","hoursAvailable":{"dayOfWeek":["Monday","Tuesday","Wednesday","Thursday","Friday"],"opens":"08:00","closes":"17:00"},"contactType":"Sales"},{"@type":"ContactPoint","telephone":"+1-801-677-2497","availableLanguage":"us-EN, English","areaServed":"USA","hoursAvailable":{"dayOfWeek":["Monday","Tuesday","Wednesday","Thursday","Friday"],"opens":"08:00","closes":"17:00"},"contactType":"Tech Support"},{"@type":"ContactPoint","telephone":"+1-855-864-3734","availableLanguage":"English","areaServed":"USA","hoursAvailable":{"dayOfWeek":["Monday","Tuesday","Wednesday","Thursday","Friday"],"opens":"08:00","closes":"17:00"},"contactType":"Customer Support"}]}]}
I like the idea of has()
and would extend it by get()
. Most schemas allow to use other schemas like product.brand
instead of duplicating code it would be easier to first define organization
and just do $graph->get(Organization::class)
after this to get the created organization as product.brand
.
Because I have to do this tomorrow in my company I would be fine to create a PR here.
And just as an idea - in big applications with multiple submodules it could be that a lot of schemas will be added from different packages. This could make it hard to get a schema from another package in real-time if it is executed later. So how about something like a Promis
that will resolve everything from graph during toArray()
so it will have access to all available schemas!?
This won't be too hard to get it working and will make the graph the most powerful part here.
For the Promise we just need a new class that knows about the whole graph and a new if in the serializeProperty()
. And the graph should be solvable be a new type with some fancy logic like the has
, get
and so on. The current serializer already handles all the things.
@nolros I've opened a PR - can you check if it solves all your problems? For us it solves all open wishes! 😃 The show/hide and link ability is the thing we missed the most - so I've added it on top to the normal graph-features (collect schemas).
Any input on this? Would be great to see this functionality included.
@bellwood can you test #62 !? It should solve this but testing by others would be great. 😊
I've had no issues with it combining BreadcrumbList, LocalBusiness and Review on L5.6
So I bet that @sebastiandedeyne or someone else of @spatie should review the PR to release it!? For me it also works like charm. Primary the relation resolving in a singleton instance is the best thing! :)
@Gummibeer seems @sebastiandedeyne is afk 😭. Maybe @freekmurze could review #62?
Hi! 👋😄
Reviewed the PR, will merge after the comments are resolved 👍
Available in 2.1.0
!
First thank you for an excellent library. I cannot tell you what a help this has been. One item I don't see that you might want to consider is graphing of multiple schemas according to the JSON LD spec. Ive included very basic example, which btw works fine. I think that the ""@context":"http:\/\/schema.org"" context needs to moved up to the @graph level but you get the idea.