To learn the platform, let’s create an app that covers a range of surfaces in Swell. This app will be comprehensive enough to show you how to implement each pattern with concrete examples.
We’ll call it Honest Reviews. The scope of our app will include the following:
To follow along this guide, get the Honest Reviews app installed.
First, you’ll need the Swell CLI and a Swell account. First, install the CLI and log in.
npm install -g @swell/cli
swell login
Next, clone the example app and push it to your test environment.
git clone [email protected]:swellstores/honest-reviews-app.git
cd honest-reviews-app
npm install
swell app push
Once installed, you can use the link in the CLI to navigate to the app details page in your dashboard. You should also see “Honest Reviews” appear as an option in your left navigation under Products. Various tabs, columns, and fields should become visible as well.
Finally, add an API key and run the storefront app locally.
cd /path/to/honest-reviews/storefront/
cat >> .env.local << EOF
NEXT_PUBLIC_SWELL_STORE_ID=<your_store_id>
NEXT_PUBLIC_SWELL_STORE_KEY=<your_public_key>
EOF
npm install
npm run dev
Use the CLI to push configuration and function changes from to the test store.
swell app push
.swell app push
[store] test [env] test
App details
✔ swell.json
✔ asserts/icon.png
Data models
✔ models/reviews.json
...
Pushed 28 configurations to Honest Reviews. View the Honest Reviews app in your dashboard at <https://example.swell.store/admin/test/apps/>...
swell app watch
.swell app watch
[store] test [env] test
Watching for changes...
✔ Pushed models/reviews.json
✔ Pushed functions/review-created.ts
When testing, view the logs from your dashboard under Developer > Console (Logs tab), or using the swell logs
CLI command.
Often the easiest way to start building an app is to first consider the data schema. A Swell app can add fields to a store with both Content models and Data models. Content models represent dashboard elements, but they also create data models and fields automatically in the background to match your inputs, while data models give you additional flexibility. Both model types can overlap on the same collection in order to finely control backend vs dashboard behavior.
Let’s start by creating a data model for Reviews in models/reviews.json
. The following is a basic shape that we’ll continue adding onto as we go.
{
"fields": {
"account_id": {
"type": "objectid",
"required": true
},
"account": {
"type": "link",
"model": "accounts",
"key": "account_id"
},
"product_id": {
"type": "objectid",
"required": true
},
"product": {
"type": "link",
"model": "products",
"key": "product_id"
},
"title": {
"type": "string",
"required": true
},
"body": {
"type": "string",
"required": true
},
"images": {
"type": "array",
"value_type": "file"
},
"rating": {
"type": "int",
"min": 1,
"max": 5,
"required": true
},
"status": {
"type": "string",
"enum": ["submitted", "approved", "rejected"],
"default": "submitted"
},
"rejected_reason": {
"type": "string"
},
"featured": {
"type": "bool",
"default": false
}
}
}
To summarize, the basic Reviews model has:
When this model is created, we now have access to a new collection and API endpoint at /apps/honest_reviews/reviews
. The collection can also be accessed at /reviews
when using an API key with permission scope assigned to the app.
Now to make this content editable in the admin dashboard, let’s create a content model for Reviews in content/reviews.json
.
{
"collection": "reviews",
"fields": [
{
"id": "account",
"label": "Reviewer",
"type": "customer_lookup",
"description": "The customer who wrote the review",
"required": true
},
{
"id": "product",
"label": "Product",
"type": "product_lookup",
"description": "The product being reviewed",
"required": true
},
{
"id": "title",
"label": "Review title",
"type": "text",
"description": "The title of the review",
"required": true
},
{
"id": "body",
"label": "Review body",
"type": "long_text",
"description": "The body of the review",
"required": true
},
{
"id": "rating",
"label": "Rating",
"type": "slider",
"unit": "stars",
"min": 1,
"max": 5,
"description": "Rating (1-5) of the reviewed product",
"admin_span": 1,
"required": true
},
{
"id": "images",
"label": "Images",
"type": "image",
"description": "Images associated with the review",
"multi": true,
"conditions": {
"$settings.images.enabled": true
}
},
{
"id": "status",
"label": "Status",
"type": "select",
"description": "Admin status of the review",
"default": "submitted",
"admin_span": 2,
"options": [
{
"label": "Submitted",
"value": "submitted"
},
{
"label": "Approved",
"value": "approved"
},
{
"label": "Rejected",
"value": "rejected"
}
]
},
{
"id": "rejected_reason",
"label": "Rejected reason",
"type": "long_text",
"description": "Reason for rejecting the review, may be visible to the customer",
"conditions": {
"status": "rejected"
}
},
{
"id": "featured",
"label": "Featured",
"type": "toggle",
"description": "Indicates the review may be featured in your storefront",
"conditions": {
"$settings.featured.enabled": true
}
}
],
"views": [
{
"id": "list",
"nav": {
"parent": "products",
"label": "Honest Reviews"
}
}
]
}
To summarize, the Reviews content model defines how the fields should be displayed in the admin dashboard. All the fields above overlap with fields we defined in the data model. We’re just getting started, but note that if we hadn’t defined the data model, the platform would automatically create a data model with a matching schema, but there will be good reasons to have both in our app as we continue to enhance it.
Also note that if a content model field exists for a field that is not specified in a data model configuration, the platform will add the field to the generated data model for you. In other words, you don’t necessarily need to worry about keeping them in sync, but you will receive an error if the field definitions conflict in a significant way.
Navigation Links
The views
property near the end of the content model has several purposes, and one of them is to indicate whether and where the collection list should be represented in the left navigation. In the Honest Reviews app, we placed a link to the list view under the products
section.
Each navigation section has an ID that matches the name, in lowercase.
Remaining features
With the basics covered, let’s add fields to represent the remaining review features.
models/reviews.json
{
...
"fields": {
...
"like_count": {
"type": "int",
"default": 0
},
"dislike_count": {
"type": "int",
"default": 0
},
"rewarded": {
"type": "bool",
"default": false,
"private": true
},
"reward_amount": {
"type": "currency",
"localized": true,
"private": true
},
"reward_disabled": {
"type": "bool",
"private": true
},
"verified_buyer": {
"type": "bool",
"default": false
},
"comments": {
"type": "collection",
"fields": {
"account_id": {
"type": "objectid",
"required": true
},
"account": {
"type": "link",
"model": "accounts",
"key": "account_id"
},
"body": {
"type": "string",
"required": true
},
"status": {
"type": "string",
"enum": ["submitted", "approved", "rejected"],
"default": "submitted"
},
"like_count": {
"type": "int",
"default": 0
},
"dislike_count": {
"type": "int",
"default": 0
},
"score": {
"type": "int",
"formula": "like_count - dislike_count"
},
"verified_buyer": {
"type": "bool",
"default": false
}
}
},
"reactions": {
"type": "collection",
"public": true,
"fields": {
"account_id": {
"type": "objectid",
"required": true,
"unique": "parent_id"
},
"liked": {
"type": "bool",
"required": true
},
"comment_id": {
"type": "objectid"
}
}
}
...
}
}
Summary:
content/reviews.json
{
...
"fields": [
...
{
"id": "reward_amount",
"label": "Reward amount",
"type": "currency",
"description": "Amount to reward the customer for writing the review",
"admin_span": 1,
"conditions": {
"$settings.rewards.enabled": true,
"status": "approved",
"reward_disabled": { "$ne": true },
"rewarded": { "$ne": true }
}
},
{
"id": "reward_disabled",
"label": "Disable reward",
"type": "toggle",
"conditions": {
"$settings.rewards.enabled": true,
"status": "approved",
"rewarded": { "$ne": true }
}
},
{
"id": "verified_buyer",
"label": "Verified buyer",
"type": "toggle",
"description": "Indicates the reviewer has purchased the product",
"conditions": {
"$settings.verified.enabled": true
}
},
{
"id": "featured",
"label": "Featured",
"type": "toggle",
"description": "Indicates the review may be featured in your storefront",
"conditions": {
"$settings.featured.enabled": true
}
},
{
"type": "field_row",
"fields": [
{
"id": "like_count",
"label": "Like count",
"type": "number",
"description": "Indicates the number of likes the review has received",
"readonly": true
},
{
"id": "dislike_count",
"label": "Dislike count",
"type": "number",
"description": "Indicates the number of dislikes the review has received",
"readonly": true
}
]
}
...
]
...
}
Now let’s create a content model for Customers (accounts) in content/accounts.json
to represent the reviewer profile features.
{
"collection": "accounts",
"fields": [
{
"id": "name",
"label": "Display name",
"type": "text",
"description": "Name of the customer displayed with reviews",
"default": "{{ record.name }}",
"fallback": true,
"public": true
},
{
"id": "photo",
"label": "Photo",
"type": "image",
"description": "Photo of the customer displayed with reviews",
"conditions": {
"$settings.photo.enabled": true
},
"public": true
},
{
"id": "about",
"label": "About me",
"type": "long_text",
"description": "Reviewer bio displayed with reviews",
"public": true
},
{
"id": "reviews",
"label": "Reviews",
"type": "collection",
"description": "Reviews submitted by this reviewer",
"collection": "reviews",
"link": {
"params": {
"account_id": "id"
}
}
},
{
"id": "review_count",
"label": "Review count",
"type": "integer",
"description": "Total number of reviews submitted by this reviewer",
"readonly": true,
"public": true
},
{
"id": "comments",
"label": "Comments",
"type": "collection",
"description": "Comments submitted by this reviewer",
"collection": "reviews:comments",
"link": {
"params": {
"account_id": "id"
}
}
},
{
"id": "comment_count",
"label": "Comment count",
"type": "integer",
"description": "Total number of comments submitted by this reviewer",
"readonly": true,
"public": true
},
{
"id": "score",
"label": "Reviewer score",
"type": "integer",
"description": "Sum of reactions to reviews and comments made by other customers",
"readonly": true,
"public": true
},
{
"id": "reward_total",
"label": "Reward total",
"type": "currency",
"description": "Total amount of rewards earned by the reviewer",
"readonly": true,
"localized": true
}
]
}
Summary:
In addition to the content fields, let’s add a couple of backend fields to associate reward credits to the reviews they were granted for. We won’t necessarily display these fields in the dashboard, but it makes our schema more complete for future enhancements.
models/accounts.json
{
"fields": {
"credits": {
"fields": {
"review_id": {
"type": "objectid"
},
"reward_id": {
"type": "objectid"
}
}
}
}
}
In this case we aren’t adding any of the fields to the data model that were defined by the content model. As mentioned earlier, the platform will automatically generate a data model (or add fields to an existing standard model such as accounts
). When this configuration is pushed to Swell, it will merge these fields with your existing schema as expected.