diff --git a/README.md b/README.md index 3f9fbdc6..25b571d1 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,9 @@ looking for inspiration on what to add. - [Movie](#movie) - [Recipe](#recipe-1) - [Software App](#software-app) + - [Organization](#organization) + - [Brand](#brand) + - [WebPage](#webpage) - [Contributors](#contributors) @@ -2725,6 +2728,170 @@ export default () => ( For reference and more info check [Google docs for Software App](https://developers.google.com/search/docs/data-types/software-app) + +### Organization + +```jsx +import React from 'react'; +import { OrganizationJsonLd } from 'next-seo'; + +export default () => ( + <> +

Organization JSON-LD

+ + +); +``` + +**Data required properties** + +| Property | Info | +| -------------------------- | -------------------------------------------------------------------------------------------------------- | +| `name` | The name of the Organization. | +| `url` | Url of the organization | +| `contactPoint` | | +| `contactPoint.telephone` | An internationalized version of the phone number, starting with the "+" symbol and country code | +| `contactPoint.contactType` | Description of the purpose of the phone number i.e. `Technical Support`. | +**Data Recommended properties** + +| Property | Info | +| -------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `logo` | ImageObject or URL an associated logo to the Organization. | +| `organizationType` | Organization type, check [here](https://schema.org/Organization#subtypes) | +| `legalName` | The official name of the organization, e.g. the registered company name. | +| `sameAs` | URL of a reference Web page that unambiguously indicates the item's identity. | +| `address` | Address of the specific business location | +| `address.addressCountry` | The 2-letter ISO 3166-1 alpha-2 country code | +| `address.addressLocality` | City | +| `address.addressRegion` | State or province, if applicable. | +| `address.postalCode` | Postal or zip code. | +| `address.streetAddress` | Street number, street name, and unit number. | +| `contactPoint.areaServed` | `String` or `Array` of geographical regions served by the business. Example `"US"` or `["US", "CA", "MX"]` | +| `contactPoint.availableLanguage` | Details about the language spoken. Example `"English"` or `["English", "French"]` | + +For reference and more info check [Docs](https://schema.org/Organization) + + +### Brand + +```jsx +import React from 'react'; +import { BrandJsonLd } from 'next-seo'; + +export default () => ( + <> +

Brand JSON-LD

+ + +); +``` + +**Data required properties** + +| Property | Info | +| -------------------------- | -------------------------------------------------------------------------------------------------------- | +| `id` | 'URL to main entity of page' | + +**Data Recommended properties** + +| Property | Info | +| ------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `logo` | ImageObject or URL an associated logo to the Organization. | +| `slogan` | A slogan or motto associated with the item. | +| `aggregateRating.ratingValue` | The rating for the content.(Check the [reference](https://schema.org/ratingValue) | +| `aggregateRating.ratingCount` | The count of total number of ratings. | +| `aggregateRating.reviewCount` | The count of total number of reviews. | +| `aggregateRating.bestRating` | The highest value allowed in this rating system. If bestRating is omitted, 5 is assumed. | + + +For reference and more info check [Docs](https://schema.org/Brand) + + +### WebPage + +```jsx +import React from 'react'; +import { WebPageJsonLd } from 'next-seo'; + +export default () => ( + <> +

WebPage JSON-LD

+ + +); +``` + +**Data required properties** + +| Property | Info | +| -------------------------- | -------------------------------------------------------------------------------------------------------- | +| `id` | 'URL to main entity of page' | + +**Data Recommended properties** + +| Property | Info | +| ------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `description` | ImageObject or URL an associated logo to the Organization. | +| `lastReviewed` | Date on which the content on this web page was last reviewed for accuracy and/or completeness. | +| `reviewedBy.type` | People or organizations that will review the content of the web page. | +| `reviewedBy.name` | Name of the entity that have reviewed the content on this web page for accuracy and/or completeness. | + + +For reference and more info check [Docs](https://schema.org/Brand) + + ## Contributors Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)): diff --git a/cypress/e2e/jsonld.spec.js b/cypress/e2e/jsonld.spec.js index f0f4ca7f..e93ab05b 100644 --- a/cypress/e2e/jsonld.spec.js +++ b/cypress/e2e/jsonld.spec.js @@ -1,7 +1,7 @@ import { assertSchema } from '@cypress/schema-tools'; import schemas from '../schemas'; -const expectedJSONResults = 23; +const expectedJSONResults = 26; const articleLdJsonIndex = 0; const breadcrumbLdJsonIndex = 1; @@ -26,6 +26,9 @@ const softwareAppJsonIndex = 19; const collectionPageLdJsonIndex = 20; const profilePageLdJsonIndex = 21; const videoGameLdJsonIndex = 22; +const organizationLdJsonIndex = 23; +const brandLdJsonIndex = 24; +const webPageLdJsonIndex = 25; describe('Validates JSON-LD For:', () => { it('Article', () => { @@ -1845,4 +1848,111 @@ describe('Validates JSON-LD For:', () => { }); }); }); + + it('Organization', () => { + cy.visit('http://localhost:3000/jsonld'); + cy.get('head script[type="application/ld+json"]') + .should('have.length', expectedJSONResults) + .then(tags => { + const jsonLD = JSON.parse(tags[organizationLdJsonIndex].innerHTML); + assertSchema(schemas)('Organization', '1.0.0')(jsonLD); + }); + }); + + it('Organization Matches', () => { + cy.visit('http://localhost:3000/jsonld'); + cy.get('head script[type="application/ld+json"]') + .should('have.length', expectedJSONResults) + .then(tags => { + const jsonLD = JSON.parse(tags[organizationLdJsonIndex].innerHTML); + expect(jsonLD).to.deep.equal({ + '@context': 'https://schema.org', + '@id': 'https://www.purpule-fox.io/#corporation', + '@type': 'Corporation', + name: 'Purple Fox', + legalName: 'Purple Fox LLC', + logo: 'https://www.example.com/photos/logo.jpg', + url: 'https://www.purpule-fox.io/', + address: { + '@type': 'PostalAddress', + streetAddress: '1600 Saratoga Ave', + addressLocality: 'San Jose', + addressRegion: 'CA', + postalCode: '95129', + addressCountry: 'US', + }, + contactPoints: [ + { + '@type': 'ContactPoint', + contactType: 'customer service', + telephone: '+1-877-746-0909', + areaServed: 'US', + availableLanguage: ['English', 'Spanish', 'French'], + contactOption: 'TollFree', + }, + ], + sameAs: ['https://www.orange-fox.com'], + }); + }); + }); + it('Brand', () => { + cy.visit('http://localhost:3000/jsonld'); + cy.get('head script[type="application/ld+json"]') + .should('have.length', expectedJSONResults) + .then(tags => { + const jsonLD = JSON.parse(tags[brandLdJsonIndex].innerHTML); + assertSchema(schemas)('Brand', '1.0.0')(jsonLD); + }); + }); + + it('Brand Matches', () => { + cy.visit('http://localhost:3000/jsonld'); + cy.get('head script[type="application/ld+json"]') + .should('have.length', expectedJSONResults) + .then(tags => { + const jsonLD = JSON.parse(tags[brandLdJsonIndex].innerHTML); + expect(jsonLD).to.deep.equal({ + '@context': 'https://schema.org', + '@type': 'Brand', + '@id': 'https://www.purpule-fox.io/#brand', + logo: 'https://www.example.com/photos/logo.jpg', + slogan: 'What does the fox say?', + aggregateRating: { + '@type': 'AggregateRating', + ratingValue: '4.4', + reviewCount: '89', + }, + }); + }); + }); + + it('WebPage', () => { + cy.visit('http://localhost:3000/jsonld'); + cy.get('head script[type="application/ld+json"]') + .should('have.length', expectedJSONResults) + .then(tags => { + const jsonLD = JSON.parse(tags[webPageLdJsonIndex].innerHTML); + assertSchema(schemas)('WebPage', '1.0.0')(jsonLD); + }); + }); + + it('WebPage Matches', () => { + cy.visit('http://localhost:3000/jsonld'); + cy.get('head script[type="application/ld+json"]') + .should('have.length', expectedJSONResults) + .then(tags => { + const jsonLD = JSON.parse(tags[webPageLdJsonIndex].innerHTML); + expect(jsonLD).to.deep.equal({ + '@context': 'https://schema.org', + '@type': 'WebPage', + '@id': 'https://www.purpule-fox.io/#info', + description: 'This is a description.', + lastReviewed: '2021-05-26T05:59:02.085Z', + reviewedBy: { + '@type': 'Organization', + name: 'Garmeeh', + }, + }); + }); + }); }); diff --git a/cypress/schemas/brand-schema.js b/cypress/schemas/brand-schema.js new file mode 100644 index 00000000..0c55697a --- /dev/null +++ b/cypress/schemas/brand-schema.js @@ -0,0 +1,53 @@ +import { versionSchemas } from '@cypress/schema-tools'; + +import { aggregateRating100 } from './common'; + +const brand100 = { + version: { + major: 1, + minor: 0, + patch: 0, + }, + schema: { + type: 'object', + title: 'Brand', + description: 'Bramd description with slogan and some characteristics.', + properties: { + '@context': { + type: 'string', + description: 'Schema.org context', + }, + '@type': { + type: 'string', + description: 'JSON-LD type: PostalAddress', + }, + '@id': { + type: 'string', + description: 'URL to main entity of page', + }, + logo: { + type: 'string', + description: "Url of the Organization's logo", + }, + slogan: { + type: 'string', + description: 'Slogan of the brand', + }, + aggregateRating: { + ...aggregateRating100.schema, + see: aggregateRating100, + }, + }, + }, + example: { + '@context': 'https://schema.org', + '@type': 'Brand', + '@id': 'https://www.purpule-fox.io/#corporation', + logo: 'https://www.example.com/photos/logo.jpg', + slogan: 'What does the fox say?', + aggregateRating: aggregateRating100.example, + }, +}; + +const brandVersions = versionSchemas(brand100); +export default brandVersions; diff --git a/cypress/schemas/contactPoint.js b/cypress/schemas/contactPoint.js new file mode 100644 index 00000000..c91fa5fb --- /dev/null +++ b/cypress/schemas/contactPoint.js @@ -0,0 +1,52 @@ +import { versionSchemas } from '@cypress/schema-tools'; + +const contactPoint100 = { + version: { + major: 1, + minor: 0, + patch: 0, + }, + schema: { + type: 'object', + description: 'Corporate Contact - ContactPoint', + properties: { + '@type': { + type: 'string', + description: 'ContactPoint', + }, + telephone: { + type: 'string', + description: 'Telephone number of the company', + }, + contactType: { + type: 'string', + description: 'The main usage of the phone number', + }, + areaServed: { + type: ['string', 'array'], + description: 'Geographical region served', + }, + availableLanguage: { + type: ['string', 'array'], + description: 'Language spoken', + }, + contactOption: { + type: 'string', + description: 'Details about the number', + }, + }, + required: ['@type', 'telephone', 'contactType'], + additionalProperties: false, + }, + example: { + '@type': 'ContactPoint', + contactType: 'customer service', + telephone: '+1-877-746-0909', + areaServed: 'US', + availableLanguage: ['English', 'Spanish', 'French'], + contactOption: 'TollFree', + }, +}; + +const contactPoint = versionSchemas(contactPoint100); +export default contactPoint; diff --git a/cypress/schemas/corporate-contact-schema.js b/cypress/schemas/corporate-contact-schema.js index 0c5e5b44..cc7c2b97 100644 --- a/cypress/schemas/corporate-contact-schema.js +++ b/cypress/schemas/corporate-contact-schema.js @@ -1,52 +1,5 @@ import { versionSchemas } from '@cypress/schema-tools'; - -const contactPoint100 = { - version: { - major: 1, - minor: 0, - patch: 0, - }, - schema: { - type: 'object', - description: 'Corporate Contact - ContactPoint', - properties: { - '@type': { - type: 'string', - description: 'ContactPoint', - }, - telephone: { - type: 'string', - description: 'Telephone number of the company', - }, - contactType: { - type: 'string', - description: 'The main usage of the phone number', - }, - areaServed: { - type: ['string', 'array'], - description: 'Geographical region served', - }, - availableLanguage: { - type: ['string', 'array'], - description: 'Language spoken', - }, - contactOption: { - type: 'string', - description: 'Details about the number', - }, - }, - required: ['@type', 'telephone', 'contactType'], - additionalProperties: false, - }, - example: { - '@type': 'ContactPoint', - contactType: 'customer service', - telephone: '+1-877-746-0909', - areaServed: 'US', - availableLanguage: ['English', 'Spanish', 'French'], - contactOption: 'TollFree', - }, -}; +import contactPoint100 from './contactPoint'; const corporateContact100 = { version: { diff --git a/cypress/schemas/index.js b/cypress/schemas/index.js index e29060b5..6250d244 100644 --- a/cypress/schemas/index.js +++ b/cypress/schemas/index.js @@ -21,6 +21,9 @@ import softwareAppVersions from './software-app-schema'; import collectionPageVersions from './collection-page-schema'; import profilePageVersions from './profile-page-schema'; import videoGameVersions from './videogame-schema'; +import organizationVersions from './organization-schema'; +import brandVersions from './brand-schema'; +import webPageVersions from './web-page-schema'; const schemas = combineSchemas( articleVersions, @@ -44,5 +47,8 @@ const schemas = combineSchemas( collectionPageVersions, profilePageVersions, videoGameVersions, + organizationVersions, + brandVersions, + webPageVersions, ); export default schemas; diff --git a/cypress/schemas/organization-schema.js b/cypress/schemas/organization-schema.js new file mode 100644 index 00000000..d4ad5d45 --- /dev/null +++ b/cypress/schemas/organization-schema.js @@ -0,0 +1,83 @@ +import { versionSchemas } from '@cypress/schema-tools'; +import address100 from './address'; +import contactPoint100 from './contactPoint'; + +const organization100 = { + version: { + major: 1, + minor: 0, + patch: 0, + }, + schema: { + type: 'object', + title: 'Organization', + description: 'An example schema describing JSON-LD for type: Organization', + properties: { + '@context': { + type: 'string', + description: 'Schema.org context', + }, + '@type': { + type: 'string', + description: 'Organization and the subtypes', + }, + '@id': { + type: 'string', + description: 'URL to main entity of page', + }, + logo: { + type: 'string', + description: "Url of the Organization's logo", + }, + legalName: { + type: 'string', + description: 'Legal name of the organization, e.g Purple Fox SA or LLC', + }, + name: { + type: 'string', + description: 'Name of the organization, e.g Purple Fox', + }, + address: { + ...address100.schema, + see: address100, + }, + sameAs: { + type: 'array', + items: { + type: 'string', + }, + description: + "Array of Organization's URL's, usually social urls like instagram, facebook etc.", + }, + contactPoints: { + type: 'array', + items: { + ...contactPoint100.schema, + see: contactPoint100, + }, + description: 'Array with contact points objects.', + }, + url: { + type: 'string', + description: 'URL to main entity of page', + }, + }, + required: ['name', 'url'], + additionalProperties: false, + }, + example: { + '@context': 'https://schema.org', + '@type': 'Corporation', + '@id': 'https://www.purpule-fox.io/#corporation', + logo: 'https://www.example.com/photos/logo.jpg', + legalName: 'Purple Fox LLC', + name: 'Purple Fox', + address: address100.example, + contactPoints: [contactPoint100.example], + sameAs: ['https://www.orange-fox.com'], + url: 'https://www.purpule-fox.io/', + }, +}; + +const organizationVersions = versionSchemas(organization100); +export default organizationVersions; diff --git a/cypress/schemas/web-page-schema.js b/cypress/schemas/web-page-schema.js new file mode 100644 index 00000000..c4d932cc --- /dev/null +++ b/cypress/schemas/web-page-schema.js @@ -0,0 +1,63 @@ +import { versionSchemas } from '@cypress/schema-tools'; + +const webPage100 = { + version: { + major: 1, + minor: 0, + patch: 0, + }, + schema: { + type: 'object', + title: 'WebPage', + description: 'WebPage description with slogan and some characteristics.', + properties: { + '@context': { + type: 'string', + description: 'Schema.org context', + }, + '@type': { + type: 'string', + description: 'JSON-LD type: PostalAddress', + }, + '@id': { + type: 'string', + description: 'URL to main entity of page', + }, + description: { + type: 'string', + description: 'Main description of the web page', + }, + lastReviewed: { + type: 'string', + description: + 'Date on which the content on this web page was last reviewed for accuracy and/or completeness.', + }, + reviewedBy: { + type: 'object', + properties: { + '@type': { + type: 'string', + description: 'A person or organization can review the page.', + }, + name: { + type: 'string', + description: 'Name of the person or organization.', + }, + }, + }, + }, + }, + example: { + '@context': 'https://schema.org', + '@type': 'WebPage', + '@id': 'https://www.purpule-fox.io/#corporation', + logo: 'https://www.example.com/photos/logo.jpg', + lastReviewed: '2021-05-26T05:59:02.085Z', + reviewedBy: { + name: 'Garmeeh', + }, + }, +}; + +const webPageVersions = versionSchemas(webPage100); +export default webPageVersions; diff --git a/e2e/pages/jsonld.tsx b/e2e/pages/jsonld.tsx index 941f02a9..5c2698bc 100644 --- a/e2e/pages/jsonld.tsx +++ b/e2e/pages/jsonld.tsx @@ -21,6 +21,9 @@ import { ProfilePageJsonLd, CollectionPageJsonLd, VideoGameJsonLd, + OrganizationJsonLd, + BrandJsonLd, + WebPageJsonLd, } from '../..'; import Links from '../components/links'; @@ -717,6 +720,49 @@ const JsonLD = () => ( ]} /> + + + + ); diff --git a/src/index.tsx b/src/index.tsx index 4ab96e7e..a84d01df 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -59,5 +59,11 @@ export { default as ProfilePageJsonLd, ProfilePageJsonLdProps, } from './jsonld/profilePage'; +export { + default as OrganizationJsonLd, + OrganizationJsonLdProps, +} from './jsonld/organization'; +export { default as WebPageJsonLd, WebPageJsonLdProps } from './jsonld/webPage'; +export { default as BrandJsonLd, BrandJsonLdProps } from './jsonld/brand'; export { DefaultSeoProps, NextSeoProps } from './types'; diff --git a/src/jsonld/brand.tsx b/src/jsonld/brand.tsx new file mode 100644 index 00000000..1914e0ab --- /dev/null +++ b/src/jsonld/brand.tsx @@ -0,0 +1,42 @@ +import React, { FC } from 'react'; +import Head from 'next/head'; + +import markup from '../utils/markup'; + +import { AggregateRating } from '../types'; +import { buildAggregateRating } from '../utils/buildAggregateRating'; + +export interface BrandJsonLdProps { + id: string; + slogan?: string; + logo?: string; + aggregateRating?: AggregateRating; +} + +const BrandJsonLd: FC = ({ + id, + slogan, + logo, + aggregateRating, +}) => { + const jslonld = `{ + "@context": "https://schema.org", + "@type": "Brand", + ${aggregateRating ? buildAggregateRating(aggregateRating) : ''} + ${slogan ? `"slogan": "${slogan}",` : ''} + ${logo ? `"logo": "${logo}",` : ''} + "@id": "${id}" + }`; + + return ( + +