diff --git a/README.md b/README.md index dcec3213..469e8984 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ If you are using **`pages`** directory then `NextSeo` is **exactly what you need - [Product](#product) - [Social Profile](#social-profile) - [News Article](#news-article) + - [Park](#park) - [Video](#video-1) - [VideoGame](#videogame) - [Event](#event) @@ -1102,6 +1103,7 @@ Below you will find a very basic page implementing each of the available JSON-LD - [Product](#product) - [Social Profile](#social-profile) - [News Article](#news-article) +- [Park](#park) Pull request very welcome to add any from the list [found on here](https://developers.google.com/search/docs/data-types/article) @@ -2241,6 +2243,91 @@ const Page = () => ( export default Page; ``` +### Park + +```jsx +import { ParkJsonLd } from 'next-seo'; + +const Page = () => ( + <> +

Park JSON-LD

+ + +); + +export default Page; +``` + +**Required properties** + +| Property | Info | +| ------------------------- | ------------------------------------------------------------- | +| `@id` | Globally unique ID of the specific park in the form of a URL. | +| `address` | Address of the specific park 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. | +| `name` | Park name. | +| `description` | Park description. | + +**Supported properties** + +| Property | Info | +| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `geo` | Geographic coordinates of the park. | +| `geo.latitude` | The latitude of the park location | +| `geo.longitude` | The longitude of the park location | +| `images` | An image or images of the park. Required for valid markup depending on the type | +| `telephone` | A business phone number meant to be the primary contact method for customers. | +| `url` | The fully-qualified URL of the specific park. | +| `openingHours` | Opening hour specification of the park. You can provide this as a single object, or an array of objects with the properties below. | +| `openingHours.opens` | The opening hour of the place or service on the given day(s) of the week. | +| `openingHours.closes` | The closing hour of the place or service on the given day(s) of the week. | +| `openingHours.dayOfWeek` | The day of the week for which these opening hours are valid. Can be a string or array of strings. Refer to [DayOfWeek](https://schema.org/DayOfWeek) | +| `openingHours.validFrom` | The date when the item becomes valid. | +| `openingHours.validThrough` | The date after when the item is not valid. | +| `isAccessibleForFree` | Whether or not the park is accessible for free. | + +**Other** +| `useAppDir` | This should be set to true if using new app directory. Not required if outside of app directory. | + [Google Docs for Social Profile](https://developers.google.com/search/docs/data-types/social-profile) ### Video diff --git a/cypress/e2e/park.spec.js b/cypress/e2e/park.spec.js new file mode 100644 index 00000000..64538adb --- /dev/null +++ b/cypress/e2e/park.spec.js @@ -0,0 +1,61 @@ +import { assertSchema } from '@cypress/schema-tools'; +import schemas from '../schemas'; + +describe('Park JSON-LD', () => { + it('matches schema', () => { + cy.visit('http://localhost:3000/jsonld/park'); + cy.get('head script[type="application/ld+json"]').then(tags => { + const jsonLD = JSON.parse(tags[0].innerHTML); + console.log('jsonLD', jsonLD); + assertSchema(schemas)('Park', '1.0.0')(jsonLD); + }); + }); + + it('renders with all props', () => { + cy.visit('http://localhost:3000/jsonld/park'); + cy.get('head script[type="application/ld+json"]').then(tags => { + const jsonLD = JSON.parse(tags[0].innerHTML); + expect(jsonLD).to.deep.equal({ + '@context': 'https://schema.org', + '@type': 'Park', + '@id': 'https://www.example.com/park/minnewaska-state-park', + name: 'Minnewaska State Park', + description: 'Description about Minnewaska State Park', + url: 'https://www.example.com/park', + telephone: '+18452550752', + image: ['https://example.com/photos/1x1/photo.jpg'], + address: { + '@type': 'PostalAddress', + streetAddress: '5281 Route 44-55', + addressLocality: 'Kerhonkson', + addressRegion: 'NY', + postalCode: '12446', + addressCountry: 'US', + }, + geo: { + latitude: '41.735149', + longitude: '-74.239037', + '@type': 'GeoCoordinates', + }, + openingHoursSpecification: [ + { + opens: '09:00', + closes: '18:00', + dayOfWeek: [ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday', + ], + validFrom: '2019-12-23', + validThrough: '2020-04-02', + '@type': 'OpeningHoursSpecification', + }, + ], + }); + }); + }); +}); diff --git a/cypress/schemas/index.js b/cypress/schemas/index.js index 731dd620..c4892c35 100644 --- a/cypress/schemas/index.js +++ b/cypress/schemas/index.js @@ -28,6 +28,8 @@ import videoVersions from './video-schema'; import howToVersions from './how-to-schema'; import imageVersions from './image-schema'; import campgroundVersions from './campground-schema'; +import parkVersions from './park-schema'; + const schemas = combineSchemas( articleVersions, @@ -58,5 +60,6 @@ const schemas = combineSchemas( howToVersions, imageVersions, campgroundVersions, + parkVersions, ); export default schemas; diff --git a/cypress/schemas/park-schema.js b/cypress/schemas/park-schema.js new file mode 100644 index 00000000..233a474a --- /dev/null +++ b/cypress/schemas/park-schema.js @@ -0,0 +1,164 @@ +import { versionSchemas } from '@cypress/schema-tools'; + +import address100 from './address'; + +const park100 = { + version: { + major: 1, + minor: 0, + patch: 0, + }, + schema: { + type: 'object', + title: 'Park', + description: 'An example schema describing JSON-LD for type: Park', + properties: { + '@context': { + type: 'string', + description: 'Schema.org context', + }, + '@type': { + type: 'string', + description: + 'Any more specific type supported by Park https://schema.org/Park', + }, + '@id': { + type: 'string', + description: + 'Globally unique ID of the specific park in the form of a URL. The ID should be stable and unchanging over time. Google Search treats the URL as an opaque string and it does not have to be a working link.', + }, + name: { + type: 'string', + description: 'The name of the park.', + }, + description: { + type: 'string', + description: 'Description for the park.', + }, + url: { + type: 'string', + description: + 'The fully-qualified URL of the specific park location. Unlike the @id property, this URL property should be a working link.', + }, + telephone: { + type: 'string', + description: + 'A park phone number meant to be the primary contact method for customers. Be sure to include the country code and area code in the phone number.', + }, + image: { + type: 'array', + items: { + type: 'string', + }, + description: "Array of image URL's", + }, + address: { + ...address100.schema, + see: address100, + }, + geo: { + type: 'object', + description: "Array of social profile URL's", + properties: { + '@type': { + type: 'string', + description: 'JSON-LD type: GeoCoordinates', + }, + latitude: { + type: 'string', + description: 'The latitude of the park location.', + }, + longitude: { + type: 'string', + description: 'The longitude of the park location.', + }, + }, + }, + openingHoursSpecification: { + type: 'array', + description: 'Opening hour specification for the park', + item: { + type: 'object', + properties: { + '@type': { + type: 'string', + description: 'JSON-LD type: OpeningHoursSpecification', + }, + opens: { + type: 'string', + description: + 'The opening hour of the place or service on the given day(s) of the week.', + }, + closes: { + type: 'string', + description: + 'The closing hour of the place or service on the given day(s) of the week.', + }, + dayOfWeek: { + type: 'array', + items: { + type: 'string', + }, + description: + 'The day of the week for which these opening hours are valid.', + }, + validFrom: { + type: 'string', + description: 'The date when the item becomes valid.', + }, + validThrough: { + type: 'string', + description: 'The date after when the item is not valid.', + }, + }, + }, + }, + }, + required: true, + additionalProperties: false, + }, + example: { + '@context': 'https://schema.org', + '@type': 'Park', + '@id': 'https://www.example.com/park/minnewaska-state-park', + name: 'Minnewaska State Park', + description: 'Description about Minnewaska State Park', + url: 'https://www.example.com/park', + telephone: '+18452550752', + image: ['https://example.com/photos/1x1/photo.jpg'], + address: { + '@type': 'PostalAddress', + streetAddress: '5281 Route 44-55', + addressLocality: 'Kerhonkson', + addressRegion: 'NY', + postalCode: '12446', + addressCountry: 'US', + }, + geo: { + latitude: '41.735149', + longitude: '-74.239037', + '@type': 'GeoCoordinates', + }, + openingHoursSpecification: [ + { + opens: '09:00', + closes: '18:00', + dayOfWeek: [ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday', + ], + validFrom: '2019-12-23', + validThrough: '2020-04-02', + '@type': 'OpeningHoursSpecification', + }, + ], + }, +}; + +const park = versionSchemas(park100); +export default park; diff --git a/e2e/pages/jsonld/index.tsx b/e2e/pages/jsonld/index.tsx index 9f1e46cf..4665310d 100644 --- a/e2e/pages/jsonld/index.tsx +++ b/e2e/pages/jsonld/index.tsx @@ -19,6 +19,7 @@ const allJsonLDPages = [ 'logo', 'newsarticle', 'organization', + 'park', 'product', 'profilePage', 'qaPage', diff --git a/e2e/pages/jsonld/park.tsx b/e2e/pages/jsonld/park.tsx new file mode 100644 index 00000000..d6a99fed --- /dev/null +++ b/e2e/pages/jsonld/park.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { ParkJsonLd } from '../../..'; + +function Park() { + return ( + <> +

Park

+ + + ); +} + +export default Park; diff --git a/src/index.tsx b/src/index.tsx index 2b53b27a..e5687e39 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -71,5 +71,5 @@ export { default as CampgroundJsonLd, CampgroundJsonLdProps, } from './jsonld/campground'; - +export { default as ParkJsonLd, ParkJsonLdProps } from './jsonld/park'; export { DefaultSeoProps, NextSeoProps } from './types'; diff --git a/src/jsonld/park.tsx b/src/jsonld/park.tsx new file mode 100644 index 00000000..58e0fb7f --- /dev/null +++ b/src/jsonld/park.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { JsonLd, JsonLdProps } from './jsonld'; +import { Address, Geo, OpeningHoursSpecification } from 'src/types'; +import { setGeo } from 'src/utils/schema/setGeo'; +import { setAddress } from 'src/utils/schema/setAddress'; +import { setOpeningHours } from 'src/utils/schema/setOpeningHours'; + +export interface ParkJsonLdProps extends JsonLdProps { + address: Address | Address[]; + description: string; + geo?: Geo; + images?: string[]; + isAccessibleForFree?: boolean; + name: string; + openingHours?: OpeningHoursSpecification | OpeningHoursSpecification[]; + telephone?: string; + type?: string; + url?: string; +} + +function ParkJsonLd({ + address, + geo, + images, + keyOverride, + openingHours, + type = 'Park', + ...rest +}: ParkJsonLdProps) { + const data = { + image: images, + openingHoursSpecification: Array.isArray(openingHours) + ? openingHours.map(setOpeningHours) + : setOpeningHours(openingHours), + address: setAddress(address), + geo: setGeo(geo), + ...rest, + }; + return ( + + ); +} + +export default ParkJsonLd;