GrowSitedevelopers

Build your first theme

A complete package, installation and your first edit.

What you will build

Build Launch Theme with a hero section, editable text, buttons and media cards, a static signature, shared header/footer, global colors and font, and a contact page. Customers can edit text, colors, corner radius and spacing. The complete example includes Polish/English dictionaries and responsive CSS. Download the ZIP package; it is checked with the actual GrowSite compiler.

1. Prepare your environment

You need the GrowSite repository, a project-compatible Node.js installation, npm dependencies and a running API/database for the final builder check. Package installation is currently a maintainer workflow, not a public customer upload.

From the repository root:

bash
npm ci

Extract the ZIP outside the installed themes directory. The same source is included at dev-site/examples/launch-theme/. Copy it to your own working directory. Do not place it directly in installed themes if you want to exercise the import workflow.

Before importing, test the package in GS Loom Playground locally, without an API or database. Run npm run loom:playground -- dev-site/examples/launch-theme, then open the local preview.

2. Set package identity

Open loom.json. Keep launch-theme for the first trial. For your own product, choose a unique key, rename the directory and set the display name. Keep that key stable across releases. The initial version is 1.0.0.

3. Trace the data

templates/home.json creates a hero section instance. sections/hero.html reads section.settings.title. Its schema declares that identifier, the editor label and default. Cards use the same pattern through block.settings.

Change the default title and description in schema. Change the card radius default from 16 to 24. Adjust grid spacing in CSS. Do not replace the setting expression with hardcoded HTML text, or the editor control will no longer affect it.

The complete source files appear below. Keep their paths and extensions; each section/block needs exactly one schema.

ZIP import in the admin panel requires archive.json and sources in theme/. See the package manifest example and archive layout before preparing a ZIP.

4. Import and build

From the repository root, with the actual extracted directory path:

bash
npm run loom:import -- /absolute/path/launch-theme
npm run themes:build
npm run themes:check
npm run build:customer
npm run build:renderer

From the api directory:

bash
php artisan themes:sync --activate

Import refuses existing keys. For subsequent releases, increase the version and follow the update procedure. Importing a theme does not publish customer websites.

5. Verify in the builder

Create a test site with the new theme. Open the hero inspector and confirm prefilled defaults. Edit its title, add another card, reorder it, set radius to zero and disable its link. Edit the static signature and confirm it cannot be removed. Save, reload and compare with public rendering. Test mobile and both dictionary languages.

Initial content in the example is intentionally bilingual. Locale dictionaries translate labels and footer strings; they do not automatically translate customer-entered text.

Complete example files

The following source listing is generated from the downloadable package. When changing its source, rebuild the portal and rerun documentation tests.

Element recipes: text, buttons, fields, forms and media cards.

assets/theme.css

