Modelling a site
A site is content plus, sometimes, a few types of your own. This page is about deciding which, writing the YAML, and shipping it so it can be installed on any tenant and kept in sync.
Reuse first
Studio ships a block library and page archetypes that cover a marketing site. Before adding a type, check whether one of these does the job:
| Group | Blocks (standard:*) |
|---|---|
| Structure | Section (a titled region holding blocks), ContentColumns, RuleDivider, Spacer |
| Content | Heading, Prose (rich text), Figure, Pullquote, ActionButton, ButtonBar |
| Marketing | Hero, Banner, FeatureGrid, Teaser (a card for another page), Slideshow + Slide |
| Interactive | TabGroup + TabPanel, Disclosure + DisclosureItem, ExternalEmbed, SocialShare, Video |
| Dynamic | CollectionList, ArticleList (queries article pages at render time) |
| Chrome | Logo, PrimaryNav (derived from the page tree), Breadcrumbs, LinkColumns, NewsletterSignup |
| Forms | FormFrame, FormRow, FormTextInput, FormChoiceInput, FormHiddenInput, FormSubmit |
| Personalisation | Variants / Switcher + Variant |
Page archetypes: standard:HomePage, standard:ContentPage, standard:ArticlePage, studio:LandingPage, standard:SiteHeader, standard:SiteFooter, and record pages such as shop:ProductPage.
A renderer that implements these blocks once renders every site that uses them. The Acme example uses nothing else.
When you add a type
Three layers, and the choice matters:
- An element type is a new block. Add one when a design needs a block that does not exist — a “pricing table”, a “team member card”. It declares fields with the fixed field kinds (
TextField,RichTextField,MediaField,ReferenceField,SectionField, …) and an editor layout. - An archetype is a page type. Add one when a page needs different fields — a job posting with a closing date, a location with an address — or a different set of allowed blocks. It names a node type for storage and lists its fields.
- A node type is storage. Add one when the thing is a record with its own indexed properties, its own workspace, its own app — products, submissions, events.
# elementtypes/team-member.yaml
name: acme:TeamMember
title: Team member
description: A person on the team — photo, name, role and a short bio.
icon: user
fields:
- { $type: MediaField, name: photo, title: Photo }
- { $type: TextField, name: name, title: Name, required: true }
- { $type: TextField, name: role, title: Role, translatable: true }
- { $type: RichTextField, name: bio, title: Bio, translatable: true }
Register it in manifest.yaml under provides.elementtypes, then allow it where it may be dropped: a block is only offered inside a SectionField whose allowed_element_types lists it. A block nobody allows is a block nobody can add.
# archetypes/content-page.yaml (yours, extending the standard one)
name: acme:ContentPage
title: Content page
base_node_type: studio:Page
extends: standard:ContentPage
fields:
- $type: SectionField
name: content
title: Content
allowed_element_types: [standard:Hero, standard:Section, standard:Prose, standard:FeatureGrid, standard:Figure, acme:TeamMember]
meta:
editor:
drop:
asset: standard:Figure # an image dropped into a section becomes a Figure
meta.editor is where a type shapes the editor: the layout of its fields, and what a drag from the Media rail does. drop (on a page type) names the block an image or a page turns into when dropped into a section; dropOnto (on a block type) names the field an image or a page sets when dropped onto an existing block — acme:TeamMember would declare dropOnto: { asset: photo }. The inline editing page has the full picture.
Two rules for fields a visitor reads: mark them translatable: true on the archetype or element type, and is_translatable: true on the node type property behind them. The first drives the editor’s translation mode; the second drives the server’s staleness tracking. Never flag identifiers, colours, icons or money. Details on the translations page.
Naming: blocks are blocks, sections are sections, components are components. Name types for what they show — acme:PricingTable, not a layout term.
The package
acme-site/
├── manifest.yaml
├── elementtypes/ # your blocks, if any
├── archetypes/ # your page types, if any
├── nodetypes/ # your records, if any
├── content/
│ ├── stories/acme/.node.yaml # the story: domain, brand, languages, embed rules
│ ├── stories/acme/home/.node.yaml # pages, one folder each, nested like the site
│ ├── stories/acme/header/.node.yaml
│ ├── stories/acme/footer/.node.yaml
│ ├── stories/acme/about/.node.yaml
│ ├── branding/acme/.node.yaml # the brand kit and its topic pages
│ └── assets/… # images referenced by the content
└── .raisin-sync.yaml
The story node carries what the site needs to be found and edited:
# content/stories/acme/.node.yaml
node_type: studio:Story
archetype: studio:StorySettings
properties:
title: Acme
domain: "{env:SITE_DOMAIN:-acme.localhost}"
dev_url: "{env:PREVIEW_SERVER:-http://localhost:4321}" # labels the preview chrome
languages: [en, de, fr]
default_language: en
brand: { raisin:ref: /acme, raisin:workspace: branding }
embed_rules:
- uuid: embed-default
element_type: studio:EmbedRule
pattern: /**
base_url: "{env:PREVIEW_SERVER:-http://localhost:4321}" # what the editor's iframe LOADS
locale_mode: prefix
The two URLs are not hardcoded. {env:NAME:-default} is resolved by the raisindb CLI whenever the package is built, validated or pushed: with nothing exported, the default is used and you get the localhost package; for a deployed environment you export the variables, or point the CLI at an env file:
raisindb package create --check . # localhost defaults
PREVIEW_SERVER=https://acme.example SITE_DOMAIN=acme.example \
raisindb deploy ./acme-site --repo studio --install # the deployed site
raisindb deploy ./acme-site --repo studio --install -e production # …or from .env.production
raisindb sync . --push --force --yes --repo studio --env-file ../.env.production
Forget the variable on a production deploy and the deployed Studio opens localhost:4321 in its preview — every author sees a blank frame. Put the two variables in the pipeline that deploys the package, next to the deploy command, so they cannot be forgotten. The same token works for anything environment-bound in content: an API base URL on an app definition, a webhook target on an automation.
A page is a node with an archetype and content:
# content/stories/acme/about/.node.yaml
node_type: studio:Page
archetype: standard:ContentPage
properties:
title: About
description: Who we are and why we make things this way.
content:
- uuid: about-hero
element_type: standard:Hero
headline: Made to last
subheadline: "<p>Everyday objects, considered.</p>"
- uuid: about-story
element_type: standard:Prose
body: "<p>We started Acme in a garage in 2014…</p>"
Every block has a uuid (stable, addressable by the editor and by translations) and an element_type; the rest are its fields by name. References are envelopes: { raisin:ref: <path or id>, raisin:workspace: <workspace> }.
Deploy and sync
Two commands, two jobs:
# Schema and anything that does not exist yet: types, workspaces, functions,
# the story and its pages on first install.
raisindb deploy ./acme-site --repo <repo> --install
# Content you keep editing in the package: pushes your files, replaces what
# .raisin-sync.yaml says you own, leaves everything else alone.
cd acme-site && raisindb sync . --push --force --yes --repo <repo> --server <url>
.raisin-sync.yaml decides ownership. Claim the roots your package authors and nothing more, so a redeploy updates your header and policies but never overwrites the page an author rewrote in Studio yesterday:
defaults:
mode: skip # seed once, never touch again
filters:
- { root: /stories/acme/header, mode: replace }
- { root: /stories/acme/footer, mode: replace }
- { root: /stories/acme/policies, mode: replace }
- { root: /branding/acme, mode: replace }
Pages under the story stay on skip: they are seeds, and after the first install they belong to the authors. Pull the other way to bring an author’s changes into the package when you want them versioned:
raisindb sync . --pull --repo <repo> --server <url>
Verify
After a deploy, look at the tenant rather than the deploy output:
- Studio → Stories shows the story; its pages open in the editor with the fields you declared.
raisindb package create --check ./acme-sitereports 0 errors (warnings aboutstudio:*andstandard:*types “not defined in this package” are expected — Studio defines them).- The story’s settings show your domain, brand and embed rule; the inline editing page says what to check there.
Pilot documentation. Studio is in a partner pilot. Screens and YAML shown here are current as of the version noted on each page; tell your Maravilla contact when something does not match what you see.