Skip to content

Commit d880023

Browse files
committed
add listing item json ld
1 parent cfe134c commit d880023

3 files changed

Lines changed: 81 additions & 1 deletion

File tree

e2e/seo.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@ async function getListingJsonLdScripts(page: import("@playwright/test").Page) {
1111
return page.locator('script[type="application/ld+json"]').allTextContents();
1212
}
1313

14+
type ListingJsonLd = {
15+
"@id"?: string;
16+
about?: {
17+
additionalProperty?: unknown;
18+
};
19+
};
20+
21+
function parseJsonLdScripts(scripts: string[]) {
22+
return scripts.map((script) => JSON.parse(script) as ListingJsonLd);
23+
}
24+
1425
async function newLocalePage(
1526
browser: import("@playwright/test").Browser,
1627
locale: string,
@@ -53,6 +64,24 @@ test("public listing pages expose crawlable listing metadata", async ({
5364
script.includes("Marrickville Neighbourhood Compost")
5465
)
5566
).toBeTruthy();
67+
68+
const listingJsonLd = parseJsonLdScripts(jsonLdScripts).find(
69+
(data) =>
70+
data["@id"] ===
71+
"https://www.peels.app/listings/demo-marrickville-compost#webpage"
72+
);
73+
expect(listingJsonLd?.about?.additionalProperty).toEqual(
74+
expect.arrayContaining([
75+
expect.objectContaining({
76+
name: "Accepted food scraps",
77+
value: expect.stringContaining("Fruit and vegetable scraps"),
78+
}),
79+
expect.objectContaining({
80+
name: "Items not accepted",
81+
value: expect.stringContaining("Plastic bags"),
82+
}),
83+
])
84+
);
5685
});
5786

5887
test("public listing pages localise Spanish SEO metadata", async ({

src/utils/listingUtils.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const communityListing = {
1515
description:
1616
"Households can subscribe to drop off their food scraps at a local community hub.",
1717
accepted_items: ["Food scraps"],
18-
rejected_items: [],
18+
rejected_items: ["Meat and dairy"],
1919
photos: ["demo/community-garden.jpg"],
2020
links: [],
2121
type: "community",
@@ -274,6 +274,20 @@ test("listing JSON-LD describes the public listing page and place conservatively
274274
assert.ok(jsonLd.about.address);
275275
assert.equal(jsonLd.about.address.addressLocality, "Marrickville");
276276
assert.equal(jsonLd.about.address.addressCountry, "Australia");
277+
assert.deepEqual(jsonLd.about.additionalProperty, [
278+
{
279+
"@type": "PropertyValue",
280+
name: "Accepted food scraps",
281+
propertyID: "acceptedItems",
282+
value: "Food scraps",
283+
},
284+
{
285+
"@type": "PropertyValue",
286+
name: "Items not accepted",
287+
propertyID: "rejectedItems",
288+
value: "Meat and dairy",
289+
},
290+
]);
277291
});
278292

279293
test("listing JSON-LD can use localised descriptions, country names, and language", () => {
@@ -312,6 +326,7 @@ test("anonymous residential listing JSON-LD omits structured location details",
312326
assert.equal(jsonLd.name, "Private Host");
313327
assert.equal(jsonLd.about.address, undefined);
314328
assert.equal(jsonLd.about.geo, undefined);
329+
assert.equal(jsonLd.about.additionalProperty, undefined);
315330
});
316331

317332
test("anonymous listing JSON-LD treats missing listing types as sensitive", () => {

src/utils/listingUtils.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ type ListingLike = {
1515
country_code?: string | null;
1616
area_name?: string | null;
1717
description?: string | null;
18+
accepted_items?: string[] | null;
19+
rejected_items?: string[] | null;
1820
photos?: string[] | null;
1921
coordinates?: ListingCoordinates | null;
2022
};
@@ -98,6 +100,36 @@ function compactTextParts(parts: Array<string | null | undefined>) {
98100
.filter((part): part is string => Boolean(part));
99101
}
100102

103+
function compactTextList(items: string[] | null | undefined) {
104+
return items
105+
?.map((item) => item.trim())
106+
.filter((item): item is string => Boolean(item));
107+
}
108+
109+
function getListingItemProperties(listing: ListingLike) {
110+
const acceptedItems = compactTextList(listing.accepted_items) ?? [];
111+
const rejectedItems = compactTextList(listing.rejected_items) ?? [];
112+
113+
return [
114+
acceptedItems.length
115+
? {
116+
"@type": "PropertyValue",
117+
name: "Accepted food scraps",
118+
propertyID: "acceptedItems",
119+
value: acceptedItems.join(", "),
120+
}
121+
: null,
122+
rejectedItems.length
123+
? {
124+
"@type": "PropertyValue",
125+
name: "Items not accepted",
126+
propertyID: "rejectedItems",
127+
value: rejectedItems.join(", "),
128+
}
129+
: null,
130+
].filter((item): item is NonNullable<typeof item> => Boolean(item));
131+
}
132+
101133
function getListingCountryName(
102134
listing: ListingLike | null | undefined,
103135
locale?: string
@@ -390,6 +422,9 @@ export function generateListingJsonLd(
390422
const structuredDataImage = canIncludePublicStructuredDetails
391423
? getListingStructuredDataImage(listing, user)
392424
: null;
425+
const itemProperties = canIncludePublicStructuredDetails
426+
? getListingItemProperties(listing)
427+
: [];
393428
const address = {
394429
"@type": "PostalAddress",
395430
...(listing.area_name ? { addressLocality: listing.area_name } : {}),
@@ -412,6 +447,7 @@ export function generateListingJsonLd(
412447
}
413448
: {}),
414449
...(structuredDataImage ? { image: structuredDataImage } : {}),
450+
...(itemProperties.length ? { additionalProperty: itemProperties } : {}),
415451
};
416452

417453
return {

0 commit comments

Comments
 (0)