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:
npm ciExtract 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:
npm run loom:import -- /absolute/path/launch-theme
npm run themes:build
npm run themes:check
npm run build:customer
npm run build:rendererFrom the api directory:
php artisan themes:sync --activateImport 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
.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
<p class="launch-signature" data-text-field="[[ block.settings_path ]].text" gs-text="block.settings.text"></p>blocks/_signature.schema.json
{
"name": "Signature",
"settings": [
{
"type": "text",
"id": "text",
"label": "Text",
"default": "Zbudowane z pomysłem / Built with care"
}
]
}blocks/button.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
{
"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
<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
{
"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
<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 != 'none'"><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
{
"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
<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
{
"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
{"current":{"background":"#f4f6ef","foreground":"#16221b","font":"Inter"}}config/settings_schema.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
<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
{"footer":{"rights":"All rights reserved."}}locales/en.schema.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
{"footer":{"rights":"Wszelkie prawa zastrzeżone."}}locales/pl.default.schema.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
{
"key": "launch-theme",
"name": "Launch Theme",
"version": "1.0.0",
"defaultLocale": "pl",
"catalog": true,
"engine": "gs-loom",
"engineVersion": 1
}sections/article.html
<gs-blog variant="blog-detail-sidebar"></gs-blog>sections/article.schema.json
{
"name": "Article / Artykuł",
"settings": [],
"presets": [
{
"name": "Article / Artykuł"
}
]
}sections/contact.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
{
"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"
}
]
}sections/footer.html
<footer class="launch-footer">© [[ date('now', '%Y') ]] [[ site.name ]]. [[ t('footer.rights') ]]<gs-menu location="footer" label="Footer"></gs-menu></footer>sections/footer.json
{"type":"footer","sections":{"main":{"type":"footer"}},"order":["main"]}sections/footer.schema.json
{
"name": "Footer",
"settings": [],
"presets": [
{
"name": "Footer"
}
]
}sections/header.html
<div class="launch-header">[[ site.name ]]</div>sections/header.json
{"type":"header","sections":{"main":{"type":"header"}},"order":["main"]}sections/header.schema.json
{
"name": "Header",
"settings": [],
"presets": [
{
"name": "Header"
}
]
}sections/hero.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
{
"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
<section class="theme-journal">
<gs-blog variant="blog-grid-sidebar"></gs-blog>
</section>sections/journal.schema.json
{
"name": "Blog",
"settings": [],
"presets": [
{
"name": "Blog"
}
]
}snippets/link.html
<a class="launch-link" href="[[ safe_url(href) ]]" gs-text="label"></a>snippets/menu.html
<gs-menu location="[[ location ]]" label="[[ label ]]"></gs-menu>templates/article.json
{
"title": "Article",
"role": "blog-details",
"sections": {
"journal": {
"type": "article"
}
},
"order": [
"journal"
]
}templates/blog.json
{
"title": "Blog",
"role": "blog",
"sections": {
"journal": {
"type": "journal"
}
},
"order": [
"journal"
]
}templates/contact.json
{"title":"Contact","sections":{"contact":{"type":"contact"}},"order":["contact"]}templates/home.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"]}