@escape-game-over/atlas 0.1.115 → 0.1.117
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/NOT-BUILT.md +2 -2
- package/package.json +2 -2
- package/src/analytics/ids.ts +1 -13
- package/src/analytics/index.ts +18 -18
- package/src/analytics/storage.ts +34 -39
- package/src/analytics/vendor-settings.ts +162 -0
- package/src/astro/axon-event.ts +2 -4
- package/src/astro/axon-pixel.ts +3 -6
- package/src/astro/clarity-event.ts +2 -4
- package/src/astro/clarity.ts +3 -6
- package/src/astro/consent.ts +3 -6
- package/src/astro/drip-event.ts +2 -4
- package/src/astro/drip.ts +2 -7
- package/src/astro/escape-navigator-consent.ts +2 -6
- package/src/astro/escapeall-calendar.ts +2 -5
- package/src/astro/google-event.ts +3 -7
- package/src/astro/google-map.ts +3 -5
- package/src/astro/images.ts +12 -26
- package/src/astro/index.ts +2 -9
- package/src/astro/meta-event.ts +2 -4
- package/src/astro/meta-pixel.ts +4 -8
- package/src/astro/public-files.ts +11 -52
- package/src/astro/site-routes.ts +11 -55
- package/src/astro/snap-event.ts +2 -4
- package/src/astro/snap-pixel.ts +3 -6
- package/src/astro/tiktok-event.ts +2 -4
- package/src/astro/tiktok-pixel.ts +70 -39
- package/src/astro/umami-event.ts +2 -6
- package/src/astro/vendors.ts +64 -0
- package/src/config.ts +8 -17
- package/src/content/rich.ts +15 -18
- package/src/i18n/catalog.ts +20 -1
- package/src/i18n/define.ts +0 -5
- package/src/i18n/translate.ts +9 -48
- package/src/index.ts +24 -166
- package/src/jsonld/article.ts +7 -17
- package/src/jsonld/business.ts +27 -28
- package/src/jsonld/creative-work.ts +8 -20
- package/src/jsonld/node.ts +37 -7
- package/src/jsonld/organization.ts +21 -30
- package/src/jsonld/place.ts +2 -6
- package/src/jsonld/product.ts +16 -22
- package/src/jsonld/service.ts +21 -37
- package/src/jsonld/video.ts +4 -8
- package/src/jsonld/website.ts +3 -5
- package/src/meta/content.ts +14 -66
- package/src/meta/index.ts +10 -32
- package/src/project.ts +4 -18
- package/src/routes/define.ts +0 -13
- package/src/routes/family.ts +135 -3
- package/src/site/api.ts +1 -21
- package/src/site/create.ts +17 -26
- package/src/site/define.ts +13 -9
- package/src/site/page.ts +1 -10
- package/src/warn.ts +6 -6
- package/src/analytics/axon-pixel.ts +0 -24
- package/src/analytics/clarity.ts +0 -29
- package/src/analytics/drip.ts +0 -20
- package/src/analytics/meta-pixel.ts +0 -29
- package/src/analytics/snap-pixel.ts +0 -28
- package/src/analytics/tiktok-pixel.ts +0 -29
package/src/jsonld/article.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { IsoDate } from "../types.ts";
|
|
|
2
2
|
import type { HttpsUrl } from "../url.ts";
|
|
3
3
|
import { warn } from "../warn.ts";
|
|
4
4
|
import type { BusinessId, OrganizationId } from "./ids.ts";
|
|
5
|
-
import { type JsonLdNode, oneOrMany } from "./node.ts";
|
|
5
|
+
import { type JsonLdNode, listed, oneOrMany, reference } from "./node.ts";
|
|
6
6
|
|
|
7
7
|
export interface ArticleInput {
|
|
8
8
|
/**
|
|
@@ -137,29 +137,19 @@ export function article(item: ArticleInput): JsonLdNode {
|
|
|
137
137
|
return {
|
|
138
138
|
"@type": item.kind ?? "Article",
|
|
139
139
|
headline: item.headline,
|
|
140
|
-
|
|
141
|
-
? {}
|
|
142
|
-
: { description: item.description }),
|
|
140
|
+
description: item.description,
|
|
143
141
|
// Not `url`: an article is a thing, and this is the page it is *on*.
|
|
144
142
|
mainEntityOfPage: item.url,
|
|
145
143
|
datePublished: item.datePublished,
|
|
146
|
-
|
|
147
|
-
? {}
|
|
148
|
-
: { dateModified: item.dateModified }),
|
|
144
|
+
dateModified: item.dateModified,
|
|
149
145
|
// Passed through in the shape it arrived in, bar a list of one — which
|
|
150
146
|
// is `schemaType`'s rule applied to a second field: writing `[x]` is
|
|
151
147
|
// fine, emitting it reads as a bug.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
: { author: { "@id": item.author } }),
|
|
156
|
-
...(item.publisher === undefined
|
|
157
|
-
? {}
|
|
158
|
-
: { publisher: { "@id": item.publisher } }),
|
|
148
|
+
image: item.image === undefined ? undefined : oneOrMany(item.image),
|
|
149
|
+
author: reference(item.author),
|
|
150
|
+
publisher: reference(item.publisher),
|
|
159
151
|
// An empty list is not a set of no keywords, it is a field saying
|
|
160
152
|
// nothing — so it is left out, as every optional here is.
|
|
161
|
-
|
|
162
|
-
? {}
|
|
163
|
-
: { keywords: item.tags }),
|
|
153
|
+
keywords: listed(item.tags),
|
|
164
154
|
};
|
|
165
155
|
}
|
package/src/jsonld/business.ts
CHANGED
|
@@ -10,7 +10,13 @@ import type { ImageAsset } from "../image.ts";
|
|
|
10
10
|
import type { NonEmpty } from "../types.ts";
|
|
11
11
|
import { absoluteUrl, type HttpsUrl } from "../url.ts";
|
|
12
12
|
import { businessId, type OrganizationId } from "./ids.ts";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
alternateName,
|
|
15
|
+
type JsonLdNode,
|
|
16
|
+
listed,
|
|
17
|
+
reference,
|
|
18
|
+
schemaType,
|
|
19
|
+
} from "./node.ts";
|
|
14
20
|
import { geoCoordinates, openingHours, postalAddress } from "./place.ts";
|
|
15
21
|
|
|
16
22
|
export interface LocalBusinessInput {
|
|
@@ -173,34 +179,27 @@ export function localBusiness(business: LocalBusinessInput): JsonLdNode {
|
|
|
173
179
|
"@type": schemaType(business.type ?? "LocalBusiness"),
|
|
174
180
|
"@id": businessId(business.url, business.location),
|
|
175
181
|
name: business.name,
|
|
176
|
-
|
|
182
|
+
alternateName: alternateName(business.name, business.alternateName),
|
|
177
183
|
url: business.url,
|
|
178
184
|
address: postalAddress(business.address),
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
: { openingHoursSpecification: openingHours(business.hours) }),
|
|
199
|
-
...(business.sameAs === undefined ? {} : { sameAs: business.sameAs }),
|
|
200
|
-
...(business.parentOrganization === undefined
|
|
201
|
-
? {}
|
|
202
|
-
: {
|
|
203
|
-
parentOrganization: { "@id": business.parentOrganization },
|
|
204
|
-
}),
|
|
185
|
+
geo:
|
|
186
|
+
business.geo === undefined
|
|
187
|
+
? undefined
|
|
188
|
+
: geoCoordinates(business.geo),
|
|
189
|
+
priceRange: business.priceRange,
|
|
190
|
+
image: listed(business.image)?.map((photo) =>
|
|
191
|
+
absoluteUrl(business.url, photo.src, "venue photo")
|
|
192
|
+
),
|
|
193
|
+
telephone:
|
|
194
|
+
business.telephone === undefined
|
|
195
|
+
? undefined
|
|
196
|
+
: e164(business.telephone),
|
|
197
|
+
email: business.email,
|
|
198
|
+
openingHoursSpecification:
|
|
199
|
+
business.hours === undefined
|
|
200
|
+
? undefined
|
|
201
|
+
: openingHours(business.hours),
|
|
202
|
+
sameAs: business.sameAs,
|
|
203
|
+
parentOrganization: reference(business.parentOrganization),
|
|
205
204
|
};
|
|
206
205
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Quantity } from "../money.ts";
|
|
2
2
|
import type { HttpsUrl } from "../url.ts";
|
|
3
3
|
import type { BusinessId, OrganizationId } from "./ids.ts";
|
|
4
|
-
import { type JsonLdNode, schemaType } from "./node.ts";
|
|
4
|
+
import { type JsonLdNode, listed, reference, schemaType } from "./node.ts";
|
|
5
5
|
import { quantitativeValue } from "./quantity.ts";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -92,19 +92,11 @@ export function creativeWork(work: CreativeWorkInput): JsonLdNode {
|
|
|
92
92
|
"@type": schemaType(["CreativeWork", ...(work.alsoA ?? [])]),
|
|
93
93
|
name: work.name,
|
|
94
94
|
url: work.url,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
? {}
|
|
101
|
-
: { genre: work.genre }),
|
|
102
|
-
...(work.creator === undefined
|
|
103
|
-
? {}
|
|
104
|
-
: { creator: { "@id": work.creator } }),
|
|
105
|
-
...(work.properties === undefined || work.properties.length === 0
|
|
106
|
-
? {}
|
|
107
|
-
: { additionalProperty: work.properties.map(propertyValue) }),
|
|
95
|
+
description: work.description,
|
|
96
|
+
image: work.image,
|
|
97
|
+
genre: listed(work.genre),
|
|
98
|
+
creator: reference(work.creator),
|
|
99
|
+
additionalProperty: listed(work.properties)?.map(propertyValue),
|
|
108
100
|
};
|
|
109
101
|
}
|
|
110
102
|
|
|
@@ -114,12 +106,8 @@ function propertyValue(property: CreativeWorkProperty): JsonLdNode {
|
|
|
114
106
|
name: property.name,
|
|
115
107
|
value: {
|
|
116
108
|
...quantitativeValue(property.value),
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
: { unitCode: property.unitCode }),
|
|
120
|
-
...(property.unitText === undefined
|
|
121
|
-
? {}
|
|
122
|
-
: { unitText: property.unitText }),
|
|
109
|
+
unitCode: property.unitCode,
|
|
110
|
+
unitText: property.unitText,
|
|
123
111
|
},
|
|
124
112
|
};
|
|
125
113
|
}
|
package/src/jsonld/node.ts
CHANGED
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
|
|
8
8
|
import { literal } from "../analytics/tags.ts";
|
|
9
9
|
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* A node in the graph. Deliberately loose: `@type` is what a consumer varies.
|
|
12
|
+
*
|
|
13
|
+
* An optional field it lacks is written as `undefined`, never branched around
|
|
14
|
+
* with a conditional spread: `serializeJsonLd` goes through JSON, which drops
|
|
15
|
+
* it, so the published node has no such key either way.
|
|
16
|
+
*/
|
|
11
17
|
export interface JsonLdNode {
|
|
12
18
|
readonly "@type": string | readonly string[];
|
|
13
19
|
readonly [key: string]: unknown;
|
|
@@ -47,8 +53,8 @@ export function schemaType(
|
|
|
47
53
|
* engine reads it as *another* name and a repeat of the first one is not that:
|
|
48
54
|
* it widens nothing and is one more string to reconcile. Decided here rather
|
|
49
55
|
* than by the caller — three nodes take the field, the rule is the same on all
|
|
50
|
-
* of them, and a project asked to write `
|
|
51
|
-
* one is being handed lib's rule to enforce by hand.
|
|
56
|
+
* of them, and a project asked to write `alt === name ? undefined : alt` at
|
|
57
|
+
* each one is being handed lib's rule to enforce by hand.
|
|
52
58
|
*
|
|
53
59
|
* Which makes `alternateName: brand.name` safe to pass unconditionally: a venue
|
|
54
60
|
* whose name already *is* the brand's simply emits nothing.
|
|
@@ -56,10 +62,34 @@ export function schemaType(
|
|
|
56
62
|
export function alternateName(
|
|
57
63
|
name: string,
|
|
58
64
|
alternate: string | undefined
|
|
59
|
-
):
|
|
60
|
-
return alternate === undefined
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
): string | undefined {
|
|
66
|
+
return alternate === name ? undefined : alternate;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A pointer to a node published elsewhere in the graph, or nothing.
|
|
71
|
+
*
|
|
72
|
+
* Every field built from it is optional, and an `undefined` field is dropped
|
|
73
|
+
* when the graph is serialised: so a node can write `author:
|
|
74
|
+
* reference(item.author)` without a branch.
|
|
75
|
+
*/
|
|
76
|
+
export function reference<Id extends string>(
|
|
77
|
+
id: Id | undefined
|
|
78
|
+
): { readonly "@id": Id } | undefined {
|
|
79
|
+
return id === undefined ? undefined : { "@id": id };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A list, or nothing when it is empty.
|
|
84
|
+
*
|
|
85
|
+
* An empty list is not a set of nothing, it is a field saying nothing — a
|
|
86
|
+
* claim to have been asked and had no answer — so it is left out like any
|
|
87
|
+
* other absent field.
|
|
88
|
+
*/
|
|
89
|
+
export function listed<T>(
|
|
90
|
+
list: readonly T[] | undefined
|
|
91
|
+
): readonly T[] | undefined {
|
|
92
|
+
return list === undefined || list.length === 0 ? undefined : list;
|
|
63
93
|
}
|
|
64
94
|
|
|
65
95
|
/** The graph, serialised for a `<script type="application/ld+json">`. */
|
|
@@ -8,7 +8,7 @@ import type { ImageAsset } from "../image.ts";
|
|
|
8
8
|
import type { IsoDate } from "../types.ts";
|
|
9
9
|
import { absoluteUrl, type HttpsUrl, joinUrl, type UrlPath } from "../url.ts";
|
|
10
10
|
import { organizationId } from "./ids.ts";
|
|
11
|
-
import { alternateName, type JsonLdNode } from "./node.ts";
|
|
11
|
+
import { alternateName, type JsonLdNode, listed } from "./node.ts";
|
|
12
12
|
import { postalAddress } from "./place.ts";
|
|
13
13
|
|
|
14
14
|
export interface OrganizationInput {
|
|
@@ -172,36 +172,27 @@ export function organization(brand: OrganizationInput): JsonLdNode {
|
|
|
172
172
|
"@id": organizationId(brand.url),
|
|
173
173
|
name: brand.name,
|
|
174
174
|
url: brand.url,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
: {
|
|
188
|
-
image: brand.image.map((photo) =>
|
|
189
|
-
absoluteUrl(brand.url, photo.src, "organization photo")
|
|
190
|
-
),
|
|
191
|
-
}),
|
|
192
|
-
...(brand.legalName === undefined
|
|
193
|
-
? {}
|
|
194
|
-
: { legalName: brand.legalName }),
|
|
195
|
-
...(brand.vatID === undefined ? {} : { vatID: brand.vatID }),
|
|
175
|
+
alternateName: alternateName(brand.name, brand.alternateName),
|
|
176
|
+
description: brand.description,
|
|
177
|
+
foundingDate: brand.foundingDate,
|
|
178
|
+
logo:
|
|
179
|
+
brand.logo === undefined
|
|
180
|
+
? undefined
|
|
181
|
+
: joinUrl(brand.url, brand.logo),
|
|
182
|
+
image: listed(brand.image)?.map((photo) =>
|
|
183
|
+
absoluteUrl(brand.url, photo.src, "organization photo")
|
|
184
|
+
),
|
|
185
|
+
legalName: brand.legalName,
|
|
186
|
+
vatID: brand.vatID,
|
|
196
187
|
// E.164, as on the business: the only form that identifies a number
|
|
197
188
|
// unambiguously enough to match a company against a listing.
|
|
198
|
-
|
|
199
|
-
?
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
189
|
+
telephone:
|
|
190
|
+
brand.telephone === undefined ? undefined : e164(brand.telephone),
|
|
191
|
+
email: brand.email,
|
|
192
|
+
sameAs: brand.sameAs,
|
|
193
|
+
address:
|
|
194
|
+
brand.address === undefined
|
|
195
|
+
? undefined
|
|
196
|
+
: postalAddress(brand.address),
|
|
206
197
|
};
|
|
207
198
|
}
|
package/src/jsonld/place.ts
CHANGED
|
@@ -37,13 +37,9 @@ export function postalAddress(address: PostalAddress): JsonLdNode {
|
|
|
37
37
|
"@type": "PostalAddress",
|
|
38
38
|
streetAddress: address.street,
|
|
39
39
|
addressLocality: address.locality,
|
|
40
|
-
|
|
41
|
-
? {}
|
|
42
|
-
: { postalCode: address.postalCode }),
|
|
40
|
+
postalCode: address.postalCode,
|
|
43
41
|
addressCountry: address.country,
|
|
44
|
-
|
|
45
|
-
? {}
|
|
46
|
-
: { addressRegion: address.region }),
|
|
42
|
+
addressRegion: address.region,
|
|
47
43
|
};
|
|
48
44
|
}
|
|
49
45
|
|
package/src/jsonld/product.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import type { NonEmpty } from "../types.ts";
|
|
12
12
|
import type { HttpsUrl } from "../url.ts";
|
|
13
13
|
import type { BusinessId, OrganizationId } from "./ids.ts";
|
|
14
|
-
import { type JsonLdNode, schemaType } from "./node.ts";
|
|
14
|
+
import { type JsonLdNode, reference, schemaType } from "./node.ts";
|
|
15
15
|
import { quantitativeValue } from "./quantity.ts";
|
|
16
16
|
|
|
17
17
|
export interface ProductInput {
|
|
@@ -91,7 +91,7 @@ export function product(item: ProductInput): JsonLdNode {
|
|
|
91
91
|
name: item.name,
|
|
92
92
|
description: item.description,
|
|
93
93
|
url: item.url,
|
|
94
|
-
|
|
94
|
+
image: item.image,
|
|
95
95
|
offers: offersFor(item),
|
|
96
96
|
};
|
|
97
97
|
}
|
|
@@ -116,30 +116,24 @@ function offerFor(
|
|
|
116
116
|
"@type": "Offer",
|
|
117
117
|
// An `Offer` is a `Thing`, so it can carry a `name`; it is what tells
|
|
118
118
|
// the adults' price from the children's.
|
|
119
|
-
|
|
119
|
+
name,
|
|
120
120
|
price: amount,
|
|
121
121
|
priceCurrency: item.price.currency,
|
|
122
122
|
availability: "https://schema.org/InStock",
|
|
123
123
|
url: item.url,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
...(unit === undefined ? {} : { unitText: unit }),
|
|
138
|
-
},
|
|
139
|
-
}),
|
|
140
|
-
...(item.seller === undefined
|
|
141
|
-
? {}
|
|
142
|
-
: { seller: { "@id": item.seller } }),
|
|
124
|
+
// What makes two prices a table rather than a contradiction:
|
|
125
|
+
// each one says the quantity it applies at.
|
|
126
|
+
//
|
|
127
|
+
// The quantity is passed as written, not through `tierRange`:
|
|
128
|
+
// the two take the same `number | { min, max }`, and widening
|
|
129
|
+
// first would publish a band of one as `minValue: 2,
|
|
130
|
+
// maxValue: 2` — a range across a single size, where `value:
|
|
131
|
+
// 2` is the same claim said plainly.
|
|
132
|
+
eligibleQuantity:
|
|
133
|
+
band === undefined
|
|
134
|
+
? undefined
|
|
135
|
+
: { ...quantitativeValue(band.quantity), unitText: unit },
|
|
136
|
+
seller: reference(item.seller),
|
|
143
137
|
};
|
|
144
138
|
}
|
|
145
139
|
|
package/src/jsonld/service.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { HttpsUrl } from "../url.ts";
|
|
2
2
|
import type { BusinessId, OrganizationId } from "./ids.ts";
|
|
3
3
|
import { serviceId } from "./ids.ts";
|
|
4
|
-
import type
|
|
4
|
+
import { type JsonLdNode, listed, reference } from "./node.ts";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* What a business *does*, for the sites that sell work rather than tickets.
|
|
@@ -82,29 +82,23 @@ export interface ServiceInput {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
/** An `Audience` node, or nothing. See `ServiceInput.audience`. */
|
|
85
|
-
function audienceFor(audienceType: string | undefined): {
|
|
86
|
-
readonly audience?: JsonLdNode;
|
|
87
|
-
} {
|
|
85
|
+
function audienceFor(audienceType: string | undefined): JsonLdNode | undefined {
|
|
88
86
|
return audienceType === undefined
|
|
89
|
-
?
|
|
90
|
-
: {
|
|
87
|
+
? undefined
|
|
88
|
+
: { "@type": "Audience", audienceType };
|
|
91
89
|
}
|
|
92
90
|
|
|
93
91
|
/** `areaServed`, as `Place` nodes. See `ServiceArea`. */
|
|
94
|
-
function areasFor(
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
function areasFor(
|
|
93
|
+
areas: readonly ServiceArea[] | undefined
|
|
94
|
+
): readonly JsonLdNode[] | undefined {
|
|
97
95
|
// Length rather than presence: a service that serves nowhere in particular
|
|
98
96
|
// says nothing, and an empty list is a claim to have been asked and had no
|
|
99
97
|
// answer.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
"@type": area.kind ?? "AdministrativeArea",
|
|
105
|
-
name: area.name,
|
|
106
|
-
})),
|
|
107
|
-
};
|
|
98
|
+
return listed(areas)?.map((area) => ({
|
|
99
|
+
"@type": area.kind ?? "AdministrativeArea",
|
|
100
|
+
name: area.name,
|
|
101
|
+
}));
|
|
108
102
|
}
|
|
109
103
|
|
|
110
104
|
/**
|
|
@@ -118,17 +112,11 @@ export function service(input: ServiceInput): JsonLdNode {
|
|
|
118
112
|
"@id": serviceId(input.url),
|
|
119
113
|
name: input.name,
|
|
120
114
|
url: input.url,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
: { serviceType: input.serviceType }),
|
|
127
|
-
...(input.provider === undefined
|
|
128
|
-
? {}
|
|
129
|
-
: { provider: { "@id": input.provider } }),
|
|
130
|
-
...audienceFor(input.audience),
|
|
131
|
-
...areasFor(input.areaServed),
|
|
115
|
+
description: input.description,
|
|
116
|
+
serviceType: input.serviceType,
|
|
117
|
+
provider: reference(input.provider),
|
|
118
|
+
audience: audienceFor(input.audience),
|
|
119
|
+
areaServed: areasFor(input.areaServed),
|
|
132
120
|
};
|
|
133
121
|
}
|
|
134
122
|
|
|
@@ -212,9 +200,7 @@ export function offerCatalog(input: OfferCatalogInput): JsonLdNode {
|
|
|
212
200
|
return {
|
|
213
201
|
"@type": "OfferCatalog",
|
|
214
202
|
name: input.name,
|
|
215
|
-
|
|
216
|
-
? {}
|
|
217
|
-
: { description: input.description }),
|
|
203
|
+
description: input.description,
|
|
218
204
|
// Unconditional: this is the node's only link to anything.
|
|
219
205
|
provider: { "@id": input.provider },
|
|
220
206
|
itemListElement: input.entries.map((entry) => ({
|
|
@@ -224,13 +210,11 @@ export function offerCatalog(input: OfferCatalogInput): JsonLdNode {
|
|
|
224
210
|
// The id only where there is a page to hang it on. A fragment
|
|
225
211
|
// invented for an entry with no page would be an identity for
|
|
226
212
|
// something nothing else can ever refer to.
|
|
227
|
-
|
|
228
|
-
?
|
|
229
|
-
|
|
213
|
+
"@id":
|
|
214
|
+
entry.url === undefined ? undefined : serviceId(entry.url),
|
|
215
|
+
url: entry.url,
|
|
230
216
|
name: entry.name,
|
|
231
|
-
|
|
232
|
-
? {}
|
|
233
|
-
: { description: entry.description }),
|
|
217
|
+
description: entry.description,
|
|
234
218
|
},
|
|
235
219
|
})),
|
|
236
220
|
};
|
package/src/jsonld/video.ts
CHANGED
|
@@ -227,13 +227,9 @@ export function videoObject(video: VideoInput): JsonLdNode {
|
|
|
227
227
|
)
|
|
228
228
|
),
|
|
229
229
|
uploadDate: video.uploadDate,
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
...(video.contentUrl === undefined
|
|
235
|
-
? {}
|
|
236
|
-
: { contentUrl: video.contentUrl }),
|
|
237
|
-
...(video.embedUrl === undefined ? {} : { embedUrl: video.embedUrl }),
|
|
230
|
+
description: video.description,
|
|
231
|
+
duration,
|
|
232
|
+
contentUrl: video.contentUrl,
|
|
233
|
+
embedUrl: video.embedUrl,
|
|
238
234
|
};
|
|
239
235
|
}
|
package/src/jsonld/website.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HttpsUrl } from "../url.ts";
|
|
2
2
|
import { type OrganizationId, websiteId } from "./ids.ts";
|
|
3
|
-
import { alternateName, type JsonLdNode } from "./node.ts";
|
|
3
|
+
import { alternateName, type JsonLdNode, reference } from "./node.ts";
|
|
4
4
|
|
|
5
5
|
export interface WebsiteInput {
|
|
6
6
|
/**
|
|
@@ -50,9 +50,7 @@ export function website(site: WebsiteInput): JsonLdNode {
|
|
|
50
50
|
"@id": websiteId(site.url),
|
|
51
51
|
name: site.name,
|
|
52
52
|
url: site.url,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
? {}
|
|
56
|
-
: { publisher: { "@id": site.publisher } }),
|
|
53
|
+
alternateName: alternateName(site.name, site.alternateName),
|
|
54
|
+
publisher: reference(site.publisher),
|
|
57
55
|
};
|
|
58
56
|
}
|
package/src/meta/content.ts
CHANGED
|
@@ -9,32 +9,9 @@ import type { RobotsPolicy } from "./robots.ts";
|
|
|
9
9
|
import type { ShareImage } from "./share-image.ts";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* What only a consumer can supply: a page's words, and
|
|
12
|
+
* What only a consumer can supply: a page's words, and whether it is an article.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
* What kind of thing a page represents, for `og:type`.
|
|
17
|
-
*
|
|
18
|
-
* The whole vocabulary Open Graph defines, and a union for the reason
|
|
19
|
-
* `RobotsDirective` is one: it is small and closed, and a value outside it is
|
|
20
|
-
* not an error anywhere — a scraper that does not recognise `og:type` falls
|
|
21
|
-
* back to `website` and renders a perfectly ordinary card. So `"artcile"` costs
|
|
22
|
-
* you the article treatment and looks exactly like success.
|
|
23
|
-
*
|
|
24
|
-
* `website` is the default and the right answer for most pages. `article` is
|
|
25
|
-
* the one that earns anything extra: it is what tells a scraper this is a dated
|
|
26
|
-
* piece of writing rather than a section of the site.
|
|
27
|
-
*
|
|
28
|
-
* Reference: <https://ogp.me/#types>
|
|
29
|
-
*/
|
|
30
|
-
export type OpenGraphType =
|
|
31
|
-
| "website"
|
|
32
|
-
| "article"
|
|
33
|
-
| "book"
|
|
34
|
-
| "profile"
|
|
35
|
-
| `music.${"song" | "album" | "playlist" | "radio_station"}`
|
|
36
|
-
| `video.${"movie" | "episode" | "tv_show" | "other"}`;
|
|
37
|
-
|
|
38
15
|
/**
|
|
39
16
|
* A page that is a dated piece of writing, said once.
|
|
40
17
|
*
|
|
@@ -48,10 +25,11 @@ export type OpenGraphType =
|
|
|
48
25
|
* Supplying this is what emits the `Article` node, `og:type: article` and the
|
|
49
26
|
* `article:*` properties together. They are three statements of one fact, and a
|
|
50
27
|
* page that could make them separately is a page that could make two of them.
|
|
51
|
-
* That is why `
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
28
|
+
* That is why there is no `type` to set: saying a page is an article without
|
|
29
|
+
* saying when it was published is the half-statement this exists to prevent.
|
|
30
|
+
* Open Graph's other kinds (`book`, `profile`, `video.*`) each come with fields
|
|
31
|
+
* of their own; one would be added the same way, as a field that is the
|
|
32
|
+
* description, with the two made exclusive then.
|
|
55
33
|
*/
|
|
56
34
|
export interface ArticleContent {
|
|
57
35
|
/**
|
|
@@ -99,34 +77,8 @@ export interface ArticleContent {
|
|
|
99
77
|
readonly images?: readonly ImageAsset[];
|
|
100
78
|
}
|
|
101
79
|
|
|
102
|
-
/**
|
|
103
|
-
|
|
104
|
-
*
|
|
105
|
-
* A union rather than two optional fields, so the pair cannot both be set. They
|
|
106
|
-
* answer the same question — `article` decides `og:type` — and a page that
|
|
107
|
-
* supplied both would have one of them silently ignored, which is the shape of
|
|
108
|
-
* mistake this folder keeps removing rather than reporting.
|
|
109
|
-
*
|
|
110
|
-
* Both branches allow the fields to be absent, so an ordinary page writes
|
|
111
|
-
* neither and gets `website`.
|
|
112
|
-
*/
|
|
113
|
-
export type PageKind =
|
|
114
|
-
| {
|
|
115
|
-
readonly type?: Exclude<OpenGraphType, "article">;
|
|
116
|
-
readonly article?: never;
|
|
117
|
-
}
|
|
118
|
-
| {
|
|
119
|
-
readonly type?: never;
|
|
120
|
-
/**
|
|
121
|
-
* This page's publication details, if it is a dated piece of writing.
|
|
122
|
-
*
|
|
123
|
-
* One field, three outputs — see `ArticleContent`.
|
|
124
|
-
*/
|
|
125
|
-
readonly article: ArticleContent;
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
/** The fields every page has, whatever kind it is. */
|
|
129
|
-
export interface MetaContentBase {
|
|
80
|
+
/** What a page passes to `metaFor`: its words, its picture, and whether it is an article. */
|
|
81
|
+
export interface MetaContent {
|
|
130
82
|
readonly title: string;
|
|
131
83
|
/**
|
|
132
84
|
* Required: a page without a description leaves search engines and every
|
|
@@ -172,18 +124,14 @@ export interface MetaContentBase {
|
|
|
172
124
|
* and never sees the page they end up on.
|
|
173
125
|
*/
|
|
174
126
|
readonly jsonLd?: readonly JsonLdNode[];
|
|
127
|
+
/**
|
|
128
|
+
* This page's publication details, if it is a dated piece of writing:
|
|
129
|
+
* `og:type` becomes `article`, with its `article:*` properties and an
|
|
130
|
+
* `Article` node. Without it a page is a `website`. See `ArticleContent`.
|
|
131
|
+
*/
|
|
132
|
+
readonly article?: ArticleContent;
|
|
175
133
|
}
|
|
176
134
|
|
|
177
|
-
/**
|
|
178
|
-
* What a page passes to `metaFor`: its words, its picture, and what kind of
|
|
179
|
-
* thing it is.
|
|
180
|
-
*
|
|
181
|
-
* An intersection rather than one interface because `PageKind` is a union, and
|
|
182
|
-
* that union is the whole point — see it for why `type` and `article` cannot
|
|
183
|
-
* both be written.
|
|
184
|
-
*/
|
|
185
|
-
export type MetaContent = MetaContentBase & PageKind;
|
|
186
|
-
|
|
187
135
|
/** Browser-UI tint. A single colour, or one per `prefers-color-scheme`. */
|
|
188
136
|
export type ThemeColor =
|
|
189
137
|
| string
|