How to create dynamic city pages

Topic summary

A Shopify merchant wants to create dynamic city pages (e.g., pages/city/berlin, pages/city/paris) that display different content based on the city name in the URL.

Two main solutions emerged:

  1. Page metafields approach: Create individual pages for each city, add custom metafields (city_name, city_description, city_image) via Settings → Custom data → Pages, then build a custom page.city.liquid template to display these values. This requires creating separate pages manually for each city.

  2. Metaobjects with dynamic routing (recommended): Create a metaobject definition called “City” with fields for name, description, and image. Enable “Publish entries as web pages” in metaobject settings, then create entries for each city (Berlin, Paris, etc.). Build a metaobject template in the theme editor that dynamically pulls data from the metaobject fields using sections like Hero.

Key implementation details:

  • URLs will follow the format /pages/city/berlin or /pages/city/paris
  • The metaobject approach allows linking dynamic content directly in theme sections without custom code
  • One user provided detailed screenshots showing the complete metaobject setup process
  • The URL path format cannot be customized in Shopify’s online store channel

The discussion includes code examples and visual guides for both approaches.

Summarized with AI on October 23. AI used: claude-sonnet-4-5-20250929.

I want to create a dynamic page, such as pages/city/berlin, where “berlin” can be changed to “paris” or “france.” How can I create this type of page?

  • Display Berlin details on the Berlin page.

  • Display Paris details on the Paris page.

1 Like

Please provide me fast solution for this

Hi @Muskan1234 ,

You can not create page like pages/city/berlin but instead of that you need to create custom functionality.

Thanks

Could you please suggest me .How we can do this

Go to Settings → Custom data → Pages

Add fields like:

city_name

city_description

city_image

Step 2 : Assign those metafields per page (Berlin, Paris, etc.)

Step 3 :

Create page.city.liquid code.

Example Code :

<div class="city-page">
  <h1>{{ page.metafields.custom.city_name }}</h1>
  <p>{{ page.metafields.custom.city_description }}</p>
  <img src="{{ page.metafields.custom.city_image | img_url: 'medium' }}" alt="{{ page.metafields.custom.city_name }}">
</div>

You just want to create different city pages and assign this page template what we created.

NOTE : I’m just making an assumption here, so this code might need some adjustments.

Thanks

@Dev_Inflame Please provide me

another solution for this

You need to provide more info about the data you plan to show on these pages – are these derived from collections and collection products; blogs and blog posts or some arbitrary data?

For arbitrary data you may look into metaobjects if you want it structured.

1 Like

@tim_1 I want to display metaobject values into the cities page .How i can show?

You could either create city pages as dynamic using metaobjects/dynamic routing in your theme. Define a metaobject for cities (name, description, image, etc.), and then write a template that utilizes a dynamic source in the form of cities/{city}. Shopify will handle the correct rendering of the data depending on the url handle.

@SealSubs-Roan Thank you for the feedback. I believe there might be a slight misunderstanding. I want to retrieve the value of the metaobjects on the page. I have created the page using the metaobject, but I am unable to obtain the value.


I want the above value

@SealSubs-Roan Provide me solution how i get it

HI @Muskan1234

Think you did not name that metaobject properly but OK. I would suggest you try to create a Metaobject template

Then choose Berlin as the object, I think, as you have that as a namespace, but it should be City, and then add sections you want, existing ones, and link title, image, description in sections you want. Withut any code. But if needed you may create a custom sections too.

1 Like

@Laza_Binaery Could you guide me step by step. How i can do for the different different cities and how to show all the sections according to the city

1 Like

Also, we do not see it in your screenshots, but make sure you have “Publish entries as web pages” enabled, like this:

OK @Muskan1234 here it is a lot of screenshots :slight_smile:

First I created a new metaobject named City with same fields as yours but type is city.

then added Berlin and Paris as entries


Next from your theme editor, from middle drop-down click on “Create new metaobject template”, then choose City, and fill SEO title and description


New metaobject template is empty but first add a Hero section. First choose image buy linking metaobject image. then do similar to text, but with text you can have some words before/after.




