Documentation

Storefront Configuration

Feature Flags

Feature flags are set via environment variables and control optional storefront behaviour.

VariableTypeDefaultDescription
NUXT_PUBLIC_SHOP_BITE_FEATURE_MULTI_CHANNELbooleanfalseEnables the sales channel switcher in the header. Use this when you operate multiple locations under one storefront.
NUXT_PUBLIC_SHOP_BITE_FEATURE_SECURE_KEYstring(empty)When set, the entire storefront is locked behind this key. Visitors must provide ?secureKey=<value> in the URL once; the key is then persisted in a cookie for 30 days. Without a valid cookie, all pages redirect to /maintenance. Leave empty to disable.
NUXT_PUBLIC_SHOP_BITE_FEATURE_CONTACT_FORMbooleanfalseEnables the /kontakt contact page. When false, the page returns a 404.
NUXT_PUBLIC_SHOP_BITE_FEATURE_DEMO_BANNERbooleanfalseShows a non-dismissible notice above the header that the shop is a demo and orders are not fulfilled. Enable it for the demo shops in demos/.
NUXT_PUBLIC_SHOP_BITE_MENU_CATEGORY_IDstring(empty)Optional. The Shopware category ID used as the root of the menu. Empty, it comes from the plugin configuration (menu category, per sales channel), else the navigation category applies. Only needed for existing installations.
NUXT_PUBLIC_SHOP_BITE_ADDRESS_AUTOCOMPLETE_BOUNDING_BOXstring(empty)Rectangle lon1,lat1,lon2,lat2 that narrows the address suggestions (Geoapify) in the checkout, e.g. 8.822251,50.055026,8.899077,50.104327. Convenience only: where the shop delivers is decided by the cart rules in Shopware. Leave empty for unrestricted suggestions.

.env example:

NUXT_PUBLIC_SHOP_BITE_FEATURE_MULTI_CHANNEL=false
NUXT_PUBLIC_SHOP_BITE_FEATURE_SECURE_KEY=
NUXT_PUBLIC_SHOP_BITE_FEATURE_CONTACT_FORM=false
NUXT_PUBLIC_SHOP_BITE_FEATURE_DEMO_BANNER=false
# optional, else the plugin configuration
NUXT_PUBLIC_SHOP_BITE_MENU_CATEGORY_ID=
NUXT_PUBLIC_SHOP_BITE_ADDRESS_AUTOCOMPLETE_BOUNDING_BOX="8.822251,50.055026,8.899077,50.104327"

SEO

The storefront outputs a sitemap, canonical URLs, share previews and page titles automatically. Environment variables control the values.

VariableDefaultDescription
NUXT_PUBLIC_STORE_URL(empty)Public URL of the shop, e.g. https://www.my-shop.com. Base for canonical URLs, og:url, og:image, the sitemap and the sitemap entry in robots.txt. Without it the request URL is used, which can be wrong behind a reverse proxy.
NUXT_PUBLIC_SITE_NAMENUXT_STORE_NAME at build timeShop name in page titles and og:site_name.
NUXT_PUBLIC_SITE_TITLE_TEMPLATEsee belowPage title template. %s is the page title, %siteName the shop name. Leave empty to output the page title only.
NUXT_PUBLIC_SITE_OG_IMAGE/card.pngShare preview image (WhatsApp, Facebook) for pages without their own image. Path relative to public/ or an absolute URL; the output is always an absolute URL.
NUXT_PUBLIC_SITE_ADDRESS_CITY(empty)City, e.g. Obertshausen. Used in the home page title and description and in the footer.
NUXT_PUBLIC_SITE_CUISINE(empty)Cuisine as a name, e.g. Italienisch-deutsche Küche or Italienisch, not an adjective such as italienische. Used in the home page description and as servesCuisine in the schema.

.env example:

NUXT_PUBLIC_STORE_URL="https://www.my-shop.com"
NUXT_PUBLIC_SITE_NAME="Pizzeria Example"
NUXT_PUBLIC_SITE_TITLE_TEMPLATE="%s | %siteName – Online bestellen"
NUXT_PUBLIC_SITE_OG_IMAGE="/card.png"
NUXT_PUBLIC_SITE_ADDRESS_CITY="Obertshausen"
NUXT_PUBLIC_SITE_CUISINE="Italienisch-deutsche Küche"

