Horizon is a reference example and starter project for building custom Next v12 storefronts powered by Swell.js.
Important
The app is provided in an 'as-is' state and will not be receiving updates or bug fixes from March 1, 2024. Swell.js itself is in active development and is recommended SDK for building full-stack JS storefronts on Swell.
Since Horizon is a standard Next.js app, deploying it on Vercel only takes a few steps. Follow this guide for a walk-through on how to do so.
- Complete purchase flow
- Subscriptions
- Content personalization through customizable Quizzes
- Multi-currency
- i18n
- Product sales
- Automatic discounts
- One-click add-to-cart for products
- Dynamic pages with a large and extendable collection of content blocks
- Product search
- Filter products by any attribute
- Flexible product customization through variant options
- A customer portal with orders and subscription handling
- Next.js v12
- React v17
- TypeScript
- TailwindCSS
- Storybook
- Swell's GraphQL API
- State management via
Zustand
- End-to-end type generation through
graphql-codegen
- Radix UI and Headless UI for accessible components
- Unit tests through Jest
- Component tests through React Testing Library
- ESLint
- Prettier
- Pre-commit hooks through Husky
The first step will be to set up the environment variables. Create an .env
file at the root of the project's directory and add the following entries:
-
NEXT_PUBLIC_SWELL_STORE_URL
: This endpoint should have the following format:https://{YOUR_STORE_URL}.swell.store
-
NEXT_PUBLIC_SWELL_PUBLIC_KEY
: This value refers to a Store's Public Key that can be found inside the Developer > API Keys route in the Admin Dashboard. -
SWELL_STORE_ID
: The store's ID. It can be found at the top of the Developer > API Keys route in the Admin Dashboard. -
SWELL_SECRET_KEY
: A Store's Secret Key. Can be found inside the Developer > API Keys route in the Admin Dashboard, and it's used to sync the store with the local editor configuration. -
SWELL_STOREFRONT_ID
: The storefront id you are using Horizon with. It can be found at the top of: All Storefronts > your storefront > Developer in the Admin Dashboard. -
NEXT_PUBLIC_SWELL_EDITOR
: This should be set totrue
to allow for correct functionality when using the swell editor. In a production environment you can safely set this to false.
The next step will be setting up the development environment. We recommend installing Node.js through nvm. Afterwards, run the following commands to match the project's Node and NPM versions and install the project's dependencies:
nvm install
yarn install
This will also automatically setup the GraphQL client with end-to-end typings and download the content and settings from your store's editor. Check out the graphql:generate
and theme:generate
commands on the package.json
file for more information.
Lastly, you can start the development server with:
yarn run dev
Horizon should now be running on https://localhost:3000 by default.
You can take a look at the available components and the props they accept by running the storybook
npm script:
yarn run storybook
Storybook should now be running on https://localhost:6006, unless the port is already taken.
assets/
: Contains the project's icons and fonts. The latter are automatically pulled from the editor's selections.build-utils/
: The scripts in this folder are used to inject styles and assets from the editor before the project is ran or built.generate-theme.mjs
: Entry point for the theme generation logic.
components/
: The UI components used throughout the project. The component library is organized using Atomic Design principles and each component contains the following files:ComponentName.stories.tsx
file: Used to generate a Storybook entry for the component.ComponentName.tsx
: The React component itself.ComponentName.test.tsx
(Optional): Behavior interaction tests for the component.index.ts
: Re-exports the above files using the Barrell Exports Pattern to simplify imports. Since this folder structure is repeated every time a new component is built, we created a script to scaffold this structure. Usage:./create_component.sh <component_type> <component_name>
config/
: Contains the editor's schema. See here for more information on how to customize it.hooks/
: Contains the React Hooks used throughout the application.useClassNames
: Simplifies toggling class names conditionally.useCurrencySubscription
: Utility for automatically refetching product prices on currency change.useDraggableScroll
: DOM manipulation hook used within theHorizontalScroller
UI component.useLiveEditorQuizNavigation
: Used to keep the focus of Horizon and the Editor in sync while editing the Quiz.useLiveEditorQuizResultsNavigation
: Used to keep the focus of Horizon and the Editor in sync while editing the Quiz Results page.useLiveEditorQuizUpdates
: Used to keep the content of Horizon and the Editor in sync while editing the Quiz.useLiveEditorQuizResultsUpdates
: Used to keep the content of Horizon and the Editor in sync while editing the Quiz Results page.useLiveEditorUpdates
: Used to keep the content of the dynamic pages in sync between Horizon and the Editor.usePageSections
: Transforms the page sections coming from the editor into a list of components.useProductPrice
: Formats the products prices in the active locale and currency.useProductSearch
: Handles the product search logic.useProductSelection
: Handles the state for the user's product selections and returns the active variation data.useProductStock
: Returns the stock status and max quantity allowed for the active variation.useQuiz
: Handles the quiz steps selection logic.useQuizResults
: Fetches and transforms the product data to be shown based on the quiz selections.
lib/
: Provides quick access to commonly used resources.editor/
: Editor-specific utils.globals/
: Shared enums, constants, sizes, stylings, etc.graphql/
: Contains the GraphQL client, queries and generated sdk.rest/
: Rest API requests.shop/
: Product specific functions.utils/
: Small utility functions divided by category.
mock/
: Mocked data used for Storybook stories and tests.page_layouts/
: Page layout templates as a stand-in for the lack of layout support in Next 12. We were able to create easily reusable layouts that fetch data server-side (for better SEO and responsiveness and to prevent layout shifts), by wrapping the pages with these templates which are used as Higher Order Components, and by wrapping thegetServerSideProps
andgetStaticSideProps
fetching functions with the decorators located withinlib/utils/fetch_decorators.ts
.pages/
: The pages that constitute Horizon.account/
: The customer portal.blog/
: The blog pages.categories/
: The Category Details pages.products/
: The Product Details pages.quiz/
: The template for each dynamically generated quiz defined within the editor.[[...slug]]
: Maps to the dynamic pages defined in the "Pages" section of the editor.
stores/
: Zustand stores for globally accessible state.cart
: Stores the cart data.currency
: Keeps track of the active currency and its formatting.locale
: Keeps track of the stores' locales and the active one.notification
: Stores the pop-up notifications shown on specific actions or errors.quiz
: Stores the data for the quizzes and their answers.settings
: Contains the data for store settings such as colors and typography. These are mapped to environment variables and used throughout the theme.
styles/
: Contains the auto-generated theme and font stylings, plus some one-off style overrides or utility classes. Most stylings are defined inline through TailwindCSS utility classes.types/
: TypeScript type definitions, organized by category.codegen.yml
: Contains the configuration options for thegraphql-codegen
used for end-to-end type safety.create_component
: Scaffolds the structure for a new component.lint-staged.config.js
: Contains the configuration forlint-staged
, which runs before every commit. See the pre-commits section below for more details.
The config/
folder contains JSON files that dictate the editor's schema and default values. The top-level structure is defined inside of the editor.json
file, while the content blocks that make up the dynamic pages are located inside of the content/
folder.
In the prepare
step these files are pushed upstream to update the store's editor and afterwards the node script builds-utils/generate-theme.mjs
is executed. This script will attempt to fetch the theme data that has been set through the editor, and that can be accessed through the endpoint https://{{your store id}}.swell.store/api/settings
. This script can also be ran on its own by using the command npm run theme:generate
.
Using this theme data, the script will create a theme.css
file that exposes these settings as css variables, so that they can then be used by the Tailwind config and throughout the application.
To modify any of the theme settings, first make the change in the Editor and then re-trigger the script to pull them into the store. Alternatively, you can also modify the theme.css
file directly to preview how the changes would look before committing to them in the Editor.
In regards to customizing the editor's schema itself, please see here for more information.
The language settings represent a set of variables that can be translated into multiple languages in the editor and used throughout Horizon.
The config/editor.json
file has a lang
key which holds all the text variables used throughout the app. If you want to add a new setting follow these steps:
Add the object containing the configuration of the language setting inside lang
under the namespace you want (ex: products)
{
"lang": {
//...
{
"id": "lang.products.premium_member_label",
"label": "Premium member label",
"default": "Premium",
"type": "short_text",
"localized": true
}
}
}
Add a default value in the config/defaults.json
for the new setting (under the same path as the id)
{
"lang": {
//...
"products": {
"premium_member_label": "Premium"
}
}
}
Push the new settings to the store by running the sync
npm script. This will add the new language setting to the editor's schema.
yarn run sync
Use the new language setting in the code with the i18n hook
const i18n = useI18n();
const premiumLabel = i18n('products.premium_member_label');
Horizon automatically enters Next.js' preview mode when the NEXT_PUBLIC_SWELL_EDITOR
environment variable is set to true. This is done through the preview.ts
API route, which sets the cookies necessary to alert Next.js that pages should be rendered on-demand instead of statically. This is important when running alongside the editor so that the changes made within it are immediately reflected in the storefront without requiring a new build.
The calls to the preview.ts
API route are done through a useEffect
in app.tsx
.
Before every commit, the following checks are made:
- That there are no TypeScript errors.
- That the ESLint rules are satisfied.
- That the correct Prettier formatting is used.
This process will automatically attempt to fix the formatting and some of the eslint errors and warnings.
ESLint and TypeScript errors will prevent commits from being completed. See lint-staged.config.js
and eslintrc.json
for more information on the checks that are made.