@shop-on/theme-server 0.0.0-stage → 0.2.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/dist/LICENSE.md +39 -0
- package/dist/account.d.ts +12 -0
- package/dist/account.js +72 -0
- package/dist/activity.d.ts +25 -0
- package/dist/activity.js +35 -0
- package/dist/analytics-proxy.d.ts +22 -0
- package/dist/analytics-proxy.js +39 -0
- package/dist/analytics-script-proxy.d.ts +18 -0
- package/dist/analytics-script-proxy.js +46 -0
- package/dist/blog.d.ts +109 -0
- package/dist/blog.js +481 -0
- package/dist/cache.d.ts +23 -0
- package/dist/cache.js +60 -0
- package/dist/canonical-host.d.ts +26 -0
- package/dist/canonical-host.js +39 -0
- package/dist/cart-customer.d.ts +26 -0
- package/dist/cart-customer.js +126 -0
- package/dist/cart-mutations.d.ts +115 -0
- package/dist/cart-mutations.js +273 -0
- package/dist/cart-recommendations.d.ts +10 -0
- package/dist/cart-recommendations.js +31 -0
- package/dist/cart-session.d.ts +8 -0
- package/dist/cart-session.js +29 -0
- package/dist/cart.d.ts +253 -0
- package/dist/cart.js +209 -0
- package/dist/checkout.d.ts +24 -0
- package/dist/checkout.js +66 -0
- package/dist/collection.d.ts +368 -0
- package/dist/collection.js +658 -0
- package/dist/contact.d.ts +25 -0
- package/dist/contact.js +33 -0
- package/dist/content-version.d.ts +29 -0
- package/dist/content-version.js +63 -0
- package/dist/csrf.js +21 -0
- package/dist/customer-handoff.d.ts +13 -0
- package/dist/customer-handoff.js +57 -0
- package/dist/customer-session.d.ts +12 -0
- package/dist/customer-session.js +51 -0
- package/dist/customer.d.ts +11 -0
- package/dist/customer.js +61 -0
- package/dist/files.d.ts +68 -0
- package/dist/files.js +137 -0
- package/dist/fonts.d.ts +9 -0
- package/dist/fonts.js +61 -0
- package/dist/graphql-env.d.ts +21128 -0
- package/dist/graphql.d.ts +64 -0
- package/dist/graphql.js +331 -0
- package/dist/html.d.ts +3 -0
- package/dist/html.js +13 -0
- package/dist/lease.d.ts +2 -0
- package/dist/lease.js +1 -0
- package/dist/liquid.d.ts +85 -0
- package/dist/liquid.js +104 -0
- package/dist/marketing-cookies.d.ts +13 -0
- package/dist/marketing-cookies.js +27 -0
- package/dist/menu.d.ts +156 -0
- package/dist/menu.js +71 -0
- package/dist/metaobject.d.ts +18 -0
- package/dist/metaobject.js +92 -0
- package/dist/newsletter.d.ts +17 -0
- package/dist/newsletter.js +28 -0
- package/dist/page.d.ts +28 -0
- package/dist/page.js +94 -0
- package/dist/password-session.d.ts +10 -0
- package/dist/password-session.js +45 -0
- package/dist/password.d.ts +38 -0
- package/dist/password.js +91 -0
- package/dist/policy.d.ts +20 -0
- package/dist/policy.js +75 -0
- package/dist/product.d.ts +100 -0
- package/dist/product.js +660 -0
- package/dist/recommendations.d.ts +26 -0
- package/dist/recommendations.js +67 -0
- package/dist/request-scope.d.ts +2 -0
- package/dist/request-scope.js +1 -0
- package/dist/review.d.ts +162 -0
- package/dist/review.js +531 -0
- package/dist/schema.storefront.graphql +9429 -0
- package/dist/search.d.ts +152 -0
- package/dist/search.js +518 -0
- package/dist/seo-context.d.ts +14 -0
- package/dist/seo-context.js +47 -0
- package/dist/seo-proxy.d.ts +11 -0
- package/dist/seo-proxy.js +26 -0
- package/dist/shop-seo.d.ts +12 -0
- package/dist/shop-seo.js +37 -0
- package/dist/shop.d.ts +44 -0
- package/dist/shop.js +63 -0
- package/dist/store-availability.d.ts +24 -0
- package/dist/store-availability.js +59 -0
- package/dist/trace.js +5 -0
- package/dist/translations.d.ts +57 -0
- package/dist/translations.js +105 -0
- package/package.json +74 -3
- package/README.md +0 -3
package/dist/LICENSE.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Cody Theme Development License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cody LLC. All rights reserved except as granted below.
|
|
4
|
+
|
|
5
|
+
"The Software" is this package, its source, and its documentation. "The
|
|
6
|
+
Platform" is Cody, the commerce platform operated by Cody LLC.
|
|
7
|
+
|
|
8
|
+
## 1. Grant
|
|
9
|
+
|
|
10
|
+
You may use, copy and modify the Software, and distribute modified copies, solely
|
|
11
|
+
to develop, test, and distribute themes, apps and other extensions that run on
|
|
12
|
+
the Platform.
|
|
13
|
+
|
|
14
|
+
## 2. Restrictions
|
|
15
|
+
|
|
16
|
+
You may not:
|
|
17
|
+
|
|
18
|
+
1. use the Software, or anything derived from it, with any commerce platform,
|
|
19
|
+
storefront or hosting service other than the Platform;
|
|
20
|
+
2. use the Software to build a product that competes with the Platform;
|
|
21
|
+
3. sublicense the Software, or distribute it other than as part of a theme,
|
|
22
|
+
app or extension for the Platform;
|
|
23
|
+
4. remove or alter this license or any copyright notice in the Software.
|
|
24
|
+
|
|
25
|
+
## 3. Your work
|
|
26
|
+
|
|
27
|
+
Themes, apps and extensions you build with the Software are yours. The parts of
|
|
28
|
+
the Software they include stay under this license.
|
|
29
|
+
|
|
30
|
+
## 4. Termination
|
|
31
|
+
|
|
32
|
+
This license ends automatically if you break any of its terms. You must then
|
|
33
|
+
stop using the Software and delete your copies of it.
|
|
34
|
+
|
|
35
|
+
## 5. No warranty
|
|
36
|
+
|
|
37
|
+
The Software is provided "as is", without warranty of any kind. Cody LLC is not
|
|
38
|
+
liable for any claim, damages or other liability arising from the Software or
|
|
39
|
+
its use.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/account.d.ts
|
|
2
|
+
export declare function normalizeReturnToPath(raw: unknown): string;
|
|
3
|
+
export type AccountRedirectSubpath = "" | `/${string}`;
|
|
4
|
+
export declare function getAccountRedirectUrl(data: {
|
|
5
|
+
subpath?: AccountRedirectSubpath;
|
|
6
|
+
returnToPath?: string;
|
|
7
|
+
}): Promise<{
|
|
8
|
+
url: string;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function handleCustomerLogoutPost(request: Request): Promise<Response>;
|
|
11
|
+
export declare function renderLogoutForm(accountLogoutUrl: string, returnTo: string): string;
|
|
12
|
+
//#endregion
|
package/dist/account.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { fetchShopInfo } from "./shop.js";
|
|
2
|
+
import { checkOrigin } from "./csrf.js";
|
|
3
|
+
import { escapeHtmlAttr } from "./html.js";
|
|
4
|
+
//#region src/account.ts
|
|
5
|
+
const FORBIDDEN_RETURN_TO = /^(?:\/[^/]+)?\/(?:account|customer_logout)(?:\/|$)/;
|
|
6
|
+
function normalizeReturnToPath(raw) {
|
|
7
|
+
if (typeof raw !== "string") return "/";
|
|
8
|
+
if (!raw.startsWith("/") || raw.startsWith("//")) return "/";
|
|
9
|
+
const pathOnly = raw.split(/[?#]/, 1)[0];
|
|
10
|
+
if (FORBIDDEN_RETURN_TO.test(pathOnly)) return "/";
|
|
11
|
+
return raw;
|
|
12
|
+
}
|
|
13
|
+
async function getAccountRedirectUrl(data) {
|
|
14
|
+
const shop = await fetchShopInfo();
|
|
15
|
+
if (!shop) throw new Error("[theme/account] shop info unavailable from Rails");
|
|
16
|
+
const subpath = data.subpath ?? "";
|
|
17
|
+
const target = new URL(`${shop.accountUrl}${subpath}`);
|
|
18
|
+
if (data.returnToPath !== void 0) {
|
|
19
|
+
const safePath = normalizeReturnToPath(data.returnToPath);
|
|
20
|
+
const { getRequestUrl } = await import("@tanstack/react-start/server");
|
|
21
|
+
const origin = getRequestUrl({
|
|
22
|
+
xForwardedHost: true,
|
|
23
|
+
xForwardedProto: true
|
|
24
|
+
}).origin;
|
|
25
|
+
target.searchParams.set("return_to", `${origin}${safePath}`);
|
|
26
|
+
}
|
|
27
|
+
return { url: target.toString() };
|
|
28
|
+
}
|
|
29
|
+
async function handleCustomerLogoutPost(request) {
|
|
30
|
+
const { getRequestUrl } = await import("@tanstack/react-start/server");
|
|
31
|
+
const expectedOrigin = getRequestUrl({
|
|
32
|
+
xForwardedHost: true,
|
|
33
|
+
xForwardedProto: true
|
|
34
|
+
}).origin;
|
|
35
|
+
const originHeader = request.headers.get("origin");
|
|
36
|
+
const originResult = checkOrigin(originHeader, expectedOrigin);
|
|
37
|
+
if (!originResult.ok) {
|
|
38
|
+
console.warn(JSON.stringify({
|
|
39
|
+
event: "theme_logout_csrf_blocked",
|
|
40
|
+
reason: originResult.reason,
|
|
41
|
+
originHeader,
|
|
42
|
+
expectedOrigin,
|
|
43
|
+
ts: Date.now()
|
|
44
|
+
}));
|
|
45
|
+
return new Response("Forbidden", { status: 403 });
|
|
46
|
+
}
|
|
47
|
+
const shopPromise = fetchShopInfo().catch(() => null);
|
|
48
|
+
const { revokeCustomerSession } = await import("./customer.js");
|
|
49
|
+
const { clearCustomerSession } = await import("./customer-session.js");
|
|
50
|
+
const { useCartSession } = await import("./cart-session.js");
|
|
51
|
+
await revokeCustomerSession();
|
|
52
|
+
await clearCustomerSession();
|
|
53
|
+
await (await useCartSession()).clear();
|
|
54
|
+
const shop = await shopPromise;
|
|
55
|
+
if (!shop) return new Response("shop unavailable", { status: 500 });
|
|
56
|
+
return new Response(renderLogoutForm(`${shop.accountUrl}/logout`, `${expectedOrigin}/`), {
|
|
57
|
+
status: 200,
|
|
58
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function renderLogoutForm(accountLogoutUrl, returnTo) {
|
|
62
|
+
return `<!DOCTYPE html>
|
|
63
|
+
<html><body>
|
|
64
|
+
<form action="${escapeHtmlAttr(accountLogoutUrl)}" method="POST">
|
|
65
|
+
<input type="hidden" name="return_to" value="${escapeHtmlAttr(returnTo)}">
|
|
66
|
+
<noscript><button type="submit">Continue</button></noscript>
|
|
67
|
+
</form>
|
|
68
|
+
<script>document.forms[0].submit()<\/script>
|
|
69
|
+
</body></html>`;
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export { getAccountRedirectUrl, handleCustomerLogoutPost, normalizeReturnToPath, renderLogoutForm };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/activity.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* In-memory last-request tracker for a running storefront isolate. The
|
|
4
|
+
* orchestrator probes `/health` to prove an install renders before it publishes
|
|
5
|
+
* the store's route — the theme holds no connection to any platform
|
|
6
|
+
* infrastructure for this. `lastRequestAt` initializes to isolate start.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createActivityTracker({ now }?: {
|
|
9
|
+
now?: () => number;
|
|
10
|
+
}): {
|
|
11
|
+
record: () => void;
|
|
12
|
+
snapshot: () => {
|
|
13
|
+
status: "ok";
|
|
14
|
+
lastRequestAt: number;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Request middleware: every theme registers this in `src/start.ts`. Serves
|
|
19
|
+
* `/health` (a 200 JSON body with `lastRequestAt`) for the orchestrator's
|
|
20
|
+
* warm-up probe, and stamps the tracker for everything else. `/health` itself
|
|
21
|
+
* must not count as activity, which the short-circuit guarantees by returning
|
|
22
|
+
* before the record.
|
|
23
|
+
*/
|
|
24
|
+
export declare const storefrontActivityMiddleware: import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, undefined>;
|
|
25
|
+
//#endregion
|
package/dist/activity.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createMiddleware } from "@tanstack/react-start";
|
|
2
|
+
//#region src/activity.ts
|
|
3
|
+
/**
|
|
4
|
+
* In-memory last-request tracker for a running storefront isolate. The
|
|
5
|
+
* orchestrator probes `/health` to prove an install renders before it publishes
|
|
6
|
+
* the store's route — the theme holds no connection to any platform
|
|
7
|
+
* infrastructure for this. `lastRequestAt` initializes to isolate start.
|
|
8
|
+
*/
|
|
9
|
+
function createActivityTracker({ now = Date.now } = {}) {
|
|
10
|
+
let lastRequestAt = now();
|
|
11
|
+
return {
|
|
12
|
+
record: () => {
|
|
13
|
+
lastRequestAt = now();
|
|
14
|
+
},
|
|
15
|
+
snapshot: () => ({
|
|
16
|
+
status: "ok",
|
|
17
|
+
lastRequestAt
|
|
18
|
+
})
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const tracker = createActivityTracker();
|
|
22
|
+
/**
|
|
23
|
+
* Request middleware: every theme registers this in `src/start.ts`. Serves
|
|
24
|
+
* `/health` (a 200 JSON body with `lastRequestAt`) for the orchestrator's
|
|
25
|
+
* warm-up probe, and stamps the tracker for everything else. `/health` itself
|
|
26
|
+
* must not count as activity, which the short-circuit guarantees by returning
|
|
27
|
+
* before the record.
|
|
28
|
+
*/
|
|
29
|
+
const storefrontActivityMiddleware = createMiddleware().server(async ({ next, request }) => {
|
|
30
|
+
if (new URL(request.url).pathname === "/health") return Response.json(tracker.snapshot());
|
|
31
|
+
tracker.record();
|
|
32
|
+
return next();
|
|
33
|
+
});
|
|
34
|
+
//#endregion
|
|
35
|
+
export { createActivityTracker, storefrontActivityMiddleware };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/analytics-proxy.d.ts
|
|
2
|
+
declare const PATHS: {
|
|
3
|
+
readonly visits: "/storefront/analytics/visits";
|
|
4
|
+
readonly events: "/storefront/analytics/events";
|
|
5
|
+
};
|
|
6
|
+
export type AnalyticsEndpoint = keyof typeof PATHS;
|
|
7
|
+
export declare function proxyAnalytics({ endpoint, body, visitToken, visitorToken, userAgent, clientIp, fbp, fbc, eventSourceUrl }: {
|
|
8
|
+
endpoint: AnalyticsEndpoint;
|
|
9
|
+
body: unknown;
|
|
10
|
+
visitToken?: string | null;
|
|
11
|
+
visitorToken?: string | null;
|
|
12
|
+
userAgent?: string | null;
|
|
13
|
+
clientIp?: string | null;
|
|
14
|
+
fbp?: string | null;
|
|
15
|
+
fbc?: string | null;
|
|
16
|
+
eventSourceUrl?: string | null;
|
|
17
|
+
}): Promise<{
|
|
18
|
+
status: number;
|
|
19
|
+
}>;
|
|
20
|
+
export declare function extractClientIp(headers: Headers): string | null;
|
|
21
|
+
//#endregion
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { requestTraceHeaders } from "./trace.js";
|
|
2
|
+
import { getStorefrontToken, storefrontApiUrl } from "./graphql.js";
|
|
3
|
+
import { isIP } from "node:net";
|
|
4
|
+
//#region src/analytics-proxy.ts
|
|
5
|
+
const PATHS = {
|
|
6
|
+
visits: "/storefront/analytics/visits",
|
|
7
|
+
events: "/storefront/analytics/events"
|
|
8
|
+
};
|
|
9
|
+
async function proxyAnalytics({ endpoint, body, visitToken, visitorToken, userAgent, clientIp, fbp, fbc, eventSourceUrl }) {
|
|
10
|
+
const headers = {
|
|
11
|
+
Authorization: `Bearer ${await getStorefrontToken()}`,
|
|
12
|
+
"Content-Type": "application/json",
|
|
13
|
+
...requestTraceHeaders()
|
|
14
|
+
};
|
|
15
|
+
if (visitToken) headers["Ahoy-Visit"] = visitToken;
|
|
16
|
+
if (visitorToken) headers["Ahoy-Visitor"] = visitorToken;
|
|
17
|
+
if (userAgent) headers["User-Agent"] = userAgent;
|
|
18
|
+
if (clientIp) headers["X-Buyer-IP"] = clientIp;
|
|
19
|
+
if (fbp) headers["X-Marketing-Fbp"] = fbp;
|
|
20
|
+
if (fbc) headers["X-Marketing-Fbc"] = fbc;
|
|
21
|
+
if (eventSourceUrl) headers["X-Event-Source-Url"] = eventSourceUrl;
|
|
22
|
+
return { status: (await fetch(`${storefrontApiUrl()}${PATHS[endpoint]}`, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers,
|
|
25
|
+
body: JSON.stringify(body ?? {})
|
|
26
|
+
})).status };
|
|
27
|
+
}
|
|
28
|
+
function extractClientIp(headers) {
|
|
29
|
+
const realIp = headers.get("x-real-ip");
|
|
30
|
+
if (realIp && isIP(realIp.trim())) return realIp.trim();
|
|
31
|
+
const xff = headers.get("x-forwarded-for");
|
|
32
|
+
if (xff) {
|
|
33
|
+
const first = xff.split(",")[0]?.trim();
|
|
34
|
+
if (first && isIP(first)) return first;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { extractClientIp, proxyAnalytics };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/analytics-script-proxy.d.ts
|
|
2
|
+
export interface AnalyticsScriptDocument {
|
|
3
|
+
body: string;
|
|
4
|
+
/** Rails' own `Cache-Control`, so the browser TTL stays a server decision. */
|
|
5
|
+
cacheControl: string | null;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The script body, memoized, under the hard `DEADLINE_MS` ceiling. Rejects when
|
|
9
|
+
* the deadline passes; the route turns any rejection into an empty 200.
|
|
10
|
+
*
|
|
11
|
+
* A rejection here abandons the read without keeping it alive for anyone else:
|
|
12
|
+
* on workerd its fetch dies with this request, and concurrent requests wait on
|
|
13
|
+
* the memo's lease rather than on this call, taking over once it lapses.
|
|
14
|
+
* `Promise.race` has already attached a handler to the memoized call, so a
|
|
15
|
+
* later failure can't surface as an unhandled rejection.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getAnalyticsScript(): Promise<AnalyticsScriptDocument>;
|
|
18
|
+
//#endregion
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { memoTTL } from "./cache.js";
|
|
2
|
+
import { getStorefrontTokenWithin, storefrontApiUrl } from "./graphql.js";
|
|
3
|
+
import { getContentVersion } from "./content-version.js";
|
|
4
|
+
//#region src/analytics-script-proxy.ts
|
|
5
|
+
const TIMEOUT_MS = 2e3;
|
|
6
|
+
const DEADLINE_MS = 2500;
|
|
7
|
+
async function fetchAnalyticsScript() {
|
|
8
|
+
const signal = AbortSignal.timeout(TIMEOUT_MS);
|
|
9
|
+
const token = await getStorefrontTokenWithin(signal);
|
|
10
|
+
const res = await fetch(`${storefrontApiUrl()}/storefront/analytics/script`, {
|
|
11
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
12
|
+
signal
|
|
13
|
+
});
|
|
14
|
+
if (!res.ok) throw new Error(`[analytics-script-proxy] upstream ${res.status}`);
|
|
15
|
+
return {
|
|
16
|
+
body: await res.text(),
|
|
17
|
+
cacheControl: res.headers.get("cache-control")
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const memoizedPixelScript = memoTTL(fetchAnalyticsScript, {
|
|
21
|
+
key: () => "analytics-script",
|
|
22
|
+
version: getContentVersion,
|
|
23
|
+
leaseMs: 2250
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* The script body, memoized, under the hard `DEADLINE_MS` ceiling. Rejects when
|
|
27
|
+
* the deadline passes; the route turns any rejection into an empty 200.
|
|
28
|
+
*
|
|
29
|
+
* A rejection here abandons the read without keeping it alive for anyone else:
|
|
30
|
+
* on workerd its fetch dies with this request, and concurrent requests wait on
|
|
31
|
+
* the memo's lease rather than on this call, taking over once it lapses.
|
|
32
|
+
* `Promise.race` has already attached a handler to the memoized call, so a
|
|
33
|
+
* later failure can't surface as an unhandled rejection.
|
|
34
|
+
*/
|
|
35
|
+
async function getAnalyticsScript() {
|
|
36
|
+
let timer;
|
|
37
|
+
try {
|
|
38
|
+
return await Promise.race([memoizedPixelScript(), new Promise((_resolve, reject) => {
|
|
39
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`[analytics-script-proxy] read exceeded ${DEADLINE_MS}ms`)), DEADLINE_MS);
|
|
40
|
+
})]);
|
|
41
|
+
} finally {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { getAnalyticsScript };
|
package/dist/blog.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { ImageFileInfo } from "./files.js";
|
|
2
|
+
//#region src/blog.d.ts
|
|
3
|
+
export type ArticleAuthor = {
|
|
4
|
+
name: string;
|
|
5
|
+
};
|
|
6
|
+
export type ArticleCardData = {
|
|
7
|
+
handle: string;
|
|
8
|
+
blogHandle: string;
|
|
9
|
+
title: string;
|
|
10
|
+
summary: string | null;
|
|
11
|
+
image: ImageFileInfo | null;
|
|
12
|
+
tags: string[];
|
|
13
|
+
author: ArticleAuthor | null;
|
|
14
|
+
publishedAt: string | null;
|
|
15
|
+
bodyExcerpt?: string;
|
|
16
|
+
};
|
|
17
|
+
export type ArticleData = ArticleCardData & {
|
|
18
|
+
bodyHtml: string;
|
|
19
|
+
summaryHtml: string | null;
|
|
20
|
+
updatedAt: string;
|
|
21
|
+
bodyPlain: string;
|
|
22
|
+
seo: {
|
|
23
|
+
title: string;
|
|
24
|
+
description: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
export type BlogMeta = {
|
|
28
|
+
id: string;
|
|
29
|
+
handle: string;
|
|
30
|
+
title: string;
|
|
31
|
+
};
|
|
32
|
+
export type BlogData = {
|
|
33
|
+
handle: string;
|
|
34
|
+
title: string;
|
|
35
|
+
articles_count: number;
|
|
36
|
+
articles: ArticleCardData[];
|
|
37
|
+
};
|
|
38
|
+
export type BlogPageData = BlogData & {
|
|
39
|
+
tags: string[];
|
|
40
|
+
total_count: number;
|
|
41
|
+
page_info: {
|
|
42
|
+
end_cursor: string | null;
|
|
43
|
+
has_next_page: boolean;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
/** Featured-blog / blog-card resolver fetcher. Returns `null` on missing
|
|
47
|
+
* blog or GraphQL error; pickers degrade silently. */
|
|
48
|
+
export declare const fetchBlog: (data: {
|
|
49
|
+
handle: string;
|
|
50
|
+
first: number;
|
|
51
|
+
locale?: string;
|
|
52
|
+
}) => Promise<BlogData | null>;
|
|
53
|
+
type FetchBlogPageInput = {
|
|
54
|
+
handle: string;
|
|
55
|
+
first?: number;
|
|
56
|
+
after?: string;
|
|
57
|
+
tag?: string;
|
|
58
|
+
locale?: string;
|
|
59
|
+
imageWidth?: number;
|
|
60
|
+
excerptLength?: number;
|
|
61
|
+
withBodyExcerpt?: boolean;
|
|
62
|
+
};
|
|
63
|
+
/** Server-driven blog listing. Uncached because cursor + tag permutations would
|
|
64
|
+
* blow the TTL cache; the underlying API is fast. Returns `null` on missing
|
|
65
|
+
* blog so the route can map to `notFound()`. */
|
|
66
|
+
export declare const fetchBlogPage: (data: FetchBlogPageInput) => Promise<BlogPageData | null>;
|
|
67
|
+
/** Single-article fetch. Used by the `article` and `article_list` resolver
|
|
68
|
+
* specs; returns `null` on missing article so the resolver drops the entry. */
|
|
69
|
+
export declare const fetchArticleByRef: (ref: string, locale?: string) => Promise<ArticleData | null>;
|
|
70
|
+
/** Detail-page fetch by `(blogHandle, handle)`. Used by the article route
|
|
71
|
+
* loader. */
|
|
72
|
+
export declare const fetchArticle: (params: {
|
|
73
|
+
blogHandle: string;
|
|
74
|
+
handle: string;
|
|
75
|
+
locale?: string;
|
|
76
|
+
}) => Promise<ArticleData | null>;
|
|
77
|
+
/** One picked article as a card, sized as `fetchBlogPage` sizes a listing's
|
|
78
|
+
* cards — for a picker whose article renders as a card, not a page. `ref` is
|
|
79
|
+
* what the `article` setting stores (`parseArticleRef`). `null` on a missing
|
|
80
|
+
* article or a failed read, so the card falls back rather than the page. */
|
|
81
|
+
export declare const fetchArticleCard: (params: {
|
|
82
|
+
ref: string;
|
|
83
|
+
imageWidth?: number;
|
|
84
|
+
excerptLength?: number;
|
|
85
|
+
withBodyExcerpt?: boolean;
|
|
86
|
+
locale?: string;
|
|
87
|
+
}) => Promise<ArticleCardData | null>;
|
|
88
|
+
/** Lean blog lookup for breadcrumbs / contextual labels. Skips articles. */
|
|
89
|
+
export declare const fetchBlogMeta: (h: string, locale?: string | undefined) => Promise<BlogMeta | null>;
|
|
90
|
+
/** The posts either side of one in its blog, by publish date. Named by age
|
|
91
|
+
* rather than previous / next because the two conventions disagree: Liquid's
|
|
92
|
+
* `blog.previous_article` is the NEWER post and `next_article` the older.
|
|
93
|
+
* Null at either end, for a post with no publish date, and on a failed read.
|
|
94
|
+
* `imageWidth`, `excerptLength` and `withBodyExcerpt` size the cards as
|
|
95
|
+
* `fetchBlogPage`'s do. */
|
|
96
|
+
export declare const fetchAdjacentArticles: (params: {
|
|
97
|
+
blogId: string;
|
|
98
|
+
handle: string;
|
|
99
|
+
publishedAt: string | null;
|
|
100
|
+
locale?: string;
|
|
101
|
+
imageWidth?: number;
|
|
102
|
+
excerptLength?: number;
|
|
103
|
+
withBodyExcerpt?: boolean;
|
|
104
|
+
}) => Promise<{
|
|
105
|
+
newer: ArticleCardData | null;
|
|
106
|
+
older: ArticleCardData | null;
|
|
107
|
+
}>;
|
|
108
|
+
//#endregion
|
|
109
|
+
export {};
|