css
.launch { font-size: 16px; line-height: 1.65; }
.launch-header, .launch-footer { padding: 24px 32px; }
.launch-header { font-weight: 700; }
.launch-hero h1 { font-size: clamp(32px, 5vw, 64px); line-height: 1.1; max-width: 18ch; margin: 20px 0; }
.launch-eyebrow { text-transform: uppercase; letter-spacing: .12em; }
.launch-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); gap: 20px; margin: 32px 0; }
.launch-card { padding: 24px; border: 1px solid #d7dfd6; }
.launch-card h2 { font-size: 24px; margin-bottom: 12px; }
.launch-link { display: inline-block; padding: 10px 18px; background: #16221b; color: #fff; border-radius: 8px; margin-top: 20px; }
.launch-signature, .launch-footer { font-size: 14px; }
.launch-copy { padding: 24px; }
.launch-copy-body { white-space: pre-line; }
.launch-cta { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; min-height: 44px; align-self: start; }
.launch-cta:hover { filter: brightness(.92); }
.launch-cta:focus-visible, .launch-link:focus-visible { outline: 3px solid currentColor; outline-offset: 4px; }
.launch-card-image { display: block; width: 100%; height: auto; aspect-ratio: 8 / 5; object-fit: cover; border-radius: inherit; margin-bottom: 20px; }
.launch-card-icon { display: inline-flex; margin-bottom: 16px; }
.launch-card-icon svg { width: var(--icon-size, 32px); height: var(--icon-size, 32px); }
.launch-contact { padding: 32px; }

blocks/_signature.html

html
<p class="launch-signature" data-text-field="[[ block.settings_path ]].text" gs-text="block.settings.text"></p>

blocks/_signature.schema.json

json
{
  "name": "Signature",
  "settings": [
    {
      "type": "text",
      "id": "text",
      "label": "Text",
      "default": "Zbudowane z pomysłem / Built with care"
    }
  ]
}

blocks/button.html

html
<template gs-if="block.settings.show_link">
<a class="launch-cta" href="[[ safe_url(block.settings.url) ]]" style="background:[[ block.settings.background ]];color:[[ block.settings.color ]];border:[[ block.settings.border_width ]]px solid [[ block.settings.border_color ]];border-radius:[[ block.settings.radius ]]px;padding:[[ block.settings.padding_y ]]px [[ block.settings.padding_x ]]px;font-size:[[ block.settings.font_size ]]px">
  <span data-text-field="[[ block.settings_path ]].label" gs-text="block.settings.label"></span>
</a>
</template>

blocks/button.schema.json

json
{
  "name": "Button / Przycisk",
  "settings": [
    {
      "type": "text",
      "id": "label",
      "label": "Label / Tekst",
      "default": "Napisz do nas / Contact us"
    },
    {
      "type": "url",
      "id": "url",
      "label": "URL",
      "default": "mailto:hello@example.com"
    },
    {
      "type": "checkbox",
      "id": "show_link",
      "label": "Visible / Widoczny",
      "default": true
    },
    {
      "type": "color",
      "id": "background",
      "label": "Background / Tło",
      "default": "#16221b"
    },
    {
      "type": "color",
      "id": "color",
      "label": "Text color / Kolor tekstu",
      "default": "#ffffff"
    },
    {
      "type": "color",
      "id": "border_color",
      "label": "Border color / Kolor obramowania",
      "default": "#16221b"
    },
    {
      "type": "range",
      "id": "border_width",
      "label": "Border width / Grubość obramowania",
      "min": 0,
      "max": 8,
      "step": 1,
      "default": 0,
      "unit": "px"
    },
    {
      "type": "range",
      "id": "radius",
      "label": "Radius / Zaokrąglenie",
      "min": 0,
      "max": 48,
      "step": 1,
      "default": 8,
      "unit": "px"
    },
    {
      "type": "range",
      "id": "padding_y",
      "label": "Vertical padding / Wypełnienie pionowe",
      "min": 4,
      "max": 32,
      "step": 1,
      "default": 12,
      "unit": "px"
    },
    {
      "type": "range",
      "id": "padding_x",
      "label": "Horizontal padding / Wypełnienie poziome",
      "min": 8,
      "max": 64,
      "step": 1,
      "default": 20,
      "unit": "px"
    },
    {
      "type": "range",
      "id": "font_size",
      "label": "Text size / Rozmiar tekstu",
      "min": 12,
      "max": 32,
      "step": 1,
      "default": 16,
      "unit": "px"
    }
  ],
  "presets": [
    {
      "name": "Button / Przycisk"
    }
  ]
}

blocks/card.html

html
<article class="launch-card" data-canvas-card="[[ block.field_path ]].card" style="background:[[ block.settings.background ]];border-radius:[[ block.settings.radius ]]px">
  <h2 data-text-field="[[ block.settings_path ]].title" gs-text="block.settings.title"></h2>
  <p data-text-field="[[ block.settings_path ]].text" gs-text="block.settings.text"></p>
  <template gs-if="block.settings.show_link">
    <template gs-component="link" gs-prop-href="block.settings.url" gs-prop-label="block.settings.label"></template>
  </template>
</article>

blocks/card.schema.json

json
{
  "name": "t:blocks.card",
  "settings": [
    {
      "type": "text",
      "id": "title",
      "label": "t:settings.title",
      "default": "Karta / Card"
    },
    {
      "type": "textarea",
      "id": "text",
      "label": "t:settings.text",
      "default": "Szczegóły oferty / Offer details"
    },
    {
      "type": "color",
      "id": "background",
      "label": "t:settings.background",
      "default": "#ffffff"
    },
    {
      "type": "range",
      "id": "radius",
      "label": "t:settings.radius",
      "default": 16,
      "min": 0,
      "max": 48,
      "step": 1,
      "unit": "px"
    },
    {
      "type": "checkbox",
      "id": "show_link",
      "label": "t:settings.show_link",
      "default": true
    },
    {
      "type": "text",
      "id": "label",
      "label": "t:settings.label",
      "default": "Kontakt / Contact"
    },
    {
      "type": "url",
      "id": "url",
      "label": "URL",
      "default": "mailto:hello@example.com"
    }
  ],
  "presets": [
    {
      "name": "Card"
    }
  ]
}

blocks/media-card.html

html
<article class="launch-card" data-canvas-card="[[ block.field_path ]].card" style="background:[[ block.settings.background ]];border:[[ block.settings.border_width ]]px solid [[ block.settings.border_color ]];border-radius:[[ block.settings.radius ]]px">
  <template gs-if="block.settings.icon != &#x27;none&#x27;"><span class="launch-card-icon" style="color:[[ block.settings.icon_color ]];--icon-size:[[ block.settings.icon_size ]]px"><gs-icon name="[[ block.settings.icon ]]" field="[[ block.field_path ]].icon"></gs-icon></span></template>
  <template gs-if="block.settings.image_url != blank"><img class="launch-card-image" src="[[ safe_url(block.settings.image_url) ]]" alt="[[ block.settings.image_alt ]]" loading="lazy" width="640" height="400"></template>
  <h2 data-text-field="[[ block.settings_path ]].title" gs-text="block.settings.title"></h2>
  <p data-text-field="[[ block.settings_path ]].text" gs-text="block.settings.text"></p>
</article>

blocks/media-card.schema.json

json
{
  "name": "Media card / Karta z obrazem",
  "settings": [
    {
      "type": "text",
      "id": "title",
      "label": "Heading / Nagłówek",
      "default": "Oferta / Offer"
    },
    {
      "type": "textarea",
      "id": "text",
      "label": "Description / Opis",
      "default": "Opisz korzyść. / Describe a benefit."
    },
    {
      "type": "image_picker",
      "id": "image_url",
      "label": "Image URL / Adres obrazu",
      "default": ""
    },
    {
      "type": "text",
      "id": "image_alt",
      "label": "Alternative text / Tekst alternatywny",
      "default": ""
    },
    {
      "type": "select",
      "id": "icon",
      "label": "Icon / Ikona",
      "default": "shield",
      "options": [
        {
          "value": "none",
          "label": "None / Brak"
        },
        {
          "value": "shield",
          "label": "Shield / Tarcza"
        },
        {
          "value": "bolt",
          "label": "Bolt / Błyskawica"
        }
      ]
    },
    {
      "type": "range",
      "id": "icon_size",
      "label": "Icon size / Rozmiar ikony",
      "min": 16,
      "max": 96,
      "step": 1,
      "default": 32,
      "unit": "px"
    },
    {
      "type": "color",
      "id": "icon_color",
      "label": "Icon color / Kolor ikony",
      "default": "#16221b"
    },
    {
      "type": "color",
      "id": "background",
      "label": "Background / Tło",
      "default": "#ffffff"
    },
    {
      "type": "color",
      "id": "border_color",
      "label": "Border color / Kolor obramowania",
      "default": "#d7dfd6"
    },
    {
      "type": "range",
      "id": "border_width",
      "label": "Border width / Grubość obramowania",
      "min": 0,
      "max": 8,
      "step": 1,
      "default": 1,
      "unit": "px"
    },
    {
      "type": "range",
      "id": "radius",
      "label": "Radius / Zaokrąglenie",
      "min": 0,
      "max": 48,
      "step": 1,
      "default": 16,
      "unit": "px"
    }
  ],
  "presets": [
    {
      "name": "Media card / Karta z obrazem"
    }
  ]
}

blocks/text.html

html
<div class="launch-copy" style="color:[[ block.settings.color ]];text-align:[[ block.settings.alignment ]]">
  <h2 data-text-field="[[ block.settings_path ]].title" style="font-size:[[ block.settings.heading_size ]]px" gs-text="block.settings.title"></h2>
  <p class="launch-copy-body" data-text-field="[[ block.settings_path ]].text" gs-text="block.settings.text"></p>
</div>

blocks/text.schema.json

json
{
  "name": "Text / Tekst",
  "settings": [
    {
      "type": "text",
      "id": "title",
      "label": "Heading / Nagłówek",
      "default": "Poznaj nas / Meet us"
    },
    {
      "type": "textarea",
      "id": "text",
      "label": "Description / Opis",
      "default": "Opowiedz swoją historię. / Tell your story."
    },
    {
      "type": "color",
      "id": "color",
      "label": "Color / Kolor",
      "default": "#16221b"
    },
    {
      "type": "range",
      "id": "heading_size",
      "label": "Heading size / Rozmiar nagłówka",
      "min": 20,
      "max": 64,
      "step": 1,
      "default": 32,
      "unit": "px"
    },
    {
      "type": "select",
      "id": "alignment",
      "label": "Alignment / Wyrównanie",
      "default": "left",
      "options": [
        {
          "value": "left",
          "label": "Left / Do lewej"
        },
        {
          "value": "center",
          "label": "Center / Do środka"
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "Text / Tekst"
    }
  ]
}

config/settings_data.json

json
{"current":{"background":"#f4f6ef","foreground":"#16221b","font":"Inter"}}

config/settings_schema.json

json
[{"name":"Appearance","settings":[{"type":"color","id":"background","label":"Background","default":"#f4f6ef"},{"type":"color","id":"foreground","label":"Text","default":"#16221b"},{"type":"font_picker","id":"font","label":"Font","default":"Inter"}]}]

layout/theme.html

html
<div class="launch" style="background:[[ settings.background ]];color:[[ settings.foreground ]];font-family:[[ settings.font ]]">
<template gs-group="header"></template>
<template gs-slot="layout"></template>
<template gs-group="footer"></template>
</div>

locales/en.json

json
{"footer":{"rights":"All rights reserved."}}

locales/en.schema.json

json
{"sections":{"hero":"Hero section"},"blocks":{"card":"Card"},"settings":{"title":"Heading","text":"Description","spacing":"Inner spacing","background":"Background","radius":"Corner radius","show_link":"Show link","label":"Link label"}}

locales/pl.default.json

json
{"footer":{"rights":"Wszelkie prawa zastrzeżone."}}

locales/pl.default.schema.json

json
{"sections":{"hero":"Sekcja powitalna"},"blocks":{"card":"Karta"},"settings":{"title":"Nagłówek","text":"Opis","spacing":"Odstęp wewnętrzny","background":"Tło","radius":"Zaokrąglenie","show_link":"Pokaż link","label":"Tekst linku"}}

loom.json

json
{
  "key": "launch-theme",
  "name": "Launch Theme",
  "version": "1.0.0",
  "defaultLocale": "pl",
  "catalog": true,
  "engine": "gs-loom",
  "engineVersion": 1
}

sections/article.html

html
<gs-blog variant="blog-detail-sidebar"></gs-blog>

sections/article.schema.json

json
{
  "name": "Article / Artykuł",
  "settings": [],
  "presets": [
    {
      "name": "Article / Artykuł"
    }
  ]
}

sections/contact.html

html
<section class="launch-contact">
  <h1 data-text-field="[[ section.settings_path ]].title" gs-text="section.settings.title"></h1>
  <p data-text-field="[[ section.settings_path ]].text" gs-text="section.settings.text"></p>
<gs-form instance-id="[[ section.settings.form_id ]]"></gs-form>
</section>

sections/contact.schema.json

json
{
  "name": "Contact / Kontakt",
  "settings": [
    {
      "type": "text",
      "id": "title",
      "label": "Heading / Nagłówek",
      "default": "Porozmawiajmy / Let's talk"
    },
    {
      "type": "textarea",
      "id": "text",
      "label": "Description / Opis",
      "default": "Dodaj formularz w kreatorze. / Add a form in the builder."
    },
    {
      "type": "text",
      "id": "form_id",
      "label": "ID formularza / Form ID",
      "default": ""
    }
  ],
  "presets": [
    {
      "name": "Contact / Kontakt"
    }
  ]
}
html
<footer class="launch-footer">© [[ date(&#x27;now&#x27;, &#x27;%Y&#x27;) ]] [[ site.name ]]. [[ t(&#x27;footer.rights&#x27;) ]]<gs-menu location="footer" label="Footer"></gs-menu></footer>
json
{"type":"footer","sections":{"main":{"type":"footer"}},"order":["main"]}
json
{
  "name": "Footer",
  "settings": [],
  "presets": [
    {
      "name": "Footer"
    }
  ]
}

sections/header.html

html
<div class="launch-header">[[ site.name ]]</div>

sections/header.json

json
{"type":"header","sections":{"main":{"type":"header"}},"order":["main"]}

sections/header.schema.json

json
{
  "name": "Header",
  "settings": [],
  "presets": [
    {
      "name": "Header"
    }
  ]
}

sections/hero.html

html
<section class="launch-hero" style="padding:[[ section.settings.spacing ]]px">
  <p class="launch-eyebrow" gs-text="site.name"></p>
  <h1 data-text-field="[[ section.settings_path ]].title" gs-text="section.settings.title"></h1>
  <p data-text-field="[[ section.settings_path ]].text" gs-text="section.settings.text"></p>
  <div class="launch-grid"><template gs-slot="blocks"></template></div>
  <template gs-block="_signature" gs-id="signature"></template>
</section>

sections/hero.schema.json

json
{
  "name": "t:sections.hero",
  "settings": [
    {
      "type": "text",
      "id": "title",
      "label": "t:settings.title",
      "default": "Stwórz coś dobrego / Create something good"
    },
    {
      "type": "textarea",
      "id": "text",
      "label": "t:settings.text",
      "default": "Opisz swoją ofertę. / Introduce your business."
    },
    {
      "type": "range",
      "id": "spacing",
      "label": "t:settings.spacing",
      "min": 16,
      "max": 80,
      "step": 4,
      "default": 32,
      "unit": "px"
    }
  ],
  "blocks": [
    {
      "type": "card"
    },
    {
      "type": "text"
    },
    {
      "type": "button"
    },
    {
      "type": "media-card"
    }
  ],
  "max_blocks": 6,
  "presets": [
    {
      "name": "Hero"
    }
  ]
}

sections/journal.html

html
<section class="theme-journal">
  <gs-blog variant="blog-grid-sidebar"></gs-blog>
</section>

sections/journal.schema.json

json
{
  "name": "Blog",
  "settings": [],
  "presets": [
    {
      "name": "Blog"
    }
  ]
}
html
<a class="launch-link" href="[[ safe_url(href) ]]" gs-text="label"></a>

snippets/menu.html

html
<gs-menu location="[[ location ]]" label="[[ label ]]"></gs-menu>

templates/article.json

json
{
  "title": "Article",
  "role": "blog-details",
  "sections": {
    "journal": {
      "type": "article"
    }
  },
  "order": [
    "journal"
  ]
}

templates/blog.json

json
{
  "title": "Blog",
  "role": "blog",
  "sections": {
    "journal": {
      "type": "journal"
    }
  },
  "order": [
    "journal"
  ]
}

templates/contact.json

json
{"title":"Contact","sections":{"contact":{"type":"contact"}},"order":["contact"]}

templates/home.json

json
{"title":"Home","sections":{"hero":{"type":"hero","blocks":{"first":{"type":"card","settings":{"title":"Twój pomysł / Your idea"}},"signature":{"type":"_signature","static":true}},"block_order":["first"]}},"order":["hero"]}
Download Markdown copy
Results · 22
GS Loom UI components

Shared components, variants and theme styling.

Font catalog

Available fonts, previews and font declarations in GS Loom.

Menus and navigation

Create menus, connect links and style navigation.

Blog and comments

Blog and comments

SEO and plugin contracts

SEO and plugin contracts

Build your first theme

A complete package, installation and your first edit.

GS Loom Playground

Test themes locally without an API, database or upload.

Architecture and files

How author files become a customer website.

Schema and settings

Control types, default values and stable identifiers.

Blocks, targeting and order

Definitions, instances, nesting and static blocks.

Preview editing

Connect text, cards, images and icons to the builder.

Text and headings

From schema to text editing, typography and persistence.

Buttons and links

Labels, destinations, colors, dimensions and accessible states.

Inputs and form fields

Field types, labels, options, limits and validation.

Forms step by step

Instances, site binding, submission and error handling.

Cards, icons, images and badges

Container appearance, icon sizes and media editing.

Dynamic sources

Site data, page context and fallback values.

Layouts and shared settings

Section groups, global configuration and page templates.

Languages and translations

Starter languages, customer-added languages and AI translations.

Snippets, CSS and assets

Reusable markup, responsive styling and safe URLs.

Testing, import and updates

Validate a package and release it while preserving customer data.

Capabilities and limits

The supported GS Loom contract and working with AI.