Working in Plangku

Theme packages

Build a theme as a .zip outside Plangku and upload it, with its settings, images and starting pages.

What a package is

A theme package is a .zip file that holds a theme. Agencies use it to build a theme in their own editor, keep it in Git, and install it on client websites.

The way in is one-way. You can upload a package, but you cannot download a theme as a package. Themes are edited and uploaded here, and they only run on Plangku's engine.

To upload one, open the code editor and click the cloud icon (Upload a theme package) in the Explorer or the left bar. You need the Theme code permission.

The layout

text
theme.json                       optional
config/settings_schema.json      optional, theme settings
config/settings_data.json        optional, a starting design (mode "all")
assets/*.css                     CSS
assets/*.png, .jpg, .webp ...    images
sections/*.plang                 sections
snippets/*.plang                 snippets
templates/index.json             optional, the home page (mode "all")
templates/<address>.json         optional, other pages (mode "all")

The files in sections/, snippets/, assets/, config/settings_schema.json and theme.json follow the same rules as in the editor. See Files and structure.

You can zip the files at the top level, or inside one folder (my-theme/sections/...). A single wrapping folder is removed. Folders and files named __MACOSX, .DS_Store, Thumbs.db and .git are ignored. Any other file that is not part of a theme is skipped, and the result lists it as "left out".

All text files must be UTF-8.

Images

Put images in assets/. Allowed types are .png, .jpg, .jpeg, .webp, .gif and .avif. Plangku uploads each one the same way as a photo added in the editor, and it must pass the usual image checks.

Use an image in three places:

  • In a schema default or preset: "default": "assets/banner.jpg".
  • In settings_data.json and templates/*.json: "photo": "assets/banner.jpg".
  • In a template: {{ 'banner.jpg' | asset_url }}.

In the first two, assets/<file> becomes the uploaded image's address. The images count towards the 60-image limit.

Pages: templates/*.json

A page file describes one page.

json
{
	"title": "Beranda",
	"sections": [
		{ "type": "hero", "settings": { "heading": "Kopi dari kebun sendiri" } },
		{ "type": "promo", "settings": { "heading": "Promo hari ini" }, "hidden": false }
	],
	"seo": { "title": "Warung Kopi Senja", "description": "Kopi Jawa, diantar ke rumah." }
}
  • templates/index.json is the home page. templates/tentang.json becomes the page at /tentang. The address is lowercase letters, numbers and -, up to 100 characters.
  • type is a built-in section type, such as hero or cta, or the handle of one of your theme's sections. Header and footer cannot be used, and a type that is neither is dropped.
  • A page can have up to 60 sections.
  • title defaults to the address with spaces (or "Beranda" for the home page).
  • hidden: true hides the section.
Tip: Settings you leave out start with the section's schema defaults (for a built-in type the theme replaces, the theme file's defaults), so a page only needs the settings that differ. A type that is neither a built-in section nor one of the package's sections is left out, and the upload result lists it.

A website can have up to 20 pages. If a package would add a page over that, the new page is skipped, and the upload result says which one.

config/settings_data.json

An optional starting design. Each part is optional.

json
{
	"theme": {
		"colors": { "brand": "#B45309", "accent": "#F59E0B" },
		"headingFont": "Poppins",
		"bodyFont": "Inter",
		"custom": { "promo_color": "#B45309" }
	},
	"header": { "settings": { "logoText": "Warung Kopi Senja" } }
}
  • theme is merged into the website's theme. Colours are merged one by one, so you can set only brand. custom holds the values of your theme settings.
  • header and footer are merged into the existing header and footer settings. You can write the settings directly ("header": { "logoText": "..." }) or under "settings".

The two upload modes

When you upload, you choose what the package replaces.

Theme code onlyTheme code, design and pages
Modecodeall
Theme filesReplaced: every theme file is replaced by the package'sReplaced, the same way
Colours, fonts, header, footerStayMerged from settings_data.json, if present
PagesStaytemplates/*.json pages are added. A page with the same address is replaced. Other pages are kept
Texts and photos in other pagesStayStay

"Replaced" means the website's theme files become exactly the package's files. A file that is in the website but not in the package is deleted. So the package must contain every file you want to keep, including theme.json and config/settings_schema.json.

Everything lands in the draft. Nothing is live until you publish.

Limits

LimitValue
Size of the .zip11 MB
Files in the .zip400
One file, unpacked8 MB
All files, unpacked40 MB

The theme's own limits also apply (150 files, 120,000 characters per text file and so on). See Files and structure.

The .zip must use standard compression (deflate or stored). Password-protected archives and ZIP64 are not supported. File names with .., backslashes or absolute paths are refused.

Errors and the backup

Plangku checks the theme's code before it changes anything. If any file has an error, the upload stops. Nothing is installed, no image is uploaded, and the dialog lists the first errors with file and line.

If the code is fine, Plangku saves a backup first. It is a website version named "Before a theme upload", and you can find it in History, under Whole theme. Restoring it brings back the old theme files. Then it installs the package.

When it finishes, the dialog shows how many sections, snippets, files, images and pages were installed. Check the preview, then publish.

A complete small example

This package has a promo section, a snippet, a theme setting, a stylesheet, a starting design, an image and two pages.

text
warung-kopi/
  theme.json
  config/
    settings_schema.json
    settings_data.json
  assets/
    site.css
    banner.jpg
  sections/
    promo.plang
  snippets/
    price-tag.plang
  templates/
    index.json
    promo.json

theme.json

json
{ "name": "Warung Kopi", "version": "1.0.0", "author": "Studio Kopi Jawa" }

config/settings_schema.json

json
[
	{
		"name": "Colours",
		"settings": [
			{ "type": "color", "id": "promo_color", "label": "Promo colour", "default": "#B45309" }
		]
	}
]

config/settings_data.json

json
{
	"theme": {
		"colors": { "brand": "#B45309", "accent": "#F59E0B" },
		"headingFont": "Poppins",
		"bodyFont": "Inter",
		"custom": { "promo_color": "#B45309" }
	},
	"header": { "settings": { "logoText": "Warung Kopi Senja", "ctaLabel": "Pesan lewat WhatsApp" } }
}

assets/site.css

css
.price-tag { margin-top: 1rem; font-size: 1.5rem; }
.price-tag strong { color: var(--ts-promo-color); }
.price-tag s { margin-left: .5rem; font-size: 1rem; color: var(--s-muted); }

assets/banner.jpg is a normal photo file (not shown here).

snippets/price-tag.plang

plang
<p class="price-tag">
	<strong>{{ price | money }}</strong>
	{%- if old_price > 0 %} <s>{{ old_price | money }}</s>{% endif %}
</p>

sections/promo.plang

plang
{%- assign s = section.settings -%}
<div class="site-wrap promo">
	<div>
		{% render 'heading', heading: s.heading, text: s.text %}
		{% render 'price-tag', price: s.price, old_price: s.old_price %}
		<p class="promo-action">{% render 'button', label: s.button_label, link: s.button_link, size: 'lg' %}</p>
	</div>
	{% render 'image', src: s.photo, alt: s.heading, class: 'promo-photo' %}
</div>

{% style %}
.promo { display: grid; gap: 2rem; align-items: center; }
.promo-photo { width: 100%; aspect-ratio: 4 / 3; border-radius: var(--r); }
.promo-action { margin-top: 1.5rem; }
@container (min-width: 48rem) {
	.promo { grid-template-columns: 1fr 1fr; }
}
{% endstyle %}

{% schema %}
{
	"name": "Promo kopi",
	"description": "A promo with a price, a photo and a WhatsApp button",
	"icon": "Coffee",
	"settings": [
		{ "type": "text", "id": "heading", "label": "Heading", "default": "Paket Kopi Hemat" },
		{ "type": "textarea", "id": "text", "label": "Text", "default": "Tiga bungkus biji kopi pilihan, gratis ongkir." },
		{ "type": "number", "id": "price", "label": "Price (Rp)", "min": 0, "step": 1000, "default": 85000 },
		{ "type": "number", "id": "old_price", "label": "Old price (Rp, 0 to hide)", "min": 0, "step": 1000, "default": 105000 },
		{ "type": "image", "id": "photo", "label": "Photo", "default": "assets/banner.jpg" },
		{ "type": "text", "id": "button_label", "label": "Button label", "default": "Pesan lewat WhatsApp" },
		{ "type": "link", "id": "button_link", "label": "Button link", "default": "wa:Halo, saya mau pesan paket kopi hemat" }
	],
	"presets": [{ "name": "Promo kopi" }]
}
{% endschema %}

templates/index.json

json
{
	"title": "Beranda",
	"sections": [
		{ "type": "hero", "settings": { "heading": "Kopi dari kebun sendiri", "text": "Disangrai setiap pagi, diantar ke rumah Anda." } },
		{
			"type": "promo",
			"settings": {
				"heading": "Paket Kopi Hemat",
				"text": "Tiga bungkus biji kopi pilihan, gratis ongkir.",
				"price": 85000,
				"old_price": 105000,
				"photo": "assets/banner.jpg",
				"button_label": "Pesan lewat WhatsApp",
				"button_link": "wa:Halo, saya mau pesan paket kopi hemat"
			}
		}
	],
	"seo": { "title": "Warung Kopi Senja", "description": "Kopi Jawa, diantar ke rumah." }
}

templates/promo.json

json
{
	"title": "Promo",
	"sections": [
		{
			"type": "promo",
			"settings": {
				"heading": "Promo bulan ini",
				"text": "Stok terbatas.",
				"price": 85000,
				"old_price": 0,
				"photo": "assets/banner.jpg",
				"button_label": "Pesan lewat WhatsApp",
				"button_link": "wa:Halo, saya mau pesan"
			}
		}
	]
}

Zip the warung-kopi folder, upload it with Theme code, design and pages, and the home page and /promo appear in the draft.