@pylonsync/functions 0.4.12 → 0.4.14

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.
@@ -344,7 +344,9 @@ export declare function isSafeRedirect(url: string, opts: {
344
344
  /** Merge auto-discovered favicons (icon.* / apple-icon.* / favicon.ico)
345
345
  * into a page's metadata. Explicit `metadata.icons.*` wins. */
346
346
  export declare function applyAutoIcons(component: string, metadata: SsrMetadata | undefined): SsrMetadata | undefined;
347
- export declare function applyAutoSocialImages(component: string, headers: Record<string, string> | undefined, metadata: SsrMetadata | undefined, requestUrl?: string): SsrMetadata | undefined;
347
+ export declare function applyAutoSocialImages(component: string, headers: Record<string, string> | undefined, metadata: SsrMetadata | undefined, requestUrl?: string,
348
+ /** True for a not-found / error boundary — see findColocatedOgImageRoute. */
349
+ isBoundary?: boolean): SsrMetadata | undefined;
348
350
  /**
349
351
  * Drain a `renderToReadableStream` reader, injecting `headBlob` immediately
350
352
  * before the first `</head>` (or, if the document has none, the blob is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/functions",
3
- "version": "0.4.12",
3
+ "version": "0.4.14",
4
4
  "description": "TypeScript function runtime for pylon — defines server-side queries, mutations, and actions.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -181,3 +181,75 @@ describe("renderMetadata drives boundary heads too", () => {
181
181
  expect(renderMetadata(require("react"), undefined)).toBeNull();
182
182
  });
183
183
  });
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // A boundary's dynamic og:image URL.
187
+ //
188
+ // Regression: the URL was composed from the REQUEST path while the file was
189
+ // resolved from the boundary's directory. A 404 therefore advertised a card
190
+ // under whatever path failed. Usually that 404s in turn — a broken card is
191
+ // worse than none, since a crawler now has something to fail on — but at a
192
+ // depth where a dynamic route really does define one, it returned 200 and
193
+ // served an unrelated page's card. Silent wrong content.
194
+ // ---------------------------------------------------------------------------
195
+
196
+ import { applyAutoSocialImages } from "./ssr-runtime";
197
+
198
+ describe("boundary og:image points at the boundary's own card", () => {
199
+ // Compare the PATH only. Absolutizing against the request origin needs a
200
+ // trusted host, which is a separate concern from which route the card lives
201
+ // at — and the path is the thing that regressed.
202
+ const ogPath = (m: any) => {
203
+ const u = m?.openGraph?.image as string | undefined;
204
+ if (!u) return u;
205
+ return u.startsWith("http") ? new URL(u).pathname : u;
206
+ };
207
+ const headers = { host: "example.com" };
208
+
209
+ test("a root boundary advertises the root card whatever URL failed", () => {
210
+ const root = tree([
211
+ "app/opengraph-image.tsx",
212
+ "app/not-found.tsx",
213
+ "app/[orgSlug]/[eventSlug]/opengraph-image.tsx",
214
+ "app/[orgSlug]/[eventSlug]/page.tsx",
215
+ ]);
216
+ const prev = process.cwd();
217
+ process.chdir(root);
218
+ try {
219
+ for (const url of [
220
+ "/a/b/c/d/e",
221
+ "/nope-does-not-exist",
222
+ // The dangerous one: a real card EXISTS at this depth, so the old
223
+ // behavior returned 200 with another event's image.
224
+ "/no-such-org/no-such-event",
225
+ ]) {
226
+ const md = applyAutoSocialImages("app/not-found", headers, undefined, url, true);
227
+ expect(ogPath(md), `for ${url}`).toBe("/opengraph-image");
228
+ }
229
+ } finally {
230
+ process.chdir(prev);
231
+ }
232
+ });
233
+
234
+ test("a page still advertises the card at its own path", () => {
235
+ // The request path IS the route path for a page — including a tail a
236
+ // catch-all consumed, which is why that shortcut exists.
237
+ const root = tree([
238
+ "app/[orgSlug]/[eventSlug]/opengraph-image.tsx",
239
+ "app/[orgSlug]/[eventSlug]/page.tsx",
240
+ ]);
241
+ const prev = process.cwd();
242
+ process.chdir(root);
243
+ try {
244
+ const md = applyAutoSocialImages(
245
+ "app/[orgSlug]/[eventSlug]/page",
246
+ headers,
247
+ undefined,
248
+ "/acme/summit",
249
+ );
250
+ expect(ogPath(md)).toBe("/acme/summit/opengraph-image");
251
+ } finally {
252
+ process.chdir(prev);
253
+ }
254
+ });
255
+ });
@@ -899,6 +899,17 @@ const OG_IMAGE_CODE_EXTS = [".tsx", ".ts", ".jsx", ".js"];
899
899
  function findColocatedOgImageRoute(
900
900
  componentPath: string,
901
901
  url: string,
902
+ /**
903
+ * True when `componentPath` is a not-found / error boundary rather than a
904
+ * page. A page's URL depth matches its directory depth, so when the image
905
+ * sits in the page's own directory the request path IS the route path —
906
+ * including the tail a catch-all consumed, which is why that shortcut
907
+ * exists. A boundary is dispatched at whatever URL failed, so the same
908
+ * shortcut advertised a card under the 404'd path: mostly a 404 of its own,
909
+ * but at a depth where a dynamic route really does define one, a 200
910
+ * serving an unrelated page's card.
911
+ */
912
+ isBoundary = false,
902
913
  ): string | null {
903
914
  const fs = require("node:fs");
904
915
  const path = require("node:path");
@@ -918,7 +929,9 @@ function findColocatedOgImageRoute(
918
929
  for (const ext of OG_IMAGE_CODE_EXTS) {
919
930
  if (fs.existsSync(path.join(cwd, dir, `opengraph-image${ext}`))) {
920
931
  const prefix =
921
- dir === pageDir ? urlSegs : urlSegs.slice(0, nonGroupDepth(dir));
932
+ dir === pageDir && !isBoundary
933
+ ? urlSegs
934
+ : urlSegs.slice(0, nonGroupDepth(dir));
922
935
  return "/" + [...prefix, "opengraph-image"].join("/");
923
936
  }
924
937
  }
@@ -1287,6 +1300,8 @@ export function applyAutoSocialImages(
1287
1300
  headers: Record<string, string> | undefined,
1288
1301
  metadata: SsrMetadata | undefined,
1289
1302
  requestUrl?: string,
1303
+ /** True for a not-found / error boundary — see findColocatedOgImageRoute. */
1304
+ isBoundary = false,
1290
1305
  ): SsrMetadata | undefined {
1291
1306
  // A relative image is the single most common way a share card breaks:
1292
1307
  // the page ships `og:image="/og.png"`, Facebook and Slack resolve it,
@@ -1310,7 +1325,7 @@ export function applyAutoSocialImages(
1310
1325
  const ogRoute =
1311
1326
  hasOg || ogFile || requestUrl == null
1312
1327
  ? null
1313
- : findColocatedOgImageRoute(component, requestUrl);
1328
+ : findColocatedOgImageRoute(component, requestUrl, isBoundary);
1314
1329
 
1315
1330
  if (!ogFile && !twFile && !ogRoute) return metadata;
1316
1331
 
@@ -2279,6 +2294,20 @@ async function tryRenderBoundary(
2279
2294
  boundaryMeta = mod.metadata;
2280
2295
  }
2281
2296
  }
2297
+ // The same file conventions a page gets, resolved against the BOUNDARY's
2298
+ // own directory: a colocated opengraph-image / icon next to not-found.tsx
2299
+ // is that boundary's, not the failing page's. Without these, one 404 URL
2300
+ // carried the social card and another didn't, depending only on whether
2301
+ // the route matched — the boundary was reached two ways and looked
2302
+ // different each way.
2303
+ boundaryMeta = applyAutoSocialImages(
2304
+ rel,
2305
+ compProps?.headers,
2306
+ boundaryMeta,
2307
+ compProps?.url,
2308
+ true,
2309
+ );
2310
+ boundaryMeta = applyAutoIcons(rel, boundaryMeta);
2282
2311
  const boundaryMetaFragment = renderMetadata(React, boundaryMeta);
2283
2312
  let tree = React.createElement(Comp, compProps);
2284
2313
  if (boundaryMetaFragment) {
@@ -3107,7 +3136,16 @@ export async function handleRenderRoute(
3107
3136
  // File conventions: auto-wire <meta og:image>/<twitter:image> from a
3108
3137
  // colocated opengraph-image.* / twitter-image.*, and <link rel="icon">
3109
3138
  // from icon.* / apple-icon.* / favicon.ico — unless the page set them.
3110
- metadata = applyAutoSocialImages(msg.component, msg.headers, metadata, msg.url);
3139
+ // The host dispatches `app/not-found` / `app/error` BY NAME for an
3140
+ // unmatched URL, so this path renders boundaries too.
3141
+ const componentIsBoundary = /(^|\/)(not-found|error)$/.test(msg.component);
3142
+ metadata = applyAutoSocialImages(
3143
+ msg.component,
3144
+ msg.headers,
3145
+ metadata,
3146
+ msg.url,
3147
+ componentIsBoundary,
3148
+ );
3111
3149
  metadata = applyAutoIcons(msg.component, metadata);
3112
3150
  const metaFragment = renderMetadata(React, metadata);
3113
3151