Repository: garmeeh/next-seo (js)

Issue:
Types `location` and `address` should accept simple `Text` type
**Describe the bug**

The types [`location`](https://schema.org/location) and [`address`](https://schema.org/address) should accept both of `PostalAddress` (or some other structed types) and simple [`Text`](https://schema.org/Text) type. Currently `next-seo` breaks the value when it is `Text`.

**Reproduction**

Case A:

```tsx
<EventJsonLd
  name="Example Event"
  startDate="2022-12-02T09:00:00.000Z"
  location="simple text"
/>
```

```
TypeError: Cannot use 'in' operator to search for 'url' in simple text
```

Case B:

```tsx
<EventJsonLd
  name="Example Event"
  startDate="2022-12-02T09:00:00.000Z"
  location={{
    name: 'Name of the Place',
    address: 'Address of the Place'
  }}
/>
```

```json
{
    "@context": "https://schema.org",
    "@type": "Event",
    "name": "Example Event",
    "startDate": "2022-12-02T09:00:00.000Z",
    "location": {
        "name": "Name of the Place",
        "address": {
            "0": "A",
            "1": "d",
            "2": "d",
            "3": "r",
            "4": "e",
            "5": "s",
            "6": "s",
            "7": " ",
            "8": "o",
            "9": "f",
            "10": " ",
            "11": "t",
            "12": "h",
            "13": "e",
            "14": " ",
            "15": "P",
            "16": "l",
            "17": "a",
            "18": "c",
            "19": "e",
            "@type": "PostalAddress"
        },
        "@type": "Place"
    }
}
```

**Expected behavior**


```json
{
    "@context": "https://schema.org",
    "@type": "Event",
    "name": "Example Event",
    "startDate": "2022-12-02T09:00:00.000Z",
    "location": "simple text"
}
```


```json
{
    "@context": "https://schema.org",
    "@type": "Event",
    "name": "Example Event",
    "startDate": "2022-12-02T09:00:00.000Z",
    "location": {
        "name": "Name of the Place",
        "address": "Address of the Place",
        "@type": "Place"
    }
}
```


Pull request:
fix: Accept type string for `location` and `address`
## Description of Change(s):

Fixes #1116

- `type Location` now includes `string` (cf: [location](https://schema.org/location))
- `type Address` now includes `string` (cf: [address](https://schema.org/address))
- `setLocation()` and `setAddress` return string itself for parameters with type string
- Add unit tests

---

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.

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/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/types.ts b/src/types.ts
index a52f034..6245a95 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'
@@ -162,13 +162,15 @@ export interface OpenGraphMedia {
   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;
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/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/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__/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,
+    });
+  });
+});