Origin is a reference example and starter project for building custom Nuxt v2 storefronts powered by Swell.js. 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.

  • Built with Nuxt.js (the Vue equivalent of Next.js).
  • Uses Tailwind CSS, so it's easy to change the design.
  • Includes page templates, sections, and components for common use cases.
  • Multi-currency and multi-language support.
  • Pre-built cart and checkout.
  • Subscriptions for physical and digital products

Clone this project repository to your local machine and navigate to the project root.

Add your Swell store ID, public key, and url to .env.

Connecting to Swell
SWELL_STORE_ID=your_store
SWELL_PUBLIC_KEY=GET_YOUR_PK_FROM_THE_ADMIN_DASHBOARD
SWELL_STORE_URL=https://your_store.swell.store
Installation
// Install dependencies
$ yarn install

// Serve with hot module reloading at localhost:3000
$ yarn run dev

// Build for production and start local server
$ yarn run build
$ yarn start

// Generate static build for hosting on Netlify/Render/Surge etc.
// For more details about static generation, check the Nuxt docs https://nuxtjs.org/api/configuration-generate/
$ yarn run generate

If you aren't already familiar with Vue, Nuxt, Tailwind, or Swell, these resources will get you up to speed:

For more advanced usage and packages you can use in your project:

The @nuxtjs/pwa module provides PWA functionality.

  • Any components in the /components folder are loaded automatically, so you don't need to explicitly import them.
  • Common cart methods are implemented as Vuex actions in /store/index.js
  • Swell.js is initialized and injected into the Nuxt context as $swell

For a detailed explanation of how these things work, check out Nuxt.js docs.

The /config/content folder stores content type definitions. Each JSON file represents a content type, which can also be referenced in other content types.

If a content type doesn't need to be referenced in multiple types (e.g., a repeatable item in a media slider), you can define the type object in the item_types value of a collection field.

name: Human-friendly label displayed in the dashboard.

description: Explanation of what the content type is intended for.

model: If a content type is only used inside another type and doesn't need to be fetched by itself, you can leave this attribute out.

There are two situations where you'll need to specify a model:

  1. Making a content type queryable
    If the content type is standalone and needs to have its own endpoint for fetching data (like pages or blog posts), the model value should be content/{pluralized_type_id} (e.g., content/pages). Note: Custom model IDs should use snake_case for consistency with system models.
  2. Adding fields to system models
    Content types can be used to add fields to system models like categories and products. Including the model attribute with the system model ID as the value will add a content object to each instance, with content type field nested inside. This way, custom content fields won't ever conflict with standard fields.

fields: An array of field definition objects (see below for details)

As you'd expect from a CMS, there are many different field types you can use to model and edit your content. Each field on a content type is an object containing the following attributes:

id: A unique field ID for referencing the field in code. IDs may be specified as an object path, enabling values to be nested together for convenience and clarity.

Custom fields should use snake_case for consistency with system fields. Since convention in the Vue ecosystem is to use camelCase for variable names, Swell.js will normalize object keys when sending and receiving data.

label: Human-friendly label displayed in the dashboard.

type + ui:

  • type is the type of field to use for storing the value.
  • ui (optional) is the interface displayed in the dashboard, allowing for a more appropriate editing experience and specific formatting. If omitted, the default UI for the field type will be used.

heading: For breaking up a set of fields in the dashboard. Not included in API responses.

heading example
{
  "label": ...,
  "type": "heading" // (Required)
}

short_text: For a small amount of plain text that fits on one line.

short_text example
{
  "id": ...,
  "label": ...,
  "type": "short_text", // (Required)
  "ui": "text|phone|email|url|slug",
  "min": Number, // Minimum character count
  "max": Number // Maximum character count
}

long_text: For a larger amount of text that spans multiple lines and may include special formatting.

long_text example
{
  "id": ...,
  "label": ...,
  "type": "short_text", // (Required)
  "ui": "textarea|basic_html|rich_html|markdown",
  "min": Number, // Minimum character count
  "max": Number, // Maximum character count
  "html_tags": [String] // Allowed HTML tags (if null, the default tags for the UI will be allowed)
}

number: For any kind of numerical value.

number example
{
  "id": ...,
  "label": ...,
  "type": "number", // (Required)
  "ui": "number|slider|currency",
  "min": Number, // Minimum value
  "max": Number, // Maximum value
  "increment": Number, // Interval between values
  "decimal_places": Number, // Enforce a specific number of decimal places
  "unit": String // Unit of measurement
}

boolean: For binary values that are true or false.

boolean example
{
  "id": ...,
  "label": ...,
  "type": "boolean", // (Required)
  "ui": "checkbox|toggle"
}

select: For choosing from a list of predefined values.

select example
{
  "id": ...,
  "label": ...,
  "type": "boolean", // (Required)
  "ui": "radio|checkboxes|dropdown|button_group",
  "multi": Boolean, // Whether multiple items can be selected at once
  "options": [String|Number]
}

lookup: For referencing objects in the system.

lookup example
{
  "id": ...,
  "label": ...,
  "type": "lookup", // (Required)
  "model": "",
  "key": "",
  "params": "",
  "query": ""
}

asset: For uploading files like images and documents.

asset example
{
  "id": ...,
  "label": ...,
  "type": "asset", // (Required)
  "asset_types": Array, // What file types are allowed (e.g. image, document, video, <mime_type>)
  "multi": Boolean // Whether multiple assets are allowed
}

collection: For lists of items that can be manually ordered. Ideal for flexible and repeatable content like landing pages or media sliders.

collection example
{
  "id": ...,
  "label": ...,
  "type": "collection", // (Required)
  "item_types": [String], // What content types are allowed (you can also define a content type inline)
  "min": Number, // Minimum number of items
  "max": Number // Maximum number of items
}

date: For inputting dates and times; saved as an ISO 8601 date string.

date example
{
  "id": ...,
  "label": ...,
  "type": "date", // (Required)
  "ui": "date|time|datetime",
  "min": String, // Earliest date
  "max": String // Latest date
}

color: For inputting a color using a color picker UI; saved as a hex code.

color_example
{
  "id": ...,
  "label": ...,
  "type": "color" // (Required)
}

tags: For Adding one or more text tags; features autocomplete.

tags_example
{
  "id": ...,
  "label": ...,
  "type": "tags", // (Required)
  "min": Number, // Minimum number of tags
  "max": Number // Maximum number of tags
}

field_group: For grouping fields together to reduce clutter in the dashboard and provide an optimal editing experience.

Fields inside groups are not nested on API responses by default, as groups are only there to improve the editing UX. However, as with any field, you can specify the child field IDs as object paths if you want to nest several fields under one key.

field_group example
{
  "id": ...,
  "label": ...,
  "type": "field_group", // (Required)
  "fields": [Object] // Fields to show inside the group
}