Creating editable Shopify Sections

Creating headless themes from scratch is easier to do with Hydrogen, but I am exploring the idea of offering sections a a monthly fee - like some Shopify apps are already doing - in headless. All the sections are centralized in Builder.io and building them would not necessarily require integrating the app through my codebase.

Adding the sections as snippet in the theme works , but when it comes to adding them to sections directly , it needs section schema.

What would be the possibilities here : I know there is a Shopify app that Syncs models as sections, but can it add section settings with editable fields ? If not what would be the best approach to allow headless sections to be implemented into regular theme with full control and customization options ?

Thank you a lor for reading me

Hello @Ecommfox

To clarify, while it’s technically possible to sync Builder.io content into Shopify themes, there are a few important limitations to be aware of:

  • Builder.io does not automatically generate Shopify section schemas, which are required for enabling editable settings in the theme editor.

  • While you can embed Builder sections as HTML or snippets, these won’t be fully customizable from the Shopify side unless you manually wrap them with Liquid and define the appropriate schema fields.

  • Existing apps that sync Builder models into Shopify typically don’t provide schema-based controls out of the box — so merchants won’t see editable options in the theme editor unless additional development work is done.

If your goal is to offer sections that work seamlessly in standard Shopify themes (with full control in the customizer), you’ll want to consider:

  • Creating a Shopify app that syncs Builder sections as .liquid files with schema included

  • Or manually defining schema for each section and using Builder purely as a layout and content engine

Hydrogen/headless setups give you more flexibility, but for Online Store 2.0 themes, there will always be a need to connect Builder content with Shopify’s schema structure.

Happy to help explore this further if you’re considering building an app or automation around it.

Best regards,

Thank you , Manish I ve been working on an automation that works with Online 2.0 themes, I am yet to explore with Hydrogen set ups. Basically the automation grabs section settins defined as custom fields and injects it in the section liquid file for each published model . Here is the automation link : Sign Up - Pipedream

1 Like

Hello @manish-sharma after investigating it appears that a custom plugin would be better to connect data from shopify sections from the template file :slight_smile:

Plug in function: fetch the json data from templates which includes the section settings and inject to context.Builder . Accepts user input to define which shopify template to fetch and custom data block which can be added in the editor

I started with the docs walkthrough and the github examples.

I also found this thread : How to build a Custom Editor plugin for my ecommerce backend with Builder.io . Could this point me in the right direction for what I’m trying to achieve ?

Hello @Ecommfox

Yes, that sounds like the right approach. A custom plugin would give you the flexibility needed to fetch JSON data from Shopify templates — including section settings — and inject it into context.Builder.

Allowing user input to specify the Shopify template and define custom data blocks for the Builder editor aligns well with how custom plugins are typically structured.

It’s great that you’ve started with the docs and GitHub examples — those are definitely the right starting points. That thread you found on How to build a Custom Editor plugin for my ecommerce backend with Builder.io should also be very helpful and relevant to what you’re trying to achieve.

Let me know if you hit any blockers along the way or need help with implementation details!

1 Like

Hello @manish-sharma coded my plugin locally settings are ok as well as input :
Similar to products and collections I want to search and filter though a list of templates fetched through asset RESTAdmin endpoint : Asset
…But still templates failed to fetch

I tried to refactor it but I think I spotted the issue in the console

image

Is there a way around this CORS blocked error?

here is the plug in code

import { registerCommercePlugin } from '@builder.io/plugin-tools';
import Client from 'shopify-buy';
import pkg from '../package.json';
import appState from '@builder.io/app-context';
import { getDataConfig } from './data-plugin';

/**
 * Helper to fetch Shopify Admin API JSON
 */
async function adminFetchJSON(opts: {
  storeDomain: string;
  apiVersion: string;
  adminAccessToken: string;
  path: string;
}) {
  const url = `https://${opts.storeDomain}/admin/api/${opts.apiVersion}${opts.path}`;
  const res = await fetch(url, {
    headers: {
      'X-Shopify-Access-Token': opts.adminAccessToken,
      'Content-Type': 'application/json',
    },
  });
  if (!res.ok) {
    throw new Error(`Admin API error ${res.status}`);
  }
  return res.json();
}

function normalizeApiVersion(version?: string) {
  return version || '2025-07';
}