Page titles and descriptions

  • Subpages and categories get the title template, e.g. Pizza | Pizzeria Example – Online bestellen. Categories use the meta title and meta description from Shopware, otherwise the category name and description.
  • The home page does not use the template. Its default title is <shop name> <city> | Online bestellen & liefern lassen; the description is built from cuisine, city and shop name.
  • Set custom home page values in content/index.yml under seo. They take precedence over the defaults, but only if they differ from the page's title and description: Nuxt Content otherwise fills seo with those values automatically.
seo:
  title: Pizzeria Example Obertshausen | Pizza online bestellen
  description: Pizza, Pasta und Salate in Obertshausen online bestellen, liefern lassen oder abholen.
  image: /card.png

Share preview (og:image)

Category pages use the category image from Shopware, the home page uses seo.image from content/index.yml. All other pages fall back to NUXT_PUBLIC_SITE_OG_IMAGE. twitter:card is summary_large_image.

The default /card.png shows the ShopBite demo. Put your own public/card.png into the shop project (1200 × 630 px recommended) to replace it. Check the preview with the Facebook Sharing Debugger. WhatsApp caches previews for a while.

Sitemap

/sitemap.xml contains:

  • the home page
  • content pages from content/ (e.g. payment and shipping)
  • all categories of the main navigation and the menu category tree (menu category) with their SEO URL, modification date and category image
  • /kontakt if the contact form is enabled

robots.txt references the sitemap automatically. Paths blocked via robots.disallow are left out of the sitemap. Exclude a single content page with sitemap: false in its frontmatter. The sitemap is cached for ten minutes.

Category URLs come from the SEO URL templates in Shopware. A trailing slash (e.g. /c/Pizza/) is kept in both the sitemap and the canonical URL.

Dishes have no page of their own. A dish opens as a quick view over its category, and that address is its URL, e.g. /Speisekarte/Pizza/?produkt=LF-21. The links are in the product cards (visible to search engines; a click still opens the quick view without leaving the page), in the structured MenuItem of each category and in /llms.txt, so Google and AI assistants can link straight to a dish.

The URL comes from the product's SEO URL in Shopware. The ShopBite plugin sets the product SEO URL template to match. You can change a product URL in Shopware as long as it keeps the ?produkt=<product number> parameter. If the SEO URL is missing or has another shape, the storefront builds the link from the category and the product number. Links to variants (e.g. ?produkt=LF-21-groß) open the quick view with the variant selected. The canonical URL stays the category; products are not listed in the sitemap individually.

Customers can also share a dish as configured. The quick view writes every change to the address: the selected variant, deselected ingredients (ohne, by name) and selected extras (extras, by product number), e.g. /Speisekarte/Pizza/?produkt=LF-21-groß&ohne=Zwiebeln&extras=EXTRA-900. Whoever opens the link sees the dish selected exactly like that, including the price. The share button in the quick view header opens the share sheet on phones (WhatsApp, messages …) and copies the link on desktop. Renamed ingredients or removed extras in an old link are ignored.

llms.txt for AI assistants

At /llms.txt the shop serves a Markdown summary following llmstxt.org: name, contact, opening hours and upcoming closing days, ordering info (delivery area, delivery time, pickup) and the full menu with prices, ingredients (Hauptzutaten) and vegetarian/vegan. AI assistants such as ChatGPT, Claude or Perplexity find all facts in one place.

The file is generated from the same data as the shop (Shopware, ShopBite plugin, NUXT_PUBLIC_SITE_*) and rebuilt at most hourly, so there is nothing to maintain. Content pages are listed under "Weitere Informationen" unless they are blocked via robots.disallow. If the shop is not indexable (NUXT_SITE_INDEXABLE=false, e.g. demo shops), the file is served with X-Robots-Tag: noindex.

AI crawlers in robots.txt

