# Store page template and data

The app's blocks cover most cases. But a brand sometimes wants to show its stores elsewhere or differently: the hours in the footer of a store page, the stores of a city in a blog post, a custom section of the agency's theme. All of this is possible in Liquid, because the app publishes each store that has a page in the shop's metaobjects, which the theme reads as it reads products.

This page explains how this data is organized, shows how to read it, then details each field.

:::caution[The app isn't released yet]
The types and fields described here may still change.
:::

## How the data is organized

The merchant manages their stores in the app. Each time a store with a page is saved, the app publishes it to the shop, in about a second, in a **Store page** metaobject entry (`$app:store_page`): its page, with its address on the shop, its title and meta description, and **all the store's data**: name, address, hours, photos, type…

A store without a page, only on the store locator (a stockist, for example), has no entry: the app's blocks (store locator, store list) read it from data of their own, which Liquid can't read outside them.

Store types ("Flagship", "Stockist"…) are **Store type** entries (`$app:store_type`), and services ("Click & Collect", "Repairs"…) **Store service** entries (`$app:store_service`): each page references them, and their names are translated.

An entry has the same handle as the store, which is also the end of its page's address: `paris-le-marais`. A draft store is published as hidden: Liquid doesn't see it. A store without a page, or deleted, has no entry.

The merchant sees these entries in **Content > Metaobjects**, read-only: they are always edited from the app.

## Find the app's identifier

In the app's blocks, the types are written `$app:store_page`. In a theme section or snippet, they need their full name, with the app's identifier on the shop: `app--<id>--store_page`. To find it:

1. In Shopify, open **Online Store > Themes**, then, in the theme's **…** menu, **Edit code**.
2. Open the `templates/metaobject` folder.
3. The store pages' template is named `app--<id>--store_page.json`: the number is the app's identifier.

The template only exists once created in the theme editor (see [Set up the app](/docs/getting-started/set-up/)).

## Add your sections to a store page

Every store page shares the **Store page** template. The **Store page** block makes up the page, but you can place other theme sections around it: a banner, a brand text, featured products. Their texts connect to the store's data without code:

1. In the theme editor, open the **Store page** template.
2. Add a theme section, for example a rich text.
3. In a text setting, click the dynamic source (the small plug icon), then choose a field of the store: **Name**, **City**, **Description**…

Each page then shows its own store's data.

## Read the data in Liquid

On the **Store page** template, `metaobject` is the **Store page** entry shown, with all the store's data. Each field is read by its key, followed by `.value`.

### Show the services as badges

```liquid
<ul class="services">
  {%- for service in metaobject.store_services.value -%}
    <li>{{ service.name.value }}</li>
  {%- endfor -%}
</ul>
```

### Show a day's hours

`opening_hours` is a JSON with a key per open day (see [its format](#opening-hours)). Monday's hours:

```liquid
{%- assign hours = metaobject.opening_hours.value -%}
{%- for slot in hours.monday -%}
  {{ slot.opens }}–{{ slot.closes }}
{%- else -%}
  Closed
{%- endfor -%}
```

Translate "Closed" with your theme's language files.

### List the stores of a city

Anywhere else in the theme, read the entries by the type's full name. For example, the links to the pages of the stores in Lyon:

```liquid
{%- for store in metaobjects['app--<id>--store_page'].values -%}
  {%- if store.city.value == 'Lyon' -%}
    <a href="{{ store.system.url }}">{{ store.name.value }}</a>
  {%- endif -%}
{%- endfor -%}
```

A given entry is read by its handle: `metaobjects['app--<id>--store_page']['paris-le-marais']`. This is how the nearby stores are read, whose handles `nearby_stores` gives.

:::note[Good to know]
- Always loop over the entries with `for`: `values.first` is empty.
- A page of results holds 250 entries at most: beyond that, paginate.
- Shopify doesn't filter metaobjects by field: as in the example, filter in the loop.
- Only the stores with a page are there: a list in Liquid can't show every store of the store locator. The store locator also skips the stores whose `store_locator` is false, which only have a page: do the same if your list must show the stores it shows.
- An entry's handle, and so the page's address, isn't translated.
:::

## A store page's fields (Store page)

| Field | Type | Content |
| --- | --- | --- |
| `name` | text | The name. Translatable. |
| `address1`, `address2` | text | The address lines. |
| `city`, `zip` | text | The city, the postal code. |
| `province_code` | text | The Shopify province code (`NY`, `QC`, `JP-13`), empty for a country without provinces. |
| `country_code` | text | The two-letter ISO country code (`FR`). |
| `address` | text | The whole address on one line. |
| `latitude`, `longitude` | decimal | The coordinates, empty until the address is placed. |
| `timezone` | text | The store's IANA time zone (`Europe/Paris`), from its coordinates. |
| `phone`, `email` | text | The contact. |
| `access` | multi-line text | How to get there, one line per stop or car park. Translatable. |
| `opening_hours` | JSON | The weekly hours (format below). |
| `special_hours` | JSON | The current and upcoming exceptional hours (format below). |
| `description` | rich text | The presentation text, to show with `metafield_tag`. Translatable. |
| `photos` | list of files | The photos; the first one is the main one. |
| `store_services` | list of references to **Store service** | The services; each one's name is in `name`, translatable. |
| `tags` | list of texts | The tags. |
| `store_type` | reference to **Store type** | The type, empty without a type. |
| `store_locator` | boolean | True if the store is also on the store locator; false if it only has a page. |
| `nearby_stores` | list of texts | The handles of the 3 nearest stores with a page, nearest first. |
| `seo_title` | text | The page's title: the one entered in the app, else the name. Translatable. |
| `seo_description` | text | The meta description: the one entered in the app, else the presentation text as plain text, cut at 160 characters. Translatable. |
| `structured_data` | multi-line text | Written by the app: the opening hours, position and address of the page's structured data, which the **Store page** block inserts. Not to translate or edit. |

The page's address: `system.url`.

### Opening hours

`opening_hours` has a key per open day, in English (`monday` to `sunday`), with its slots in the store's local time. A closed day has no key; a day with a lunch break has several slots.

```json
{
  "monday": [{ "opens": "10:00", "closes": "13:00" }, { "opens": "14:00", "closes": "19:00" }],
  "saturday": [{ "opens": "10:00", "closes": "19:00" }]
}
```

`special_hours` lists the days and periods outside the usual week (public holidays, vacations, late openings), in date order. `from` and `to` are dates in the store's calendar, the same for a single day; `label` is optional. A period without slots is closed. On those days, it replaces the weekly hours. Past periods go away by themselves.

```json
[
  { "from": "2026-12-24", "to": "2026-12-24", "label": "Christmas Eve", "slots": [{ "opens": "10:00", "closes": "16:00" }] },
  { "from": "2026-12-25", "to": "2026-12-25", "label": "Christmas", "slots": [] }
]
```

## A type's fields (Store type)

| Field | Type | Content |
| --- | --- | --- |
| `name` | text | The type's name ("Flagship"). Translatable. |
| `color` | color | The color of its markers, empty without a color. |
| `marker` | URL | The address of its custom marker's SVG, empty without a marker. |

## A country page's fields (Country page)

One entry per country with enough stores, created by the app when country pages are on; the texts are edited in **Content > Metaobjects > Country page**. The stores are not in the entry: the **Country or city page** block, on this template, reads them in the store locator's data.

| Field | Type | Content |
| --- | --- | --- |
| `country_code` | text | The country's code (ISO 3166-1, `FR`), written by the app. |
| `seo_title` | text | The page's title: the country's name by default. Translatable. |
| `seo_description` | text | The meta description. Translatable. |
| `introduction` | rich text | The text shown under the country's name. Translatable. |

## A city page's fields (City page)

One entry per city with enough stores, created by the app when city pages are on; the texts are edited in **Content > Metaobjects > City page**. The city is its name as written on the stores.

| Field | Type | Content |
| --- | --- | --- |
| `country_code` | text | The country's code (ISO 3166-1, `FR`), written by the app. |
| `city` | text | The city, as written on its stores, written by the app. |
| `seo_title` | text | The page's title: the city's name by default. Translatable. |
| `seo_description` | text | The meta description. Translatable. |
| `introduction` | rich text | The text shown under the city's name. Translatable. |

## The import format

The file the merchant imports into the app (CSV or Excel) has its columns and formats described in [the import format reference](https://app.getlavia.app/docs/import-format.md), served by the app and readable by AI assistants. To bring in the stores from another tool or from a brand's file, start from this reference.

## Learn more

- [Theme blocks](/docs/developers/theme-blocks/): what the blocks already do with this data.
- [SEO](/docs/developers/seo/): the pages' title, meta description and structured data.