registerCommercePlugin(
  {
    name: 'Shopify',
    id: pkg.name,
    settings: [
      {
        name: 'storefrontAccessToken',
        type: 'string',
        helperText: 'Required to fetch storefront product data',
        required: true,
      },
      {
        name: 'storeDomain',
        type: 'text',
        helperText: 'Your entire store domain, such as "your-store.myshopify.com"',
        required: true,
      },
      {
        name: 'apiVersion',
        type: 'text',
        helperText: 'Your Shopify API version, such as "2025-07"',
      },
      {
        name: 'adminAccessToken',
        type: 'string',
        helperText: 'Private Admin API access token (for template JSON)',
        required: true,
      },
      {
        name: 'themeId',
        type: 'string',
        helperText: 'Theme ID to fetch templates from',
        required: true,
      },
    ],
    ctaText: `Connect your Shopify custom app`,
  },
  async settings => {
    const client = Client.buildClient({
      storefrontAccessToken: settings.get('storefrontAccessToken'),
      domain: settings.get('storeDomain'),
      apiVersion: '2025-07',
    });

    const service: any = {
      product: {
        async findById(id: string) {
          return client.product.fetch(id);
        },
        async findByHandle(handle: string) {
          return client.product.fetchByHandle(handle);
        },
        async search(search: string) {
          const sources =
            (await client.product.fetchQuery({
              query: search ? `title:*${search}*` : '',
              sortKey: 'TITLE',
              first: 250,
            })) || [];
          return sources.map((src: any) => ({
            ...src,
            image: src.image || src.images?.[0],
          }));
        },
        getRequestObject(id: string) {
          return {
            '@type': '@builder.io/core:Request' as const,
            request: {
              url: `${appState.config.apiRoot()}/api/v1/shopify/storefront/product/${id}?apiKey=${
                appState.user.apiKey
              }&pluginId=${pkg.name}`,
            },
            options: { product: id },
          };
        },
      },
      collection: {
        async findById(id: string) {
          return client.collection.fetch(id);
        },
        async findByHandle(handle: string) {
          return client.collection.fetchByHandle(handle);
        },
        async search(search: string) {
          return client.collection.fetchQuery({
            query: search ? `title:*${search}*` : '',
            sortKey: 'TITLE',
            first: 250,
          });
        },
        getRequestObject(id: string) {
          return {
            '@type': '@builder.io/core:Request' as const,
            request: {
              url: `${appState.config.apiRoot()}/api/v1/shopify/storefront/collection/${id}?apiKey=${
                appState.user.apiKey
              }&pluginId=${pkg.name}`,
            },
            options: { collection: id },
          };
        },
      },
      template: {
        async findById(templateName: string) {
          const themeId = settings.get('themeId');
          const token = settings.get('adminAccessToken');
          const storeDomain = settings.get('storeDomain');
          const apiVersion = normalizeApiVersion(settings.get('apiVersion'));

          const path = `/themes/${encodeURIComponent(themeId)}/assets.json?asset[key]=templates/${encodeURIComponent(
            templateName
          )}.json`;

          const data = await adminFetchJSON({
            storeDomain,
            apiVersion,
            adminAccessToken: token,
            path,
          });

          const parsed = JSON.parse(data?.asset?.value || '{}');
          // Store template JSON globally so sections can access it
         // appState.globalState.templateJson = parsed;

         console.log('Parsed template JSON:', parsed);

          return {
            id: templateName,
            title: templateName,
            name: templateName,
            json: parsed,
            sections: parsed?.sections || {},
          };
        },
        async search(search: string = '') {
          const themeId = settings.get('themeId');
          const token = settings.get('adminAccessToken');
          const storeDomain = settings.get('storeDomain');
          const apiVersion = normalizeApiVersion(settings.get('apiVersion'));

          const path = `/themes/${encodeURIComponent(themeId)}/assets.json`;
          const data = await adminFetchJSON({
            storeDomain,
            apiVersion,
            adminAccessToken: token,
            path,
          });

          const assets: Array<{ key: string }> = data?.assets || [];
          let templates = assets
            .map(a => a.key)
            .filter(key => key.startsWith('templates/') && key.endsWith('.json'));

          if (search) {
            const re = new RegExp(search, 'i');
            templates = templates.filter(k => re.test(k));
          }

          return templates.map(key => {
            const base = key.replace(/^templates\//, '').replace(/\.json$/, '');
            return { id: base, title: base };
          });
        },
        getRequestObject(templateName: string) {
          // Builder server proxy endpoint — still fine to use /storefront/ here
          return {
            '@type': '@builder.io/core:Request' as const,
            request: {
              url: `${appState.config.apiRoot()}/api/lastest/shopify/storefront/template/${templateName}?apiKey=${
                appState.user.apiKey
              }&pluginId=${pkg.name}`,
            },
            options: { template: templateName },
          };
        },
      },
    };

    appState.registerDataPlugin(getDataConfig(service as any));

    return service;
  }
);
 

Based on your explanation, the error occurs because Shopify’s Admin REST API does not return CORS headers, as it is designed for server-to-server communication rather than direct browser requests. This is why the preflight OPTIONS request is failing before your actual GET request can run.

Adding Access-Control-Allow-Origin on your proxy won’t resolve the issue unless the proxy itself is correctly handling both the OPTIONS preflight and the actual request. Shopify itself won’t pass these headers back.

Here are a few possible solutions you could try:

  1. Server-side Proxy (Recommended):
    Set up a small API route (for example, in Next.js, Express, or Cloudflare Workers) that fetches the Shopify templates server-to-server using Admin API credentials. Then have your plugin call this API endpoint instead of Shopify directly. Ensure that your proxy responds with the necessary CORS headers:

    Access-Control-Allow-Origin: *
    Access-Control-Allow-Methods: GET, OPTIONS Access-Control-Allow-Headers: Content-Type, Authorization 
    
  2. Check Proxy Implementation:
    Make sure your current proxy is handling both OPTIONS requests and the GET call. If the proxy is only forwarding requests and not responding to preflights, the browser will still block the call.

  3. Alternative Approaches:
    If the plugin only needs to run within Builder.io’s environment, you may be able to handle this by fetching templates server-side (outside of the browser runtime) and passing them into the editor.