Repository: garmeeh/next-seo
Pull request #1105 (2022-11-21 15:52:03)

feat: enhance NewsArticleJsonLd and ArticleJsonLd author
## Description of Change(s):

`NewsArticleJsonLd` author couldn't be customised with `@type` and `url` parameter.
With this PR `NewsArticleJsonLd` `authorName` prop accepts also `ArticleAuthor`.

Also there was no way to override `@type` property of `author`. With this PR the default is set to `Person` but can be customised.

---

To help get PR's merged faster, the following is required:

[] 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/NewsArticle
* https://schema.org/author

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/APP_DIRECTORY.md b/APP_DIRECTORY.md
index 0cb9236..34c98b4 100644
--- a/APP_DIRECTORY.md
+++ b/APP_DIRECTORY.md
@@ -18,7 +18,7 @@ If you are interested I carried out some testing and gave feedback to the Next.j
 
 ### Key Changes
 
-- Next.js no longer de-duplicates tags in the head. This means that we can no longer use `<DefaultSeo />` in the same way. Going forward we will solely be use `<NextSeo />`
+- Next.js no longer de-duplicates tags in the head. This means that we can no longer use `<DefaultSeo />` in the same way. Going forward we will solely use `<NextSeo />`
 - `head.js` does not currently support `<script>` tags that do no have `src=""`. So for JSON-LD components they now have to be added via the page. This will result in them being added to the `<body>` of the page. This is 100% ok for SEO and not to worry about. This was confirmed by Google. I have also validated this using schema tool.
 - Next.js no longer auto adds the tags below to your app that you will likely need. I suggest adding them to your root layout in a `<head>` tag.
 
