If you want to deploy your Shopify Hydrogen "2" (2023-01+) app to Vercel, you can do so by following these steps:
*Note: The updates below apply for Hydrogen version (2024.1.0 or higher)
1) In root folder, rename server.ts to server-dev.ts (for running on development).
2) Create new server.ts with content below:
// Virtual entry point for the app
import * as remixBuild from '@remix-run/dev/server-build';
import {createRequestHandler} from '@remix-run/server-runtime';
import {
cartGetIdDefault,
cartSetIdDefault,
createCartHandler,
createStorefrontClient,
} from '@shopify/hydrogen';
import {AppSession} from '~/lib/session.server';
import {getLocaleFromRequest} from '~/lib/utils';
/**
* Export a fetch handler in module format.
*/
export default async function (request: Request): Promise {
try {
/**
* This has to be done so messy because process.env can't be destructured
* and only variables explicitly named are present inside a Vercel Edge Function.
* See https://github.com/vercel/next.js/pull/31237/files
*/
const env: Env = {
SESSION_SECRET: '',
PUBLIC_STOREFRONT_API_TOKEN: '',
PRIVATE_STOREFRONT_API_TOKEN: '',
PUBLIC_STORE_DOMAIN: '',
PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID: '',
PUBLIC_CUSTOMER_ACCOUNT_API_URL: '',
};
env.SESSION_SECRET = process.env.SESSION_SECRET as string;
env.PUBLIC_STOREFRONT_API_TOKEN = process.env
.PUBLIC_STOREFRONT_API_TOKEN as string;
env.PRIVATE_STOREFRONT_API_TOKEN = process.env
.PRIVATE_STOREFRONT_API_TOKEN as string;
env.PUBLIC_STORE_DOMAIN = process.env.PUBLIC_STORE_DOMAIN as string;
env.PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID = process.env
.PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID as string;
env.PUBLIC_CUSTOMER_ACCOUNT_API_URL = process.env.PUBLIC_CUSTOMER_ACCOUNT_API_URL as string;
/**
* Open a cache instance in the worker and a custom session instance.
*/
if (!env?.SESSION_SECRET) {
throw new Error('SESSION_SECRET process.environment variable is not set');
}
const [session] = await Promise.all([
AppSession.init(request, [process.env.SESSION_SECRET as string]),
]);
/**
* Create Hydrogen's Storefront client.
*/
const {storefront} = createStorefrontClient({
buyerIp: request.headers.get('x-forwarded-for') ?? undefined,
i18n: getLocaleFromRequest(request),
publicStorefrontToken: env.PUBLIC_STOREFRONT_API_TOKEN,
privateStorefrontToken: env.PRIVATE_STOREFRONT_API_TOKEN,
storeDomain: env.PUBLIC_STORE_DOMAIN,
// storefrontId: process.env.PUBLIC_STOREFRONT_ID,
// requestGroupId: request.headers.get('request-id'),
});
const cart = createCartHandler({
storefront,
getCartId: cartGetIdDefault(request.headers),
setCartId: cartSetIdDefault(),
});
const handleRequest = createRequestHandler(remixBuild as any, 'production');
const response = await handleRequest(request, {
session,
storefront,
cart,
env,
waitUntil: () => Promise.resolve(),
});
if (response.status === 404) {
/**
* Check for redirects only when there's a 404 from the app.
* If the redirect doesn't exist, then `storefrontRedirect`
* will pass through the 404 response.
*/
// return storefrontRedirect({request, response, storefront});
}
return response;
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
return new Response('An unexpected error occurred', {status: 500});
}
}
3) Update remix.config.ts with content below:
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
appDirectory: 'app',
ignoredRouteFiles: ['**/.*'],
watchPaths: ['./public', './.env'],
server:
process.env.NODE_ENV === 'development' ? './server-dev.ts' : './server.ts',
/**
* The following settings are required to deploy Hydrogen apps to Oxygen:
*/
publicPath: (process.env.HYDROGEN_ASSET_BASE_URL ?? '/') + 'build/',
assetsBuildDirectory: 'dist/client/build',
serverBuildPath: 'dist/worker/index.js',
serverMainFields: ['browser', 'module', 'main'],
serverConditions: ['worker', process.env.NODE_ENV],
serverDependenciesToBundle: 'all',
serverModuleFormat: 'esm',
serverPlatform: 'neutral',
serverMinify: process.env.NODE_ENV === 'production',
postcss: true,
tailwind: true,
future: {
v3_fetcherPersist: true,
v3_relativeSplatpath: true,
},
serverNodeBuiltinsPolyfill: {
modules: {
buffer: true, // Provide a JSPM polyfill
fs: "empty", // Provide an empty polyfill
crypto: true,
zlib: true,
stream: true,
events: true,
https: true,
http: true,
net: true,
tls: true,
url: true
},
globals: {
Buffer: true,
},
}
};
4) Modify remix.env.d.ts
import type {WithCache, HydrogenCart} from '@shopify/hydrogen';
import type {Storefront} from '~/lib/type';
import type {AppSession} from '~/lib/session.server';
declare global {
/**
* A global `process` object is only available during build to access NODE_ENV.
*/
const process: {env: {NODE_ENV: 'production' | 'development'} & Env};
/**
* Declare expected Env parameter in fetch handler.
*/
interface Env {
SESSION_SECRET: string;
PUBLIC_STOREFRONT_API_TOKEN: string;
PRIVATE_STOREFRONT_API_TOKEN: string;
PUBLIC_STORE_DOMAIN: string;
PUBLIC_STOREFRONT_ID: string;
PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID: string;
PUBLIC_CUSTOMER_ACCOUNT_API_URL: string;
}
}
declare module '@shopify/remix-oxygen' {
/**
* Declare local additions to the Remix loader context.
*/
export interface AppLoadContext {
waitUntil: ExecutionContext['waitUntil'];
session: AppSession;
storefront: Storefront;
cart: HydrogenCart;
env: Env;
}
/**
* Declare the data we expect to access via `context.session`.
*/
export interface SessionData {
customerAccessToken: string;
}
}
// Needed to make this file a module.
export {}
5) Update app/lib/session.server.ts
import {type HydrogenSession} from '@shopify/hydrogen';
import {
createCookieSessionStorageFactory,
createCookieFactory,
} from '@remix-run/server-runtime';
import type {
SignFunction,
UnsignFunction,
type SessionStorage,
type Session,
} from '@remix-run/server-runtime';
const encoder = new TextEncoder();
export const sign: SignFunction = async (value, secret) => {
const data = encoder.encode(value);
const key = await createKey(secret, ['sign']);
const signature = await crypto.subtle.sign('HMAC', key, data);
const hash = btoa(String.fromCharCode(...new Uint8Array(signature))).replace(
/=+$/,
'',
);
return value + '.' + hash;
};
export const unsign: UnsignFunction = async (cookie, secret) => {
const value = cookie.slice(0, cookie.lastIndexOf('.'));
const hash = cookie.slice(cookie.lastIndexOf('.') + 1);
const data = encoder.encode(value);
const key = await createKey(secret, ['verify']);
const signature = byteStringToUint8Array(atob(hash));
const valid = await crypto.subtle.verify('HMAC', key, signature, data);
return valid ? value : false;
};
async function createKey(secret: string, usages: CryptoKey['usages']): Promise {
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{name: 'HMAC', hash: 'SHA-256'},
false,
usages,
);
return key;
}
function byteStringToUint8Array(byteString: string): Uint8Array {
const array = new Uint8Array(byteString.length);
for (let i = 0; i < byteString.length; i++) {
array[i] = byteString.charCodeAt(i);
}
return array;
}
/**
* This is a custom session implementation for your Hydrogen shop.
* Feel free to customize it to your needs, add helper methods, or
* swap out the cookie-based implementation with something else!
*/
export class AppSession implements HydrogenSession {
#sessionStorage;
#session;
constructor(sessionStorage: SessionStorage, session: Session) {
this.#sessionStorage = sessionStorage;
this.#session = session;
}
static async init(request: Request, secrets: string[]) {
const createCookie = createCookieFactory({sign, unsign});
const createCookieSessionStorage =
createCookieSessionStorageFactory(createCookie);
const storage = createCookieSessionStorage({
cookie: {
name: 'session',
httpOnly: true,
path: '/',
sameSite: 'lax',
secrets,
},
});
const session = await storage
.getSession(request.headers.get('Cookie'))
.catch(() => storage.getSession());
return new this(storage, session);
}
get has() {
return this.#session.has;
}
get get() {
return this.#session.get;
}
get flash() {
return this.#session.flash;
}
get unset() {
return this.#session.unset;
}
get set() {
return this.#session.set;
}
destroy() {
return this.#sessionStorage.destroySession(this.#session);
}
commit() {
return this.#sessionStorage.commitSession(this.#session);
}
}
Done! You should now be able to deploy your Shopify Hydrogen 2.0 to Vercel. Please config for your vercel Remix project like screenshot below:
Watch the video below If you can't deploy Hydrogen 2.0 to Vercel by following the steps above: