After volunteering to get Local 13 Food & Beverage accepting online orders using Shopify, I decided to detail how I set it up in this guide. By the end you’ll have a store you can use to sell food for curbside pickup or takeout with as efficient (and safe) an experience as possible!

We’ll start by getting the store functional and working, then move on to making the experience for customers (and you) even better. Best of all, there’s no Shopify add-ons required.

Part of Shopify restaurant homepage with location, hours, and quick online ordering

Create a store

If you haven’t already, sign up for a store over at Shopify.

Are you setting up the store for the restaurant? Consider signing up for the partner program and creating the new Shopify store from within your Shopify Partner dashboard. This allows you to get a recurring commission for no additional cost to restaurant.

Adding Menu/Food Items

Adding a Menu Item

First, we’ll add a menu item to the store as a product. Under Products, click Add product. Add the title, short description, and price.

Adding new shopify product for menu item

On the left hand side in the Tags section, add a new tag called “entrees.” We’ll use this later to create a collection of all the entrees on the menu.

Adding entrees tag to menu item

In the Inventory section uncheck “Track quantity” and under Shipping uncheck “This is a physical product.” While restaurant menu items like Nachos are a physical product, we won’t be putting a box of them in the mail anytime soon 🙂

Setting inventory and shipping settings for a shopify menu item product

Lastly we can add any variants or options, such as “Add chicken”. Note that you should add the first variant as a default option (ie. “Vegetarian”) and specify the price as the new total with the option selected:

Adding options as shopify variants

You can have multiple groups of options, for example choosing what sauce is used on wings or what drink is included in a combo:

Adding additional options like what drink is included as shopify variants

When all finished, click Save to add the product to the store.

Group Together Menu Items as a Collection

We’ll use the “entrees” tag we added to the product to group all entrees together. This way, when we add an Entrees collection to the homepage, any newly added items will automatically appear.

Under Products, choose Collections and click Create collection. Type “Entrees” for the title and under Collection type, make it Automated based on the Product Tag being equal to “entrees”:

When finished, click Save to create the collection. Now let’s add this collection to the homepage!

Adding the Restaurant Menu to the Homepage

We’re going to use the default Debut theme and add the food menu on the homepage. In the admin, click Online Store then the Customize button to pull up the homepage editor:

Initial shopify customization screen

Let’s delete everything that’s there now by clicking each section then the Remove Section button at the bottom. You’ll then end up with an empty slate:

Customization screen with default elements removed

Click Add Section, scroll down to Advanced Layout and click “Custom content” and click Add. Enter Entrees in the heading field, and remove all the content that’s there by default:

Finally click “Add Content” and select Collection from the dropdown list. Pick our Entrees collection and set the Container width to 100%:

Click Save (top-right corner) to update the homepage.

Congrats – if your store was live, someone could click on that collection, find the menu item they want, then add the item to the cart!

However, customers would need to go back to the homepage and repeat that process for every item they’d like to order from your restaurant. Not a great experience to add menu items for the whole family, or encourage impulsively adding additional menu items to their order (mmm, dessert).

List Menu Items from the Collection

Let’s customize the code a bit so the products in the collection are listed with an Add to Cart button right on the homepage.

We’ll need to edit some code in the theme by clicking on the Theme actions button then selecting Edit code:

Once in the code editor scroll down in the file browser, click Sections, then click custom-content.liquid:

On line 117 change:

{% include 'collection-grid-item', collection: collection %}

to:

{% include 'collection-grid-item-homepage', collection: collection %}

Be sure to save the file by clicking the Save button in the top-right.

Now we can create our own way of displaying a custom content section with a collection added to it, by showing all the products instead of a big image.

Next, scroll down to Snippets, and click the “Add a new snippet” link:

In the box that appears enter collection-grid-item-homepage to create a collection-grid-item-homepage.liquid liquid template file. Enter the following code:

{% for collection_product in collection.products %}
	{% include 'product-card-grid-homepage', product: collection_product %}
{% endfor %}

This will loop through each of the products in our collections (ie. all of the menu items that are Entrees) and show each of them individually.

Again click “Add a new snippet” under Snippets and type product-card-grid-homepage in the create file box. Copy and paste the following code:

<div class="product-template__container page-width"
  id="ProductSection-{{ product.id }}"
  data-section-id="{{ product.id }}"
  data-section-type="product"
  data-enable-history-state="true"
  data-ajax-enabled="{{ settings.enable_ajax }}"
>
  {% comment %}
    Get first variant, or deep linked one
  {% endcomment %}
  {%- assign current_variant = product.selected_or_first_available_variant -%}
  {%- assign product_image_zoom_size = '1024x1024' -%}
  {%- assign product_image_scale = '2' -%}
  {%- assign enable_image_zoom = section.settings.enable_image_zoom -%}
  {%- assign compare_at_price = current_variant.compare_at_price -%}
  {%- assign price = current_variant.price -%}

  {% case section.settings.media_size %}
    {% when 'small' %}
      {%- assign product_media_width = 'medium-up--one-third' -%}
      {%- assign product_description_width = 'medium-up--two-thirds' -%}
      {%- assign height = 345 -%}
    {% when 'medium' %}
      {%- assign product_media_width = 'medium-up--one-half' -%}
      {%- assign product_description_width = 'medium-up--one-half' -%}
      {%- assign height = 530 -%}
    {% when 'large' %}
      {%- assign product_media_width = 'medium-up--two-thirds' -%}
      {%- assign product_description_width = 'medium-up--one-third' -%}
      {%- assign height = 720 -%}
    {% when 'full' %}
      {%- assign product_media_width = '' -%}
      {%- assign product_description_width = '' -%}
      {%- assign height = 1090 -%}
      {%- assign enable_image_zoom = false -%}
  {% endcase %}

  <div class="grid product-single{% if section.settings.enable_payment_button %} product-single--{{ section.settings.media_size }}-media{% endif %}">
    <div class="grid__item {{ product_description_width }}">
      <div class="product-single__meta">

        <div class="h4 grid-view-item__title product-card__title" aria-hidden="true">{{ product.title }}</div>

        {%- if current_variant.image -%}
          <div class="grid-view-product__image-wrapper">
            <img class="grid-view-product__image" src="{{ current_variant.image | img_url: '300x300', scale: 2 }}" alt="{{ current_variant.image.alt }}">
          </div>
        {%- elsif product.images.size > 0 -%}
          <div class="grid-view-product__image-wrapper">
            <img class="grid-view-product__image" src="{{ product | img_url: '300x300', scale: 2 }}">
          </div>
        {%- endif -%}

        <div>
          {{ product.description }}
        </div>

          <div class="price--listing">
            {% include 'product-price', variant: current_variant, show_vendor: section.settings.show_vendor %}
          </div>

          {%- if shop.taxes_included or shop.shipping_policy.body != blank -%}
            <div class="product__policies rte" data-product-policies>
              {%- if shop.taxes_included -%}
                {{ 'products.product.include_taxes' | t }}
              {%- endif -%}
              {%- if shop.shipping_policy.body != blank -%}
                {{ 'products.product.shipping_policy_html' | t: link: shop.shipping_policy.url }}
              {%- endif -%}
            </div>
          {%- endif -%}

          {% capture "form_classes" -%}
            product-form product-form-{{ product.id }}
            {%- unless section.settings.show_variant_labels %} product-form--hide-variant-labels {% endunless %}
            {%- if section.settings.enable_payment_button and product.has_only_default_variant %} product-form--payment-button-no-variants {%- endif -%}
            {%- if current_variant.available == false %} product-form--variant-sold-out {%- endif -%}
          {%- endcapture %}

          {% form 'product', product, class:form_classes, novalidate: 'novalidate', data-product-form: '' %}
            {% unless product.has_only_default_variant %}
              <div class="product-form__controls-group">
                {% for option in product.options_with_values %}
                  <div class="selector-wrapper js product-form__item">
                    <label {% if option.name == 'default' %}class="label--hidden" {% endif %}for="SingleOptionSelector-{{ forloop.index0 }}">
                      {{ option.name }}
                    </label>
                    <select class="single-option-selector single-option-selector-{{ product.id }} product-form__input"
                      id="SingleOptionSelector-{{ forloop.index0 }}"
                      data-index="option{{ forloop.index }}"
                    >
                      {% for value in option.values %}
                        <option value="{{ value | escape }}"{% if option.selected_value == value %} selected="selected"{% endif %}>{{ value }}</option>
                      {% endfor %}
                    </select>
                  </div>
                {% endfor %}
              </div>
            {% endunless %}

            <select name="id" id="ProductSelect-{{ product.id }}" class="product-form__variants no-js">
              {% for variant in product.variants %}
                <option value="{{ variant.id }}"
                  {%- if variant == current_variant %} selected="selected" {%- endif -%}
                >
                  {{ variant.title }}  {%- if variant.available == false %} - {{ 'products.product.sold_out' | t }}{% endif %}
                </option>
              {% endfor %}
            </select>

            {% if section.settings.show_quantity_selector %}
              <div class="product-form__controls-group">
                <div class="product-form__item">
                  <label for="Quantity-{{ product.id }}">{{ 'products.product.quantity' | t }}</label>
                  <input type="number" id="Quantity-{{ product.id }}"
                    name="quantity" value="1" min="1" pattern="[0-9]*"
                    class="product-form__input product-form__input--quantity" data-quantity-input
                  >
                </div>
              </div>
            {% endif %}

            <div class="product-form__error-message-wrapper product-form__error-message-wrapper--hidden{% if section.settings.enable_payment_button %} product-form__error-message-wrapper--has-payment-button{% endif %}"
              data-error-message-wrapper
              role="alert"
            >
              <span class="visually-hidden">{{ 'general.accessibility.error' | t }} </span>
              {% include 'icon-error' %}
              <span class="product-form__error-message" data-error-message>{{ 'products.product.quantity_minimum_message' | t }}</span>
            </div>

            <div class="product-form__controls-group product-form__controls-group--submit">
              <div class="product-form__item product-form__item--submit
                {%- if section.settings.enable_payment_button %} product-form__item--payment-button {%- endif -%}
                product-form__item--no-variants"
              >
                <button type="submit" name="add"
                  {% unless current_variant.available %} aria-disabled="true"{% endunless %}
                  aria-label="{% unless current_variant.available %}{{ 'products.product.sold_out' | t }}{% else %}{{ 'products.product.add_to_cart' | t }}{% endunless %}"
                  class="btn product-form__cart-submit{% if section.settings.enable_payment_button %} btn--secondary-accent{% endif %}"
                  data-add-to-cart>
                  <span data-add-to-cart-text>
                    {% unless current_variant.available %}
                      {{ 'products.product.sold_out' | t }}
                    {% else %}
                      {{ 'products.product.add_to_cart' | t }}
                    {% endunless %}
                  </span>
                  <span class="hide" data-loader>
                    {% include 'icon-spinner' %}
                  </span>
                </button>
                {% if section.settings.enable_payment_button %}
                  {{ form | payment_button }}
                {% endif %}
              </div>
            </div>
          {% endform %}
        </div>

        {%- comment -%}
          Live region for announcing updated price and availability to screen readers
        {%- endcomment -%}
        <p class="visually-hidden" data-product-status
          aria-live="polite"
          role="status"
        ></p>

        {%- comment -%}
          Live region for announcing that the product form has been submitted and the
          product is in the process being added to the cart
        {%- endcomment -%}
        <p class="visually-hidden" data-loader-status
          aria-live="assertive"
          role="alert"
          aria-hidden="true"
        >{{ 'products.product.loader_label' | t }}</p>

        {% if section.settings.show_share_buttons %}
          {% include 'social-sharing', share_title: product.title, share_permalink: product.url, share_image: product.featured_media %}
        {% endif %}
    </div>
  </div>
</div>

{% unless product == empty %}
  <script type="application/json" id="ProductJson-{{ product.id }}">
    {{ product | json }}
  </script>
  <script type="application/json" id="ModelJson-{{ product.id }}">
    {{ product.media | where: 'media_type', 'model' | json }}
  </script>
{% endunless %}

This is modified a bit from the product-template.liquid file under Sections, to display the title and description of our menu items, along with the detail of the product. We’ve also added an Add to Cart button, and modified some of the code throughout the file so it actually works how it should 🙂

Make sure to save all the files if you haven’t already by clicking the tabs in the editor, then click the Save button. You’ll see a dot if there’s unsaved changes in a file:

Click the Preview link just above the row of tabs to see how it looks:

Voila – our menu items are now listed, and customers can select any variants they wish before adding an item to their cart!

Adding Additional Collections

We can add additional collections for appetizers, beverages, kids menu, desserts and other sections of the menu. Just repeat the steps above to add additional menu items as products with an appropriate tag, create a new collection for that tag, then add the collection to the homepage as a new Custom content section.

Change the Order of Menu Items in a Collection

Want to re-order the menu items inside a collection? If you go to Collections, then click on a collection (ie. Entrees), scroll down to the Products listing. Here change the sort order to “Manually” so you can drag and drop to re-order:

Letting Customers Choose a Preferred Pickup Date and Time

Since most customers don’t like eating cold food that should be hot, getting the timing right is super important for a restaurant. While you could call every customer and confirm on a pickup time, we’ll add a date/time picker to let them choose a preference.

We’ll make this smarter to give you a certain lead time to confirm and get the order ready (40 minutes by default), and we’ll only show the days and times you’re open. We’ll also add messaging to the order confirmation emails and text messages so if you need to update their pickup time because you’re too busy, you can do so and Shopify will let them know automatically.

Adding Date and Time Picker to the Cart Page

We’ll be adding a new Snippet, a couple new Assets, and modifying a couple of the existing templates to add the date/time picker to our cart page. When we’re done it will look like this:

In the admin, click Online Store then after clicking the Actions button choose Edit Code:

Update the Cart Template

First, under Sections, click cart-template.liquid to edit the file.

Just above the line that has this:

<div class="cart__buttons-container">

Add the following:

              <h3>Preferred Contactless Pickup Time</h3>

              <p>Select a preferred time to pickup your order below. We'll send you a notification after your order is placed with your <strong>confirmed time</strong>.</p>
              <p>This helps us ensure we can maintain social distancing and have your food fresh when you pick up!</p>

          <input id="lsd-restaurant-pickup-app" name="lsd-restaurant-pickup-app" type="text" placeholder="" required>
          <button id="lsd-restaurant-pickup-app-btn">Choose a Date/Time</button>

          <input name="attributes[Pickup-Date]" type="hidden">
          <input name="attributes[Pickup-Time]" type="hidden">

Feel free to update the wording as you wish! Now we need to add the script to actually make the “Choose a Date/Time” button do something.

Add Snippet to Load Needed Assets

Under Snippets, click Add a new snippet and name it lsd-restaurant-pickup (LSD for Learn Shopify Dev).

Copy and paste the following code then click Save:

{% comment %}
    Functions for a restaurant curbside pickup store, ie. pickup date/time selection, and limiting
    when products can be ordered.

    https://learnshopifydev.com
{% endcomment %}

<link rel="stylesheet" type="text/css" href='{{ "lsd-restaurant-pickup.css" | asset_url }}' />
<link rel="stylesheet" type="text/css" href='https://cdnjs.cloudflare.com/ajax/libs/pickadate.js/3.6.4/themes/classic.css' />
<link rel="stylesheet" type="text/css" href='https://cdnjs.cloudflare.com/ajax/libs/pickadate.js/3.6.4/themes/classic.date.css' />
<link rel="stylesheet" type="text/css" href='https://cdnjs.cloudflare.com/ajax/libs/pickadate.js/3.6.4/themes/classic.time.css' />

{% if template contains 'cart' %}
    {% if cart.item_count >= 1 %}
        {{ 'lsd-restaurant-pickup.js' | asset_url | script_tag }}
    {% endif %}
{% endif %}

Once we create the assets, this will load our styling and only load the script for the date/time picker on the cart page.

Add the Styling (CSS) Asset File

Under Assets, click Add a new asset. Enter lsd-restaurant-pickup and choose .css in the dropdown.

Copy and paste the following inside the file:

.cart__footer .picker__input {
    display: none;
}
#lsd-restaurant-pickup-app {
    width: 100%;
    margin-bottom: 5px;
    cursor: pointer;
}
#lsd-restaurant-pickup-app-btn {
    cursor: pointer;
    width: 100%;
    margin-bottom: 15px;
    color: #fff;
    background-color: #0a0a0a;
}
.cart__footer label.error {
    color: red;
    font-size: 12px;
    margin-top: -5px;
}
.cart__footer input.error {
    border: 1px solid red;
}

Add the JavaScript (JS) File

Under assets, click Add a new asset again. This time enter lsd-restaurant-pickup as the name and pick .js (for JavaScript) in the dropdown. Copy and paste the following code:

(function() {
  let loadScript = function(url, callback){
    let script = document.createElement('script');
    script.src = url;
    script.async = false;
    script.onload = () => callback(script);
    document.head.append(script);
  };

  let getMaxDate = function(maxDaysInFuture){
    let result = new Date();
    result.setDate(result.getDate() + maxDaysInFuture);
    return result;
  };

  const minimumHours = [
    16, // Sunday
    16, // Monday
    16, // Tuesday
    16, // Wednesday
    16, // Thursday
    12, // Friday
    12, // Saturday
  ];

  const minimumMinutes = [
    0, // Sunday
    0, // Monday
    0, // Tuesday
    0, // Wednesday
    0, // Thursday
    0, // Friday
    0, // Saturday
  ];
  
  const maximumHours = [
    20, // Sunday
    20, // Monday
    20, // Tuesday
    20, // Wednesday
    20, // Thursday
    20, // Friday
    20, // Saturday
  ];

  const maximumMinutes = [
    0, // Sunday
    0, // Monday
    0, // Tuesday
    0, // Wednesday
    0, // Thursday
    0, // Friday
    0, // Saturday
  ];

  let getMinimumHour = function(date){
    return minimumHours[date.getDay()];
  };

  let getMinimumMinute = function(date){
    return minimumMinutes[date.getDay()];
  }

  let getMaximumHour = function(date){
    return maximumHours[date.getDay()];
  }

  let getMaximumMinute = function(date){
    return maximumMinutes[date.getDay()];
  }

  /**
   * Get the current minimum time you can select for pickup today based on lead time.
   * Returns false if it's too late in the day to order.
   *
   * @param minimumOrderLeadTimeInMinutes
   * @param timeIntervalInMinutes
   * @returns {false|Date}
   */
  let getMinTimeForToday = function(minimumOrderLeadTimeInMinutes, timeIntervalInMinutes){
    const today = new Date();
    const minimumHour = getMinimumHour(today);
    const minimumMinute = getMinimumMinute(today);
    let result = new Date();
    result.setMinutes(result.getMinutes() + minimumOrderLeadTimeInMinutes);
    result.setMinutes(Math.ceil(result.getMinutes() / timeIntervalInMinutes) * timeIntervalInMinutes);
    if (result.getHours() > getMaximumHour(today) || (result.getHours() === getMaximumHour(today) && result.getMinutes() > getMaximumMinute(today))) {
      // it's too late in the day to order
      return false;
    }
    if (result.getHours() < minimumHour || (result.getHours() === minimumHour && result.getMinutes() < minimumMinute)) {
      result.setHours(minimumHour);
      result.setMinutes(minimumMinute);
    }
    return result;
  };

  let getTomorrow = function(){
    let result = new Date();
    result.setDate(result.getDate() + 1);
    return result;
  };

  let initializePicker = function($){
    const disabledDays = [1,2]; // Sunday, Monday
    const maxDaysInFuture = 7;
    const minimumOrderLeadTimeInMinutes = 40;
    const blackoutDates = [];
    const timeIntervalInMinutes = 15;
    const minTimeForToday = getMinTimeForToday(minimumOrderLeadTimeInMinutes, timeIntervalInMinutes);

    const $appInput = $('#lsd-restaurant-pickup-app');
    const $appButton = $('#lsd-restaurant-pickup-app-btn');
    const $timePicker = $('input[name="attributes[Pickup-Time]"]').pickatime({
      interval: timeIntervalInMinutes,
      clear: '',
      onSet: function(context) {
        if ($datePicker.pickadate('picker').get() && $timePicker.pickatime('picker').get()) {
          $appInput.val($datePicker.pickadate('picker').get() + ' at ' + $timePicker.pickatime('picker').get());
          $validator.resetForm();
        } else {
          $appInput.val('');
        }
      }
    });
    const $datePicker = $('input[name="attributes[Pickup-Date]"]').pickadate({
      format: 'dddd, dd mmm, yyyy',
      formatSubmit: 'dddd dd mmm yyyy', // can parse with moment(str, 'dddd dd mmm yyyy')
      today: '',
      clear: '',
      close: '',
      min: new Date(),
      max: getMaxDate(maxDaysInFuture),
      disable: [...disabledDays, ...blackoutDates],
      hiddenName: true,
      onSet: function(context) {
        // context gives select: with the timestamp in MS of the date selected
        if (context.select) {
          const selectedDate = new Date(context.select);
          const today = new Date();

          $timePicker.pickatime('picker').clear();
          if (selectedDate.getDate() === today.getDate() && selectedDate.getMonth() === today.getMonth()) {
            // limit time to only in future + lead time
            // we assume there are times because we disable selecting today if there are none
            $timePicker.pickatime('picker').set('min', [minTimeForToday.getHours(), minTimeForToday.getMinutes()]);
            $timePicker.pickatime('picker').set('max', [getMaximumHour(selectedDate), getMaximumMinute(selectedDate)]);
          } else {
            // show all available times
            $timePicker.pickatime('picker').set('min', [getMinimumHour(selectedDate), getMinimumMinute(selectedDate)]);
            $timePicker.pickatime('picker').set('max', [getMaximumHour(selectedDate), getMaximumMinute(selectedDate)]);
          }

          $timePicker.pickatime('picker').open();
        }
      }
    });

    const $validator = $('form.cart').validate({
      messages: {
        'lsd-restaurant-pickup-app': 'Please select a preferred pickup date/time'
      },
    });

    const handleAppClick = function(event){
      event.stopPropagation();
      event.preventDefault();

      const today = new Date();

      // if it's too late in the day to order, don't let today be a valid date to select
      if (minTimeForToday === false) {
        $datePicker.pickadate('picker').set('min', getTomorrow());
      } else {
        $datePicker.pickadate('picker').set('min', today);
      }

      $validator.resetForm();

      $datePicker.pickadate('picker').open();
    };

    $appInput.on('click', handleAppClick);
    $appButton.on('click', handleAppClick);
  };

  let lsdAppJavaScript = function($){
    $(document).ready(function() {
      if (typeof $('#lsd-restaurant-pickup-app').pickadate === 'undefined') {
        console.log('loading picker');
        loadScript('https://cdnjs.cloudflare.com/ajax/libs/pickadate.js/3.6.4/picker.js', function(){
          loadScript('https://cdnjs.cloudflare.com/ajax/libs/pickadate.js/3.6.4/picker.date.js', function(){
            loadScript('https://cdnjs.cloudflare.com/ajax/libs/pickadate.js/3.6.4/picker.time.js', function(){
              loadScript('https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.1/jquery.validate.min.js', function(){
	            initializePicker($);
              });
            });
          });
        });
      } else {
        initializePicker($);
      }
    });
  };

  window.onload = function() {
    if ((typeof window.jQuery === 'undefined') || parseInt(window.jQuery.fn.jquery) < 2 || (parseInt(window.jQuery.fn.jquery) === 1 && parseFloat(window.jQuery.fn.jquery.replace(/^1\./,'')) < 7.1)) {  
      loadScript('//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js', function(){
        lsdAppJavaScript(window.jQuery);
      });
    } else {
        lsdAppJavaScript(window.jQuery);
    }
  }
})();

I won’t go into detail on all that this script does, but in the end it will add our date/time picker to the cart page.

There are some values you’ll likely need to change based on your opening hours and preferences.

Set Opening and Closing Times

The hours are specified near the top inside minimumHours, minimumMinutes, maximumHours and maximumMinutes (these default values are 4-8pm on Sunday-Thursday, 12-8pm Friday and Saturday, in 24-hour time).

Closed Days of the Week

Around line 105, you can change the value of disabledDays to the days of the week the restaurant is closed. The value of 1 is for Sunday, 2 for Monday and so on, so if you were closed Monday and Tuesday you’d set it to:

const disabledDays = [2,3];
Maximum Days in Advance

Around line 106, set this to the number of days in the future they can order. The default is 7 days. You’ll want to set it to long enough so there’s always a day available, ie. if you’re closed two days a week a good value might be 3.

const maxDaysInFuture = 3;
Order Lead Time

You’ll want some time to review the order, confirm the date and time for pickup, and actually make the food. Around line 107 the value set to 40 minutes by default.

const minimumOrderLeadTimeInMinutes = 40;
Blackout Dates

Around line 108, you can set any dates you are closed, such as holidays. For example to specify the 20th of December and January 1st you can put:

const blackoutDates = [new Date(2020,11,20), new Date(2021,0,1)];

Note that the month numbers starts at zero in JavaScript, which is pretty confusing! This is why we have 11 for December 20th (not 12) and 0 for January 1st above.

Time Slots

On line 109, this is set to every 15 minutes by default (ie. 6pm, 6:15pm, 6:30pm, 6:45pm, etc). You can set this to a shorter or longer amount of time, ie. ten minutes:

const timeIntervalInMinutes = 10;

Load the Date/Time Picker Assets on the Cart Page

Finally, open the theme.liquid file (under Layout) and add the following line just before the </head> around line 110:

  {% include "lsd-restaurant-pickup" %}

That’s it! You should now be able to add something to your cart, and see an option to pick a date and time.

Adding Pickup Date and Time to Order Notifications

Ensure you have your restaurant’s phone number set under Settings > General > Store address, then check the Phone value has been entered.

In the Shopify click Settings (bottom-left corner) then click Notifications.

First, click Order confirmation to edit the first email they receive after the order is placed. Change the Email subject to:

Order {{name}} pending

Then under Email body (HTML), change the first part before the <!DOCTYPE html> to:

{% capture email_title %}Thank you for your purchase! {% endcapture %}
{% capture email_body %}
Preferred Pickup time: {{ attributes.Pickup-Date }} at {{ attributes.Pickup-Time }}
<h3>You will receive an email with your confirmed pickup time shortly! This ensure your order is fresh and helps us maintain social distancing measures.</h3>
<p>If needed we can be reached by calling <a href="tel:1{{ shop.phone | replace: '-', '' }}">{{ shop.phone }}</a> - we look forward to serving you!</p>
{% endcapture %}

This will display a message that the time they selected is currently not confirmed, along with a link to easily call the restaurant if they have questions. Click Save in the top-right corner.

Next, click on the SMS tab to edit the text message that a customer would receive if they enter their phone number instead of an email address:

Note: If you cannot edit the SMS notification, it usually means your Shopify account is still in the trial period. Shopify recently added this policy as some abuse of the notifications to send spam text messages was happening.

To edit the SMS notification you’ll need to reach out to Shopfiy Support and ask that your account be taken off of the trial, after transferring ownership to the restaurant owner if you created the store under the Shopify Partners program. This will charge your monthly fee right away.

If shopify won’t update the account, you can choose “Customers can only check out using email” under Settings > Checkout > Customer Contact in the Shopify admin to avoid sending SMS notifications at all.

Change the content of the SMS notification to:

Hi{% unless order.customer.first_name == blank %} {{ order.customer.first_name }}{% endunless %}, thanks for your order from {{shop.name}}! You will receive a notification with your confirmed pickup time shortly. This ensures your order is fresh and helps us maintain social distancing measures. Questions? Call {{ shop.phone }} {% if order.order_status_url %}View order ({{ order.name }}): {{ order.order_status_url }}{% endif %}

Text {{ 'stop' | upcase }} to unsubscribe.

Now, back in the Notifications list, click on Shipping Confirmation to edit the notification customers receive when you mark the order as fulfilled (which you’ll want to do as soon as possible after the order comes in, before you actually make the food).

Change the Email subject to:

Your order {{ name }} is confirmed!

Then remove everything above <!DOCTYPE html> and replace with the following:

{% capture email_title %}Your order is confirmed!{% endcapture %}
{% capture email_body %}
<h3>Your pickup time is: {{ attributes.Pickup-Date }} at {{ attributes.Pickup-Time }}</h3>
<h4>Your order will be available for contactless pickup. We stagger pickups to ensure your order is fresh and to help us maintain social distancing measures.</h4>
<p>If needed we can be reached by calling <a href="tel:1{{ shop.phone | replace: '-', '' }}">{{ shop.phone }}</a> - we look forward to serving you!</p>
{% endcapture %}

Feel free to add additional details about the pickup location (ie. door or parking location) inside the email_body capture.

Lastly, click the SMS tab and replace the Content with:

Your order from {{ shop.name }} is confirmed! Your curbside pickup time is: {{ order.attributes.Pickup-Date }} at {{ order.attributes.Pickup-Time }}. Questions? Call {{ shop.phone }} - see you soon!

Fulfilling an Order

Now the fun part, how to handle an order when it comes in! You’ll want to do this as soon as possible after the order is received, before you actually make the food, so the customer receives the confirmation notification.

Verify or Change Date and Time of Pickup

When you click on an order, you’ll see the selected date and time under Additional Details:

If you need to change this, click Edit and modify the values in the popup window then click Save.

Mark Order as Fulfilled

By marking an order as fulfilled, this will trigger an email notification with the confirmed pickup time.

On the order page, click Mark as Fulfilled. Ensure the “Send shipment details to your customer now” box is checked, then click Fulfill Items:

That’s it! Other than actually making the food and giving it to the customer of course 🙂

Setting up the Payments and Checkout Experience

To actually get paid for their orders, we need to add and enable a payment gateway along with our bank details. We’ll also customize a couple of the settings so we can give the customers (and ourselves) the best experience possible.

Adding Tax Settings

Make sure to enable taxes under Settings > Taxes if needed. I’m not a tax expert nor want to be liable for these being set incorrectly, so I’ll stop talking now 🙂

Turn on Payments

Under Settings > Payments, you can enable one or more payment gateways. At a minimum clicking Activate Shopify Payments will walk you through adding your bank account details and allowing live payments on your site.

Changing Checkout Settings

Under Settings > Checkout, Form Options section, set Shipping address phone number to “Required” to ask for a phone number if they put in their email address for the order notification. This lets you quickly call the customer if there are any questions or issues with their order.

I’d also uncheck both options under To receive shipping updates since they will be getting email or SMS alerts with the order status.

Disabling Apple and Google pay

If this isn’t done, customers won’t be forced to choose a date and time, nor enter a phone number. If you’ve enabled tipping they also won’t be able to put in a tip when using an express payment method like Apple and Google pay.

Under Settings > Payments, click the Manage link in the shopify payments box. Uncheck all the options under Accelerated Checkouts and click Save.

Launch Your Restaurant Store

I’ll go through this quick as these are general Shopify launch steps – it’s definitely worth going through the Shopify documentation or reaching out to the Shopify Support team if you have questions to launch your completed store.

Add Location and Hours to the Homepage

If you go to Online Store then click the Customize button, we can add a location and hours section to the homepage.

Click Add section then under “Store Information” click Map

After clicking Add you can edit the store information settings, along with adding an image to the background. If you don’t have one on hand, sites like pixabay are a good source for royalty-free images to use.

You can also register for a Google Maps API key to display an actual map though this is a bit of an involved process, and can have a cost if you get a lot of traffic (or someone steals your key).

Add Location and Hours to the Footer

Since the Map section we added above only appears on the homepage, we can also add our hours and address in the footer of every page.

From the Online Store section click on the Customize button, then click Footer. By default you have the Quick links and Newsletter boxes. I’d recommend removing the Quick links section by clicking on “Quick links” then the “Remove content” link.

Next, edit the “Talk about your business” section to put in your location and hours.

Change the Navigation Menu

We currently have a link in our navigation menu to “Catalog” which we don’t need, since customers can add menu items to cart right from the homepage.

From Online Store click the Customize button, then click on Header. Here you can either edit the menu to remove the “Catalog” item, or just click Remove to delete the navigation menu entirely.

Add an Announcement to the Header

If you have a special you’d like to announce, or a change in procedures due to COVID-19, you can add an announcement to the top of the page.

Under Online Store click Customize, then click on Header. Enable “Show announcement” and add some text you’d like to display.

Add Domain

Click Online Store, then click Domains. Here you can either connect an existing domain you already own or buy a new one. More information in the Shopify documentation on domains.

Switch email and setup SPF record

Go to Settings > General, and edit your Customer email address. This should preferably be an email address for your domain, ie. orders@yourdomain.com

To make sure customers get their order notification emails as reliably as possible, you’ll want to set up the SPF records on your domain. More information in the Setup Your Email article from Shopify.

Disable Store Password

Click Online Store, then click the Preferences sub-menu (I often miss this!) then scroll down to the Password protection section. Uncheck “Enable password” and click Save to remove the password from your site.

Transfer Website to Restaurant Owner

If you set up the website under the Shopify Partners program, you can now transfer ownership to the restaurant owner.

Do a Live Test

Make sure everything is working as expected by doing a real purchase with a real payment method and fulfill that order. Check the notifications look as you wish.

Even though you can turn on the testing payment method, it’s best to be sure things will work as expected for your customers.

Additional Improvements

Show a message after adding to cart

You can add a message under the Add to Cart button after it’s clicked, letting them know it’s been added.

Just following this tutorial but change this line in the ajaxify-cart.liquid file from:

        _showFeedback('success','<i class="fa fa-check"></i> Added to cart! <a href="/cart">View cart</a> or <a href="/collections/all">continue shopping</a>.',$addToCartForm);

to this:

        _showFeedback('success','<i class="fa fa-check"></i> Added to cart! <a href="/cart">Order now</a> or keep shopping. You can also tap Add to Cart again to add more of this item to the cart.',$addToCartForm);

Enable tipping

Under the admin area, go to Settings (bottom-left corner) then click Checkout. Scroll down to the Tipping section and check “Show tipping options at checkout.”

You can specify the tip options, which we set to 10, 15 and 20 percent. Customers can also enter a custom amount if they wish.

Potential Issues and How to Fix Them

Handling Orders with Billing Addresses in a Different Tax Region

Because we’re marking our Shopify products as digital products (ie. “This is a physical product” is unchecked), customers will only be asked for their Billing address. Shopify then uses it to determine what tax rate they should be charged. If it’s different from your physical store, chances are the tax being charged is incorrect.

After contacting Shopify, it seems one potential solution is to create a new Collection but make the collection type manual (rather than our automated collections based on a tag).

Once created, add all your products by going to the Products list, select all products, then under More Actions choose “Add to collection(s)”. Choose your “All products” collection and click Save.

You can then use this collection in a Tax override, and specify the taxes that should always be charged regardless of the customer’s billing address.

Conclusion

While I’ve done many WordPress and custom-made websites it was the first Shopify site I’ve launched, and learned a lot in the process. You now have an easy to maintain website that customers can use to place online orders with minimal hassle.

If you have any suggestions for improvements or problems you ran into I’d love to hear about them. Feel free to create a new topic on the Shopify Discussion forum and tag @brianhogg in the post, or ping me on Twitter 🙂

Published by Brian Hogg