You can remove the button or add link you want.

The next part is a bit tricky, it depends on your theme. I used Horizon here and I used the Generate section. Because you have list of products, there should be in section product list input option. And not a collection like it is in most sections.

Open your theme editor, click on blocks, then click on new file icon. Enter name like city-product-list.liquid (mine was AI-generated so I had ai_gen_block_ab0f621.liquid, but you should be able to name it however you want) and enter this code:

{% assign ai_gen_id = block.id | replace: '_', '' | downcase %}

{% style %}
  .ai-metaobject-carousel-{{ ai_gen_id }} {
    position: relative;
    width: 100%;
    padding: {{ block.settings.section_padding }}px 0;
    background-color: {{ block.settings.background_color }};
  }

  .ai-metaobject-carousel__container-{{ ai_gen_id }} {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 20px;
  }

  .ai-metaobject-carousel__header-{{ ai_gen_id }} {
    text-align: {{ block.settings.heading_alignment }};
    margin-bottom: 30px;
  }

  .ai-metaobject-carousel__heading-{{ ai_gen_id }} {
    margin: 0;
    color: {{ block.settings.heading_color }};
    font-size: {{ block.settings.heading_size }}px;
  }

  .ai-metaobject-carousel__wrapper-{{ ai_gen_id }} {
    position: relative;
    overflow: hidden;
  }

  .ai-metaobject-carousel__track-{{ ai_gen_id }} {
    display: flex;
    gap: {{ block.settings.gap }}px;
    transition: transform 0.5s ease;
  }

  .ai-metaobject-carousel__slide-{{ ai_gen_id }} {
    flex: 0 0 calc((100% - ({{ block.settings.products_per_row }} - 1) * {{ block.settings.gap }}px) / {{ block.settings.products_per_row }});
    min-width: 0;
  }

  @media screen and (max-width: 749px) {
    .ai-metaobject-carousel__slide-{{ ai_gen_id }} {
      flex: 0 0 calc((100% - ({{ block.settings.products_per_row_mobile }} - 1) * {{ block.settings.gap }}px) / {{ block.settings.products_per_row_mobile }});
    }
  }

  .ai-metaobject-carousel__product-card-{{ ai_gen_id }} {
    background-color: {{ block.settings.card_background_color }};
    border-radius: {{ block.settings.card_border_radius }}px;
    overflow: hidden;
    height: 100%;
    display: flex;
    flex-direction: column;
    border: {{ block.settings.card_border_width }}px solid {{ block.settings.card_border_color }};
  }

  .ai-metaobject-carousel__product-image-{{ ai_gen_id }} {
    position: relative;
    width: 100%;
    padding-bottom: 100%;
    overflow: hidden;
    background-color: #f5f5f5;
  }

  .ai-metaobject-carousel__product-image-{{ ai_gen_id }} img {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .ai-metaobject-carousel__product-image-placeholder-{{ ai_gen_id }} {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: #f5f5f5;
  }

  .ai-metaobject-carousel__product-image-placeholder-{{ ai_gen_id }} svg {
    width: 60%;
    height: 60%;
    opacity: 0.3;
  }

  .ai-metaobject-carousel__product-info-{{ ai_gen_id }} {
    padding: 15px;
    flex-grow: 1;
    display: flex;
    flex-direction: column;
  }

  .ai-metaobject-carousel__product-title-{{ ai_gen_id }} {
    margin: 0 0 8px;
    font-size: 16px;
    font-weight: 600;
    color: {{ block.settings.text_color }};
  }

  .ai-metaobject-carousel__product-title-{{ ai_gen_id }} a {
    color: inherit;
    text-decoration: none;
  }

  .ai-metaobject-carousel__product-title-{{ ai_gen_id }} a:hover {
    opacity: 0.7;
  }

  .ai-metaobject-carousel__product-price-{{ ai_gen_id }} {
    font-size: 14px;
    color: {{ block.settings.text_color }};
    margin-bottom: 10px;
  }

  .ai-metaobject-carousel__product-button-{{ ai_gen_id }} {
    margin-top: auto;
    padding: 10px 20px;
    background-color: {{ block.settings.button_background_color }};
    color: {{ block.settings.button_text_color }};
    border: none;
    border-radius: {{ block.settings.button_border_radius }}px;
    cursor: pointer;
    text-decoration: none;
    display: inline-block;
    text-align: center;
    transition: background-color 0.3s ease;
  }

  .ai-metaobject-carousel__product-button-{{ ai_gen_id }}:hover {
    background-color: {{ block.settings.button_hover_background_color }};
  }

  .ai-metaobject-carousel__nav-{{ ai_gen_id }} {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    background-color: {{ block.settings.nav_button_background }};
    color: {{ block.settings.nav_button_color }};
    border: none;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 2;
    transition: opacity 0.3s ease;
  }

  .ai-metaobject-carousel__nav-{{ ai_gen_id }}:hover {
    opacity: 0.8;
  }

  .ai-metaobject-carousel__nav-{{ ai_gen_id }}:disabled {
    opacity: 0.3;
    cursor: not-allowed;
  }

  .ai-metaobject-carousel__nav--prev-{{ ai_gen_id }} {
    left: -20px;
  }

  .ai-metaobject-carousel__nav--next-{{ ai_gen_id }} {
    right: -20px;
  }

  .ai-metaobject-carousel__empty-state-{{ ai_gen_id }} {
    text-align: center;
    padding: 60px 20px;
    color: #999;
  }

  .ai-metaobject-carousel__empty-state-{{ ai_gen_id }} svg {
    width: 80px;
    height: 80px;
    margin-bottom: 20px;
    opacity: 0.3;
  }

  .ai-metaobject-carousel__empty-state-text-{{ ai_gen_id }} {
    font-size: 16px;
    font-style: italic;
  }

  @media screen and (max-width: 749px) {
    .ai-metaobject-carousel__nav-{{ ai_gen_id }} {
      width: 32px;
      height: 32px;
    }

    .ai-metaobject-carousel__nav--prev-{{ ai_gen_id }} {
      left: 0;
    }

    .ai-metaobject-carousel__nav--next-{{ ai_gen_id }} {
      right: 0;
    }
  }
{% endstyle %}

<metaobject-carousel-{{ ai_gen_id }}
  class="ai-metaobject-carousel-{{ ai_gen_id }}"
  {{ block.shopify_attributes }}
>
  <div class="ai-metaobject-carousel__container-{{ ai_gen_id }}">
    {% if block.settings.heading != blank %}
      <div class="ai-metaobject-carousel__header-{{ ai_gen_id }}">
        <h2 class="ai-metaobject-carousel__heading-{{ ai_gen_id }}">{{ block.settings.heading }}</h2>
      </div>
    {% endif %}

    {% if block.settings.product_list.count > 0 %}
      <div class="ai-metaobject-carousel__wrapper-{{ ai_gen_id }}">
        <button
          class="ai-metaobject-carousel__nav-{{ ai_gen_id }} ai-metaobject-carousel__nav--prev-{{ ai_gen_id }}"
          aria-label="Previous"
          data-direction="prev"
        >
          <svg
            width="24"
            height="24"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          >
            <polyline points="15 18 9 12 15 6"></polyline>
          </svg>
        </button>

        <div class="ai-metaobject-carousel__track-{{ ai_gen_id }}">
          {% for product in block.settings.product_list %}
            <div class="ai-metaobject-carousel__slide-{{ ai_gen_id }}">
              <div class="ai-metaobject-carousel__product-card-{{ ai_gen_id }}">
                <a href="{{ product.url }}" class="ai-metaobject-carousel__product-image-{{ ai_gen_id }}">
                  {% if product.featured_image %}
                    <img
                      src="{{ product.featured_image | image_url: width: 600 }}"
                      alt="{{ product.featured_image.alt | escape }}"
                      loading="lazy"
                      width="600"
                      height="600"
                    >
                  {% else %}
                    <div class="ai-metaobject-carousel__product-image-placeholder-{{ ai_gen_id }}">
                      {{ 'product-1' | placeholder_svg_tag }}
                    </div>
                  {% endif %}
                </a>

                <div class="ai-metaobject-carousel__product-info-{{ ai_gen_id }}">
                  <h3 class="ai-metaobject-carousel__product-title-{{ ai_gen_id }}">
                    <a href="{{ product.url }}">{{ product.title }}</a>
                  </h3>

                  <div class="ai-metaobject-carousel__product-price-{{ ai_gen_id }}">
                    {{ product.price | money }}
                  </div>

                  <a
                    href="{{ product.url }}"
                    class="ai-metaobject-carousel__product-button-{{ ai_gen_id }}"
                  >
                    {{ block.settings.button_text }}
                  </a>
                </div>
              </div>
            </div>
          {% endfor %}
        </div>

        <button
          class="ai-metaobject-carousel__nav-{{ ai_gen_id }} ai-metaobject-carousel__nav--next-{{ ai_gen_id }}"
          aria-label="Next"
          data-direction="next"
        >
          <svg
            width="24"
            height="24"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          >
            <polyline points="9 18 15 12 9 6"></polyline>
          </svg>
        </button>
      </div>
    {% else %}
      <div class="ai-metaobject-carousel__empty-state-{{ ai_gen_id }}">
        {{ 'product-1' | placeholder_svg_tag }}
        <div class="ai-metaobject-carousel__empty-state-text-{{ ai_gen_id }}">
          Add products to display in the carousel
        </div>
      </div>
    {% endif %}
  </div>
</metaobject-carousel-{{ ai_gen_id }}>

<script>
  (function() {
    class MetaobjectCarousel{{ ai_gen_id }} extends HTMLElement {
      constructor() {
        super();
        this.currentIndex = 0;
        this.productsPerView = {{ block.settings.products_per_row }};
        this.productsPerViewMobile = {{ block.settings.products_per_row_mobile }};
      }

      connectedCallback() {
        this.track = this.querySelector('.ai-metaobject-carousel__track-{{ ai_gen_id }}');
        this.slides = this.querySelectorAll('.ai-metaobject-carousel__slide-{{ ai_gen_id }}');
        this.prevButton = this.querySelector('.ai-metaobject-carousel__nav--prev-{{ ai_gen_id }}');
        this.nextButton = this.querySelector('.ai-metaobject-carousel__nav--next-{{ ai_gen_id }}');

        if (!this.track || this.slides.length === 0) return;

        this.setupEventListeners();
        this.updateCarousel();
        this.handleResize();

        window.addEventListener('resize', () => this.handleResize());
      }

      setupEventListeners() {
        this.prevButton.addEventListener('click', () => this.navigate('prev'));
        this.nextButton.addEventListener('click', () => this.navigate('next'));
      }

      handleResize() {
        this.currentProductsPerView = window.innerWidth <= 749 ? this.productsPerViewMobile : this.productsPerView;
        this.updateCarousel();
      }

      navigate(direction) {
        const maxIndex = Math.max(0, this.slides.length - this.currentProductsPerView);

        if (direction === 'prev') {
          this.currentIndex = Math.max(0, this.currentIndex - 1);
        } else {
          this.currentIndex = Math.min(maxIndex, this.currentIndex + 1);
        }

        this.updateCarousel();
      }

      updateCarousel() {
        if (!this.track || this.slides.length === 0) return;

        const slideWidth = this.slides[0].offsetWidth;
        const gap = {{ block.settings.gap }};
        const offset = this.currentIndex * (slideWidth + gap);

        this.track.style.transform = `translateX(-${offset}px)`;

        const maxIndex = Math.max(0, this.slides.length - this.currentProductsPerView);
        this.prevButton.disabled = this.currentIndex === 0;
        this.nextButton.disabled = this.currentIndex >= maxIndex;
      }
    }

    customElements.define('metaobject-carousel-{{ ai_gen_id }}', MetaobjectCarousel{{ ai_gen_id }});
  })();
</script>

{% schema %}
{
  "name": "Product carousel",
  "tag": null,
  "settings": [
    {
      "type": "product_list",
      "id": "product_list",
      "label": "Products"
    },
    {
      "type": "header",
      "content": "Content"
    },
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Featured products"
    },
    {
      "type": "select",
      "id": "heading_alignment",
      "label": "Heading alignment",
      "options": [
        {
          "value": "left",
          "label": "Left"
        },
        {
          "value": "center",
          "label": "Center"
        },
        {
          "value": "right",
          "label": "Right"
        }
      ],
      "default": "center"
    },
    {
      "type": "text",
      "id": "button_text",
      "label": "Button text",
      "default": "View product"
    },
    {
      "type": "header",
      "content": "Layout"
    },
    {
      "type": "range",
      "id": "products_per_row",
      "min": 2,
      "max": 5,
      "step": 1,
      "label": "Products per row (desktop)",
      "default": 4
    },
    {
      "type": "range",
      "id": "products_per_row_mobile",
      "min": 1,
      "max": 3,
      "step": 1,
      "label": "Products per row (mobile)",
      "default": 2
    },
    {
      "type": "range",
      "id": "gap",
      "min": 0,
      "max": 40,
      "step": 4,
      "unit": "px",
      "label": "Gap between products",
      "default": 20
    },
    {
      "type": "range",
      "id": "section_padding",
      "min": 0,
      "max": 100,
      "step": 4,
      "unit": "px",
      "label": "Section padding",
      "default": 40
    },
    {
      "type": "header",
      "content": "Colors"
    },
    {
      "type": "color",
      "id": "background_color",
      "label": "Background",
      "default": "#ffffff"
    },
    {
      "type": "color",
      "id": "heading_color",
      "label": "Heading",
      "default": "#000000"
    },
    {
      "type": "color",
      "id": "text_color",
      "label": "Text",
      "default": "#000000"
    },
    {
      "type": "color",
      "id": "card_background_color",
      "label": "Card background",
      "default": "#ffffff"
    },
    {
      "type": "color",
      "id": "card_border_color",
      "label": "Card border",
      "default": "#e6e6e6"
    },
    {
      "type": "color",
      "id": "button_background_color",
      "label": "Button background",
      "default": "#000000"
    },
    {
      "type": "color",
      "id": "button_text_color",
      "label": "Button text",
      "default": "#ffffff"
    },
    {
      "type": "color",
      "id": "button_hover_background_color",
      "label": "Button hover background",
      "default": "#333333"
    },
    {
      "type": "color",
      "id": "nav_button_background",
      "label": "Navigation button background",
      "default": "#000000"
    },
    {
      "type": "color",
      "id": "nav_button_color",
      "label": "Navigation button icon",
      "default": "#ffffff"
    },
    {
      "type": "header",
      "content": "Style"
    },
    {
      "type": "range",
      "id": "heading_size",
      "min": 16,
      "max": 48,
      "step": 2,
      "unit": "px",
      "label": "Heading size",
      "default": 32
    },
    {
      "type": "range",
      "id": "card_border_radius",
      "min": 0,
      "max": 20,
      "step": 2,
      "unit": "px",
      "label": "Card border radius",
      "default": 8
    },
    {
      "type": "range",
      "id": "card_border_width",
      "min": 0,
      "max": 4,
      "step": 1,
      "unit": "px",
      "label": "Card border width",
      "default": 1
    },
    {
      "type": "range",
      "id": "button_border_radius",
      "min": 0,
      "max": 40,
      "step": 2,
      "unit": "px",
      "label": "Button border radius",
      "default": 4
    }
  ],
  "presets": [
    {
      "name": "Product carousel"
    }
  ]
}
{% endschema %}

And the results are


Note that your URL will be /pages/city/berlin and pages/city/paris for these two.
Let me know if you have any questions.

2 Likes

Specifically see the shopify manual to understand the capabilities availble to you.
Your business literally depends on it.
https://help.shopify.com/en/manual/custom-data/metaobjects/webpages
:technologist: Keep in mind you cannot change the url path format in shopify’s online-store channel.