Skip to content

Commit 061f07d

Browse files
committed
enforce residential listing name null
1 parent 0820594 commit 061f07d

4 files changed

Lines changed: 45 additions & 43 deletions

File tree

src/components/ListingWrite/listingWriteController.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export function buildListingDraft({
149149
type: listingType,
150150
...(listingType !== "residential" &&
151151
values.avatar && { avatar: values.avatar }),
152-
...(listingType !== "residential" && { name: validatedName }),
152+
name: listingType === "residential" ? null : validatedName,
153153
description: values.description,
154154
location: `POINT(${values.coordinates.longitude} ${values.coordinates.latitude})`,
155155
area_name: values.areaName,

src/types/listing.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export type ListingDraftInput = {
2424
owner_id: string;
2525
type: ListingType;
2626
avatar?: string;
27-
name?: string;
27+
name?: string | null;
2828
description: string;
2929
location: string;
3030
area_name: string;

src/utils/listingSeo.ts

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getLocale, getTranslations } from "next-intl/server";
2+
import { cache } from "react";
23
import type { ListingSeoCopy } from "./listingUtils.ts";
34

45
function splitKeywordList(value: string) {
@@ -8,38 +9,40 @@ function splitKeywordList(value: string) {
89
.filter(Boolean);
910
}
1011

11-
export async function getListingSeoOptions() {
12-
const locale = await getLocale();
13-
const t = await getTranslations({ locale, namespace: "Listings.seo" });
12+
export const getListingSeoOptions = cache(
13+
async function getListingSeoOptions() {
14+
const locale = await getLocale();
15+
const t = await getTranslations({ locale, namespace: "Listings.seo" });
1416

15-
const seoCopy: ListingSeoCopy = {
16-
privateHostName: t("privateHostName"),
17-
fallbackListingName: t("fallbackListingName"),
18-
residentialConnectName: t("residentialConnectName"),
19-
residentialIntro: ({ name, location }) =>
20-
location
21-
? t("residentialIntroWithLocation", { name, location })
22-
: t("residentialIntro", { name }),
23-
businessIntro: ({ name, location }) =>
24-
location
25-
? t("businessIntroWithLocation", { name, location })
26-
: t("businessIntro", { name }),
27-
connect: ({ name, siteName }) =>
28-
t("connect", { name, siteName, explainer: t("siteExplainer") }),
29-
acceptedItemsLabel: t("acceptedItemsLabel"),
30-
rejectedItemsLabel: t("rejectedItemsLabel"),
31-
locationKeywords: ({ location }) => [
32-
t("keywords.location", { location }),
33-
t("keywords.foodScraps", { location }),
34-
t("keywords.compost", { location }),
35-
t("keywords.foodScrapDropOff", { location }),
36-
t("keywords.compostDropOff", { location }),
37-
],
38-
baseKeywords: () => splitKeywordList(t("keywords.base")),
39-
};
17+
const seoCopy: ListingSeoCopy = {
18+
privateHostName: t("privateHostName"),
19+
fallbackListingName: t("fallbackListingName"),
20+
residentialConnectName: t("residentialConnectName"),
21+
residentialIntro: ({ name, location }) =>
22+
location
23+
? t("residentialIntroWithLocation", { name, location })
24+
: t("residentialIntro", { name }),
25+
businessIntro: ({ name, location }) =>
26+
location
27+
? t("businessIntroWithLocation", { name, location })
28+
: t("businessIntro", { name }),
29+
connect: ({ name, siteName }) =>
30+
t("connect", { name, siteName, explainer: t("siteExplainer") }),
31+
acceptedItemsLabel: t("acceptedItemsLabel"),
32+
rejectedItemsLabel: t("rejectedItemsLabel"),
33+
locationKeywords: ({ location }) => [
34+
t("keywords.location", { location }),
35+
t("keywords.foodScraps", { location }),
36+
t("keywords.compost", { location }),
37+
t("keywords.foodScrapDropOff", { location }),
38+
t("keywords.compostDropOff", { location }),
39+
],
40+
baseKeywords: () => splitKeywordList(t("keywords.base")),
41+
};
4042

41-
return {
42-
locale,
43-
seoCopy,
44-
};
45-
}
43+
return {
44+
locale,
45+
seoCopy,
46+
};
47+
}
48+
);

src/utils/listingUtils.test.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ test("anonymous residential metadata emits an indexable teaser without private d
224224
const residentialListing = {
225225
...communityListing,
226226
type: "residential",
227-
name: "Sam's backyard bin",
227+
name: null,
228228
description:
229229
"Sam's side gate is open after 5pm and the compost bin is beside the shed.",
230230
accepted_items: ["Banana peels for Sam's worms"],
@@ -252,11 +252,11 @@ test("anonymous residential metadata emits an indexable teaser without private d
252252
assert.doesNotMatch(description, /Sam|side gate|shed|Banana|Citrus/);
253253
});
254254

255-
test("anonymous residential metadata avoids leaking listing names with localised SEO copy", () => {
255+
test("anonymous residential metadata avoids leaking profile names with localised SEO copy", () => {
256256
const residentialListing = {
257257
...communityListing,
258258
type: "residential",
259-
name: "Sam's backyard bin",
259+
name: null,
260260
owner_first_name: "Sam",
261261
slug: "private-host",
262262
} satisfies Listing;
@@ -277,7 +277,6 @@ test("anonymous residential metadata avoids leaking listing names with localised
277277
);
278278
assert.match(description, /Contacta con esta persona en Peels/);
279279
assert.doesNotMatch(description, /Sam/);
280-
assert.doesNotMatch(description, /backyard bin/);
281280
});
282281

283282
test("anonymous unknown-type metadata emits a private teaser", () => {
@@ -319,7 +318,7 @@ test("authenticated residential metadata can use private display names", () => {
319318
const residentialListing = {
320319
...communityListing,
321320
type: "residential",
322-
name: "Sam's backyard bin",
321+
name: null,
323322
owner_first_name: "Sam",
324323
slug: "private-host",
325324
} satisfies Listing;
@@ -337,15 +336,15 @@ test("authenticated residential metadata can use private display names", () => {
337336
);
338337
assert.match(String(metadata.description), /Connect with Sam on Peels/);
339338
assert.doesNotMatch(String(metadata.description), /helps people compost/);
340-
assert.doesNotMatch(String(metadata.description), /Sam's backyard bin/);
339+
assert.equal(residentialListing.name, null);
341340
assert.equal(metadata.robots, undefined);
342341
});
343342

344343
test("anonymous residential avatars can use localised private-host alt text", () => {
345344
const residentialListing = {
346345
...communityListing,
347346
type: "residential",
348-
name: "Sam's backyard bin",
347+
name: null,
349348
owner_first_name: "Sam",
350349
slug: "private-host",
351350
} satisfies Listing;
@@ -433,7 +432,7 @@ test("anonymous residential listing JSON-LD omits structured location details",
433432
const residentialListing = {
434433
...communityListing,
435434
type: "residential",
436-
name: "Sam's backyard bin",
435+
name: null,
437436
description:
438437
"Sam's side gate is open after 5pm and the compost bin is beside the shed.",
439438
accepted_items: ["Banana peels for Sam's worms"],

0 commit comments

Comments
 (0)