Skip to content
Online store and company website. No cut of your revenue. Start for free.

Partner frontend

You do not have to assemble a client's design and frontend from the platform template. You can change the look through files, or build your own application on top of the Shop API. Checkout, payments and the customer account always stay on the platform.

Quick start

A client without programming. A client changes the look of their shop in the administration: colours, fonts and the shop template under Shop › Look and feel, website pages under Content › Website pages. They preview the change, publish it on the Look versions screen and go back to the previous version with one click. The assistant in the administration can do the same.

A partner with the starter. The starter @portican/create is being prepared for release on public npm; until it is released, npm create @portican@latest does not work. The starter sets up a frontend on top of the SDK with the platform blocks, the cart handed over to checkout, legal texts, the consent bar and SEO.

A partner with a template. You change the look in files and upload it with portican vzhlad push as a draft; the merchant publishes it. You save a finished look to your template library and pick it for your next client already at the registration of their shop.

Upload and hosting. You upload a finished custom frontend as a static site to Portican hosting with portican frontend deploy (section Uploading a frontend), or run it yourself.

Two ways

  • Look as files. The store runs on a platform template and you change its theme, the store layout, the home page, custom CSS and website pages. You keep the files in git and upload them with portican vzhlad as a draft; the merchant publishes it. See the section Look as files below.
  • Your own frontend. You write your own application that reads the store catalogue and content through the Shop API using the @portican/storefront SDK. You upload it to Portican hosting as a finished static site with portican frontend deploy: the hosting only serves files, so there is no server-side rendering (SSR) there. A frontend that needs SSR runs on your own hosting. See the sections SDK client and Uploading a frontend below.
  • You can combine either way with what Markup hooks, Blocks and fields and Website preset describe. Read the content the merchant is meant to edit from the store (settings, website pages, products) or from your own blocks with a content schema (section Partner blocks): the admin edits only what the platform knows, and it knows a custom block through the schema you upload.

From the key to a deployed frontend

  1. A frontend on your own hosting needs a frontend key: the store operator creates it in the admin under Shop › Integrations, names it and enters the website addresses the browser will call from. An address is https without a path and must not belong to the platform or its subdomain; at most 5 addresses per key and 5 keys per market. The key is shown only once.
  2. The market token and the Shop API address of the store are on the same screen, with a button to copy the address; the server builds the address from the market domain, so do not guess it from examples. The key is valid only with the token of its own market, sent in the portican-kluc-frontendu header together with the vendure-token header.
  3. The SDK @portican/storefront is being prepared for release on public npm; until it is released, npm install @portican/storefront does not work. The package will have no dependencies and every version will be published only by Portican CI from the main branch.
  4. A static site (Portican hosting included) creates the client in the browser, without the key. The key belongs only on your own server, when you render the frontend on a server outside Portican. The example is below in the section SDK client.
  5. Read the catalogue, settings and website pages. Keep the cart on your side and hand it to the platform with a one-time code at checkout (section Cart and checkout).
  6. You upload the finished static frontend to Portican hosting (section Uploading a frontend); the platform gives it the Shop API address, the market token and the language. A frontend on your own hosting must run at the address you entered with the key.

What the key allows

  • A frontend key allows only public read queries from the list below and a single mutation, odovzdajKosik. Every other operation ends with the error KLUC_FRONTENDU_OPERACIA.
  • A request with the key, or from the registered website address, carries no customer session. Partner code does not create a Vendure cart and cannot see the account, orders, payments or links with a token.
  • The limit is 3,000 requests per minute per key. To the platform a partner server is a single address for all its visitors, so it can sign the visitor's address with the key secret in the x-portican-navstevnik header (the SDK does that when you pass adresaNavstevnika); the limits then apply per visitor. Without a signature, a limit per server address applies alongside the key limit.
  • When the limit is hit you get the PRILIS_VELA_POZIADAVIEK error with the number of seconds to wait. A wrong, revoked or foreign key returns KLUC_FRONTENDU_NEPLATI. The operator revokes a key in the admin and the website stops loading products.
  • The key belongs on the server only. The browser calls the Shop API without the key, and only from the registered website address (CORS). A key in code delivered to the browser could be used by anyone.

Queries the key allows

The public catalogue and store content. Nothing about the customer, the order or links with a token. The list is an allow list, not a block list: a new plugin query is not open by itself.