robots.txt names the known AI crawlers explicitly, in two groups:

  • AI search and assistants (OAI-SearchBot, ChatGPT-User, Claude-SearchBot, Claude-User, PerplexityBot, Perplexity-User, MistralAI-User): they fetch pages to answer a question and link the shop as the source. Allowed by default.
  • AI training (GPTBot, ClaudeBot, Google-Extended, Applebot-Extended, CCBot, meta-externalagent): they collect content to train models. Allowed by default.

Both groups repeat the paths blocked via robots.disallow.

VariableDefaultDescription
NUXT_AI_CRAWLERS_SEARCHtruefalse blocks AI search and assistants. ChatGPT, for example, can then no longer cite the shop.
NUXT_AI_CRAWLERS_TRAININGtruefalse blocks the training crawlers. AI search stays allowed.

Google-Extended only controls the use for Gemini, not the AI Overviews in Google Search: those come from the regular Googlebot. If the shop is not indexable (NUXT_SITE_INDEXABLE=false), robots.txt keeps Disallow: / for everyone.

Cache and backoffice

The storefront keeps dishes, lists and categories from Shopware in its cache for up to 24 hours so the shop loads fast. So that changes from the ShopBite backoffice (a price, taking a dish off the menu, renaming a section) show at once, the backoffice empties the cache after every save via POST /api/_cache/purge.

VariableDefaultDescription
NUXT_CACHE_PURGE_SECRET(empty)Secret key of the purge route, at least 32 random characters (e.g. openssl rand -hex 32). The backoffice uses the same value. Leave it empty to switch the route off (404).

The route expects the header Authorization: Bearer <key> and answers 401 without a valid key. Without a body it empties everything; with {"products": ["<id>"], "categories": ["<id>"]} it empties these dishes and categories plus all lists and extras.

curl -X POST -H "Authorization: Bearer $NUXT_CACHE_PURGE_SECRET" https://www.my-shop.com/api/_cache/purge

The footer shows the address, phone number, a link to the Google Business Profile and the opening hours on every page as text in the HTML. Search engines read name, address and phone (NAP) straight from the shop, which strengthens local ranking. Keep the values identical to the Google Business Profile and the imprint.

VariableDefaultDescription
NUXT_PUBLIC_SITE_ADDRESS_STREET(empty)Street and number, e.g. Kantstr. 6.
NUXT_PUBLIC_SITE_ADDRESS_POSTAL_CODE(empty)Postal code, e.g. 63179.
NUXT_PUBLIC_SITE_ADDRESS_CITY(empty)City, see SEO.
NUXT_PUBLIC_SITE_TELEPHONE(empty)Phone number as it should be displayed, e.g. +49 6104 71427. The tel: link is generated from it; it also sits behind the phone icon in the header and the "Jetzt anrufen" button on the order page. Without a number these elements are hidden.
NUXT_PUBLIC_SITE_GOOGLE_BUSINESS_PROFILE_URL(empty)Link to the Google Business Profile, e.g. from Google Maps via "Share".

The shop name comes from NUXT_PUBLIC_SITE_NAME. Empty values are hidden. Without contact details and opening hours the footer stays as before.

Opening hours come from the business hours of the ShopBite plugin in Shopware. Consecutive days with the same hours are grouped (e.g. Mi–Fr); days without hours show as "Ruhetag".

.env example:

NUXT_PUBLIC_SITE_ADDRESS_STREET="Kantstr. 6"
NUXT_PUBLIC_SITE_ADDRESS_POSTAL_CODE="63179"
NUXT_PUBLIC_SITE_ADDRESS_CITY="Obertshausen"
NUXT_PUBLIC_SITE_TELEPHONE="+49 6104 71427"
NUXT_PUBLIC_SITE_GOOGLE_BUSINESS_PROFILE_URL="https://maps.app.goo.gl/..."

Do not also maintain address and phone as link categories in the Shopware footer navigation, or they show up twice.

Style presets

A style preset gives the storefront its look: colours, fonts, corner radii, light or dark mode and how the menu is shown. There are three presets:

PresetSuitsFontsMode
trattoriapizzeria, Italian cuisineYoung Serif, Figtreelight, terracotta ground
grilldöner, grill, pideBricolage Grotesquedark, amber
asiaThai, Vietnamese, sushiOutfitlight, jade green

