AbiirSkincare/Hydrogen/web/app/entry.server.tsx

142 lines
4.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {ServerRouter} from 'react-router';
import {renderToReadableStream} from 'react-dom/server';
import {
createContentSecurityPolicy,
type HydrogenRouterContextProvider,
} from '@shopify/hydrogen';
import type {EntryContext} from 'react-router';
/**
* Product video / HLS URLs are often served from the shop’s **primary domain** (canonical
* `https://www.…/cdn/shop/...`), not only `cdn.shopify.com`. Oxygen preview hosts
* (`*.o2.myshopify.dev`) still need those origins in CSP even when `PUBLIC_SHOP_MEDIA_ORIGIN`
* is unset on that deployment.
*/
const SHOP_CANONICAL_MEDIA_ORIGINS: readonly string[] = [
'https://www.abiirskincare.com',
'https://abiirskincare.com',
'https://abiir-b9caf04a6de81250d3f0.o2.myshopify.dev'
];
/**
* CSP `connect-src`: HLS playlists/segments are fetched as XHR/fetch (esp. mobile Safari).
* `media-src` uses the same hosts plus `blob:` for MSE edge cases.
*/
function buildShopifyMediaConnectOrigins(env: Env): string[] {
const origins = new Set<string>([
"'self'",
'https://cdn.shopify.com',
'https://cdn.shopifycdn.com',
'https://videos.files.shopifycdn.com',
]);
for (const o of SHOP_CANONICAL_MEDIA_ORIGINS) {
origins.add(o);
}
const addOrigin = (raw: string | undefined) => {
const s = raw?.trim();
if (!s) return;
try {
const withScheme = /^https?:\/\//i.test(s) ? s : `https://${s}`;
origins.add(new URL(withScheme).origin);
} catch {
/* ignore invalid URL */
}
};
addOrigin(env.PUBLIC_SHOP_MEDIA_ORIGIN);
addOrigin(env.PUBLIC_STORE_DOMAIN);
return [...origins];
}
function buildShopifyMediaSrcDirectives(env: Env): string[] {
return [...buildShopifyMediaConnectOrigins(env), 'blob:'];
}
/** GROQ asset URLs (`asset->url`) are served from Sanity’s CDN; must match `img-src` / `media-src`. */
const SANITY_ASSET_CDN = 'https://cdn.sanity.io' as const;
function buildSanityConnectOrigins(env: Env): string[] {
const pid = env.SANITY_PROJECT_ID?.trim();
if (!pid) return [];
return [`https://${pid}.api.sanity.io`, `wss://${pid}.api.sanity.io`];
}
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
reactRouterContext: EntryContext,
context: HydrogenRouterContextProvider,
) {
const {nonce, header, NonceProvider} = createContentSecurityPolicy({
shop: {
checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
storeDomain: context.env.PUBLIC_STORE_DOMAIN,
},
styleSrc: [
"'self'",
"'unsafe-inline'",
'https://fonts.googleapis.com',
],
fontSrc: ["'self'", 'https://fonts.gstatic.com', 'data:'],
/**
* Hydrogen does not ship a default `img-src`; passing only `imgSrc` replaces the implicit
* reliance on `default-src` for images. Include Shopify CDN (store files + Oxygen asset URLs)
* or production will block every `<img>`.
*/
imgSrc: [
"'self'",
'data:',
'https://cdn.shopify.com',
'https://cdn.shopifycdn.com',
'https://images.unsplash.com',
SANITY_ASSET_CDN,
'https://www.facebook.com',
],
scriptSrc: [
"'self'",
'https://cdn.shopify.com',
'https://connect.facebook.net',
],
mediaSrc: [...buildShopifyMediaSrcDirectives(context.env), SANITY_ASSET_CDN],
/** HLS playlists / segments are often fetched via XHR; merged with Hydrogen defaults by `createContentSecurityPolicy`. */
connectSrc: [
...buildShopifyMediaConnectOrigins(context.env),
...buildSanityConnectOrigins(context.env),
'https://www.facebook.com',
'https://connect.facebook.net',
],
});
const body = await renderToReadableStream(
<NonceProvider>
<ServerRouter
context={reactRouterContext}
url={request.url}
nonce={nonce}
/>
</NonceProvider>,
{
nonce,
signal: request.signal,
onError(error) {
console.error(error);
responseStatusCode = 500;
},
},
);
// Resolve all Suspense / `<Await>` boundaries before sending HTML so the
// document matches `useLoaderData` during hydration. Without this, streamed
// fallbacks (e.g. header account placeholder) can differ from the client's
// first paint and trigger hydration failure + disappearing UI.
await body.allReady;
responseHeaders.set('Content-Type', 'text/html');
responseHeaders.set('Content-Security-Policy', header);
return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}