From the platform code. This list is built directly from the platform source code, and every deployment checks that the two still match.

  • activeChannel
  • availableCountries
  • collections
  • collection
  • facets
  • facet
  • products
  • product
  • search
  • storefrontSettings
  • schemaBlokov
  • strankyWebu
  • strankaWebu
  • suboroveStrankyObchodu
  • suborovaStrankaObchodu
  • pravneTextyObchodu
  • pravnyTextObchodu
  • vypisBlogu
  • clankyBlogu
  • clanokBlogu
  • adresyClankov
  • jazykyClanku
  • mapaClankov
  • akcioveProdukty
  • produktyPodlaId
  • najpredavanejsieProdukty
  • novinkyProdukty
  • odporucaniaKProduktu
  • vacsieVarianty
  • recenzieProduktu
  • socialnyDokazProduktu
  • nastavenieVypredanych
  • skryteProdukty
  • cenovePasma
  • presmerovaneKategorie
  • presmerovaneKarty
  • presmerovaniaObchodu
  • zobrazenieTrhu
  • sviatocneTerminy
  • blokyPartnera

SDK client

The SDK is a typed Shop API client with no dependencies. It runs in Node 18 and later, in Cloudflare Workers, in Deno and in the browser. It reads store settings, categories, products, search and website pages.

Errors carry a code (kod), the HTTP status (stav) and, at the limit, cakajSekund. The headers and error codes in the SDK match the platform core; a package test guards that.

What the SDK does besides reading: legal texts of the store (pravneTexty, pravnyText), the platform block schema (schemaBlokov), reading page blocks, the content of partner blocks (blokyPartnera, it calls the query blokyPartnera and returns verzia, odtlacok, jazyk and bloky, see Reading block content and the webhook), SEO helpers, consent and handing over the cart. What the SDK does not do: it does not render blocks, you draw them.

import { vytvorKlienta, ChybaObchodu } from '@portican/storefront';

// Statický web (aj na hostingu Porticanu): v prehliadači, bez kľúča, zo zapísanej adresy webu.
// Adresu Shop API, token trhu a jazyk zapíše hosting Porticanu do /portican-env.json.
const premenne = await fetch('/portican-env.json').then(r => r.json());
const obchod = vytvorKlienta({
    adresaApi: premenne.PORTICAN_ADRESA_SHOP_API, // Shop API obchodu
    tokenTrhu: premenne.PORTICAN_TOKEN_TRHU,
    jazyk: premenne.PORTICAN_JAZYK,
});
// Vlastný server mimo Porticanu (SSR): vytvorServerovehoKlienta z @portican/storefront/server
// s kluc: process.env.PORTICAN_KLUC_FRONTENDU a pri dopyte { adresaNavstevnika }, aby stropy platili na návštevníka.

try {
    const produkt = await obchod.produkt('kytica-ruzi');
    const vysledky = await obchod.hladaj({ vyraz: 'ruže', take: 24 });
    const nastavenia = await obchod.nastavenia();
    const stranka = await obchod.stranka('o-nas');
} catch (e) {
    // Pri strope príde kód chyby so sekundami, ktoré treba počkať.
    if (e instanceof ChybaObchodu && e.strop) await pockaj(e.cakajSekund ?? 1);
    else throw e;
}

Cart and checkout

  • The cart is a list of variants and quantities in your frontend, at most 50 lines and 999 units of one variant. The platform does not accept a bigger cart and does not round it quietly: the partner sees an error.
  • At checkout you call odovzdajKosik. You get a one-time code that is valid for 10 minutes and works once. At most 120 hand-overs per minute are allowed per key or website address.
  • The customer brings the code in a POST form to /prevziat-kosik of the store (or /<language>/prevziat-kosik), never in the address. Checkout takes the cart over only from the website that handed it over, so the form must be sent from the registered website address.
  • Prices, stock, delivery and discounts are calculated by the platform, not by your code. A variant outside the store is rejected at hand-over, and a sold-out unit simply drops out of the cart on take-over. The order is created only by checkout, in the customer's session.
import { nacitajKosik, pridajDoKosika, ulozKosik, formularPokladne } from '@portican/storefront';

// Košík je zoznam variantov a množstiev u vás (localStorage, cookie, KV).
const kosik = pridajDoKosika(nacitajKosik(localStorage), variantId, 1);
ulozKosik(localStorage, kosik);

// Pri pokladni ho odovzdáte platforme; ceny, sklad a dopravu prepočíta ona.
const { kod } = await obchod.odovzdajDoPokladne(kosik);
const f = formularPokladne(kod, 'https://www.obchod-klienta.sk', 'sk');
// <form method="POST" action={f.action}><input type="hidden" name="kod" value={f.polia.kod} /></form>