Set the preset in the shop's nuxt.config.ts. Single colours can be overridden, for example the primary colour of the buttons:

export default defineNuxtConfig({
  extends: ["@shopbite-de/storefront"],
  shopBite: {
    preset: "trattoria",
    colors: { primary: "#3F7D20" },
  },
})

Alternatively, the environment variable NUXT_SHOPBITE_PRESET sets the preset at build time. Without a preset the current look stays.

The preset fonts are downloaded at build time and served by the storefront itself. Every preset is checked for WCAG AA contrast. If an overridden colour drops a contrast below the minimum, the build warns with the affected colour pair. Red is not recommended as the primary button colour because it reads as a warning.

Brand colour and contrast

The brand colour is set in app/assets/css/main.css as a colour scale --color-brand-50 to --color-brand-950, either mapped to a Tailwind scale or as custom hex values. The storefront derives the primary colour from it: brand-600 in light mode, brand-400 in dark mode (the Nuxt UI convention).

@import "@shopbite-de/storefront/app/assets/css/main.css";

@theme {
    --color-brand-50:  var(--color-rose-50);
    /* ... */
    --color-brand-400: var(--color-rose-400);
    --color-brand-500: var(--color-rose-500);
    --color-brand-600: var(--color-rose-600);
    /* ... */
    --color-brand-950: var(--color-rose-950);
}

Text in the primary colour (headlines, links, product numbers) and text on primary buttons need a contrast ratio of at least 4.5:1 (WCAG AA), otherwise Lighthouse reports a contrast failure under "Accessibility". Subtle buttons and badges (variant: subtle) put the colour on a light tint of itself, so the light step should reach 5:1 on white. Check two values, e.g. with the Contrast Checker:

StepBackgroundMinimum contrast
brand-600White #ffffff5:1
brand-400Dark #1717174.5:1

If the customer's colour misses that, pick darker or lighter values for the 600 and 400 steps; the 500 step stays free for accents without text.

Custom font

Put the shop font as woff2 into public/fonts/, one file per weight, named <Family>-<Weight>.woff2 (Kalam-Regular.woff2, Kalam-Bold.woff2), together with its licence file. The Latin subset is enough for German (umlauts and ß included) and keeps a weight at about 20–30 KB; for Google Fonts, download the files linked as latin in https://fonts.googleapis.com/css2?family=<Family>:wght@400;700.

Do not write an @font-face in main.css, only set the font stack:

@theme {
    --font-sans: 'Kalam', sans-serif;
}

Then let @nuxt/fonts resolve the files and preload the weights used above the fold (hero, headings) in nuxt.config.ts:

export default defineNuxtConfig({
    app: {
        head: {
            link: ["Regular", "Bold"].map((face) => ({
                rel: "preload",
                as: "font",
                type: "font/woff2",
                href: `/fonts/Kalam-${face}.woff2`,
                crossorigin: "",
                media: "(min-width: 640px)",
            })),
        },
    },
    fonts: {
        families: [
            { name: "Kalam", provider: "local", weights: [400, 700], preload: false },
        ],
    },
})
  • List every weight the shop uses (400 for text, 700 for bold text and headings), otherwise the browser synthesizes the bold face.
  • @nuxt/fonts generates fallback faces with adjusted metrics, so the text keeps roughly its size while the font loads. A font with different widths can still rewrap lines when it arrives after the first paint; that is what the preloads prevent. The module preloads only one weight per family, hence the explicit links.
  • media: "(min-width: 640px)" leaves out phones: on a slow mobile connection the preloads compete with the stylesheet and delayed the first paint by about 100 ms in our measurements. Drop it if the font changes the mobile layout noticeably.
  • After adding a font, measure the layout shift (CLS) on desktop and mobile, e.g. with PageSpeed Insights. The fallback fonts differ per system (Roboto on Android, Helvetica Neue on iOS, neither on Windows), so a shift can appear on desktop only.

Nuxt Config

Some examples how to configure the storefront. Checkout the Nuxt Config for more information.

