Repository: garmeeh/next-seo
Pull request #1292 (2023-10-19 15:30:41)

feature: add CampgroundJsonLd component
## Description of Change(s):
Adding [Campground](https://schema.org/Campground) schema

---

To help get PR's merged faster, the following is required:

[X] Updated the documentation
[X] Unit/Cypress test covering all cases

Please link to relevant Google Docs or schema.org docs for what you are adding so we can review.
https://schema.org/Campground

Please have a read of the Contributing Guide for full details.

https://github.com/garmeeh/next-seo/blob/master/CONTRIBUTING.md



Patch:
diff --git a/README.md b/README.md
index 5f2c1de..ccd2a8e 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,7 @@ If you are using **`pages`** directory then `NextSeo` is **exactly what you need
   - [Article](#article-1)
   - [Breadcrumb](#breadcrumb)
   - [Blog](#blog)
+  - [Campground](#campground)
   - [Recipe](#recipe)
   - [Sitelinks Search Box](#sitelinks-search-box)
   - [Course](#course)
@@ -1089,6 +1090,7 @@ Below you will find a very basic page implementing each of the available JSON-LD
 - [Article](#article-1)
 - [Breadcrumb](#breadcrumb)
 - [Blog](#blog)
+- [Campground](#campground)
 - [Recipe](#recipe)
 - [Sitelinks Search Box](#sitelinks-search-box)
 - [Course](#course)
@@ -1243,6 +1245,109 @@ const Page = () => (
 export default Page;
 ```
 
+### Campground
+
+```jsx
+import { CampgroundJsonLd } from 'next-seo';
+
+const Page = () => (
+  <>
+    <h1>Campground JSON-LD</h1>
+    <CampgroundJsonLd
+      id="https://www.example.com/campground/rip-van-winkle-campground"
+      name="Rip Van Winkle Campgrounds"
+      url="https://www.example.com/campground"
+      telephone="+18452468114"
+      images={['https://example.com/photos/1x1/photo.jpg']}
+      address={{
+        streetAddress: '149 Blue Mountain Rd',
+        addressLocality: 'Saugerties',
+        addressRegion: 'NY',
+        postalCode: '12477',
+        addressCountry: 'US',
+      }}
+      description="Description about Rip Van Winkle Campgrounds"
+      geo={{
+        latitude: '42.092599',
+        longitude: '-74.018580',
+      }}
+      openingHours={[
+        {
+          opens: '09:00',
+          closes: '17:00',
+          dayOfWeek: [
+            'Monday',
+            'Tuesday',
+            'Wednesday',
+            'Thursday',
+            'Friday',
+            'Saturday',
+            'Sunday',
+          ],
+          validFrom: '2019-12-23',
+          validThrough: '2020-04-02',
+        },
+      ]}
+      petsAllowed
+      rating={{
+        ratingValue: '5',
+        ratingCount: '18',
+      }}
+      amenityFeature={{
+        name: 'Showers',
+        value: true,
+      }}
+      priceRange="$$"
+    />
+  </>
+);
+
+export default Page;
+```
+
+**Required properties**
+
+| Property                  | Info                                                                |
+| ------------------------- | ------------------------------------------------------------------- |
+| `@id`                     | Globally unique ID of the specific campground in the form of a URL. |
+| `address`                 | Address of the specific campground 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`                    | Campground name.                                                    |
+| `description`             | Campground description.                                             |
+
+**Supported properties**
+
+| Property                    | Info                                                                                                                                                 |
+| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `geo`                       | Geographic coordinates of the campground.                                                                                                            |
+| `geo.latitude`              | The latitude of the campground location                                                                                                              |
+| `geo.longitude`             | The longitude of the campground location                                                                                                             |
+| `images`                    | An image or images of the campground. Required for valid markup depending on the type                                                                |
+| `telephone`                 | A campground phone number meant to be the primary contact method for customers.                                                                      |
+| `url`                       | The fully-qualified URL of the specific campground.                                                                                                  |
+| `openingHours`              | Opening hour specification of the campground. 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 campground is accessible for free.                                                                                                |
+| `petsAllowed`               | Whether or not the campgroud allows pets.                                                                                                            |
+| `amenityFeature`            | An amenity feature (e.g. a characteristic or service) of the campground.                                                                             |
+| `amenityFeature.name`       | The name of the amenity.                                                                                                                             |
+| `amenityFeature.value`      | The value of the amenity.                                                                                                                            |
+| `priceRange`                | The price range of the campground, for example $$$.                                                                                                  |
+| `rating`                    | The average rating of the campground based on multiple ratings or reviews.                                                                           |
+| `rating.ratingValue`        | The rating for the content.                                                                                                                          |
+| `rating.ratingCount`        | The count of total number of ratings.                                                                                                                |
+
+**Other**
+| `useAppDir` | This should be set to true if using new app directory. Not required if outside of app directory. |
+
 ### Recipe
 
 ```jsx
@@ -3114,6 +3219,7 @@ export default () => (
       aggregateRating={{ ratingValue: '4.6', reviewCount: '8864' }}
       operatingSystem="ANDROID"
       applicationCategory="GameApplication"
+      keywords="angrybirds, arcade, slingshot"
     />
   </>
 );
@@ -3139,7 +3245,13 @@ export default () => (
 | `operatingSystem`     | The operating System supported by the game it self. |
 | `applicationCategory` | Desktop Software or Video Game...                   |
 
-For reference and more info check [Google docs for Software App](https://developers.google.com/search/docs/data-types/software-app)
+**Data other properties**
+
+| Property   | Info                                                                                                                   |
+| ---------- | ---------------------------------------------------------------------------------------------------------------------- |
+| `keywords` | Keywords or tags used to describe this content. Multiple entries in a keywords list are typically delimited by commas. |
+
+For reference and more info check [Google docs for Software App](https://developers.google.com/search/docs/data-types/software-app) and [Schema.org docs](https://schema.org/SoftwareApplication)
 
 ### Organization
 
diff --git a/cypress/schemas/campground-schema.js b/cypress/schemas/campground-schema.js
new file mode 100644
index 0000000..3068a72
--- /dev/null
+++ b/cypress/schemas/campground-schema.js
@@ -0,0 +1,236 @@
+import { versionSchemas } from '@cypress/schema-tools';
+
+import address100 from './address';
+
+const campground100 = {
+  version: {
+    major: 1,
+    minor: 0,
+    patch: 0,
+  },
+  schema: {
+    type: 'object',
+    title: 'Campground',
+    description: 'An example schema describing JSON-LD for type: Campground',
+    properties: {
+      '@context': {
+        type: 'string',
+        description: 'Schema.org context',
+      },
+      '@type': {
+        type: 'string',
+        description:
+          'Any more specific type supported by Campground https://schema.org/Campground',
+      },
+      '@id': {
+        type: 'string',
+        description:
+          'Globally unique ID of the specific campground 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 campground.',
+      },
+      description: {
+        type: 'string',
+        description: 'Description for the campground.',
+      },
+      url: {
+        type: 'string',
+        description:
+          'The fully-qualified URL of the specific campground location. Unlike the @id property, this URL property should be a working link.',
+      },
+      telephone: {
+        type: 'string',
+        description:
+          'A campground 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: 'An object with latitude and longitude',
+        properties: {
+          '@type': {
+            type: 'string',
+            description: 'JSON-LD type: GeoCoordinates',
+          },
+          latitude: {
+            type: 'string',
+            description: 'The latitude of the campground location.',
+          },
+          longitude: {
+            type: 'string',
+            description: 'The longitude of the campground location.',
+          },
+        },
+      },
+      openingHoursSpecification: {
+        type: 'array',
+        description: 'Opening hour specification for the campground',
+        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.',
+            },
+          },
+        },
+      },
+      petsAllowed: {
+        type: 'boolean',
+        description:
+          'Indicates whether pets are allowed to enter the campground.',
+      },
+      aggregateRating: {
+        type: 'object',
+        properties: {
+          '@type': {
+            type: 'string',
+            description: 'JSON-LD type: AggregateRating.',
+          },
+          ratingValue: {
+            type: 'string',
+            description: 'Ratings/Reviews aggregate value of the campground.',
+          },
+          ratingCount: {
+            type: 'string',
+            description: 'Ratings/Reviews number of the campground.',
+          },
+        },
+        description: 'Aggregate rating of the campground.',
+      },
+      amenityFeature: {
+        type: ['array', 'object'],
+        item: {
+          type: 'object',
+          properties: {
+            '@type': {
+              type: 'string',
+              description: 'JSON-LD type: LocationFeatureSpecification',
+            },
+            name: {
+              type: 'string',
+              description: 'The name of the amenity.',
+            },
+            value: {
+              type: ['string', 'integer', 'boolean'],
+              description: 'The value of the amenity.',
+            },
+          },
+        },
+      },
+      priceRange: {
+        type: 'string',
+        description:
+          'The approximate price of the campground. You can specify as many currency symbols as there are digits in the price, or you can specify an actual range.',
+      },
+    },
+    required: true,
+    additionalProperties: false,
+  },
+  example: {
+    '@context': 'https://schema.org',
+    '@type': 'Campground',
+    '@id': 'https://www.example.com/campground/rip-van-winkle-campground',
+    name: 'Rip Van Winkle Campgrounds',
+    description: 'Description about Rip Van Winkle Campgrounds',
+    url: 'https://www.example.com/campground',
+    telephone: '+18452468114',
+    image: ['https://example.com/photos/1x1/photo.jpg'],
+    address: {
+      '@type': 'PostalAddress',
+      streetAddress: '149 Blue Mountain Rd',
+      addressLocality: 'Saugerties',
+      addressRegion: 'NY',
+      postalCode: '12477',
+      addressCountry: 'US',
+    },
+    geo: {
+      latitude: '42.092599',
+      longitude: '-74.018580',
+      '@type': 'GeoCoordinates',
+    },
+    openingHoursSpecification: [
+      {
+        opens: '09:00',
+        closes: '17:00',
+        dayOfWeek: [
+          'Monday',
+          'Tuesday',
+          'Wednesday',
+          'Thursday',
+          'Friday',
+          'Saturday',
+          'Sunday',
+        ],
+        validFrom: '2019-12-23',
+        validThrough: '2020-04-02',
+        '@type': 'OpeningHoursSpecification',
+      },
+    ],
+    petsAllowed: true,
+    aggregateRating: {
+      '@type': 'AggregateRating',
+      ratingValue: '5',
+      ratingCount: '18',
+    },
+    amenityFeature: [
+      {
+        '@type': 'LocationFeatureSpecification',
+        name: 'Showers',
+        value: true,
+      },
+      {
+        '@type': 'LocationFeatureSpecification',
+        name: 'RV Hookup',
+        value: false,
+      },
+      {
+        '@type': 'LocationFeatureSpecification',
+        name: 'Campfire',
+        value: true,
+      },
+    ],
+    priceRange: '$$',
+  },
+};
+
+const campground = versionSchemas(campground100);
+export default campground;
diff --git a/cypress/schemas/index.js b/cypress/schemas/index.js
index 0938655..c4892c3 100644
--- a/cypress/schemas/index.js
+++ b/cypress/schemas/index.js
@@ -27,8 +27,10 @@ import webPageVersions from './web-page-schema';
 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,
   breadCrumbVersions,
@@ -57,6 +59,7 @@ const schemas = combineSchemas(
   videoVersions,
   howToVersions,
   imageVersions,
+  campgroundVersions,
   parkVersions,
 );
 export default schemas;
diff --git a/cypress/schemas/software-app-schema.js b/cypress/schemas/software-app-schema.js
index 66bbad1..d2cd71b 100644
--- a/cypress/schemas/software-app-schema.js
+++ b/cypress/schemas/software-app-schema.js
@@ -52,6 +52,9 @@ const softwareApp100 = {
           },
         },
       },
+      keywords: {
+        type: 'string',
+      },
     },
   },
   example: {
@@ -70,6 +73,7 @@ const softwareApp100 = {
       price: '1.00',
       priceCurrency: 'USD',
     },
+    keywords: 'angrybirds, arcade, slingshot',
   },
 };
 
diff --git a/src/index.tsx b/src/index.tsx
index b92f622..e5687e3 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -67,6 +67,9 @@ export {
 } from './jsonld/socialProfile';
 export { default as HowToJsonLd, HowToJsonLdProps } from './jsonld/howTo';
 export { default as ImageJsonLd, ImageJsonLdProps } from './jsonld/image';
+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/campground.tsx b/src/jsonld/campground.tsx
new file mode 100644
index 0000000..da57804
--- /dev/null
+++ b/src/jsonld/campground.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { JsonLd, JsonLdProps } from './jsonld';
+import {
+  Address,
+  AggregateRating,
+  AmenityFeature,
+  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';
+import { setAmenityFeature } from 'src/utils/schema/setAmenityFeature';
+import { setAggregateRating } from 'src/utils/schema/setAggregateRating';
+
+export interface CampgroundJsonLdProps extends JsonLdProps {
+  address: Address | Address[];
+  amenityFeature?: AmenityFeature | AmenityFeature[];
+  description: string;
+  geo?: Geo;
+  images?: string[];
+  isAccessibleForFree?: boolean;
+  name: string;
+  openingHours?: OpeningHoursSpecification | OpeningHoursSpecification[];
+  petsAllowed?: boolean;
+  priceRange?: string;
+  rating?: AggregateRating;
+  telephone?: string;
+  type?: string;
+  url?: string;
+}
+
+function CampgroundJsonLd({
+  address,
+  geo,
+  images,
+  keyOverride,
+  openingHours,
+  type = 'Campground',
+  amenityFeature,
+  rating,
+  ...rest
+}: CampgroundJsonLdProps) {
+  const data = {
+    image: images,
+    openingHoursSpecification: Array.isArray(openingHours)
+      ? openingHours.map(setOpeningHours)
+      : setOpeningHours(openingHours),
+    address: setAddress(address),
+    geo: setGeo(geo),
+    amenityFeature: setAmenityFeature(amenityFeature),
+    aggregateRating: setAggregateRating(rating),
+    ...rest,
+  };
+  return (
+    <JsonLd
+      type={type}
+      keyOverride={keyOverride}
+      {...data}
+      scriptKey="campground"
+    />
+  );
+}
+
+export default CampgroundJsonLd;
diff --git a/src/jsonld/softwareApp.tsx b/src/jsonld/softwareApp.tsx
index 28cc781..e0bfddc 100644
--- a/src/jsonld/softwareApp.tsx
+++ b/src/jsonld/softwareApp.tsx
@@ -13,6 +13,7 @@ export interface SoftwareAppJsonLdProps extends JsonLdProps {
   operatingSystem?: string;
   review?: Review;
   aggregateRating?: AggregateRating;
+  keywords?: string;
 }
 
 function SoftwareAppJsonLd({
@@ -22,6 +23,7 @@ function SoftwareAppJsonLd({
   price,
   aggregateRating,
   review,
+  keywords,
   ...rest
 }: SoftwareAppJsonLdProps) {
   const data = {
@@ -33,6 +35,7 @@ function SoftwareAppJsonLd({
     },
     aggregateRating: setAggregateRating(aggregateRating),
     review: setReviews(review),
+    keywords,
   };
   return (
     <JsonLd
diff --git a/src/types.ts b/src/types.ts
index c333741..90abbbc 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -400,6 +400,19 @@ export type ReviewedBy = {
   name: string;
 };
 
+export type AmenityFeature = {
+  name: string;
+  value: string | number | boolean;
+};
+
+export type LocationFeatureSpecification = 'LocationFeatureSpecification';
+
+export type DecoratedAmenityFeature = {
+  '@type': LocationFeatureSpecification;
+  name: string;
+  value: string | number | boolean;
+};
+
 export type ApplicationCategory =
   | 'Game'
   | 'SocialNetworking'
diff --git a/src/utils/schema/setAmenityFeature.ts b/src/utils/schema/setAmenityFeature.ts
new file mode 100644
index 0000000..6f60cf8
--- /dev/null
+++ b/src/utils/schema/setAmenityFeature.ts
@@ -0,0 +1,26 @@
+import { AmenityFeature, DecoratedAmenityFeature } from 'src/types';
+
+export function setAmenityFeature(
+  amenityFeature?: AmenityFeature | AmenityFeature[],
+) {
+  if (!amenityFeature) return undefined;
+
+  if (!Array.isArray(amenityFeature)) {
+    return decorateAmenityFeature(amenityFeature);
+  }
+
+  if (amenityFeature.length === 1) {
+    return decorateAmenityFeature(amenityFeature[0]);
+  }
+
+  return amenityFeature.map(decorateAmenityFeature);
+}
+
+function decorateAmenityFeature(
+  amenity: AmenityFeature,
+): DecoratedAmenityFeature {
+  return {
+    '@type': 'LocationFeatureSpecification',
+    ...amenity,
+  };
+}


Test patch:
diff --git a/cypress/e2e/campground.spec.js b/cypress/e2e/campground.spec.js
new file mode 100644
index 0000000..f28f354
--- /dev/null
+++ b/cypress/e2e/campground.spec.js
@@ -0,0 +1,145 @@
+import { assertSchema } from '@cypress/schema-tools';
+import schemas from '../schemas';
+
+describe('Campground JSON-LD', () => {
+  it('matches schema', () => {
+    cy.visit('http://localhost:3000/jsonld/campground');
+    cy.get('head script[type="application/ld+json"]').then(tags => {
+      const jsonLD = JSON.parse(tags[0].innerHTML);
+      console.log('jsonLD', jsonLD);
+      assertSchema(schemas)('Campground', '1.0.0')(jsonLD);
+    });
+  });
+
+  it('renders with one amenity feature', () => {
+    cy.visit('http://localhost:3000/jsonld/campground');
+    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': 'Campground',
+        '@id': 'https://www.example.com/campground/rip-van-winkle-campground',
+        name: 'Rip Van Winkle Campgrounds',
+        description: 'Description about Rip Van Winkle Campgrounds',
+        url: 'https://www.example.com/campground',
+        telephone: '+18452468114',
+        image: ['https://example.com/photos/1x1/photo.jpg'],
+        address: {
+          '@type': 'PostalAddress',
+          streetAddress: '149 Blue Mountain Rd',
+          addressLocality: 'Saugerties',
+          addressRegion: 'NY',
+          postalCode: '12477',
+          addressCountry: 'US',
+        },
+        geo: {
+          '@type': 'GeoCoordinates',
+          latitude: '42.092599',
+          longitude: '-74.018580',
+        },
+        openingHoursSpecification: [
+          {
+            opens: '09:00',
+            closes: '17:00',
+            dayOfWeek: [
+              'Monday',
+              'Tuesday',
+              'Wednesday',
+              'Thursday',
+              'Friday',
+              'Saturday',
+              'Sunday',
+            ],
+            validFrom: '2019-12-23',
+            validThrough: '2020-04-02',
+            '@type': 'OpeningHoursSpecification',
+          },
+        ],
+        petsAllowed: true,
+        aggregateRating: {
+          ratingValue: '5',
+          ratingCount: '18',
+          '@type': 'AggregateRating',
+        },
+        amenityFeature: {
+          '@type': 'LocationFeatureSpecification',
+          name: 'Showers',
+          value: true,
+        },
+        priceRange: '$$',
+      });
+    });
+  });
+
+  it('renders with multiple amenity features', () => {
+    cy.visit('http://localhost:3000/jsonld/campground/multipleAmenityFeatures');
+    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': 'Campground',
+        '@id': 'https://www.example.com/campground/rip-van-winkle-campground',
+        name: 'Rip Van Winkle Campgrounds',
+        description: 'Description about Rip Van Winkle Campgrounds',
+        url: 'https://www.example.com/campground',
+        telephone: '+18452468114',
+        image: ['https://example.com/photos/1x1/photo.jpg'],
+        address: {
+          '@type': 'PostalAddress',
+          streetAddress: '149 Blue Mountain Rd',
+          addressLocality: 'Saugerties',
+          addressRegion: 'NY',
+          postalCode: '12477',
+          addressCountry: 'US',
+        },
+        geo: {
+          '@type': 'GeoCoordinates',
+          latitude: '42.092599',
+          longitude: '-74.018580',
+        },
+        openingHoursSpecification: [
+          {
+            opens: '09:00',
+            closes: '17:00',
+            dayOfWeek: [
+              'Monday',
+              'Tuesday',
+              'Wednesday',
+              'Thursday',
+              'Friday',
+              'Saturday',
+              'Sunday',
+            ],
+            validFrom: '2019-12-23',
+            validThrough: '2020-04-02',
+            '@type': 'OpeningHoursSpecification',
+          },
+        ],
+        petsAllowed: true,
+        aggregateRating: {
+          ratingValue: '5',
+          ratingCount: '18',
+          '@type': 'AggregateRating',
+        },
+        amenityFeature: [
+          {
+            '@type': 'LocationFeatureSpecification',
+            name: 'Showers',
+            value: true,
+          },
+          {
+            '@type': 'LocationFeatureSpecification',
+            name: 'RV Hookup',
+            value: false,
+          },
+          {
+            '@type': 'LocationFeatureSpecification',
+            name: 'Campfire',
+            value: true,
+          },
+        ],
+        priceRange: '$$',
+      });
+    });
+  });
+});
diff --git a/cypress/e2e/softwareApp.spec.js b/cypress/e2e/softwareApp.spec.js
index d03e591..f05e120 100644
--- a/cypress/e2e/softwareApp.spec.js
+++ b/cypress/e2e/softwareApp.spec.js
@@ -30,6 +30,7 @@ describe('SoftwareApp JSON-LD', () => {
           price: '1.00',
           priceCurrency: 'USD',
         },
+        keywords: 'angrybirds, arcade, slingshot',
       });
     });
   });
diff --git a/e2e/pages/jsonld/campground/index.tsx b/e2e/pages/jsonld/campground/index.tsx
new file mode 100644
index 0000000..8401b7a
--- /dev/null
+++ b/e2e/pages/jsonld/campground/index.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import { CampgroundJsonLd } from '../../../..';
+
+function Campground() {
+  return (
+    <>
+      <h1>Campground</h1>
+      <CampgroundJsonLd
+        id="https://www.example.com/campground/rip-van-winkle-campground"
+        name="Rip Van Winkle Campgrounds"
+        url="https://www.example.com/campground"
+        telephone="+18452468114"
+        images={['https://example.com/photos/1x1/photo.jpg']}
+        address={{
+          streetAddress: '149 Blue Mountain Rd',
+          addressLocality: 'Saugerties',
+          addressRegion: 'NY',
+          postalCode: '12477',
+          addressCountry: 'US',
+        }}
+        description="Description about Rip Van Winkle Campgrounds"
+        geo={{
+          latitude: '42.092599',
+          longitude: '-74.018580',
+        }}
+        openingHours={[
+          {
+            opens: '09:00',
+            closes: '17:00',
+            dayOfWeek: [
+              'Monday',
+              'Tuesday',
+              'Wednesday',
+              'Thursday',
+              'Friday',
+              'Saturday',
+              'Sunday',
+            ],
+            validFrom: '2019-12-23',
+            validThrough: '2020-04-02',
+          },
+        ]}
+        petsAllowed
+        rating={{
+          ratingValue: '5',
+          ratingCount: '18',
+        }}
+        amenityFeature={{
+          name: 'Showers',
+          value: true,
+        }}
+        priceRange="$$"
+      />
+    </>
+  );
+}
+
+export default Campground;
diff --git a/e2e/pages/jsonld/campground/multipleAmenityFeatures.tsx b/e2e/pages/jsonld/campground/multipleAmenityFeatures.tsx
new file mode 100644
index 0000000..bd950da
--- /dev/null
+++ b/e2e/pages/jsonld/campground/multipleAmenityFeatures.tsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import { CampgroundJsonLd } from '../../../..';
+
+function Campground() {
+  return (
+    <>
+      <h1>Campground</h1>
+      <CampgroundJsonLd
+        id="https://www.example.com/campground/rip-van-winkle-campground"
+        name="Rip Van Winkle Campgrounds"
+        url="https://www.example.com/campground"
+        telephone="+18452468114"
+        images={['https://example.com/photos/1x1/photo.jpg']}
+        address={{
+          streetAddress: '149 Blue Mountain Rd',
+          addressLocality: 'Saugerties',
+          addressRegion: 'NY',
+          postalCode: '12477',
+          addressCountry: 'US',
+        }}
+        description="Description about Rip Van Winkle Campgrounds"
+        geo={{
+          latitude: '42.092599',
+          longitude: '-74.018580',
+        }}
+        openingHours={[
+          {
+            opens: '09:00',
+            closes: '17:00',
+            dayOfWeek: [
+              'Monday',
+              'Tuesday',
+              'Wednesday',
+              'Thursday',
+              'Friday',
+              'Saturday',
+              'Sunday',
+            ],
+            validFrom: '2019-12-23',
+            validThrough: '2020-04-02',
+          },
+        ]}
+        petsAllowed
+        rating={{
+          ratingValue: '5',
+          ratingCount: '18',
+        }}
+        amenityFeature={[
+          {
+            name: 'Showers',
+            value: true,
+          },
+          {
+            name: 'RV Hookup',
+            value: false,
+          },
+          {
+            name: 'Campfire',
+            value: true,
+          },
+        ]}
+        priceRange="$$"
+      />
+    </>
+  );
+}
+
+export default Campground;
diff --git a/e2e/pages/jsonld/index.tsx b/e2e/pages/jsonld/index.tsx
index d6117da..4665310 100644
--- a/e2e/pages/jsonld/index.tsx
+++ b/e2e/pages/jsonld/index.tsx
@@ -6,6 +6,7 @@ const allJsonLDPages = [
   'blog',
   'brand',
   'breadcrumb',
+  'campground',
   'collectionPage',
   'corporateContact',
   'course',
diff --git a/e2e/pages/jsonld/softwareApp.tsx b/e2e/pages/jsonld/softwareApp.tsx
index bb6e3bd..1ad2a92 100644
--- a/e2e/pages/jsonld/softwareApp.tsx
+++ b/e2e/pages/jsonld/softwareApp.tsx
@@ -12,6 +12,7 @@ function SoftwareApp() {
         aggregateRating={{ ratingValue: '4.6', ratingCount: '8864' }}
         operatingSystem="ANDROID"
         applicationCategory="GameApplication"
+        keywords="angrybirds, arcade, slingshot"
       />
     </>
   );
diff --git a/src/utils/schema/__tests__/setAmenityFeature.test.ts b/src/utils/schema/__tests__/setAmenityFeature.test.ts
new file mode 100644
index 0000000..c03692c
--- /dev/null
+++ b/src/utils/schema/__tests__/setAmenityFeature.test.ts
@@ -0,0 +1,55 @@
+import { AmenityFeature, DecoratedAmenityFeature } from 'src/types';
+import { setAmenityFeature } from '../setAmenityFeature';
+
+const amenityFeatureEx1: AmenityFeature = {
+  name: 'Shower',
+  value: true,
+};
+
+const decoratedAmenityFeatureEx1: DecoratedAmenityFeature = {
+  '@type': 'LocationFeatureSpecification',
+  ...amenityFeatureEx1,
+};
+
+const amenityFeatureEx2: AmenityFeature[] = [
+  {
+    name: 'Showers',
+    value: true,
+  },
+  {
+    name: 'RV Hookup',
+    value: false,
+  },
+  {
+    name: 'Campfire',
+    value: true,
+  },
+];
+
+const decoratedAmenityFeatureEx2: DecoratedAmenityFeature[] =
+  amenityFeatureEx2.map(amenity => ({
+    '@type': 'LocationFeatureSpecification',
+    ...amenity,
+  }));
+
+describe('setAmenityFeature', () => {
+  test('should return undefined if amenityFeature is undefined', () => {
+    const data = setAmenityFeature(undefined);
+    expect(data).toBeUndefined();
+  });
+
+  test('should accept an object, decorates it, and returns it', () => {
+    const data = setAmenityFeature(amenityFeatureEx1);
+    expect(data).toEqual(decoratedAmenityFeatureEx1);
+  });
+
+  test('should accept an array of objects, decorates all of them, and returns it', () => {
+    const data = setAmenityFeature(amenityFeatureEx2);
+    expect(data).toEqual(decoratedAmenityFeatureEx2);
+  });
+
+  test('single object array  returns correctly', () => {
+    const data = setAmenityFeature([amenityFeatureEx1]);
+    expect(data).toEqual(decoratedAmenityFeatureEx1);
+  });
+});