diff --git a/README.md b/README.md
index 1c7e9c4..0bdbe00 100644
--- a/README.md
+++ b/README.md
@@ -3125,7 +3125,7 @@ export default () => (
 | `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)
+For reference and more info check [Docs](https://schema.org/WebPage)
 
 ### Image Metadata
 
diff --git a/src/jsonld/event.tsx b/src/jsonld/event.tsx
index ab31d10..b36eebc 100644
--- a/src/jsonld/event.tsx
+++ b/src/jsonld/event.tsx
@@ -56,8 +56,10 @@ function EventJsonLd({
     organizer: Array.isArray(organizer)
       ? organizer.map(setOrganizer)
       : setOrganizer(organizer),
-    eventStatus: `https://schema.org/${eventStatus}`,
-    eventAttendanceMode: `https://schema.org/${eventAttendanceMode}`,
+    eventStatus: eventStatus ? `https://schema.org/${eventStatus}` : undefined,
+    eventAttendanceMode: eventAttendanceMode
+      ? `https://schema.org/${eventAttendanceMode}`
+      : undefined,
   };
 
   return (
diff --git a/src/jsonld/newsarticle.tsx b/src/jsonld/newsarticle.tsx
index 5a3b34c..366fa8b 100644
--- a/src/jsonld/newsarticle.tsx
+++ b/src/jsonld/newsarticle.tsx
@@ -3,6 +3,7 @@ import { setAuthor } from 'src/utils/schema/setAuthor';
 import { setPublisher } from 'src/utils/schema/setPublisher';
 
 import { JsonLd, JsonLdProps } from './jsonld';
+import { ArticleAuthor } from '../types';
 
 export interface NewsArticleJsonLdProps extends JsonLdProps {
   url: string;
@@ -13,7 +14,7 @@ export interface NewsArticleJsonLdProps extends JsonLdProps {
   dateCreated: string;
   datePublished: string;
   dateModified?: string;
-  authorName: string | string[];
+  authorName: string | string[] | ArticleAuthor | ArticleAuthor[];
   description: string;
   body: string;
   publisherName: string;
@@ -32,6 +33,7 @@ function NewsArticleJsonLd({
   datePublished,
   dateModified,
   authorName,
+  authorType,
   publisherName,
   publisherLogo,
   body,
diff --git a/src/jsonld/product.tsx b/src/jsonld/product.tsx
index 4949b15..c59b41e 100644
--- a/src/jsonld/product.tsx
+++ b/src/jsonld/product.tsx
@@ -20,7 +20,7 @@ export interface ProductJsonLdProps extends JsonLdProps {
   images?: string[];
   description?: string;
   brand?: string;
-  reviews?: Review[];
+  reviews?: Review | Review[];
   aggregateRating?: AggregateRating;
   offers?: Offers | Offers[];
   aggregateOffer?: AggregateOffer;
diff --git a/src/jsonld/webPage.tsx b/src/jsonld/webPage.tsx
index ebd6c94..bf77658 100644
--- a/src/jsonld/webPage.tsx
+++ b/src/jsonld/webPage.tsx
@@ -1,10 +1,10 @@
 import React from 'react';
 import type { ReviewedBy } from 'src/types';
 
-import { JsonLd } from './jsonld';
+import { JsonLd, JsonLdProps } from './jsonld';
 import { setReviewedBy } from 'src/utils/schema/setReviewedBy';
 
-export interface WebPageJsonLdProps {
+export interface WebPageJsonLdProps extends JsonLdProps {
   keyOverride?: string;
   id: string;
   description?: string;
diff --git a/src/types.ts b/src/types.ts
index a52f034..4bb522c 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -95,7 +95,7 @@ export interface VirtualLocation {
   url: string;
 }
 
-export type Location = Place | VirtualLocation;
+export type Location = string | Place | VirtualLocation;
 
 export type EventStatus =
   | 'EventCancelled'
@@ -155,20 +155,22 @@ export interface ItemListElements {
 }
 export interface OpenGraphMedia {
   url: string;
-  width?: number;
-  height?: number;
+  width?: number | null;
+  height?: number | null;
   alt?: string;
   type?: string;
   secureUrl?: string;
 }
 
-export interface Address {
-  streetAddress: string;
-  addressLocality: string;
-  addressRegion?: string;
-  postalCode: string;
-  addressCountry: string;
-}
+export type Address =
+  | string
+  | {
+      streetAddress: string;
+      addressLocality: string;
+      addressRegion?: string;
+      postalCode: string;
+      addressCountry: string;
+    };
 
 export interface Video {
   name: string;
@@ -378,7 +380,8 @@ export type Author = {
 
 export type ArticleAuthor = {
   name: string;
-  url: string;
+  url?: string;
+  type?: 'Person' | 'Organization';
 };
 
 export type Publisher = {
diff --git a/src/utils/schema/setAddress.ts b/src/utils/schema/setAddress.ts
index 07dfab2..16a9c75 100644
--- a/src/utils/schema/setAddress.ts
+++ b/src/utils/schema/setAddress.ts
@@ -2,7 +2,7 @@ import { Address } from 'src/types';
 
 export function setAddress(address?: Address | Address[]) {
   if (!address) return undefined;
-  
+
   if (!Array.isArray(address)) return toPostalAddress(address);
 
   // If array of one address, replace with single address
@@ -13,5 +13,6 @@ export function setAddress(address?: Address | Address[]) {
 }
 
 function toPostalAddress(address: Address) {
+  if (typeof address === 'string') return address;
   return { '@type': 'PostalAddress', ...address };
 }
diff --git a/src/utils/schema/setAuthor.ts b/src/utils/schema/setAuthor.ts
index a6c6f09..8734d17 100644
--- a/src/utils/schema/setAuthor.ts
+++ b/src/utils/schema/setAuthor.ts
@@ -1,14 +1,5 @@
 import { ArticleAuthor } from '../../types';
 
-/**
- * Include name and url
- * @param author
- * @returns
- */
-function includeNameAndUrl(author: ArticleAuthor): boolean {
-  return typeof author === 'object' && !!(author.name && author.url);
-}
-
 /**
  * Generate author information
  * @param author
@@ -20,11 +11,11 @@ function generateAuthorInfo(author: string | ArticleAuthor) {
       '@type': 'Person',
       name: author,
     };
-  } else if (includeNameAndUrl(author)) {
+  } else if (!!author.name) {
     return {
-      '@type': 'Person',
+      '@type': author?.type ?? 'Person',
       name: author.name,
-      url: author.url,
+      url: author?.url,
     };
   }
 
diff --git a/src/utils/schema/setLocation.ts b/src/utils/schema/setLocation.ts
index 6b3be55..9574e54 100644
--- a/src/utils/schema/setLocation.ts
+++ b/src/utils/schema/setLocation.ts
@@ -1,11 +1,15 @@
 import { Location, Place, VirtualLocation } from 'src/types';
 import { setAddress } from './setAddress';
 
-export function setLocation(location: Location) {
+export function setLocation(location?: Location) {
   if (!location) {
     return undefined;
   }
 
+  if (typeof location === 'string') {
+    return location;
+  }
+
   if ('url' in location) {
     return setVirtualLocation(location);
   } else {


Test patch:
diff --git a/e2e/pages/jsonld/index.tsx b/e2e/pages/jsonld/index.tsx
index a7f6dd0..191f490 100644
--- a/e2e/pages/jsonld/index.tsx
+++ b/e2e/pages/jsonld/index.tsx
@@ -10,7 +10,7 @@ const allJsonLDPages = [
   'corporateContact',
   'course',
   'dataset',
-  'even',
+  'event',
   'faq',
   'howTo',
   'jobPosting',
diff --git a/src/utils/schema/__tests__/setAddress.test.ts b/src/utils/schema/__tests__/setAddress.test.ts
index 2f22bb2..a38bff0 100644
--- a/src/utils/schema/__tests__/setAddress.test.ts
+++ b/src/utils/schema/__tests__/setAddress.test.ts
@@ -22,6 +22,10 @@ describe('setAddress', () => {
     expect(setAddress(undefined)).toBeUndefined();
   });
 
+  test('should accepts simple string and returns it', () => {
+    expect(setAddress('AddressText')).toBe('AddressText');
+  });
+
   test('single address returns correctly', () => {
     const data = setAddress(addressOne);
 
diff --git a/src/utils/schema/__tests__/setAuthor.test.ts b/src/utils/schema/__tests__/setAuthor.test.ts
new file mode 100644
index 0000000..16684ed
--- /dev/null
+++ b/src/utils/schema/__tests__/setAuthor.test.ts
@@ -0,0 +1,79 @@
+import { ArticleAuthor } from 'src/types';
+import { setAuthor } from '../setAuthor';
+
+describe('setAuthor', () => {
+  test('should return undefined if author is undefined', () => {
+    expect(setAuthor(undefined)).toBeUndefined();
+  });
+
+  test('works correctly when ArticleAuthor is passed', () => {
+    const author: ArticleAuthor = {
+      name: 'Acme',
+      type: 'Organization',
+      url: '/acme',
+    };
+
+    const data = setAuthor(author);
+
+    expect(data).toEqual({
+      '@type': 'Organization',
+      name: 'Acme',
+      url: '/acme',
+    });
+  });
+
+  test('works correctly when just ArticleAuthor name is passed', () => {
+    const data = setAuthor({ name: 'John Doe' });
+
+    expect(data).toEqual({
+      '@type': 'Person',
+      name: 'John Doe',
+    });
+  });
+
+  test('works correctly when an array of ArticleAuthor is passed', () => {
+    const author: ArticleAuthor[] = [
+      {
+        name: 'Acme',
+        type: 'Organization',
+        url: '/acme',
+      },
+      { name: 'John Doe' },
+    ];
+
+    const data = setAuthor(author);
+
+    expect(data).toEqual([
+      {
+        '@type': 'Organization',
+        name: 'Acme',
+        url: '/acme',
+      },
+      { '@type': 'Person', name: 'John Doe' },
+    ]);
+  });
+
+  test('works correctly when string is passed', () => {
+    const data = setAuthor('John Doe');
+
+    expect(data).toEqual({
+      '@type': 'Person',
+      name: 'John Doe',
+    });
+  });
+
+  test('works correctly when an array string is passed', () => {
+    const data = setAuthor(['John Doe', 'Foo Bar']);
+
+    expect(data).toEqual([
+      {
+        '@type': 'Person',
+        name: 'John Doe',
+      },
+      {
+        '@type': 'Person',
+        name: 'Foo Bar',
+      },
+    ]);
+  });
+});
diff --git a/src/utils/schema/__tests__/setLocation.test.ts b/src/utils/schema/__tests__/setLocation.test.ts
new file mode 100644
index 0000000..af114bc
--- /dev/null
+++ b/src/utils/schema/__tests__/setLocation.test.ts
@@ -0,0 +1,37 @@
+import { Place, VirtualLocation } from 'src/types';
+import { setLocation } from '../setLocation';
+
+const virtualLocation: VirtualLocation = {
+  name: 'Virtual Location',
+  sameAs: 'https://example.com',
+  url: 'https://example.com',
+};
+
+const place: Place = {
+  name: 'Place',
+  address: 'Address String',
+};
+
+describe('setLocation', () => {
+  test('should return undefined if location is undefined', () => {
+    expect(setLocation(undefined)).toBeUndefined();
+  });
+
+  test('should accepts simple string and returns it', () => {
+    expect(setLocation('LocationText')).toBe('LocationText');
+  });
+
+  test('returns VirtualLocation correctly', () => {
+    expect(setLocation(virtualLocation)).toMatchObject({
+      '@type': 'VirtualLocation',
+      ...virtualLocation,
+    });
+  });
+
+  test('returns Place correctly', () => {
+    expect(setLocation(place)).toMatchObject({
+      '@type': 'Place',
+      ...place,
+    });
+  });
+});