If you want to deploy your Shopify Hydrogen "2" (2024-01+) app to Vercel, you can do so by following these steps:
Before continue, make sure that you have installed Shopify Hydrogen quickstart, or you can install it by run the command line below:
npm create @shopify/hydrogen@latest -- --quickstart
1. Remove env.d.ts and vite.config.js
From root folder, remove env.d.ts and vite.config.js
2. Modify package.json
Modify package.json file, update as below:
- Remove this line:
"type": "module",
- Modify the lines below:
"typescript": "^5.2.2",
"vite": "^5.1.0",
"vite": "^5.1.0",
"vite-tsconfig-paths": "^4.3.1"
Change to:
"typescript": "^5.2.2"
3. Install semver
Open the terminal from root folder of Hydrogen project then run command line below to install semver:
npm install semver
4. Create server-dev.js
From root folder, rename server.js to server-dev.js (for running on development).
5. Create new server.js file with content below:
// @ts-ignore
// 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,
createCustomerAccountClient,
} from '@shopify/hydrogen';
import {AppSession} from '~/lib/session';
import {CART_QUERY_FRAGMENT} from '~/lib/fragments';
/**
* Export a fetch handler in module format.
*/
export default async function (request) {
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 = {
SESSION_SECRET: '',
PUBLIC_STOREFRONT_API_TOKEN: '',
PRIVATE_STOREFRONT_API_TOKEN: '',
PUBLIC_STORE_DOMAIN: '',
PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID: '',
PUBLIC_CUSTOMER_ACCOUNT_API_URL: '',
PUBLIC_CHECKOUT_DOMAIN: '',
};
env.SESSION_SECRET = process.env.SESSION_SECRET;
env.PUBLIC_STOREFRONT_API_TOKEN = process.env.PUBLIC_STOREFRONT_API_TOKEN;
env.PRIVATE_STOREFRONT_API_TOKEN = process.env.PRIVATE_STOREFRONT_API_TOKEN;
env.PUBLIC_STORE_DOMAIN = process.env.PUBLIC_STORE_DOMAIN;
env.PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID = process.env.PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID;
env.PUBLIC_CUSTOMER_ACCOUNT_API_URL = process.env.PUBLIC_CUSTOMER_ACCOUNT_API_URL;
env.PUBLIC_CHECKOUT_DOMAIN = process.env.PUBLIC_CHECKOUT_DOMAIN;
/**
* 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]),
]);
/**
* Create Hydrogen's Storefront client.
*/
const {storefront} = createStorefrontClient({
buyerIp: request.headers.get('x-forwarded-for') ?? undefined,
i18n: {language: 'EN', country: 'US'},
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'),
});
/**
* Create a client for Customer Account API.
*/
const customerAccount = createCustomerAccountClient({
request,
session,
customerAccountId: env.PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID,
customerAccountUrl: env.PUBLIC_CUSTOMER_ACCOUNT_API_URL,
});
const cart = createCartHandler({
storefront,
customerAccount,
getCartId: cartGetIdDefault(request.headers),
setCartId: cartSetIdDefault(),
cartQueryFragment: CART_QUERY_FRAGMENT,
});
const handleRequest = createRequestHandler(remixBuild, 'production');
const response = await handleRequest(request, {
session,
storefront,
customerAccount,
cart,
env,
waitUntil: () => Promise.resolve(),
});
if (session.isPending) {
response.headers.set(
'Set-Cookie',
await session.commit(),
);
}
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});
}
}
6. Create remix.config.js 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.js' : './server.js',
/**
* 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,
v3_throwAbortReason: 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,
},
}
};
7. Create remix.env.d.ts with content below:
// Enhance TypeScript's built-in typings.
import '@total-typescript/ts-reset';
import type {
Storefront,
CustomerAccount,
HydrogenCart,
HydrogenSessionData,
} from '@shopify/hydrogen';
import type {
LanguageCode,
CountryCode,
} from '@shopify/hydrogen/storefront-api-types';
import type {AppSession} from '~/lib/session';
declare global {
/**
* A global `process` object is only available during build to access NODE_ENV.
*/
const process: {env: {NODE_ENV: 'production' | 'development'}};
/**
* 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;
PUBLIC_CHECKOUT_DOMAIN: string;
}
/**
* The I18nLocale used for Storefront API query context.
*/
type I18nLocale = {
language: LanguageCode;
country: CountryCode;
pathPrefix: string;
};
}
declare module '@shopify/remix-oxygen' {
/**
* Declare local additions to the Remix loader context.
*/
interface AppLoadContext {
env: Env;
cart: HydrogenCart;
storefront: Storefront;
customerAccount: CustomerAccount;
session: AppSession;
waitUntil: ExecutionContext['waitUntil'];
}
/**
* Declare local additions to the Remix session data.
*/
interface SessionData extends HydrogenSessionData {}
}
// Needed to make this file a module.
export {}
8. Modify app/lib/session.js
import {
createCookieSessionStorageFactory,
createCookieFactory,
} from '@remix-run/server-runtime';
const encoder = new TextEncoder();
export const sign = 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 = 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, usages) {
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{name: 'HMAC', hash: 'SHA-256'},
false,
usages,
);
return key;
}
function byteStringToUint8Array(byteString) {
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{
isPending = false;
#sessionStorage;
#session;
constructor(sessionStorage, session) {
this.#sessionStorage = sessionStorage;
this.#session = session;
}
static async init(request, secrets) {
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() {
this.isPending = true;
return this.#session.unset;
}
get set() {
this.isPending = true;
return this.#session.set;
}
destroy() {
return this.#sessionStorage.destroySession(this.#session);
}
commit() {
this.isPending = false;
return this.#sessionStorage.commitSession(this.#session);
}
}
Done! You should now be able to deploy your Shopify Hydrogen 2024.x version to Vercel. Please config for your vercel Remix project like screenshot below:
You should add all env variables to Vercel before deploy:
Check out the pull request below if you can't deploy Hydrogen 2024.x to Vercel by following the steps above:
https://github.com/JaxsonLuu/hydrogen-vercel/pull/1
Thanks for reading! We hope you found this article helpful!