export default defineNuxtConfig({
    app: {
        head: {
            title: "My Store",
            htmlAttrs: {
                lang: "en",
            },
            meta: [
                {charset: "utf-8"},
                {name: "viewport", content: "width=device-width, initial-scale=1"},
                {name: "description", content: "My Store description"},
                {property: "og:title", content: "My Store"},
                {property: "og:description", content: "My Store description",},
            ],
            link: [{rel: "icon", href: "/favicon.ico"}],
        },
    },
    runtimeConfig: {
        public: {
            site: {
                name: "My Store",
                description: "My Store description",
                countryId: "",
                titleTemplate: "%s | %siteName – Online bestellen",
                ogImage: "/card.png",
                cuisine: "Italian cuisine",
                address: {
                    street: "Kantstr. 6",
                    postalCode: "63179",
                    city: "Obertshausen",
                },
                telephone: "+49 6104 71427",
                googleBusinessProfileUrl: "https://maps.app.goo.gl/...",
            },
        },
    },
    colorMode: {
        preference: "light",
    },
    pwa: {
        manifest: {
            name: "My Store",
            short_name: "mystore",
            theme_color: "#4d7c0f",
            icons: [
                {
                    src: "log192x192.png",
                    sizes: "192x192",
                    type: "image/png",
                    purpose: "any maskable",
                }
            ],
        },
    },
})

Structured data (schema.org)

The storefront emits JSON-LD so Google understands the shop as a restaurant with opening hours and a menu:

  • Home page: Restaurant (a LocalBusiness) with name, address, phone, cuisine, price range, image, link to the Google Business Profile, the weekly opening hours (openingHoursSpecification) and closing days (specialOpeningHoursSpecification) from the ShopBite plugin, plus hasMenu with one MenuSection per menu category. When online ordering is enabled, an OrderAction is added (ordering via the menu, pickup when an address is set, own delivery when delivery areas are set), as well as the active payment methods from Shopware (paymentAccepted) and the currency. This helps AI assistants such as ChatGPT or Perplexity answer questions like "Who delivers to me?" with your shop.
  • Category pages: MenuSection with the category's products as MenuItem including the price (offers), next to the existing CollectionPage and BreadcrumbList.

The values come from the same variables as the footer (NUXT_PUBLIC_SITE_NAME, NUXT_PUBLIC_SITE_ADDRESS_*, NUXT_PUBLIC_SITE_TELEPHONE, NUXT_PUBLIC_SITE_CUISINE, NUXT_PUBLIC_SITE_GOOGLE_BUSINESS_PROFILE_URL, NUXT_PUBLIC_SITE_OG_IMAGE). These variables exist for the schema only:

VariableDefaultDescription
NUXT_PUBLIC_SITE_ADDRESS_COUNTRYDECountry of the address as ISO code.
NUXT_PUBLIC_SITE_PRICE_RANGE(empty)Price range, e.g. €€.
NUXT_PUBLIC_SITE_DELIVERY_AREAS(empty)Delivery areas, comma-separated, e.g. 63179 Obertshausen, 63165 Lämmerspiel (areaServed).
NUXT_PUBLIC_SITE_GEO_LATITUDE(empty)Latitude of the restaurant, e.g. 50.0716 (right-click the location in Google Maps).
NUXT_PUBLIC_SITE_GEO_LONGITUDE(empty)Longitude, e.g. 8.8483. geo is only emitted with both coordinates.
NUXT_PUBLIC_SITE_ACCEPTS_RESERVATIONS(empty)Table reservations: true, false or a text such as Nur telefonisch.

.env example:

NUXT_PUBLIC_SITE_DELIVERY_AREAS="63179 Obertshausen, 63165 Lämmerspiel"
NUXT_PUBLIC_SITE_GEO_LATITUDE=50.0716
NUXT_PUBLIC_SITE_GEO_LONGITUDE=8.8483
NUXT_PUBLIC_SITE_ACCEPTS_RESERVATIONS="Nur telefonisch"

Empty values are left out. Check the result with the Rich Results Test or the Schema Markup Validator: both should detect a Restaurant with opening hours on the home page.