@getvitops/utils 8.2.0-alpha.0 → 8.3.0-alpha.0
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/CHANGELOG.md +399 -23
- package/dist/{adc-D6aw2qgq.mjs → adc-BiW9BzqA.mjs} +2 -3
- package/dist/{adc-D6aw2qgq.mjs.map → adc-BiW9BzqA.mjs.map} +1 -1
- package/dist/{adc-BoEoogyo.d.mts → adc-DvX3-P7a.d.mts} +2 -4
- package/dist/adc-DvX3-P7a.d.mts.map +1 -0
- package/dist/ads/index.d.mts +37 -57
- package/dist/ads/index.d.mts.map +1 -1
- package/dist/ads/index.mjs +21 -10
- package/dist/ads/index.mjs.map +1 -1
- package/dist/audit/index.d.mts +180 -0
- package/dist/audit/index.d.mts.map +1 -0
- package/dist/audit/index.mjs +509 -0
- package/dist/audit/index.mjs.map +1 -0
- package/dist/color/index.d.mts +7 -5
- package/dist/color/index.d.mts.map +1 -1
- package/dist/color/index.mjs +10 -5
- package/dist/color/index.mjs.map +1 -1
- package/dist/content.d.mts +78 -0
- package/dist/content.d.mts.map +1 -0
- package/dist/content.mjs +45 -0
- package/dist/content.mjs.map +1 -0
- package/dist/crm/index.d.mts +5 -87
- package/dist/crm/index.d.mts.map +1 -1
- package/dist/crm/index.mjs +10 -19
- package/dist/crm/index.mjs.map +1 -1
- package/dist/email/index.d.mts +1 -11
- package/dist/email/index.d.mts.map +1 -1
- package/dist/email/index.mjs +6 -10
- package/dist/email/index.mjs.map +1 -1
- package/dist/favicon.d.mts +0 -2
- package/dist/favicon.d.mts.map +1 -1
- package/dist/favicon.mjs +1 -2
- package/dist/git/index.d.mts +0 -2
- package/dist/git/index.d.mts.map +1 -1
- package/dist/git/index.mjs +1 -2
- package/dist/hosting/index.d.mts +0 -8
- package/dist/hosting/index.d.mts.map +1 -1
- package/dist/hosting/index.mjs +4 -7
- package/dist/hosting/index.mjs.map +1 -1
- package/dist/{identity-BoRddTx3.mjs → identity-C4yInICW.mjs} +2 -3
- package/dist/{identity-BoRddTx3.mjs.map → identity-C4yInICW.mjs.map} +1 -1
- package/dist/index.d.mts +2 -88
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +36 -52
- package/dist/index.mjs.map +1 -1
- package/dist/indexing/index.d.mts +3 -42
- package/dist/indexing/index.d.mts.map +1 -1
- package/dist/indexing/index.mjs +8 -13
- package/dist/indexing/index.mjs.map +1 -1
- package/dist/{json-store-30CFCbZZ.mjs → json-store-VJKEaoVX.mjs} +2 -3
- package/dist/{json-store-30CFCbZZ.mjs.map → json-store-VJKEaoVX.mjs.map} +1 -1
- package/dist/media/index.d.mts +0 -12
- package/dist/media/index.d.mts.map +1 -1
- package/dist/media/index.mjs +7 -13
- package/dist/media/index.mjs.map +1 -1
- package/dist/notify/index.d.mts +64 -44
- package/dist/notify/index.d.mts.map +1 -1
- package/dist/notify/index.mjs +138 -11
- package/dist/notify/index.mjs.map +1 -1
- package/dist/onboarding/index.d.mts +26 -6
- package/dist/onboarding/index.d.mts.map +1 -1
- package/dist/onboarding/index.mjs +92 -5
- package/dist/onboarding/index.mjs.map +1 -1
- package/dist/{plan-BLKikoOG.mjs → plan-B9USuc17.mjs} +3 -4
- package/dist/{plan-BLKikoOG.mjs.map → plan-B9USuc17.mjs.map} +1 -1
- package/dist/{plan-Hq8Pb0Uw.d.mts → plan-CH3Wt5ZV.d.mts} +8 -30
- package/dist/plan-CH3Wt5ZV.d.mts.map +1 -0
- package/dist/plan-CddqsmJJ.d.mts +82 -0
- package/dist/plan-CddqsmJJ.d.mts.map +1 -0
- package/dist/plan-DpHv6NO8.mjs +93 -0
- package/dist/plan-DpHv6NO8.mjs.map +1 -0
- package/dist/{plan-format-A18SvJ7C.mjs → plan-format-CIz6Feg2.mjs} +2 -3
- package/dist/{plan-format-A18SvJ7C.mjs.map → plan-format-CIz6Feg2.mjs.map} +1 -1
- package/dist/{platform-0nlOIy8h.mjs → platform-Cb_RtsF1.mjs} +3 -5
- package/dist/{platform-0nlOIy8h.mjs.map → platform-Cb_RtsF1.mjs.map} +1 -1
- package/dist/reviews/index.d.mts +4 -120
- package/dist/reviews/index.d.mts.map +1 -1
- package/dist/reviews/index.mjs +6 -99
- package/dist/reviews/index.mjs.map +1 -1
- package/dist/reviews/select.d.mts +2 -0
- package/dist/reviews/select.mjs +3 -0
- package/dist/schema/index.d.mts +0 -12
- package/dist/schema/index.d.mts.map +1 -1
- package/dist/schema/index.mjs +6 -12
- package/dist/schema/index.mjs.map +1 -1
- package/dist/security/index.d.mts +0 -2
- package/dist/security/index.d.mts.map +1 -1
- package/dist/security/index.mjs +1 -2
- package/dist/{token-DmmoQ7um.d.mts → token-CHR1O4w5.d.mts} +3 -3
- package/dist/token-CHR1O4w5.d.mts.map +1 -0
- package/dist/{token-DkKC5m23.mjs → token-CJSl9eIS.mjs} +33 -14
- package/dist/token-CJSl9eIS.mjs.map +1 -0
- package/dist/tracking/index.d.mts +1 -9
- package/dist/tracking/index.d.mts.map +1 -1
- package/dist/tracking/index.mjs +3 -4
- package/dist/turnstile/index.d.mts +105 -0
- package/dist/turnstile/index.d.mts.map +1 -0
- package/dist/turnstile/index.mjs +281 -0
- package/dist/turnstile/index.mjs.map +1 -0
- package/dist/{types-7ELci8RQ.d.mts → types-oac2P5or.d.mts} +3 -23
- package/dist/types-oac2P5or.d.mts.map +1 -0
- package/dist/url/index.d.mts +18 -0
- package/dist/url/index.d.mts.map +1 -0
- package/dist/url/index.mjs +34 -0
- package/dist/url/index.mjs.map +1 -0
- package/package.json +43 -2
- package/dist/adc-BoEoogyo.d.mts.map +0 -1
- package/dist/cloudflare-BiK61Oq4.d.mts +0 -27
- package/dist/cloudflare-BiK61Oq4.d.mts.map +0 -1
- package/dist/cloudflare-CD8vwrgj.mjs +0 -92
- package/dist/cloudflare-CD8vwrgj.mjs.map +0 -1
- package/dist/plan-Hq8Pb0Uw.d.mts.map +0 -1
- package/dist/token-DkKC5m23.mjs.map +0 -1
- package/dist/token-DmmoQ7um.d.mts.map +0 -1
- package/dist/types-7ELci8RQ.d.mts.map +0 -1
package/dist/reviews/index.d.mts
CHANGED
|
@@ -1,119 +1,6 @@
|
|
|
1
|
-
import { a as SCOPES, c as googleHeaders, i as GoogleOAuth, n as GoogleAuthLike, r as GoogleCredential, s as googleAccessToken, t as GoogleAuth } from "../token-
|
|
2
|
-
import { n as adcQuotaProject, r as parseAdcUser, t as adcCredentialsPath } from "../adc-
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* The reviews module's own option surface.
|
|
6
|
-
*
|
|
7
|
-
* Deliberately structural rather than `Config['site']['reviews']` imported from
|
|
8
|
-
* `@getvitops/generator` — the generator already depends on this package, so
|
|
9
|
-
* importing back would be a cycle. The vocabulary mirrors that block field for
|
|
10
|
-
* field, the same arrangement as `IndexingConfig` in `../indexing/types.ts`.
|
|
11
|
-
*/
|
|
12
|
-
/** How one location's reviews should be selected and ordered for display. */
|
|
13
|
-
interface ReviewLocationConfig {
|
|
14
|
-
/** GBP account resource id (`accounts/…`), overriding the top-level default. */
|
|
15
|
-
accountId?: string | undefined;
|
|
16
|
-
/** GBP location resource id (`locations/…`) — required to fetch this location. */
|
|
17
|
-
locationId: string;
|
|
18
|
-
/** Explicit review ids, in order. These are always included, ahead of everything else. */
|
|
19
|
-
featured?: string[] | undefined;
|
|
20
|
-
/** Drop reviews below this star rating (default 4). */
|
|
21
|
-
minRating?: number | undefined;
|
|
22
|
-
/** Keep at most this many reviews after filtering. */
|
|
23
|
-
limit?: number | undefined;
|
|
24
|
-
/** Ordering for the non-featured remainder (default `recent`). */
|
|
25
|
-
sort?: 'recent' | 'rating' | undefined;
|
|
26
|
-
}
|
|
27
|
-
/** Everything `plan()` needs to decide what to fetch. */
|
|
28
|
-
interface ReviewsConfig {
|
|
29
|
-
/** Default GBP account resource id for locations that don't override it. */
|
|
30
|
-
accountId?: string | undefined;
|
|
31
|
-
/** Keyed by `organization.locations[].key`. */
|
|
32
|
-
locations: Record<string, ReviewLocationConfig>;
|
|
33
|
-
}
|
|
34
|
-
/** One review, as GBP returns it and the cache stores it. */
|
|
35
|
-
interface Review {
|
|
36
|
-
id: string;
|
|
37
|
-
/** The `organization.locations[]` key this review belongs to. */
|
|
38
|
-
location: string;
|
|
39
|
-
/** 1–5. */
|
|
40
|
-
rating: number;
|
|
41
|
-
author: string;
|
|
42
|
-
authorPhoto?: string | undefined;
|
|
43
|
-
text?: string | undefined;
|
|
44
|
-
publishedAt: string;
|
|
45
|
-
updatedAt: string;
|
|
46
|
-
reply?: {
|
|
47
|
-
text: string;
|
|
48
|
-
at: string;
|
|
49
|
-
} | undefined;
|
|
50
|
-
/** Link back to the review on Google — never omit when linking to it. */
|
|
51
|
-
url?: string | undefined;
|
|
52
|
-
}
|
|
53
|
-
/** What a fetch of one location produced. */
|
|
54
|
-
interface LocationReviews {
|
|
55
|
-
locationId: string;
|
|
56
|
-
accountId: string;
|
|
57
|
-
fetchedAt: string;
|
|
58
|
-
averageRating?: number | undefined;
|
|
59
|
-
totalReviewCount?: number | undefined;
|
|
60
|
-
reviews: Review[];
|
|
61
|
-
}
|
|
62
|
-
/** The on-disk cache, as written by `writeCache`. */
|
|
63
|
-
interface ReviewCache {
|
|
64
|
-
version: 1;
|
|
65
|
-
locations: Record<string, LocationReviews>;
|
|
66
|
-
}
|
|
67
|
-
//#endregion
|
|
68
|
-
//#region src/reviews/plan.d.ts
|
|
69
|
-
/** One location's fetch plan, or the reason it was skipped. */
|
|
70
|
-
interface LocationPlan {
|
|
71
|
-
key: string;
|
|
72
|
-
accountId?: string;
|
|
73
|
-
locationId?: string;
|
|
74
|
-
/** Present iff this location will not be fetched. */
|
|
75
|
-
skip?: string;
|
|
76
|
-
}
|
|
77
|
-
interface ReviewsPlan {
|
|
78
|
-
/** Set when the whole run is refused — nothing is fetched. */
|
|
79
|
-
blocked?: string;
|
|
80
|
-
locations: LocationPlan[];
|
|
81
|
-
notes: string[];
|
|
82
|
-
}
|
|
83
|
-
interface PlanInput {
|
|
84
|
-
config: ReviewsConfig | undefined;
|
|
85
|
-
/** Location keys with a Google listing configured (`organization.locations[].listings.google`). */
|
|
86
|
-
knownLocations: Set<string>;
|
|
87
|
-
/** `--location`: fetch exactly this one, skipping the rest. */
|
|
88
|
-
onlyLocation?: string | undefined;
|
|
89
|
-
/** Whether a Google credential was resolved. */
|
|
90
|
-
hasCredential: boolean;
|
|
91
|
-
}
|
|
92
|
-
/** Decide which locations to fetch, and why any are skipped. */
|
|
93
|
-
export declare function plan(input: PlanInput): ReviewsPlan;
|
|
94
|
-
/** Render a plan as the human-readable block `--dry` prints. */
|
|
95
|
-
export declare function formatPlan(p: ReviewsPlan): string;
|
|
96
|
-
/**
|
|
97
|
-
* Filter and order one location's reviews for display.
|
|
98
|
-
*
|
|
99
|
-
* Featured ids always win and keep the author's order — an explicit choice
|
|
100
|
-
* overrides any automatic ranking. The remainder is filtered by `minRating`
|
|
101
|
-
* (default 4 — a star-rating widget showing a 1-star review it did not have to
|
|
102
|
-
* is a design choice a config field should not have to spell out every time),
|
|
103
|
-
* sorted, then capped by `limit`.
|
|
104
|
-
*/
|
|
105
|
-
export declare function selectReviews(reviews: Review[], config: ReviewLocationConfig): Review[];
|
|
106
|
-
/**
|
|
107
|
-
* Merge a fresh fetch into the existing cache.
|
|
108
|
-
*
|
|
109
|
-
* Only the locations actually fetched this run are replaced; every other
|
|
110
|
-
* location's prior reviews pass through untouched. This is what makes a partial
|
|
111
|
-
* failure (one location's fetch errors) non-destructive — `cmdReviews` merges
|
|
112
|
-
* only the locations that succeeded and leaves the rest of the cache alone.
|
|
113
|
-
*/
|
|
114
|
-
export declare function mergeCache(existing: ReviewCache | undefined, fetched: Record<string, LocationReviews>): ReviewCache;
|
|
115
|
-
//#endregion
|
|
116
|
-
//#region src/reviews/cache.d.ts
|
|
1
|
+
import { a as SCOPES, c as googleHeaders, i as GoogleOAuth, n as GoogleAuthLike, r as GoogleCredential, s as googleAccessToken, t as GoogleAuth } from "../token-CHR1O4w5.mjs";
|
|
2
|
+
import { n as adcQuotaProject, r as parseAdcUser, t as adcCredentialsPath } from "../adc-DvX3-P7a.mjs";
|
|
3
|
+
import { a as mergeCache, c as LocationReviews, d as ReviewLocationConfig, f as ReviewsConfig, i as formatPlan, l as Review, n as PlanInput, o as plan, r as ReviewsPlan, s as selectReviews, t as LocationPlan, u as ReviewCache } from "../plan-CddqsmJJ.mjs";
|
|
117
4
|
/** Default location, relative to the consumer's project root. */
|
|
118
5
|
export declare const REVIEWS_CACHE_PATH = ".vitops/reviews.json";
|
|
119
6
|
/**
|
|
@@ -129,8 +16,6 @@ export declare function readCache(path: string): ReviewCache | undefined;
|
|
|
129
16
|
export declare function toCache(locations: Record<string, LocationReviews>): ReviewCache;
|
|
130
17
|
/** Write the cache, creating its directory. */
|
|
131
18
|
export declare function writeCache(path: string, cache: ReviewCache): void;
|
|
132
|
-
//#endregion
|
|
133
|
-
//#region src/reviews/gbp.d.ts
|
|
134
19
|
/** One page (or, having paginated, the whole set) of a location's reviews. */
|
|
135
20
|
interface FetchReviewsResult {
|
|
136
21
|
ok: boolean;
|
|
@@ -148,6 +33,5 @@ interface FetchReviewsResult {
|
|
|
148
33
|
* `../indexing/gsc.ts`'s `inspectUrl` keeps.
|
|
149
34
|
*/
|
|
150
35
|
export declare function fetchReviews(auth: GoogleAuthLike, accountId: string, locationId: string, fetchImpl?: typeof fetch): Promise<FetchReviewsResult>;
|
|
151
|
-
|
|
152
|
-
export { type FetchReviewsResult, type GoogleAuth, type GoogleAuthLike, type GoogleCredential, type GoogleOAuth, type LocationPlan, type LocationReviews, type PlanInput, type Review, type ReviewCache, type ReviewLocationConfig, type ReviewsConfig, type ReviewsPlan, SCOPES, adcCredentialsPath, adcQuotaProject, googleAccessToken, googleHeaders, parseAdcUser };
|
|
36
|
+
export { type FetchReviewsResult, type GoogleAuth, type GoogleAuthLike, type GoogleCredential, type GoogleOAuth, type LocationPlan, type LocationReviews, type PlanInput, type Review, type ReviewCache, type ReviewLocationConfig, type ReviewsConfig, type ReviewsPlan, SCOPES, adcCredentialsPath, adcQuotaProject, formatPlan, googleAccessToken, googleHeaders, mergeCache, parseAdcUser, plan, selectReviews };
|
|
153
37
|
//# sourceMappingURL=index.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/reviews/
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/reviews/cache.ts","../../src/reviews/gbp.ts"],"mappings":";;;;qBAca;;;;;;;;;wBAUG,UAAU,eAAe;;wBAKzB,QAAQ,WAAW,eAAe,mBAAmB;;wBAarD,WAAW,cAAc,OAAO;;UCoB/B;EACf;EACA,SAAS;EACT;EACA;EACA;EACA;;;;;;;;;wBAUoB,aACpB,MAAM,gBACN,mBACA,oBACA,mBAAkB,QACjB,QAAQ"}
|
package/dist/reviews/index.mjs
CHANGED
|
@@ -1,100 +1,9 @@
|
|
|
1
|
-
import { n as writeVersioned, t as readVersioned } from "../json-store-
|
|
2
|
-
import { a as googleHeaders, i as googleAccessToken, t as SCOPES } from "../token-
|
|
3
|
-
import { n as adcQuotaProject, r as parseAdcUser, t as adcCredentialsPath } from "../adc-
|
|
1
|
+
import { n as writeVersioned, t as readVersioned } from "../json-store-VJKEaoVX.mjs";
|
|
2
|
+
import { a as googleHeaders, i as googleAccessToken, t as SCOPES } from "../token-CJSl9eIS.mjs";
|
|
3
|
+
import { n as adcQuotaProject, r as parseAdcUser, t as adcCredentialsPath } from "../adc-BiW9BzqA.mjs";
|
|
4
|
+
import { i as selectReviews, n as mergeCache, r as plan, t as formatPlan } from "../plan-DpHv6NO8.mjs";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
/** Decide which locations to fetch, and why any are skipped. */
|
|
7
|
-
function plan(input) {
|
|
8
|
-
const { config, knownLocations, onlyLocation, hasCredential } = input;
|
|
9
|
-
const notes = [];
|
|
10
|
-
if (!config || Object.keys(config.locations).length === 0) return {
|
|
11
|
-
blocked: "no site.reviews.locations configured — nothing to fetch",
|
|
12
|
-
locations: [],
|
|
13
|
-
notes
|
|
14
|
-
};
|
|
15
|
-
if (!hasCredential) return {
|
|
16
|
-
blocked: "no Google credential — set VITOPS_GOOGLE_CLIENT_ID / _CLIENT_SECRET / _REFRESH_TOKEN, or run `gcloud auth application-default login`",
|
|
17
|
-
locations: [],
|
|
18
|
-
notes
|
|
19
|
-
};
|
|
20
|
-
const entries = onlyLocation ? Object.entries(config.locations).filter(([key]) => key === onlyLocation) : Object.entries(config.locations);
|
|
21
|
-
if (onlyLocation && entries.length === 0) return {
|
|
22
|
-
blocked: `--location "${onlyLocation}" is not in site.reviews.locations`,
|
|
23
|
-
locations: [],
|
|
24
|
-
notes
|
|
25
|
-
};
|
|
26
|
-
const locations = entries.map(([key, loc]) => {
|
|
27
|
-
if (!knownLocations.has(key)) return {
|
|
28
|
-
key,
|
|
29
|
-
skip: `organization.locations has no entry keyed "${key}" with a listings.google id`
|
|
30
|
-
};
|
|
31
|
-
const accountId = loc.accountId ?? config.accountId;
|
|
32
|
-
if (!accountId) return {
|
|
33
|
-
key,
|
|
34
|
-
skip: `no accountId — set site.reviews.accountId or locations.${key}.accountId`
|
|
35
|
-
};
|
|
36
|
-
return {
|
|
37
|
-
key,
|
|
38
|
-
accountId,
|
|
39
|
-
locationId: loc.locationId
|
|
40
|
-
};
|
|
41
|
-
});
|
|
42
|
-
const blocked = locations.filter((l) => l.skip);
|
|
43
|
-
if (blocked.length) for (const l of blocked) notes.push(`${l.key}: skipped — ${l.skip}`);
|
|
44
|
-
return {
|
|
45
|
-
locations,
|
|
46
|
-
notes
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
/** Render a plan as the human-readable block `--dry` prints. */
|
|
50
|
-
function formatPlan(p) {
|
|
51
|
-
if (p.blocked) return `✗ blocked — ${p.blocked}`;
|
|
52
|
-
const lines = [];
|
|
53
|
-
const active = p.locations.filter((l) => !l.skip);
|
|
54
|
-
lines.push(`locations ${active.length} of ${p.locations.length} will be fetched`);
|
|
55
|
-
for (const l of active) lines.push(` ${l.key} ${l.accountId}/${l.locationId}`);
|
|
56
|
-
for (const n of p.notes) lines.push(` ! ${n}`);
|
|
57
|
-
return lines.join("\n");
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Filter and order one location's reviews for display.
|
|
61
|
-
*
|
|
62
|
-
* Featured ids always win and keep the author's order — an explicit choice
|
|
63
|
-
* overrides any automatic ranking. The remainder is filtered by `minRating`
|
|
64
|
-
* (default 4 — a star-rating widget showing a 1-star review it did not have to
|
|
65
|
-
* is a design choice a config field should not have to spell out every time),
|
|
66
|
-
* sorted, then capped by `limit`.
|
|
67
|
-
*/
|
|
68
|
-
function selectReviews(reviews, config) {
|
|
69
|
-
const featuredIds = config.featured ?? [];
|
|
70
|
-
const byId = new Map(reviews.map((r) => [r.id, r]));
|
|
71
|
-
const featured = featuredIds.map((id) => byId.get(id)).filter((r) => !!r);
|
|
72
|
-
const featuredSet = new Set(featured.map((r) => r.id));
|
|
73
|
-
const minRating = config.minRating ?? 4;
|
|
74
|
-
const rest = reviews.filter((r) => !featuredSet.has(r.id) && r.rating >= minRating).sort((a, b) => config.sort === "rating" ? b.rating - a.rating : new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime());
|
|
75
|
-
const combined = [...featured, ...rest];
|
|
76
|
-
return config.limit !== void 0 ? combined.slice(0, config.limit) : combined;
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Merge a fresh fetch into the existing cache.
|
|
80
|
-
*
|
|
81
|
-
* Only the locations actually fetched this run are replaced; every other
|
|
82
|
-
* location's prior reviews pass through untouched. This is what makes a partial
|
|
83
|
-
* failure (one location's fetch errors) non-destructive — `cmdReviews` merges
|
|
84
|
-
* only the locations that succeeded and leaves the rest of the cache alone.
|
|
85
|
-
*/
|
|
86
|
-
function mergeCache(existing, fetched) {
|
|
87
|
-
return {
|
|
88
|
-
version: 1,
|
|
89
|
-
locations: {
|
|
90
|
-
...existing?.locations,
|
|
91
|
-
...fetched
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
//#endregion
|
|
97
|
-
//#region src/reviews/cache.ts
|
|
6
|
+
// src/reviews/cache.ts
|
|
98
7
|
/** Default location, relative to the consumer's project root. */
|
|
99
8
|
const REVIEWS_CACHE_PATH = ".vitops/reviews.json";
|
|
100
9
|
/**
|
|
@@ -128,8 +37,7 @@ function writeCache(path, cache) {
|
|
|
128
37
|
writeVersioned(path, cache);
|
|
129
38
|
}
|
|
130
39
|
|
|
131
|
-
|
|
132
|
-
//#region src/reviews/gbp.ts
|
|
40
|
+
// src/reviews/gbp.ts
|
|
133
41
|
/**
|
|
134
42
|
* Google Business Profile — fetching one location's reviews.
|
|
135
43
|
*
|
|
@@ -213,6 +121,5 @@ async function fetchReviews(auth, accountId, locationId, fetchImpl = fetch) {
|
|
|
213
121
|
};
|
|
214
122
|
}
|
|
215
123
|
|
|
216
|
-
//#endregion
|
|
217
124
|
export { REVIEWS_CACHE_PATH, SCOPES, adcCredentialsPath, adcQuotaProject, fetchReviews, formatPlan, googleAccessToken, googleHeaders, mergeCache, parseAdcUser, plan, readCache, selectReviews, toCache, writeCache };
|
|
218
125
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/reviews/plan.ts","../../src/reviews/cache.ts","../../src/reviews/gbp.ts"],"sourcesContent":["/**\n * Every decision `vitops reviews` makes, as pure functions.\n *\n * Same split as `../indexing/plan.ts`: no `fetch`, no `fs`, no clock. Which\n * locations to fetch (and why one is blocked), and how a location's reviews are\n * filtered down to what a site actually displays, are decided here — `gbp.ts`\n * and `cache.ts` do what they're told and nothing else. That is what lets Astro's\n * `<ReviewCard>` and a future Bricks renderer both call `selectReviews` instead\n * of each re-implementing \"top 5 by rating, minus the ones already featured\".\n */\nimport type {\n LocationReviews,\n Review,\n ReviewCache,\n ReviewLocationConfig,\n ReviewsConfig,\n} from './types.ts';\n\n/** One location's fetch plan, or the reason it was skipped. */\nexport interface LocationPlan {\n key: string;\n accountId?: string;\n locationId?: string;\n /** Present iff this location will not be fetched. */\n skip?: string;\n}\n\nexport interface ReviewsPlan {\n /** Set when the whole run is refused — nothing is fetched. */\n blocked?: string;\n locations: LocationPlan[];\n notes: string[];\n}\n\nexport interface PlanInput {\n config: ReviewsConfig | undefined;\n /** Location keys with a Google listing configured (`organization.locations[].listings.google`). */\n knownLocations: Set<string>;\n /** `--location`: fetch exactly this one, skipping the rest. */\n onlyLocation?: string | undefined;\n /** Whether a Google credential was resolved. */\n hasCredential: boolean;\n}\n\n/** Decide which locations to fetch, and why any are skipped. */\nexport function plan(input: PlanInput): ReviewsPlan {\n const { config, knownLocations, onlyLocation, hasCredential } = input;\n const notes: string[] = [];\n\n if (!config || Object.keys(config.locations).length === 0) {\n return {\n blocked: 'no site.reviews.locations configured — nothing to fetch',\n locations: [],\n notes,\n };\n }\n\n if (!hasCredential) {\n return {\n blocked:\n 'no Google credential — set VITOPS_GOOGLE_CLIENT_ID / _CLIENT_SECRET / _REFRESH_TOKEN, or run `gcloud auth application-default login`',\n locations: [],\n notes,\n };\n }\n\n const entries = onlyLocation\n ? Object.entries(config.locations).filter(([key]) => key === onlyLocation)\n : Object.entries(config.locations);\n\n if (onlyLocation && entries.length === 0) {\n return {\n blocked: `--location \"${onlyLocation}\" is not in site.reviews.locations`,\n locations: [],\n notes,\n };\n }\n\n const locations: LocationPlan[] = entries.map(([key, loc]) => {\n if (!knownLocations.has(key))\n return {\n key,\n skip: `organization.locations has no entry keyed \"${key}\" with a listings.google id`,\n };\n const accountId = loc.accountId ?? config.accountId;\n if (!accountId)\n return {\n key,\n skip: `no accountId — set site.reviews.accountId or locations.${key}.accountId`,\n };\n return { key, accountId, locationId: loc.locationId };\n });\n\n const blocked = locations.filter((l) => l.skip);\n if (blocked.length) for (const l of blocked) notes.push(`${l.key}: skipped — ${l.skip}`);\n\n return { locations, notes };\n}\n\n/** Render a plan as the human-readable block `--dry` prints. */\nexport function formatPlan(p: ReviewsPlan): string {\n if (p.blocked) return `✗ blocked — ${p.blocked}`;\n const lines: string[] = [];\n const active = p.locations.filter((l) => !l.skip);\n lines.push(`locations ${active.length} of ${p.locations.length} will be fetched`);\n for (const l of active) lines.push(` ${l.key} ${l.accountId}/${l.locationId}`);\n for (const n of p.notes) lines.push(` ! ${n}`);\n return lines.join('\\n');\n}\n\n/**\n * Filter and order one location's reviews for display.\n *\n * Featured ids always win and keep the author's order — an explicit choice\n * overrides any automatic ranking. The remainder is filtered by `minRating`\n * (default 4 — a star-rating widget showing a 1-star review it did not have to\n * is a design choice a config field should not have to spell out every time),\n * sorted, then capped by `limit`.\n */\nexport function selectReviews(reviews: Review[], config: ReviewLocationConfig): Review[] {\n const featuredIds = config.featured ?? [];\n const byId = new Map(reviews.map((r) => [r.id, r]));\n const featured = featuredIds.map((id) => byId.get(id)).filter((r): r is Review => !!r);\n const featuredSet = new Set(featured.map((r) => r.id));\n\n const minRating = config.minRating ?? 4;\n const rest = reviews\n .filter((r) => !featuredSet.has(r.id) && r.rating >= minRating)\n .sort((a, b) =>\n config.sort === 'rating'\n ? b.rating - a.rating\n : new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime(),\n );\n\n const combined = [...featured, ...rest];\n return config.limit !== undefined ? combined.slice(0, config.limit) : combined;\n}\n\n/**\n * Merge a fresh fetch into the existing cache.\n *\n * Only the locations actually fetched this run are replaced; every other\n * location's prior reviews pass through untouched. This is what makes a partial\n * failure (one location's fetch errors) non-destructive — `cmdReviews` merges\n * only the locations that succeeded and leaves the rest of the cache alone.\n */\nexport function mergeCache(\n existing: ReviewCache | undefined,\n fetched: Record<string, LocationReviews>,\n): ReviewCache {\n return {\n version: 1,\n locations: { ...existing?.locations, ...fetched },\n };\n}\n","/**\n * The cache — what makes a re-render free and offline.\n *\n * Committed, like `../media/manifest.ts` and for the same reason: reviews change\n * slowly, and a build should not need a live Google credential to render them. The\n * one field that keeps it from being byte-identical across runs is `fetchedAt` per\n * location — acceptable because `vitops reviews` is invoked deliberately\n * (scheduled or manual), not rewritten on every build the way `media-manifest.json`\n * is.\n */\nimport type { LocationReviews, ReviewCache } from './types.ts';\nimport { readVersioned, writeVersioned } from '../json-store.ts';\n\n/** Default location, relative to the consumer's project root. */\nexport const REVIEWS_CACHE_PATH = '.vitops/reviews.json';\n\n/**\n * Read the cache, or `undefined` when there isn't a usable one.\n *\n * A corrupt or wrong-version file reads as absent, exactly `readManifest`'s rule:\n * the alternative — reading it as an empty cache — would be indistinguishable\n * from \"this business genuinely has no reviews\" and silently ship an empty\n * carousel.\n */\nexport function readCache(path: string): ReviewCache | undefined {\n return readVersioned<ReviewCache, 'locations'>(path, 'locations');\n}\n\n/** Build a cache with its keys and each location's reviews in a stable order. */\nexport function toCache(locations: Record<string, LocationReviews>): ReviewCache {\n const sorted: Record<string, LocationReviews> = {};\n for (const key of Object.keys(locations).sort()) {\n const entry = locations[key] as LocationReviews;\n sorted[key] = {\n ...entry,\n reviews: [...entry.reviews].sort((a, b) => a.id.localeCompare(b.id)),\n };\n }\n return { version: 1, locations: sorted };\n}\n\n/** Write the cache, creating its directory. */\nexport function writeCache(path: string, cache: ReviewCache): void {\n writeVersioned(path, cache);\n}\n","/**\n * Google Business Profile — fetching one location's reviews.\n *\n * Reviews live only on the legacy v4 Business Information API\n * (`mybusiness.googleapis.com/v4`); the newer split APIs (Account Management,\n * Business Information v1) never grew a reviews resource. **The calling GCP\n * project must be allowlisted by Google before any call here succeeds** — a\n * one-time access-request form, turnaround measured in days to weeks — so a 403\n * is treated as that known state rather than surfaced as a raw failure. Scope:\n * `SCOPES.businessProfile` (`../google/token.ts`).\n *\n * TODO — re-check when Google migrates the Reviews endpoint off v4 onto a\n * successor API. Endpoint, auth, pagination and the review object's fields may\n * all change; this module is the one place that assumption lives.\n *\n * Auth is the shared exchange in `../google/token.ts`, same as `../indexing/gsc.ts`\n * — no `googleapis` dependency, one REST endpoint.\n */\nimport { googleHeaders, type GoogleAuthLike } from '../google/token.ts';\nimport type { Review } from './types.ts';\n\nconst STAR_RATING: Record<string, number> = {\n ONE: 1,\n TWO: 2,\n THREE: 3,\n FOUR: 4,\n FIVE: 5,\n};\n\ninterface RawReview {\n reviewId: string;\n reviewer?: { displayName?: string; profilePhotoUrl?: string; isAnonymous?: boolean };\n starRating?: string;\n comment?: string;\n createTime: string;\n updateTime: string;\n reviewReply?: { comment: string; updateTime: string };\n}\n\nfunction toReview(raw: RawReview, location: string): Review {\n return {\n id: raw.reviewId,\n location,\n rating: STAR_RATING[raw.starRating ?? ''] ?? 0,\n author: raw.reviewer?.isAnonymous\n ? 'A Google user'\n : (raw.reviewer?.displayName ?? 'A Google user'),\n ...(raw.reviewer?.profilePhotoUrl ? { authorPhoto: raw.reviewer.profilePhotoUrl } : {}),\n ...(raw.comment ? { text: raw.comment } : {}),\n publishedAt: raw.createTime,\n updatedAt: raw.updateTime,\n ...(raw.reviewReply\n ? { reply: { text: raw.reviewReply.comment, at: raw.reviewReply.updateTime } }\n : {}),\n // v4 carries no public review URL on the resource itself. The stable link is\n // `organization.locations[].listings.google` / `sameAs` — the whole profile,\n // not the single review — which is what a \"read more on Google\" affordance\n // should point at instead.\n };\n}\n\n/** One page (or, having paginated, the whole set) of a location's reviews. */\nexport interface FetchReviewsResult {\n ok: boolean;\n reviews: Review[];\n averageRating?: number;\n totalReviewCount?: number;\n status?: number;\n message?: string;\n}\n\n/**\n * Fetch every review for one location, paginating to completion.\n *\n * Never throws — a per-location failure is a structured result so a run across\n * several locations can fetch what it can and report the rest, the same contract\n * `../indexing/gsc.ts`'s `inspectUrl` keeps.\n */\nexport async function fetchReviews(\n auth: GoogleAuthLike,\n accountId: string,\n locationId: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<FetchReviewsResult> {\n const location = locationId.startsWith('locations/') ? locationId : `locations/${locationId}`;\n const account = accountId.startsWith('accounts/') ? accountId : `accounts/${accountId}`;\n const reviews: Review[] = [];\n let averageRating: number | undefined;\n let totalReviewCount: number | undefined;\n let pageToken: string | undefined;\n\n do {\n const url = new URL(`https://mybusiness.googleapis.com/v4/${account}/${location}/reviews`);\n url.searchParams.set('pageSize', '50');\n if (pageToken) url.searchParams.set('pageToken', pageToken);\n\n const res = await fetchImpl(url, { headers: googleHeaders(auth) });\n if (!res.ok) {\n const message =\n res.status === 403\n ? `403 — this GCP project is most likely not allowlisted for the Business Profile API yet ` +\n `(a one-time Google approval, requested separately from every other API here), or the ` +\n `credential is not an owner/manager of ${account}/${location}`\n : `${res.status}: ${await res.text()}`;\n return { ok: false, reviews, status: res.status, message };\n }\n\n const body = (await res.json()) as {\n reviews?: RawReview[];\n averageRating?: number;\n totalReviewCount?: number;\n nextPageToken?: string;\n };\n for (const r of body.reviews ?? []) reviews.push(toReview(r, locationId));\n averageRating = body.averageRating ?? averageRating;\n totalReviewCount = body.totalReviewCount ?? totalReviewCount;\n pageToken = body.nextPageToken;\n } while (pageToken);\n\n return {\n ok: true,\n reviews,\n ...(averageRating !== undefined ? { averageRating } : {}),\n ...(totalReviewCount !== undefined ? { totalReviewCount } : {}),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/reviews/cache.ts","../../src/reviews/gbp.ts"],"sourcesContent":["/**\n * The cache — what makes a re-render free and offline.\n *\n * Committed, like `../media/manifest.ts` and for the same reason: reviews change\n * slowly, and a build should not need a live Google credential to render them. The\n * one field that keeps it from being byte-identical across runs is `fetchedAt` per\n * location — acceptable because `vitops reviews` is invoked deliberately\n * (scheduled or manual), not rewritten on every build the way `media-manifest.json`\n * is.\n */\nimport type { LocationReviews, ReviewCache } from './types.ts';\nimport { readVersioned, writeVersioned } from '../json-store.ts';\n\n/** Default location, relative to the consumer's project root. */\nexport const REVIEWS_CACHE_PATH = '.vitops/reviews.json';\n\n/**\n * Read the cache, or `undefined` when there isn't a usable one.\n *\n * A corrupt or wrong-version file reads as absent, exactly `readManifest`'s rule:\n * the alternative — reading it as an empty cache — would be indistinguishable\n * from \"this business genuinely has no reviews\" and silently ship an empty\n * carousel.\n */\nexport function readCache(path: string): ReviewCache | undefined {\n return readVersioned<ReviewCache, 'locations'>(path, 'locations');\n}\n\n/** Build a cache with its keys and each location's reviews in a stable order. */\nexport function toCache(locations: Record<string, LocationReviews>): ReviewCache {\n const sorted: Record<string, LocationReviews> = {};\n for (const key of Object.keys(locations).sort()) {\n const entry = locations[key] as LocationReviews;\n sorted[key] = {\n ...entry,\n reviews: [...entry.reviews].sort((a, b) => a.id.localeCompare(b.id)),\n };\n }\n return { version: 1, locations: sorted };\n}\n\n/** Write the cache, creating its directory. */\nexport function writeCache(path: string, cache: ReviewCache): void {\n writeVersioned(path, cache);\n}\n","/**\n * Google Business Profile — fetching one location's reviews.\n *\n * Reviews live only on the legacy v4 Business Information API\n * (`mybusiness.googleapis.com/v4`); the newer split APIs (Account Management,\n * Business Information v1) never grew a reviews resource. **The calling GCP\n * project must be allowlisted by Google before any call here succeeds** — a\n * one-time access-request form, turnaround measured in days to weeks — so a 403\n * is treated as that known state rather than surfaced as a raw failure. Scope:\n * `SCOPES.businessProfile` (`../google/token.ts`).\n *\n * TODO — re-check when Google migrates the Reviews endpoint off v4 onto a\n * successor API. Endpoint, auth, pagination and the review object's fields may\n * all change; this module is the one place that assumption lives.\n *\n * Auth is the shared exchange in `../google/token.ts`, same as `../indexing/gsc.ts`\n * — no `googleapis` dependency, one REST endpoint.\n */\nimport { googleHeaders, type GoogleAuthLike } from '../google/token.ts';\nimport type { Review } from './types.ts';\n\nconst STAR_RATING: Record<string, number> = {\n ONE: 1,\n TWO: 2,\n THREE: 3,\n FOUR: 4,\n FIVE: 5,\n};\n\ninterface RawReview {\n reviewId: string;\n reviewer?: { displayName?: string; profilePhotoUrl?: string; isAnonymous?: boolean };\n starRating?: string;\n comment?: string;\n createTime: string;\n updateTime: string;\n reviewReply?: { comment: string; updateTime: string };\n}\n\nfunction toReview(raw: RawReview, location: string): Review {\n return {\n id: raw.reviewId,\n location,\n rating: STAR_RATING[raw.starRating ?? ''] ?? 0,\n author: raw.reviewer?.isAnonymous\n ? 'A Google user'\n : (raw.reviewer?.displayName ?? 'A Google user'),\n ...(raw.reviewer?.profilePhotoUrl ? { authorPhoto: raw.reviewer.profilePhotoUrl } : {}),\n ...(raw.comment ? { text: raw.comment } : {}),\n publishedAt: raw.createTime,\n updatedAt: raw.updateTime,\n ...(raw.reviewReply\n ? { reply: { text: raw.reviewReply.comment, at: raw.reviewReply.updateTime } }\n : {}),\n // v4 carries no public review URL on the resource itself. The stable link is\n // `organization.locations[].listings.google` / `sameAs` — the whole profile,\n // not the single review — which is what a \"read more on Google\" affordance\n // should point at instead.\n };\n}\n\n/** One page (or, having paginated, the whole set) of a location's reviews. */\nexport interface FetchReviewsResult {\n ok: boolean;\n reviews: Review[];\n averageRating?: number;\n totalReviewCount?: number;\n status?: number;\n message?: string;\n}\n\n/**\n * Fetch every review for one location, paginating to completion.\n *\n * Never throws — a per-location failure is a structured result so a run across\n * several locations can fetch what it can and report the rest, the same contract\n * `../indexing/gsc.ts`'s `inspectUrl` keeps.\n */\nexport async function fetchReviews(\n auth: GoogleAuthLike,\n accountId: string,\n locationId: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<FetchReviewsResult> {\n const location = locationId.startsWith('locations/') ? locationId : `locations/${locationId}`;\n const account = accountId.startsWith('accounts/') ? accountId : `accounts/${accountId}`;\n const reviews: Review[] = [];\n let averageRating: number | undefined;\n let totalReviewCount: number | undefined;\n let pageToken: string | undefined;\n\n do {\n const url = new URL(`https://mybusiness.googleapis.com/v4/${account}/${location}/reviews`);\n url.searchParams.set('pageSize', '50');\n if (pageToken) url.searchParams.set('pageToken', pageToken);\n\n const res = await fetchImpl(url, { headers: googleHeaders(auth) });\n if (!res.ok) {\n const message =\n res.status === 403\n ? `403 — this GCP project is most likely not allowlisted for the Business Profile API yet ` +\n `(a one-time Google approval, requested separately from every other API here), or the ` +\n `credential is not an owner/manager of ${account}/${location}`\n : `${res.status}: ${await res.text()}`;\n return { ok: false, reviews, status: res.status, message };\n }\n\n const body = (await res.json()) as {\n reviews?: RawReview[];\n averageRating?: number;\n totalReviewCount?: number;\n nextPageToken?: string;\n };\n for (const r of body.reviews ?? []) reviews.push(toReview(r, locationId));\n averageRating = body.averageRating ?? averageRating;\n totalReviewCount = body.totalReviewCount ?? totalReviewCount;\n pageToken = body.nextPageToken;\n } while (pageToken);\n\n return {\n ok: true,\n reviews,\n ...(averageRating !== undefined ? { averageRating } : {}),\n ...(totalReviewCount !== undefined ? { totalReviewCount } : {}),\n };\n}\n"],"mappings":";;;;;;;AAcA,MAAa,qBAAqB;;;;;;;;;AAUlC,SAAgB,UAAU,MAAuC;CAC/D,OAAO,cAAwC,MAAM,WAAW;AAClE;;AAGA,SAAgB,QAAQ,WAAyD;CAC/E,MAAM,SAA0C,CAAC;CACjD,KAAK,MAAM,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,KAAK,GAAG;EAC/C,MAAM,QAAQ,UAAU;EACxB,OAAO,OAAO;GACZ,GAAG;GACH,SAAS,CAAC,GAAG,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,GAAG,cAAc,EAAE,EAAE,CAAC;EACrE;CACF;CACA,OAAO;EAAE,SAAS;EAAG,WAAW;CAAO;AACzC;;AAGA,SAAgB,WAAW,MAAc,OAA0B;CACjE,eAAe,MAAM,KAAK;AAC5B;;;;;;;;;;;;;;;;;;;;;ACvBA,MAAM,cAAsC;CAC1C,KAAK;CACL,KAAK;CACL,OAAO;CACP,MAAM;CACN,MAAM;AACR;AAYA,SAAS,SAAS,KAAgB,UAA0B;CAC1D,OAAO;EACL,IAAI,IAAI;EACR;EACA,QAAQ,YAAY,IAAI,cAAc,OAAO;EAC7C,QAAQ,IAAI,UAAU,cAClB,kBACC,IAAI,UAAU,eAAe;EAClC,GAAI,IAAI,UAAU,kBAAkB,EAAE,aAAa,IAAI,SAAS,gBAAgB,IAAI,CAAC;EACrF,GAAI,IAAI,UAAU,EAAE,MAAM,IAAI,QAAQ,IAAI,CAAC;EAC3C,aAAa,IAAI;EACjB,WAAW,IAAI;EACf,GAAI,IAAI,cACJ,EAAE,OAAO;GAAE,MAAM,IAAI,YAAY;GAAS,IAAI,IAAI,YAAY;EAAW,EAAE,IAC3E,CAAC;CAKP;AACF;;;;;;;;AAmBA,eAAsB,aACpB,MACA,WACA,YACA,YAA0B,OACG;CAC7B,MAAM,WAAW,WAAW,WAAW,YAAY,IAAI,aAAa,aAAa;CACjF,MAAM,UAAU,UAAU,WAAW,WAAW,IAAI,YAAY,YAAY;CAC5E,MAAM,UAAoB,CAAC;CAC3B,IAAI;CACJ,IAAI;CACJ,IAAI;CAEJ,GAAG;EACD,MAAM,MAAM,IAAI,IAAI,wCAAwC,QAAQ,GAAG,SAAS,SAAS;EACzF,IAAI,aAAa,IAAI,YAAY,IAAI;EACrC,IAAI,WAAW,IAAI,aAAa,IAAI,aAAa,SAAS;EAE1D,MAAM,MAAM,MAAM,UAAU,KAAK,EAAE,SAAS,cAAc,IAAI,EAAE,CAAC;EACjE,IAAI,CAAC,IAAI,IAAI;GACX,MAAM,UACJ,IAAI,WAAW,MACX,qNAEyC,QAAQ,GAAG,aACpD,GAAG,IAAI,OAAO,IAAI,MAAM,IAAI,KAAK;GACvC,OAAO;IAAE,IAAI;IAAO;IAAS,QAAQ,IAAI;IAAQ;GAAQ;EAC3D;EAEA,MAAM,OAAQ,MAAM,IAAI,KAAK;EAM7B,KAAK,MAAM,KAAK,KAAK,WAAW,CAAC,GAAG,QAAQ,KAAK,SAAS,GAAG,UAAU,CAAC;EACxE,gBAAgB,KAAK,iBAAiB;EACtC,mBAAmB,KAAK,oBAAoB;EAC5C,YAAY,KAAK;CACnB,SAAS;CAET,OAAO;EACL,IAAI;EACJ;EACA,GAAI,kBAAkB,SAAY,EAAE,cAAc,IAAI,CAAC;EACvD,GAAI,qBAAqB,SAAY,EAAE,iBAAiB,IAAI,CAAC;CAC/D;AACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { c as LocationReviews, d as ReviewLocationConfig, f as ReviewsConfig, l as Review, s as selectReviews, u as ReviewCache } from "../plan-CddqsmJJ.mjs";
|
|
2
|
+
export { type LocationReviews, type Review, type ReviewCache, type ReviewLocationConfig, type ReviewsConfig, selectReviews };
|
package/dist/schema/index.d.mts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//#region src/schema/article.d.ts
|
|
2
1
|
interface ArticleAuthor {
|
|
3
2
|
name: string;
|
|
4
3
|
url?: string;
|
|
@@ -21,8 +20,6 @@ interface ArticleGraphOptions {
|
|
|
21
20
|
}
|
|
22
21
|
/** Build a schema.org Article JSON-LD graph. */
|
|
23
22
|
export declare function articleGraph(options: ArticleGraphOptions): Record<string, unknown>;
|
|
24
|
-
//#endregion
|
|
25
|
-
//#region src/schema/breadcrumb.d.ts
|
|
26
23
|
interface BreadcrumbItem {
|
|
27
24
|
name: string;
|
|
28
25
|
url: string;
|
|
@@ -32,8 +29,6 @@ interface BreadcrumbGraphOptions {
|
|
|
32
29
|
}
|
|
33
30
|
/** Build a schema.org BreadcrumbList JSON-LD graph. */
|
|
34
31
|
export declare function breadcrumbGraph(options: BreadcrumbGraphOptions): Record<string, unknown>;
|
|
35
|
-
//#endregion
|
|
36
|
-
//#region src/schema/faq.d.ts
|
|
37
32
|
interface FAQItem {
|
|
38
33
|
question: string;
|
|
39
34
|
answer: string;
|
|
@@ -48,8 +43,6 @@ interface FAQGraphOptions {
|
|
|
48
43
|
* and health sites. For other sites, consider using QAPage schema instead.
|
|
49
44
|
*/
|
|
50
45
|
export declare function faqGraph(options: FAQGraphOptions): Record<string, unknown>;
|
|
51
|
-
//#endregion
|
|
52
|
-
//#region src/schema/itemList.d.ts
|
|
53
46
|
type ItemListEntryType = 'Article' | 'Recipe' | 'Movie' | 'Course' | 'Restaurant' | 'Product' | (string & {});
|
|
54
47
|
interface ItemListEntry {
|
|
55
48
|
type: ItemListEntryType;
|
|
@@ -70,8 +63,6 @@ interface ItemListGraphOptions {
|
|
|
70
63
|
}
|
|
71
64
|
/** Build a schema.org ItemList JSON-LD graph (e.g. for a carousel rich result). */
|
|
72
65
|
export declare function itemListGraph(options: ItemListGraphOptions): Record<string, unknown>;
|
|
73
|
-
//#endregion
|
|
74
|
-
//#region src/schema/localBusiness.d.ts
|
|
75
66
|
interface OpeningHoursSpecification {
|
|
76
67
|
dayOfWeek: ('Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday' | 'Sunday')[];
|
|
77
68
|
opens: string;
|
|
@@ -137,8 +128,6 @@ interface LocalBusinessGraphOptions {
|
|
|
137
128
|
}
|
|
138
129
|
/** Build a schema.org LocalBusiness (or subtype) JSON-LD graph. */
|
|
139
130
|
export declare function localBusinessGraph(options: LocalBusinessGraphOptions): Record<string, unknown>;
|
|
140
|
-
//#endregion
|
|
141
|
-
//#region src/schema/organization.d.ts
|
|
142
131
|
interface OrganizationGraphOptions {
|
|
143
132
|
name: string;
|
|
144
133
|
url?: string;
|
|
@@ -186,6 +175,5 @@ interface OrganizationGraphOptions {
|
|
|
186
175
|
}
|
|
187
176
|
/** Build a schema.org Organization JSON-LD graph. */
|
|
188
177
|
export declare function organizationGraph(options: OrganizationGraphOptions): Record<string, unknown>;
|
|
189
|
-
//#endregion
|
|
190
178
|
export type { ArticleAuthor, ArticleGraphOptions, BreadcrumbGraphOptions, BreadcrumbItem, FAQGraphOptions, FAQItem, ItemListEntry, ItemListEntryType, ItemListGraphOptions, LocalBusinessGraphOptions, OpeningHoursSpecification, OrganizationGraphOptions, SpecialHoursSpecification };
|
|
191
179
|
//# sourceMappingURL=index.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/schema/article.ts","../../src/schema/breadcrumb.ts","../../src/schema/faq.ts","../../src/schema/itemList.ts","../../src/schema/localBusiness.ts","../../src/schema/organization.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/schema/article.ts","../../src/schema/breadcrumb.ts","../../src/schema/faq.ts","../../src/schema/itemList.ts","../../src/schema/localBusiness.ts","../../src/schema/organization.ts"],"mappings":"A,UAAiB;EACf;EACA;;UAGe;EACf;EACA;EACA;EACA;EACA;EACA,QAAQ,gBAAgB;EACxB;IACE;IACA;;EAEF;EACA;EACA;EACA;;;wBAIc,aAAa,SAAS,sBAAsB;UCvB3C;EACf;EACA;;UAGe;EACf,OAAO;;;wBAIO,gBAAgB,SAAS,yBAAyB;UCVjD;EACf;EACA;;UAGe;EACf,OAAO;;;;;;;;wBASO,SAAS,SAAS,kBAAkB;KCfxC;UAWK;EACf,MAAM;EACN;;;;;;EAMA;EACA;EACA;;UAGe;EACf,OAAO;;EAEP;;;wBAIc,cAAc,SAAS,uBAAuB;UC/B7C;EACf;EACA;EACA;;;;;;;;UASe;EACf;EACA;EACA;EACA;;UAGe;;EAEf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;IACE;IACA;IACA;IACA;IACA;;EAEF;IACE;IACA;;EAEF,4BAA4B;EAC5B,mCAAmC;EACnC;IACE;IACA;IACA;IACA;IACA;;EAEF;IACE;IACA;IACA;IACA;MACE;MACA;MACA;;;EAGJ;EACA;EACA;EACA;EACA;;;wBAIc,mBAAmB,SAAS,4BAA4B;UCnEvD;EACf;EACA;EACA;EACA;EACA;EACA;EACA;IACE;IACA;IACA;IACA;IACA;;EAEF;EACA;EACA;EACA;IAA+B;IAAkB;;EACjD;IACE;IACA;;EAEF;IACE;IACA;;EAEF;IACE;IACA;IACA;IACA;IACA;IACA;;EAEF;EACA;EACA;EACA;IACE;IACA;;;;wBAKY,kBAAkB,SAAS,2BAA2B"}
|
package/dist/schema/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/schema/article.ts
|
|
2
2
|
/** Build a schema.org Article JSON-LD graph. */
|
|
3
3
|
function articleGraph(options) {
|
|
4
4
|
const { headline, description, image, datePublished, dateModified, author, publisher, mainEntityOfPage, articleSection, wordCount, keywords } = options;
|
|
@@ -34,8 +34,7 @@ function articleGraph(options) {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
//#region src/schema/breadcrumb.ts
|
|
37
|
+
// src/schema/breadcrumb.ts
|
|
39
38
|
/** Build a schema.org BreadcrumbList JSON-LD graph. */
|
|
40
39
|
function breadcrumbGraph(options) {
|
|
41
40
|
return {
|
|
@@ -50,8 +49,7 @@ function breadcrumbGraph(options) {
|
|
|
50
49
|
};
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
//#region src/schema/faq.ts
|
|
52
|
+
// src/schema/faq.ts
|
|
55
53
|
/**
|
|
56
54
|
* Build a schema.org FAQPage JSON-LD graph.
|
|
57
55
|
*
|
|
@@ -73,8 +71,7 @@ function faqGraph(options) {
|
|
|
73
71
|
};
|
|
74
72
|
}
|
|
75
73
|
|
|
76
|
-
|
|
77
|
-
//#region src/schema/itemList.ts
|
|
74
|
+
// src/schema/itemList.ts
|
|
78
75
|
/** Build a schema.org ItemList JSON-LD graph (e.g. for a carousel rich result). */
|
|
79
76
|
function itemListGraph(options) {
|
|
80
77
|
const { items, name } = options;
|
|
@@ -96,8 +93,7 @@ function itemListGraph(options) {
|
|
|
96
93
|
};
|
|
97
94
|
}
|
|
98
95
|
|
|
99
|
-
|
|
100
|
-
//#region src/schema/localBusiness.ts
|
|
96
|
+
// src/schema/localBusiness.ts
|
|
101
97
|
/** Build a schema.org LocalBusiness (or subtype) JSON-LD graph. */
|
|
102
98
|
function localBusinessGraph(options) {
|
|
103
99
|
const { type = "LocalBusiness", name, description, url, telephone, email, image, priceRange, address, geo, openingHoursSpecification, specialOpeningHoursSpecification, aggregateRating, review, paymentAccepted, currenciesAccepted, areaServed, sameAs, knowsLanguage } = options;
|
|
@@ -168,8 +164,7 @@ function localBusinessGraph(options) {
|
|
|
168
164
|
};
|
|
169
165
|
}
|
|
170
166
|
|
|
171
|
-
|
|
172
|
-
//#region src/schema/organization.ts
|
|
167
|
+
// src/schema/organization.ts
|
|
173
168
|
/** Build a schema.org Organization JSON-LD graph. */
|
|
174
169
|
function organizationGraph(options) {
|
|
175
170
|
const { name, url, logo, description, email, telephone, address, sameAs, foundingDate, founders, numberOfEmployees, parentOrganization, subOrganization, contactPoint, areaServed, brand, award, memberOf } = options;
|
|
@@ -236,6 +231,5 @@ function organizationGraph(options) {
|
|
|
236
231
|
};
|
|
237
232
|
}
|
|
238
233
|
|
|
239
|
-
//#endregion
|
|
240
234
|
export { articleGraph, breadcrumbGraph, faqGraph, itemListGraph, localBusinessGraph, organizationGraph };
|
|
241
235
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/schema/article.ts","../../src/schema/breadcrumb.ts","../../src/schema/faq.ts","../../src/schema/itemList.ts","../../src/schema/localBusiness.ts","../../src/schema/organization.ts"],"sourcesContent":["export interface ArticleAuthor {\n name: string;\n url?: string;\n}\n\nexport interface ArticleGraphOptions {\n headline: string;\n description?: string;\n image?: string | string[];\n datePublished: string;\n dateModified?: string;\n author: ArticleAuthor | ArticleAuthor[];\n publisher?: {\n name: string;\n logo?: string;\n };\n mainEntityOfPage?: string;\n articleSection?: string;\n wordCount?: number;\n keywords?: string[];\n}\n\n/** Build a schema.org Article JSON-LD graph. */\nexport function articleGraph(options: ArticleGraphOptions): Record<string, unknown> {\n const {\n headline,\n description,\n image,\n datePublished,\n dateModified,\n author,\n publisher,\n mainEntityOfPage,\n articleSection,\n wordCount,\n keywords,\n } = options;\n\n const authors = Array.isArray(author) ? author : [author];\n\n return {\n '@context': 'https://schema.org',\n '@type': 'Article',\n headline,\n ...(description && { description }),\n ...(image && { image: Array.isArray(image) ? image : [image] }),\n datePublished,\n ...(dateModified && { dateModified }),\n author: authors.map((a) => ({\n '@type': 'Person',\n name: a.name,\n ...(a.url && { url: a.url }),\n })),\n ...(publisher && {\n publisher: {\n '@type': 'Organization',\n name: publisher.name,\n ...(publisher.logo && { logo: { '@type': 'ImageObject', url: publisher.logo } }),\n },\n }),\n ...(mainEntityOfPage && { mainEntityOfPage: { '@type': 'WebPage', '@id': mainEntityOfPage } }),\n ...(articleSection && { articleSection }),\n ...(wordCount && { wordCount }),\n ...(keywords && { keywords: keywords.join(', ') }),\n };\n}\n","export interface BreadcrumbItem {\n name: string;\n url: string;\n}\n\nexport interface BreadcrumbGraphOptions {\n items: BreadcrumbItem[];\n}\n\n/** Build a schema.org BreadcrumbList JSON-LD graph. */\nexport function breadcrumbGraph(options: BreadcrumbGraphOptions): Record<string, unknown> {\n return {\n '@context': 'https://schema.org',\n '@type': 'BreadcrumbList',\n itemListElement: options.items.map((item, index) => ({\n '@type': 'ListItem',\n position: index + 1,\n name: item.name,\n item: item.url,\n })),\n };\n}\n","export interface FAQItem {\n question: string;\n answer: string;\n}\n\nexport interface FAQGraphOptions {\n items: FAQItem[];\n}\n\n/**\n * Build a schema.org FAQPage JSON-LD graph.\n *\n * NOTE: Google recommends FAQ markup only for authoritative government\n * and health sites. For other sites, consider using QAPage schema instead.\n */\nexport function faqGraph(options: FAQGraphOptions): Record<string, unknown> {\n return {\n '@context': 'https://schema.org',\n '@type': 'FAQPage',\n mainEntity: options.items.map((item) => ({\n '@type': 'Question',\n name: item.question,\n acceptedAnswer: {\n '@type': 'Answer',\n text: item.answer,\n },\n })),\n };\n}\n","export type ItemListEntryType =\n | 'Article'\n | 'Recipe'\n | 'Movie'\n | 'Course'\n | 'Restaurant'\n | 'Product'\n // Anything else is still valid ItemList markup, just not a Google\n // carousel rich-result candidate — those six are the full supported set.\n | (string & {});\n\nexport interface ItemListEntry {\n type: ItemListEntryType;\n name: string;\n /**\n * Summary-page format: the item's own detail page (unique, same domain).\n * All-in-one-page format: this page's URL plus an HTML anchor pointing at\n * the item on it.\n */\n url: string;\n image?: string;\n description?: string;\n}\n\nexport interface ItemListGraphOptions {\n items: ItemListEntry[];\n /** Recommended, not required by the spec — names the list itself. */\n name?: string;\n}\n\n/** Build a schema.org ItemList JSON-LD graph (e.g. for a carousel rich result). */\nexport function itemListGraph(options: ItemListGraphOptions): Record<string, unknown> {\n const { items, name } = options;\n\n return {\n '@context': 'https://schema.org',\n '@type': 'ItemList',\n ...(name && { name }),\n itemListElement: items.map((item, index) => ({\n '@type': 'ListItem',\n position: index + 1,\n item: {\n '@type': item.type,\n name: item.name,\n url: item.url,\n ...(item.image && { image: item.image }),\n ...(item.description && { description: item.description }),\n },\n })),\n };\n}\n","export interface OpeningHoursSpecification {\n dayOfWeek: ('Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday' | 'Sunday')[];\n opens: string;\n closes: string;\n}\n\n/**\n * A dated deviation from the recurring `openingHoursSpecification` — holiday\n * hours, a one-off closure, a temporary schedule. Same schema.org shape\n * (`OpeningHoursSpecification`), distinguished by carrying `validFrom`. A full\n * closure omits `opens`/`closes` rather than encoding it as an open interval.\n */\nexport interface SpecialHoursSpecification {\n validFrom: string;\n validThrough?: string;\n opens?: string;\n closes?: string;\n}\n\nexport interface LocalBusinessGraphOptions {\n /** schema.org `LocalBusiness` subtype, e.g. `'Restaurant'`, `'ProfessionalService'`. Defaults to `'LocalBusiness'`. */\n type?: string;\n name: string;\n description?: string;\n url?: string;\n telephone?: string;\n email?: string;\n image?: string | string[];\n priceRange?: string;\n address?: {\n streetAddress?: string;\n addressLocality?: string;\n addressRegion?: string;\n postalCode?: string;\n addressCountry?: string;\n };\n geo?: {\n latitude: number;\n longitude: number;\n };\n openingHoursSpecification?: OpeningHoursSpecification[];\n specialOpeningHoursSpecification?: SpecialHoursSpecification[];\n aggregateRating?: {\n ratingValue: number;\n reviewCount?: number;\n ratingCount?: number;\n bestRating?: number;\n worstRating?: number;\n };\n review?: {\n author: string;\n datePublished: string;\n reviewBody: string;\n reviewRating?: {\n ratingValue: number;\n bestRating?: number;\n worstRating?: number;\n };\n }[];\n paymentAccepted?: string[];\n currenciesAccepted?: string;\n areaServed?: string[];\n sameAs?: string[];\n knowsLanguage?: string[];\n}\n\n/** Build a schema.org LocalBusiness (or subtype) JSON-LD graph. */\nexport function localBusinessGraph(options: LocalBusinessGraphOptions): Record<string, unknown> {\n const {\n type = 'LocalBusiness',\n name,\n description,\n url,\n telephone,\n email,\n image,\n priceRange,\n address,\n geo,\n openingHoursSpecification,\n specialOpeningHoursSpecification,\n aggregateRating,\n review,\n paymentAccepted,\n currenciesAccepted,\n areaServed,\n sameAs,\n knowsLanguage,\n } = options;\n\n const hours = [\n ...(openingHoursSpecification ?? []).map((spec) => ({\n '@type': 'OpeningHoursSpecification',\n dayOfWeek: spec.dayOfWeek,\n opens: spec.opens,\n closes: spec.closes,\n })),\n ...(specialOpeningHoursSpecification ?? []).map((spec) => ({\n '@type': 'OpeningHoursSpecification',\n validFrom: spec.validFrom,\n ...(spec.validThrough && { validThrough: spec.validThrough }),\n ...(spec.opens && { opens: spec.opens }),\n ...(spec.closes && { closes: spec.closes }),\n })),\n ];\n\n return {\n '@context': 'https://schema.org',\n '@type': type,\n name,\n ...(description && { description }),\n ...(url && { url }),\n ...(telephone && { telephone }),\n ...(email && { email }),\n ...(image && { image: Array.isArray(image) ? image : [image] }),\n ...(priceRange && { priceRange }),\n ...(address && {\n address: {\n '@type': 'PostalAddress',\n ...(address.streetAddress && { streetAddress: address.streetAddress }),\n ...(address.addressLocality && { addressLocality: address.addressLocality }),\n ...(address.addressRegion && { addressRegion: address.addressRegion }),\n ...(address.postalCode && { postalCode: address.postalCode }),\n ...(address.addressCountry && { addressCountry: address.addressCountry }),\n },\n }),\n ...(geo && {\n geo: {\n '@type': 'GeoCoordinates',\n latitude: geo.latitude,\n longitude: geo.longitude,\n },\n }),\n ...(hours.length > 0 && { openingHoursSpecification: hours }),\n ...(aggregateRating && {\n aggregateRating: {\n '@type': 'AggregateRating',\n ratingValue: aggregateRating.ratingValue,\n ...(aggregateRating.reviewCount && { reviewCount: aggregateRating.reviewCount }),\n ...(aggregateRating.ratingCount && { ratingCount: aggregateRating.ratingCount }),\n bestRating: aggregateRating.bestRating ?? 5,\n worstRating: aggregateRating.worstRating ?? 1,\n },\n }),\n ...(review && {\n review: review.map((r) => ({\n '@type': 'Review',\n author: { '@type': 'Person', name: r.author },\n datePublished: r.datePublished,\n reviewBody: r.reviewBody,\n ...(r.reviewRating && {\n reviewRating: {\n '@type': 'Rating',\n ratingValue: r.reviewRating.ratingValue,\n bestRating: r.reviewRating.bestRating ?? 5,\n worstRating: r.reviewRating.worstRating ?? 1,\n },\n }),\n })),\n }),\n ...(paymentAccepted && { paymentAccepted }),\n ...(currenciesAccepted && { currenciesAccepted }),\n ...(areaServed && { areaServed }),\n ...(sameAs && { sameAs }),\n ...(knowsLanguage && { knowsLanguage }),\n };\n}\n","export interface OrganizationGraphOptions {\n name: string;\n url?: string;\n logo?: string;\n description?: string;\n email?: string;\n telephone?: string;\n address?: {\n streetAddress?: string;\n addressLocality?: string;\n addressRegion?: string;\n postalCode?: string;\n addressCountry?: string;\n };\n sameAs?: string[];\n foundingDate?: string;\n founders?: string[];\n numberOfEmployees?: number | { minValue: number; maxValue: number };\n parentOrganization?: {\n name: string;\n url?: string;\n };\n subOrganization?: {\n name: string;\n url?: string;\n }[];\n contactPoint?: {\n type?: 'customer service' | 'technical support' | 'sales' | 'billing support' | 'reservations';\n telephone: string;\n email?: string;\n contactType: string;\n availableLanguage?: string[];\n areaServed?: string[];\n }[];\n areaServed?: string[];\n brand?: string;\n award?: string[];\n memberOf?: {\n name: string;\n url?: string;\n }[];\n}\n\n/** Build a schema.org Organization JSON-LD graph. */\nexport function organizationGraph(options: OrganizationGraphOptions): Record<string, unknown> {\n const {\n name,\n url,\n logo,\n description,\n email,\n telephone,\n address,\n sameAs,\n foundingDate,\n founders,\n numberOfEmployees,\n parentOrganization,\n subOrganization,\n contactPoint,\n areaServed,\n brand,\n award,\n memberOf,\n } = options;\n\n return {\n '@context': 'https://schema.org',\n '@type': 'Organization',\n name,\n ...(url && { url }),\n ...(logo && { logo }),\n ...(description && { description }),\n ...(email && { email }),\n ...(telephone && { telephone }),\n ...(address && {\n address: {\n '@type': 'PostalAddress',\n ...(address.streetAddress && { streetAddress: address.streetAddress }),\n ...(address.addressLocality && { addressLocality: address.addressLocality }),\n ...(address.addressRegion && { addressRegion: address.addressRegion }),\n ...(address.postalCode && { postalCode: address.postalCode }),\n ...(address.addressCountry && { addressCountry: address.addressCountry }),\n },\n }),\n ...(sameAs && { sameAs }),\n ...(foundingDate && { foundingDate }),\n ...(founders && {\n founders: founders.map((f) => ({\n '@type': 'Person',\n name: f,\n })),\n }),\n ...(numberOfEmployees && {\n numberOfEmployees:\n typeof numberOfEmployees === 'number'\n ? { '@type': 'QuantitativeValue', value: numberOfEmployees }\n : {\n '@type': 'QuantitativeValue',\n minValue: numberOfEmployees.minValue,\n maxValue: numberOfEmployees.maxValue,\n },\n }),\n ...(parentOrganization && {\n parentOrganization: {\n '@type': 'Organization',\n name: parentOrganization.name,\n ...(parentOrganization.url && { url: parentOrganization.url }),\n },\n }),\n ...(subOrganization && {\n subOrganization: subOrganization.map((sub) => ({\n '@type': 'Organization',\n name: sub.name,\n ...(sub.url && { url: sub.url }),\n })),\n }),\n ...(contactPoint && {\n contactPoint: contactPoint.map((cp) => ({\n '@type': 'ContactPoint',\n telephone: cp.telephone,\n contactType: cp.contactType,\n ...(cp.email && { email: cp.email }),\n ...(cp.availableLanguage && { availableLanguage: cp.availableLanguage }),\n ...(cp.areaServed && { areaServed: cp.areaServed }),\n })),\n }),\n ...(areaServed && { areaServed }),\n ...(brand && { brand: { '@type': 'Brand', name: brand } }),\n ...(award && { award }),\n ...(memberOf && {\n memberOf: memberOf.map((m) => ({\n '@type': 'Organization',\n name: m.name,\n ...(m.url && { url: m.url }),\n })),\n }),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/schema/article.ts","../../src/schema/breadcrumb.ts","../../src/schema/faq.ts","../../src/schema/itemList.ts","../../src/schema/localBusiness.ts","../../src/schema/organization.ts"],"sourcesContent":["export interface ArticleAuthor {\n name: string;\n url?: string;\n}\n\nexport interface ArticleGraphOptions {\n headline: string;\n description?: string;\n image?: string | string[];\n datePublished: string;\n dateModified?: string;\n author: ArticleAuthor | ArticleAuthor[];\n publisher?: {\n name: string;\n logo?: string;\n };\n mainEntityOfPage?: string;\n articleSection?: string;\n wordCount?: number;\n keywords?: string[];\n}\n\n/** Build a schema.org Article JSON-LD graph. */\nexport function articleGraph(options: ArticleGraphOptions): Record<string, unknown> {\n const {\n headline,\n description,\n image,\n datePublished,\n dateModified,\n author,\n publisher,\n mainEntityOfPage,\n articleSection,\n wordCount,\n keywords,\n } = options;\n\n const authors = Array.isArray(author) ? author : [author];\n\n return {\n '@context': 'https://schema.org',\n '@type': 'Article',\n headline,\n ...(description && { description }),\n ...(image && { image: Array.isArray(image) ? image : [image] }),\n datePublished,\n ...(dateModified && { dateModified }),\n author: authors.map((a) => ({\n '@type': 'Person',\n name: a.name,\n ...(a.url && { url: a.url }),\n })),\n ...(publisher && {\n publisher: {\n '@type': 'Organization',\n name: publisher.name,\n ...(publisher.logo && { logo: { '@type': 'ImageObject', url: publisher.logo } }),\n },\n }),\n ...(mainEntityOfPage && { mainEntityOfPage: { '@type': 'WebPage', '@id': mainEntityOfPage } }),\n ...(articleSection && { articleSection }),\n ...(wordCount && { wordCount }),\n ...(keywords && { keywords: keywords.join(', ') }),\n };\n}\n","export interface BreadcrumbItem {\n name: string;\n url: string;\n}\n\nexport interface BreadcrumbGraphOptions {\n items: BreadcrumbItem[];\n}\n\n/** Build a schema.org BreadcrumbList JSON-LD graph. */\nexport function breadcrumbGraph(options: BreadcrumbGraphOptions): Record<string, unknown> {\n return {\n '@context': 'https://schema.org',\n '@type': 'BreadcrumbList',\n itemListElement: options.items.map((item, index) => ({\n '@type': 'ListItem',\n position: index + 1,\n name: item.name,\n item: item.url,\n })),\n };\n}\n","export interface FAQItem {\n question: string;\n answer: string;\n}\n\nexport interface FAQGraphOptions {\n items: FAQItem[];\n}\n\n/**\n * Build a schema.org FAQPage JSON-LD graph.\n *\n * NOTE: Google recommends FAQ markup only for authoritative government\n * and health sites. For other sites, consider using QAPage schema instead.\n */\nexport function faqGraph(options: FAQGraphOptions): Record<string, unknown> {\n return {\n '@context': 'https://schema.org',\n '@type': 'FAQPage',\n mainEntity: options.items.map((item) => ({\n '@type': 'Question',\n name: item.question,\n acceptedAnswer: {\n '@type': 'Answer',\n text: item.answer,\n },\n })),\n };\n}\n","export type ItemListEntryType =\n | 'Article'\n | 'Recipe'\n | 'Movie'\n | 'Course'\n | 'Restaurant'\n | 'Product'\n // Anything else is still valid ItemList markup, just not a Google\n // carousel rich-result candidate — those six are the full supported set.\n | (string & {});\n\nexport interface ItemListEntry {\n type: ItemListEntryType;\n name: string;\n /**\n * Summary-page format: the item's own detail page (unique, same domain).\n * All-in-one-page format: this page's URL plus an HTML anchor pointing at\n * the item on it.\n */\n url: string;\n image?: string;\n description?: string;\n}\n\nexport interface ItemListGraphOptions {\n items: ItemListEntry[];\n /** Recommended, not required by the spec — names the list itself. */\n name?: string;\n}\n\n/** Build a schema.org ItemList JSON-LD graph (e.g. for a carousel rich result). */\nexport function itemListGraph(options: ItemListGraphOptions): Record<string, unknown> {\n const { items, name } = options;\n\n return {\n '@context': 'https://schema.org',\n '@type': 'ItemList',\n ...(name && { name }),\n itemListElement: items.map((item, index) => ({\n '@type': 'ListItem',\n position: index + 1,\n item: {\n '@type': item.type,\n name: item.name,\n url: item.url,\n ...(item.image && { image: item.image }),\n ...(item.description && { description: item.description }),\n },\n })),\n };\n}\n","export interface OpeningHoursSpecification {\n dayOfWeek: ('Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday' | 'Sunday')[];\n opens: string;\n closes: string;\n}\n\n/**\n * A dated deviation from the recurring `openingHoursSpecification` — holiday\n * hours, a one-off closure, a temporary schedule. Same schema.org shape\n * (`OpeningHoursSpecification`), distinguished by carrying `validFrom`. A full\n * closure omits `opens`/`closes` rather than encoding it as an open interval.\n */\nexport interface SpecialHoursSpecification {\n validFrom: string;\n validThrough?: string;\n opens?: string;\n closes?: string;\n}\n\nexport interface LocalBusinessGraphOptions {\n /** schema.org `LocalBusiness` subtype, e.g. `'Restaurant'`, `'ProfessionalService'`. Defaults to `'LocalBusiness'`. */\n type?: string;\n name: string;\n description?: string;\n url?: string;\n telephone?: string;\n email?: string;\n image?: string | string[];\n priceRange?: string;\n address?: {\n streetAddress?: string;\n addressLocality?: string;\n addressRegion?: string;\n postalCode?: string;\n addressCountry?: string;\n };\n geo?: {\n latitude: number;\n longitude: number;\n };\n openingHoursSpecification?: OpeningHoursSpecification[];\n specialOpeningHoursSpecification?: SpecialHoursSpecification[];\n aggregateRating?: {\n ratingValue: number;\n reviewCount?: number;\n ratingCount?: number;\n bestRating?: number;\n worstRating?: number;\n };\n review?: {\n author: string;\n datePublished: string;\n reviewBody: string;\n reviewRating?: {\n ratingValue: number;\n bestRating?: number;\n worstRating?: number;\n };\n }[];\n paymentAccepted?: string[];\n currenciesAccepted?: string;\n areaServed?: string[];\n sameAs?: string[];\n knowsLanguage?: string[];\n}\n\n/** Build a schema.org LocalBusiness (or subtype) JSON-LD graph. */\nexport function localBusinessGraph(options: LocalBusinessGraphOptions): Record<string, unknown> {\n const {\n type = 'LocalBusiness',\n name,\n description,\n url,\n telephone,\n email,\n image,\n priceRange,\n address,\n geo,\n openingHoursSpecification,\n specialOpeningHoursSpecification,\n aggregateRating,\n review,\n paymentAccepted,\n currenciesAccepted,\n areaServed,\n sameAs,\n knowsLanguage,\n } = options;\n\n const hours = [\n ...(openingHoursSpecification ?? []).map((spec) => ({\n '@type': 'OpeningHoursSpecification',\n dayOfWeek: spec.dayOfWeek,\n opens: spec.opens,\n closes: spec.closes,\n })),\n ...(specialOpeningHoursSpecification ?? []).map((spec) => ({\n '@type': 'OpeningHoursSpecification',\n validFrom: spec.validFrom,\n ...(spec.validThrough && { validThrough: spec.validThrough }),\n ...(spec.opens && { opens: spec.opens }),\n ...(spec.closes && { closes: spec.closes }),\n })),\n ];\n\n return {\n '@context': 'https://schema.org',\n '@type': type,\n name,\n ...(description && { description }),\n ...(url && { url }),\n ...(telephone && { telephone }),\n ...(email && { email }),\n ...(image && { image: Array.isArray(image) ? image : [image] }),\n ...(priceRange && { priceRange }),\n ...(address && {\n address: {\n '@type': 'PostalAddress',\n ...(address.streetAddress && { streetAddress: address.streetAddress }),\n ...(address.addressLocality && { addressLocality: address.addressLocality }),\n ...(address.addressRegion && { addressRegion: address.addressRegion }),\n ...(address.postalCode && { postalCode: address.postalCode }),\n ...(address.addressCountry && { addressCountry: address.addressCountry }),\n },\n }),\n ...(geo && {\n geo: {\n '@type': 'GeoCoordinates',\n latitude: geo.latitude,\n longitude: geo.longitude,\n },\n }),\n ...(hours.length > 0 && { openingHoursSpecification: hours }),\n ...(aggregateRating && {\n aggregateRating: {\n '@type': 'AggregateRating',\n ratingValue: aggregateRating.ratingValue,\n ...(aggregateRating.reviewCount && { reviewCount: aggregateRating.reviewCount }),\n ...(aggregateRating.ratingCount && { ratingCount: aggregateRating.ratingCount }),\n bestRating: aggregateRating.bestRating ?? 5,\n worstRating: aggregateRating.worstRating ?? 1,\n },\n }),\n ...(review && {\n review: review.map((r) => ({\n '@type': 'Review',\n author: { '@type': 'Person', name: r.author },\n datePublished: r.datePublished,\n reviewBody: r.reviewBody,\n ...(r.reviewRating && {\n reviewRating: {\n '@type': 'Rating',\n ratingValue: r.reviewRating.ratingValue,\n bestRating: r.reviewRating.bestRating ?? 5,\n worstRating: r.reviewRating.worstRating ?? 1,\n },\n }),\n })),\n }),\n ...(paymentAccepted && { paymentAccepted }),\n ...(currenciesAccepted && { currenciesAccepted }),\n ...(areaServed && { areaServed }),\n ...(sameAs && { sameAs }),\n ...(knowsLanguage && { knowsLanguage }),\n };\n}\n","export interface OrganizationGraphOptions {\n name: string;\n url?: string;\n logo?: string;\n description?: string;\n email?: string;\n telephone?: string;\n address?: {\n streetAddress?: string;\n addressLocality?: string;\n addressRegion?: string;\n postalCode?: string;\n addressCountry?: string;\n };\n sameAs?: string[];\n foundingDate?: string;\n founders?: string[];\n numberOfEmployees?: number | { minValue: number; maxValue: number };\n parentOrganization?: {\n name: string;\n url?: string;\n };\n subOrganization?: {\n name: string;\n url?: string;\n }[];\n contactPoint?: {\n type?: 'customer service' | 'technical support' | 'sales' | 'billing support' | 'reservations';\n telephone: string;\n email?: string;\n contactType: string;\n availableLanguage?: string[];\n areaServed?: string[];\n }[];\n areaServed?: string[];\n brand?: string;\n award?: string[];\n memberOf?: {\n name: string;\n url?: string;\n }[];\n}\n\n/** Build a schema.org Organization JSON-LD graph. */\nexport function organizationGraph(options: OrganizationGraphOptions): Record<string, unknown> {\n const {\n name,\n url,\n logo,\n description,\n email,\n telephone,\n address,\n sameAs,\n foundingDate,\n founders,\n numberOfEmployees,\n parentOrganization,\n subOrganization,\n contactPoint,\n areaServed,\n brand,\n award,\n memberOf,\n } = options;\n\n return {\n '@context': 'https://schema.org',\n '@type': 'Organization',\n name,\n ...(url && { url }),\n ...(logo && { logo }),\n ...(description && { description }),\n ...(email && { email }),\n ...(telephone && { telephone }),\n ...(address && {\n address: {\n '@type': 'PostalAddress',\n ...(address.streetAddress && { streetAddress: address.streetAddress }),\n ...(address.addressLocality && { addressLocality: address.addressLocality }),\n ...(address.addressRegion && { addressRegion: address.addressRegion }),\n ...(address.postalCode && { postalCode: address.postalCode }),\n ...(address.addressCountry && { addressCountry: address.addressCountry }),\n },\n }),\n ...(sameAs && { sameAs }),\n ...(foundingDate && { foundingDate }),\n ...(founders && {\n founders: founders.map((f) => ({\n '@type': 'Person',\n name: f,\n })),\n }),\n ...(numberOfEmployees && {\n numberOfEmployees:\n typeof numberOfEmployees === 'number'\n ? { '@type': 'QuantitativeValue', value: numberOfEmployees }\n : {\n '@type': 'QuantitativeValue',\n minValue: numberOfEmployees.minValue,\n maxValue: numberOfEmployees.maxValue,\n },\n }),\n ...(parentOrganization && {\n parentOrganization: {\n '@type': 'Organization',\n name: parentOrganization.name,\n ...(parentOrganization.url && { url: parentOrganization.url }),\n },\n }),\n ...(subOrganization && {\n subOrganization: subOrganization.map((sub) => ({\n '@type': 'Organization',\n name: sub.name,\n ...(sub.url && { url: sub.url }),\n })),\n }),\n ...(contactPoint && {\n contactPoint: contactPoint.map((cp) => ({\n '@type': 'ContactPoint',\n telephone: cp.telephone,\n contactType: cp.contactType,\n ...(cp.email && { email: cp.email }),\n ...(cp.availableLanguage && { availableLanguage: cp.availableLanguage }),\n ...(cp.areaServed && { areaServed: cp.areaServed }),\n })),\n }),\n ...(areaServed && { areaServed }),\n ...(brand && { brand: { '@type': 'Brand', name: brand } }),\n ...(award && { award }),\n ...(memberOf && {\n memberOf: memberOf.map((m) => ({\n '@type': 'Organization',\n name: m.name,\n ...(m.url && { url: m.url }),\n })),\n }),\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//#region src/security/turnstile.d.ts
|
|
2
1
|
/**
|
|
3
2
|
* Cloudflare Turnstile server-side verification.
|
|
4
3
|
*
|
|
@@ -43,6 +42,5 @@ interface TurnstileVerifyResult {
|
|
|
43
42
|
* (`createConversionRoute`) decides what a failure means for the request.
|
|
44
43
|
*/
|
|
45
44
|
export declare function verifyTurnstileToken(token: string, secretKey: string, remoteIp?: string, fetchImpl?: typeof fetch): Promise<TurnstileVerifyResult>;
|
|
46
|
-
//#endregion
|
|
47
45
|
export type { TurnstileVerifyResult };
|
|
48
46
|
//# sourceMappingURL=index.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/security/turnstile.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/security/turnstile.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;UA+BiB;EACf;;EAEA;;;;;;;;;;;;wBAaoB,qBACpB,eACA,mBACA,mBACA,mBAAkB,QACjB,QAAQ"}
|
package/dist/security/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/security/turnstile.ts
|
|
2
2
|
/**
|
|
3
3
|
* Cloudflare Turnstile server-side verification.
|
|
4
4
|
*
|
|
@@ -75,6 +75,5 @@ async function verifyTurnstileToken(token, secretKey, remoteIp, fetchImpl = fetc
|
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
//#endregion
|
|
79
78
|
export { verifyTurnstileToken };
|
|
80
79
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//#region src/google/token.d.ts
|
|
2
1
|
/** The fields we need from a service-account JSON key. */
|
|
3
2
|
interface ServiceAccount {
|
|
4
3
|
client_email: string;
|
|
@@ -71,7 +70,8 @@ declare const SCOPES: {
|
|
|
71
70
|
readonly siteVerification: "https://www.googleapis.com/auth/siteverification";
|
|
72
71
|
/** Business Profile (reviews). The GCP project must be allowlisted by Google. */
|
|
73
72
|
readonly businessProfile: "https://www.googleapis.com/auth/business.manage";
|
|
73
|
+
/** Google Ads API (offline conversion upload). Needs an approved developer token too. */
|
|
74
|
+
readonly adwords: "https://www.googleapis.com/auth/adwords";
|
|
74
75
|
};
|
|
75
|
-
//#endregion
|
|
76
76
|
export { SCOPES as a, googleHeaders as c, GoogleOAuth as i, GoogleAuthLike as n, ServiceAccount as o, GoogleCredential as r, googleAccessToken as s, GoogleAuth as t };
|
|
77
|
-
//# sourceMappingURL=token-
|
|
77
|
+
//# sourceMappingURL=token-CHR1O4w5.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-CHR1O4w5.d.mts","names":[],"sources":["../src/google/token.ts"],"mappings":";UAqCiB;EACf;EACA;;;UAIe;EACf;EACA;EACA;;;;;;;;;;KAWU;EACN;EAAyB,SAAS;EAAgB;;EAClD;EAAe,OAAO;;;;;;;;;;;iBAwEN,kBACpB,YAAY,kBACZ,mBAAkB,OAClB,eACC;;;;;;;;;;;;;;UA6Bc;EACf;EACA;;;;;;;;;KAUU,0BAA0B;;iBAGtB,cACd,MAAM,gBACN,QAAQ,yBACP;;cA2BU"}
|