Look as files: command line

  • The portican vzhlad command downloads the store look (theme, store layout, home page, custom CSS and website pages) as files that you keep in git, and uploads them back as a draft. The merchant previews the draft and publishes it on their own in the admin on the Look versions screen. The command line publishes nothing.
  • It needs an API key with the Look drafts scope (Shop › Integrations, see Webhooks and API). The key belongs in the PORTICAN_API_KLUC environment variable, never in the command: it would stay in the shell history. For a store with several markets set the market token in PORTICAN_TRH.
  • verzie lists the look versions, pull downloads the published (or a chosen) version, push uploads the folder as a new draft and prints how many parts it changes compared with the live look, and nahlad prints a preview link that is valid for an hour. A package must not exceed 2 MB.
  • The JSON files have sorted keys and indentation, so the git diff shows only the real change. The command line @portican/cli is being prepared for release on public npm; until it is released, neither npx @portican/cli vzhlad nor npm install --save-dev @portican/cli works.
export PORTICAN_API_KLUC="..."   # kľúč s rozsahom Vzhľad, nikdy v príkaze

portican vzhlad verzie
portican vzhlad pull --do vzhlad
portican vzhlad push --z vzhlad --nazov "Jesenná kampaň"
portican vzhlad nahlad

Files in the vzhlad/ folder

pull writes these files and push uploads them as a draft. A part whose file is missing from the folder is not carried by the draft.

FileWhat it holds
vzhlad.jsonThe package version and the look version it was downloaded from. pull writes it, do not edit it by hand.
tema.jsonTheme colours and fonts.
podoba-eshopu.jsonStore layout and template.
uvodna-strana.jsonHome page sections.
bloky-partnera.jsonThe block schema of the partner's own frontend (see the Partner blocks section below).
sekcie-stranok.jsonOrder, visibility and band of the page sections (see the Sections below the product card section).
navigacia.jsonHeader menu and shop footer.
vlastne-css.jsonWhether custom CSS is on, and its version.
vlastne.cssCustom CSS as a real .css file.
stranky/<jazyk>/<slug>.jsonThe website pages, one file per page in the language folder. A slash in the page address becomes ~ in the file name: the page o-nas/tim is the file o-nas~tim.json.

Sections below the product card

  • The sekcie-stranok.json file changes the order of the sections that the platform store draws below the product card, not their content. Each section has zapnuta (yes or no) and pozadie (platno, pas). The product photos and details stay on top, they belong to buying, not to a section.
  • The sections come from a closed platform vocabulary: zalozky, dovera, recenzie, odporucania, podobne, otazky, otazkyProduktu, tabulkaVelkosti, bezpecnostVyrobku, nedavno, podkategorie, filtre, vypis, popis, uzavierka, odmeny, zasielky, zlavovykod, body, zdielanie. An unknown section and a second occurrence of the same section are dropped. A section the file does not mention stays where it is, and a section the platform adds later arrives switched off.
  • A draft without this file leaves the order unchanged: it takes the one that is live. It goes live only when the merchant publishes the draft, not the command line. The registry knows the pages produkt, kategoria, kosik; more arrive when the platform adds them, and this section lists them on its own.
{
  "produkt": [
    {
      "druh": "recenzie",
      "zapnuta": true,
      "pozadie": "pas"
    },
    {
      "druh": "zalozky",
      "zapnuta": true,
      "pozadie": "platno"
    },
    {
      "druh": "nedavno",
      "zapnuta": false,
      "pozadie": "platno"
    }
  ]
}

produkt

Product page: the sections below the card in today's order.

SectionWhat it drawsDefault
zalozkyDescription, parameters, files and the returns answer in tabs; drawn only with the zalozky product layout.on
doveraTrust badges; drawn only when the store has at least one badge switched on.on
recenzieProduct reviews.on
odporucaniaThe paired offer below the card.on
podobneSimilar products from the category.on
otazkyThe store's frequently asked questions, the same as on the Contact page; the returns answer is for this product.on
otazkyProduktuCustomer questions about the product and the shop's answers; off by default.off
tabulkaVelkostiProduct size chart; off by default.off
bezpecnostVyrobkuManufacturer and safety information under GPSR; off by default.off
nedavnoStrip of recently viewed products.on

kategoria

Category page: sections above the listing and the listing, in today's order.

SectionWhat it drawsDefault
podkategorieSubcategories of the current category.on
filtreFilters of the category listing.on
vypisProduct listing of the category.on
popisCategory description.on

kosik

Cart: sections around the order summary; the checkout does not change with them.

SectionWhat it drawsDefault
odporucaniaThe paired offer below the card.on
uzavierkaA line saying by when to order for the next delivery.on
odmenyRewards for the order value in the cart.on
zasielkySplitting the order into several shipments in the cart.on
zlavovykodDiscount code field.on
bodyLoyalty points in the cart.on
zdielanieLink for sharing the cart.on

Section background

  • platno: the section on the page canvas, as before.
  • pas: the section in a full-width band in the theme surface, like the website blocks.

Block schema

  • The Shop API serves the schemaBlokov query: a machine-readable description of page block and home page section kinds with fields, types, length and count limits, options and a version. With it you can check which fields a block carries without reading the platform code.
  • The schema is public and read-only, the same for every market and free of store data. The version rises with every change of fields, types or limits and is confirmed by a fingerprint, so you can tell whether you know exactly that version.
  • The schemaBlokov query is in the list of frontend key queries above, so the SDK can call it with the key too. The schema is public and the same for every market, so the key reveals nothing new.

Partner blocks: content schema

  • A custom frontend draws its own blocks, but you do not hard-code the texts, images and links in them. The merchant changes them in the admin on the screen Shop › Partner blocks, or the assistant does. That is why every block carries a content schema: fields, their types, limits and defaults. The platform builds the form from it and validates the stored content against it. A block without a single field is rejected (finding bez-schemy-obsahu), because its texts would stay in the code.
  • You write the schema into the file bloky-partnera.json in the look (see the section Look as files) and upload it with the draft using portican vzhlad push. It goes live only when the merchant publishes that look version. Raise verzia with every change of fields: the platform rejects a changed schema that has the same version as the one live in the shop.
  • Field types: text, viacriadkovy, adresa, obrazok, cislo, volba, prepinac, zoznam, produkt, kolekcia, stranka. Text allows 200 characters by default (up to 1,000), multi-line text 2,000 (up to 10,000). A number has the limits najmenej and najviac, a choice has the list hodnoty, a list has the fields polozky (no nested list) and najviac items: 20 by default, at most 100.
  • Schema limits: at most 60 blocks, 40 fields per block and per list item, a schema of at most 256 KiB and the content of one block in one language of at most 64 KiB. The block code is slug-like (uvod-pas), the field name an identifier as in JavaScript (nadpis); you can give names for the merchant in other languages too (nazvy).
  • The default is what the website shows until the merchant fills in the field, and after the button Back to the partner default. Stored content is not deleted when the schema changes: a field that left the schema is not delivered to the frontend, and when the schema comes back the text is back.
{
  "verzia": 1,
  "bloky": [
    {
      "kod": "uvod-pas",
      "nazov": "Úvodný pás",
      "nazvy": {
        "cs": "Úvodní pás",
        "en": "Hero"
      },
      "polia": [
        {
          "pole": "nadpis",
          "typ": "text",
          "nazov": "Nadpis",
          "najviacZnakov": 80,
          "predvolba": "Kvety na každý deň"
        },
        {
          "pole": "fotka",
          "typ": "obrazok",
          "nazov": "Fotka"
        },
        {
          "pole": "tlacidlo",
          "typ": "adresa",
          "nazov": "Odkaz tlačidla",
          "predvolba": "/kategorie"
        },
        {
          "pole": "vyhody",
          "typ": "zoznam",
          "nazov": "Výhody",
          "najviac": 4,
          "polozky": [
            {
              "pole": "text",
              "typ": "text",
              "nazov": "Výhoda",
              "najviacZnakov": 60
            }
          ]
        }
      ]
    }
  ]
}

Reading block content and the webhook

  • The frontend reads block content with the Shop API query blokyPartnera (example below); it is among the queries a frontend key allows. The answer has the schema verzia and odtlacok, the jazyk and the bloky by block code with the values of all fields: a missing or invalid value is replaced by the partner default. Without an uploaded and published schema verzia and odtlacok are null and bloky is empty.
  • The language is optional: without it the request language applies, and a language the market does not have gets the default language of the market, so the frontend always draws something. The fingerprint tells the frontend whether it reads the schema it was built with.
  • The @portican/storefront SDK cannot call the query blokyPartnera today. You call it as plain GraphQL with the key and market headers, like the other queries.
  • The frontend learns about a content change from the webhook obsah.zmeneny: the field znacky in its body contains bloky-partnera when the merchant changed block content or the block schema changed. The webhook is signed with the header portican-podpis with a tolerance of 300 seconds and retried on failure; headers, signature check and retries are the same as for the other events, see Webhooks and API. The merchant enters the webhook address on the screen Integrations.
POST {adresa Shop API obchodu}
vendure-token: {token trhu}
portican-kluc-frontendu: {kľúč frontendu}      # len na serveri

{ "query": "{ blokyPartnera(jazyk: \"sk\") }" }

# Odpoveď (pole blokyPartnera v data):
{
  "verzia": 1,
  "odtlacok": "9f2c…",
  "jazyk": "sk",
  "bloky": {
    "uvod-pas": {
      "nadpis": "Kvety na každý deň",
      "fotka": null,
      "tlacidlo": "/kategorie",
      "vyhody": [{ "text": "Doručenie v deň objednávky" }]
    }
  }
}

Uploading a frontend

  • You upload the finished frontend (not the source code) from your own CI with portican frontend deploy. The command reads a folder (dist by default), sends it to the platform as one version and prints the address of its preview. Nothing changes live.
  • Portican hosting serves static output only: index.html, scripts, stylesheets and images. No partner code runs on the server, so there is no server-side rendering (SSR), no server functions and no API routes. The site is assembled in the browser and calls the Shop API without the key. The platform writes the market Shop API address, the market token, the checkout and website addresses, the language and the shop domain to /portican-env.json next to index.html, so one build runs on every market you release it to. The @portican/create starter builds such a site for you.
  • The root of the folder must contain the manifest portican.json: verzia (2), assety (the website folder inside the bundle, containing index.html) and, optionally, hostitelia. Outside the website folder only the manifest, package-lock.json and bloky-partnera.json (the custom block schema, section Partner blocks) are allowed. A worker bundle (a version 1 manifest with a hlavny field) is rejected with a sentence telling you what to change.
  • You need an API key with the scope Frontend deployment (nasadenie-frontendu; Shop › Integrations, see Webhooks and API), stored in the secrets of your CI as PORTICAN_API_KLUC. Such a key can only upload a version and read the list of versions (portican frontend verzie); it neither publishes nor rolls back.
  • The platform stops the upload when: the manifest is missing or has another version, index.html is missing or a file sits outside the website folder where it does not belong, package-lock.json is missing (the CLI can add it with --zamok), the bundle has a known high-severity vulnerability, the version exceeds 25 MB or 2,000 files (a single upload at most 14 MiB), or it has a custom block without a content schema. Keep hidden files (.env) and source maps out of the bundle. The CLI prints the findings of the check. A market can have at most 10 unpublished versions at a time.
  • From the browser, the site may call the Shop API of the shop and the hosts in the hostitelia field of the manifest (at most 10, https only, no wildcard, never an IP address or a platform domain); everything else is blocked by the content security policy (CSP) the hosting adds to every page. The policy does not allow an inline <script> in the HTML, so scripts belong in files. Static files must not sit under paths that the platform serves on the shop domain (/shop-api, /assets, /health, /na-stiahnutie, /sukromne-subory, /_next, /api, /.well-known, /robots.txt, /sitemap, /llms.txt, /feed, /ucp): such a bundle is not uploaded.
  • Search engines. A static site returns the same index.html for every address and the content is rendered in the browser. Google and Bing render it and read titles, descriptions, canonical addresses and structured data (JSON-LD from the SDK helpers), only later; tools that do not run scripts (link previews, some crawlers) see an empty page. The sitemap, product feeds, robots.txt and llms.txt are served on the shop domain by the platform from the store data. Pages that rarely change can be prerendered to plain HTML at build time; content the merchant edits then shows up only after the next build.
  • A version has the state draft, preview, published or archive; at most one is published per market. The merchant publishes, not the key: in the admin the assistant does it (“publish the new frontend”) and it can also go back to the previous version or to the platform template. Going back to the template is one step.
  • Until the platform switches on frontend hosting on its server, the upload ends with a sentence about hosting that is not connected and nothing is stored. A frontend on your own hosting with the frontend key keeps working without it.
// portican.json v koreni priečinka balik/
{
  "verzia": 2,
  "assety": "verejne",
  "hostitelia": [
    "https://cms.partner.sk"
  ]
}

// balik/verejne/index.html, balik/verejne/static/…, balik/package-lock.json

# v CI partnera:
npm run build                    # statický web (napríklad vite build) a balík v balik/
export PORTICAN_API_KLUC="..."   # kľúč s rozsahom Nasadenie frontendu
portican frontend deploy --z balik --nazov "$GITHUB_SHA"
portican frontend verzie

What the platform does not do

  • Checkout, sign-in, the customer account, payments and refunds are always on the platform. A partner frontend has no access to them and payment data does not pass through it.
  • Portican neither builds nor runs a partner frontend: you build it in your own CI and upload the finished static site. Portican hosting does not support server-side rendering (SSR) or partner server code; a frontend that needs them runs on your own hosting with the frontend key.
  • Do not hard-code texts and images that the merchant is meant to change: read them from the settings, website pages and products the merchant edits in the admin.