# Defining Page Types
Source: https://docs.primo.build/building-sites/defining-page-types
Set up page types to control structure, available blocks, and editor guardrails.
Page types are the blueprints that define what kind of pages can exist in your site. They control which blocks editors can use, what page-level fields are available, and what constraints apply. This guide covers everything you need to know about defining effective page types.
## What Are Page Types?
Page types are templates that define:
* **Available blocks** - Which blocks editors can add to pages
* **Required blocks** - Blocks that must appear on every page
* **Page fields** - Metadata like title, description, publish date
* **Constraints** - What editors can and cannot modify
* **URL structure** - How pages of this type are accessed
Think of page types as guardrails. They give editors freedom to create content while ensuring designs stay intact and structure remains consistent.
## Creating a Page Type
In your site dashboard, click "Page Types" in the sidebar, then "Create Page Type".
Configure:
* **Name**: Display name (e.g., "Blog Post", "Default")
* **Description**: Internal description for your reference
* **Slug**: URL identifier (e.g., "blog-post", "default")
The slug determines the URL structure. A page of type "blog-post" will be accessible at `/blog-post/[page-slug]` by default.
Select which blocks from your component library can be used on pages of this type.
Only blocks you've created in your component library will appear here. Create blocks first, then add them to page types.
Mark certain blocks as required if they must appear on every page of this type.
A "Blog Post" page type might require a "Header" block at the top and a "Footer" block at the bottom.
Define page-level fields that apply to all pages of this type:
* SEO fields (meta title, description)
* Publishing fields (publish date, author)
* Custom metadata (category, tags, featured image)
Page fields are separate from block fields. They apply to the entire page, not individual blocks.
Click "Create" to save your page type. It's now available for editors to use when creating pages.
## Common Page Type Patterns
### Blog Post
A typical blog post page type:
**Available Blocks:**
* Text (for article content)
* Image (for illustrations)
* Code Block (for code examples)
* Quote (for pull quotes)
**Page Fields:**
* `title` (Text, required) - Post title
* `author` (Text) - Author name
* `publish_date` (Date) - Publication date
* `category` (Select) - Post category
* `tags` (Tags) - Post tags
* `featured_image` (Image) - Featured image
* `excerpt` (Text) - Short description
* `meta_title` (Text) - SEO title
* `meta_description` (Text) - SEO description
**Required Blocks:**
* None (flexibility for different post layouts)
### Product Page
An e-commerce product page:
**Available Blocks:**
* Hero (with product image)
* Text (for description)
* Image Gallery
* Specifications
* Reviews
* Related Products
**Page Fields:**
* `title` (Text, required) - Product name
* `price` (Number) - Product price
* `sku` (Text) - SKU code
* `in_stock` (Boolean) - Stock status
* `category` (Select) - Product category
* `featured_image` (Image) - Main product image
* `meta_title` (Text) - SEO title
* `meta_description` (Text) - SEO description
**Required Blocks:**
* Hero (must have product image)
## Page Fields
Page fields are metadata that applies to the entire page, not individual blocks. Common uses:
### SEO Fields
Every page type should include SEO fields:
* `meta_title` (Text) - Page title for search engines
* `meta_description` (Text) - Page description for search results
* `og_image` (Image, optional) - Social sharing image
### Publishing Fields
For content with publication workflows:
* `publish_date` (Date) - When the page was/will be published
* `author` (Text) - Content author
* `last_updated` (Date, auto) - Last modification date
### Taxonomy Fields
For organizing content:
* `category` (Select) - Single category
* `tags` (Tags) - Multiple tags
* `topic` (Select) - Topic classification
### Custom Metadata
Any additional data you need:
* `featured` (Boolean) - Whether to feature this page
* `priority` (Number) - Display priority
* `external_url` (Text) - Link to external resource
A "Team Member" page type might have fields for: name, role, bio, email, social links, and profile photo. These are page-level because they describe the person, not individual blocks on the page.
## Available Blocks
### Choosing Which Blocks to Include
Consider what editors need:
* **Too few blocks**: Editors feel constrained, can't create varied content
* **Too many blocks**: Overwhelming, harder to find the right block
* **Just right**: Enough variety for flexibility, but not overwhelming
Start with essential blocks, then add more as needed. You can always edit page types later to add or remove blocks.
**Rule of thumb:** 5-10 blocks per page type is usually optimal. More than 15 blocks can be overwhelming for editors.
### Block Organization
Group related blocks in your component library to make selection easier:
* **Content blocks**: Text, Image, Video, Code
* **Layout blocks**: Card Grid, Columns, Container
* **Interactive blocks**: Form, Button, CTA
* **Specialized blocks**: Testimonials, Pricing, FAQ
## Required Blocks
Required blocks must appear on every page of a given type. Use them sparingly:
**Good use cases:**
* Header block that must appear at the top
* Footer block that must appear at the bottom
* Navigation block for specific page types
**Avoid requiring:**
* Content blocks (editors need flexibility)
* Multiple blocks (too restrictive)
Required blocks can't be removed by editors. Only mark blocks as required if they're truly essential for every page of that type.
**Best practice:** Start with no required blocks. Add them only if you discover editors consistently need the same block on every page. Flexibility is usually better than constraints.
## URL Structure
Page types influence URL structure:
### Default Behavior
By default, pages follow this pattern:
```
/[page-type-slug]/[page-slug]
```
A page of type "blog-post" with slug "my-first-post" would be accessible at `/blog-post/my-first-post`.
### Custom URL Patterns
You can configure custom URL patterns in page type settings:
* **Prefix**: Add a prefix to all pages of this type
* **Suffix**: Add a suffix to all pages
* **Custom pattern**: Define your own URL structure
URL patterns are configured at the site level, but page types can have their own defaults.
## Page Type Settings
### Publishing Workflow
Configure how pages of this type are published:
* **Draft mode**: Pages start as drafts
* **Auto-publish**: Pages publish immediately when saved
* **Scheduled publishing**: Allow scheduling future publication
### Editor Permissions
Control what editors can do:
* **Can duplicate pages**: Allow editors to clone pages
* **Can delete pages**: Allow editors to remove pages
* **Can change page type**: Allow switching between types
Be careful with editor permissions. Restricting certain actions helps maintain site structure and prevents accidental deletions.
## Best Practices
### 1. Start Simple
Begin with essential blocks and fields. You can always add more later:
```yaml theme={null}
Simple Blog Post:
Blocks: Text, Image
Fields: title, author, publish_date
```
### 2. Use Descriptive Names
Clear names help editors understand what each page type is for:
* ✅ "Blog Post"
* ✅ "Default"
* ✅ "Team Member Profile"
* ❌ "Type 1"
* ❌ "Generic"
### 3. Document Your Page Types
Add descriptions that explain when to use each type:
**Blog Post**: For articles, news, and blog content. Includes author, publish date, and category fields.
### 4. Plan for Growth
Consider how page types might evolve:
* Will you need more fields later?
* Will editors need additional blocks?
* Are there related page types you'll create?
### 5. Test with Editors
Get feedback from content editors:
* Are the available blocks sufficient?
* Are page fields clear and useful?
* Is anything missing or confusing?
## Advanced Patterns
### Conditional Blocks
Some page types benefit from conditional block availability:
A "Product" page type might show different blocks based on product category. While Primo doesn't support conditional blocks directly, you can create separate page types for different product categories.
### Nested Page Types
Create page types that reference other page types:
A "Category" page type might include a block that displays pages of type "Blog Post" filtered by category.
## Editing Page Types
You can edit page types after creation:
Go to Page Types → Select the page type you want to edit.
Update:
* Available blocks (add or remove)
* Required blocks
* Page fields
* Settings
Click "Save" to update the page type.
Changes to page types affect all existing pages of that type. Removing a required block or field may cause issues with existing pages.
## Next Steps
Learn about all available field types for pages and blocks.
Configure page types to make editing easy for non-technical users.
See how editors use page types to create content.
See page types in action with a complete example.
The best page types balance structure with flexibility. Give editors enough options to be creative, but enough constraints to maintain design consistency.
# Understanding Slots
Source: https://docs.primo.build/building-sites/understanding-slots
Learn how slots organize blocks into header, body, and footer areas.
Slots are areas where blocks can be placed within a page type. They help you create consistent layouts while giving editors control over content.
## The Three Slots
Every page type has three slots:
### Header Slot
The **header slot** appears at the top of every page of this type.
**Use for:**
* Site navigation
* Logo
* Top banner
* Search bar
**Characteristics:**
* Defined at the page type level
* Same content appears on all pages
* Editors cannot modify on individual pages
* Perfect for site-wide UI elements
**Example:**
```
┌─────────────────────────────┐
│ Header Slot │ ← Navigation, Logo
│ (Same on all pages) │
├─────────────────────────────┤
│ │
│ Body Slot │
│ (Editable per page) │
│ │
├─────────────────────────────┤
│ Footer Slot │ ← Copyright, Links
│ (Same on all pages) │
└─────────────────────────────┘
```
### Body Slot
The **body slot** is the main content area where editors have full control.
**Use for:**
* Main page content
* Hero sections
* Text blocks
* Image galleries
* Call-to-action sections
**Characteristics:**
* Editors can add, remove, and reorder blocks
* Different content on each page
* Most flexible slot
* Blocks stack vertically
**Editor Capabilities:**
* ✅ Add blocks (from available blocks)
* ✅ Remove blocks
* ✅ Reorder blocks (drag and drop)
* ✅ Edit block content
* ❌ Cannot modify header/footer
* ❌ Cannot add blocks not in "available blocks"
### Footer Slot
The **footer slot** appears at the bottom of every page of this type.
**Use for:**
* Site footer
* Copyright notice
* Footer navigation
* Social media links
* Newsletter signup
**Characteristics:**
* Defined at the page type level
* Same content appears on all pages
* Editors cannot modify on individual pages
* Perfect for consistent site-wide elements
## Static vs Dynamic Body Slots
The body slot behavior changes based on whether the page type has available blocks:
### Dynamic Body (Default)
**When:** Page type has "Available Blocks" configured
**Behavior:**
* Editors can add blocks from the list
* Editors can remove blocks
* Editors can reorder blocks
* Each page can have different blocks
**Example:**
```
Page Type: Blog Post
Available Blocks: [Hero, Text, Image, Code Block, CTA]
Page 1 (Blog Post):
- Hero
- Text
- Image
- CTA
Page 2 (Blog Post):
- Text
- Code Block
- Text
- Image
```
### Static Body
**When:** Page type has NO available blocks configured
**Behavior:**
* Body blocks are pre-defined in the page type
* Same structure on all pages
* Content varies per page, structure stays the same
* Editors can only edit block content, not add/remove
**Example:**
```
Page Type: Team Member
Available Blocks: [] (empty)
Body Slot Blocks: [Profile Header, Bio Text, Contact Info]
All team member pages have:
- Profile Header (content varies)
- Bio Text (content varies)
- Contact Info (content varies)
```
Use static body slots when you want a consistent structure across all pages of a type. Great for profiles, product pages, or any templated content.
## Configuring Slots in Page Types
Navigate to Pages → Page Types → Create/Edit
Under "Header Slot":
* Click "Add Block"
* Select blocks to appear at the top of every page
* Order matters - blocks appear top to bottom
Under "Available Blocks":
* Add blocks editors can use in the body slot
* Leave empty for static body slot
If Available Blocks is empty:
* Configure default blocks for the body
* These appear on all pages
Under "Footer Slot":
* Click "Add Block"
* Select blocks to appear at the bottom of every page
## Common Patterns
### Blog Post Template
```
Header Slot: [Site Navigation]
Available Blocks: [Hero, Text Block, Image, Code Block, Quote, CTA]
Footer Slot: [Site Footer]
```
**Result:** Consistent header/footer, flexible body content
### Default Page Template
```
Header Slot: [Site Navigation]
Available Blocks: [Hero, Text, Image, Card Grid, CTA]
Footer Slot: [Site Footer]
```
**Result:** Flexible layout for general content pages
### Product Page Template
```
Header Slot: [Site Navigation]
Available Blocks: [] (empty)
Body Slot: [Product Hero, Specs, Gallery, Reviews, Related Products]
Footer Slot: [Site Footer]
```
**Result:** Consistent product page structure
### Documentation Page
```
Header Slot: [Docs Navigation, Search]
Available Blocks: [Text, Code Block, Image, Accordion, Tabs, Alert]
Footer Slot: [Docs Footer, Previous/Next]
```
**Result:** Documentation-specific UI and blocks
## Best Practices
Heavy headers/footers slow down editing and can overwhelm the page. Include only what's truly site-wide.
Instead of adding 20 individual blocks to available blocks, consider creating composite blocks that include common combinations.
Product pages, team profiles, and case studies benefit from consistent structure. Use static body slots to maintain consistency.
Always preview pages to ensure header/footer blocks work well with body content. Check responsive behavior.
Header blocks should handle mobile viewports. Test navigation collapse/hamburger menus.
## Semantic HTML
Primo wraps each slot in semantic HTML tags:
```html theme={null}
```
This provides:
* ✅ Better accessibility (screen readers)
* ✅ Proper document structure
* ✅ SEO benefits
* ✅ Valid HTML5
The semantic tags are added automatically. You don't need to include them in your block code.
## Next Steps
Learn how to create and configure page types
Add editable fields to your blocks
# Using AI to Build Components
Source: https://docs.primo.build/building-sites/using-ai-to-build-components
Generate components with AI assistance.
Use AI to quickly generate block code, then paste into Primo and configure fields manually.
A native AI integration is coming soon! Primo's philosophy is to be a creative tool where AI handles the boring parts and gets out of the way most of the time. The upcoming integration will help with repetitive tasks without being heavy-handed.
## Prompt Template
```
Generate a Svelte 5 component for a [describe your block].
Requirements:
- Use Svelte 5 syntax (no export let - fields are globally available)
- Use snake_case for all field names
- Include null checks (e.g., {#if image?.url})
- Make it responsive
- Don't include imports unless needed
Fields:
- [list fields with types, e.g., "headline (text)", "image (image with url and alt)"]
Style: [describe design]
```
## Workflow
Use the prompt with Claude, ChatGPT, etc.
Copy generated code into a new block's code editor.
Switch to Fields tab and manually add each field with proper types.
Preview and refine as needed.
## Example
**Prompt:**
```
Generate a Svelte 5 component for a feature card.
Requirements:
- Use Svelte 5 syntax (no export let - fields are globally available)
- Use snake_case for all field names
- Include null checks
- Responsive
Fields:
- icon (image with url and alt)
- headline (text)
- description (text)
- cta_link (link with url and label)
Style: Card with shadow, rounded corners, hover effect
```
**Result:**
```svelte theme={null}
```
Then add fields in Primo: icon (Image), headline (Text), description (Text), cta\_link (Link)
## Common Issues
**AI uses `export let`:**
Remove it. Fields are globally available in Primo.
```svelte theme={null}
{headline}
```
**Missing null checks:**
Add them, especially for images and links.
```svelte theme={null}
{#if image?.url}
{/if}
```
**Unnecessary imports:**
Only import if you actually need Svelte lifecycle hooks or advanced features.
## Next Steps
Learn to write components from scratch.
Master field configuration.
# Using the Component Library
Source: https://docs.primo.build/building-sites/using-the-component-library
Reuse blocks across sites and browse the marketplace for starter templates.
The Component Library is your personal collection of reusable blocks. Once you create a block, it's saved to your library and available across all your sites. This guide covers managing your library, reusing blocks, and exploring the marketplace.
## What is the Component Library?
The Component Library stores all the blocks you create:
* **Blocks you build** - Your custom components
* **Available everywhere** - Use blocks in any site
* **Version controlled** - Track changes and updates
* **Shareable** - Export and import blocks
Blocks in your library are independent of sites. Create a block once, use it in multiple sites, update it everywhere at once.
## Managing Your Library
### Viewing Your Blocks
Click "Component Library" in the sidebar of any site.
You'll see all blocks you've created, organized by:
* **Recently used** - Blocks you've used recently
* **All blocks** - Complete list of your blocks
* **Categories** - If you've organized them
Use the search bar to quickly find blocks by name or description.
### Creating New Blocks
Click "Create Block" in the Component Library.
Use the in-browser editor to write your component code.
The editor provides syntax highlighting, auto-completion, and instant preview.
Attach content fields to make the block editable.
See the [Working with Fields](/building-sites/working-with-fields) guide for details.
Click "Save Block" and give it a name. The block is now in your library.
### Editing Existing Blocks
Browse or search for the block you want to edit.
Click on the block to open it in the editor.
Update the component code, fields, or settings as needed.
Click "Save" to update the block.
Changes to blocks affect all sites using that block. Test changes carefully.
### Organizing Blocks
Keep your library organized:
Name blocks clearly:
* ✅ "Hero Block"
* ✅ "Text with Image"
* ✅ "Card Grid"
* ❌ "Block 1"
* ❌ "Test"
Include descriptions that explain what each block does:
**Name**: "Hero Block"
**Description**: "Full-width hero section with headline, subheadline, CTA button, and optional background image. Perfect for landing pages."
Group related blocks:
* **Content**: Text, Image, Video
* **Layout**: Grid, Columns, Container
* **Interactive**: Form, Button, CTA
* **Specialized**: Testimonials, Pricing, FAQ
## Reusing Blocks Across Sites
### Adding Blocks to a Site
Once a block is in your library, use it in any site:
Go to Page Types and edit the page type where you want to use the block.
In the "Available Blocks" section, click "Add Block" and select from your library.
Editors can now use these blocks when creating pages of this type.
Blocks in your library are available to all your sites. Create once, use everywhere.
### Updating Blocks Globally
When you update a block in your library:
Make your changes in the Component Library.
Click "Save" to update the block.
All sites using that block get the update automatically.
Test updates carefully. Breaking changes affect all sites using the block.
### Block Versions
Primo tracks block versions:
* **View history** - See previous versions of a block
* **Revert changes** - Go back to an earlier version if needed
* **Compare versions** - See what changed between versions
Use version history to safely experiment with block changes. You can always revert if something breaks.
## Building a Server Library
### Start with Essentials
Build a core set of blocks:
1. **Hero** - Landing page hero sections
2. **Text** - Rich text content
3. **Image** - Image with optional caption
4. **Card Grid** - Flexible grid layouts
5. **Call-to-Action** - CTA sections
6. **Footer** - Site footer
### Expand Gradually
Add blocks as you need them:
* **Content blocks** - Text, Image, Video, Code
* **Layout blocks** - Grid, Columns, Container, Spacer
* **Interactive blocks** - Form, Button, CTA, Modal
* **Specialized blocks** - Testimonials, Pricing Table, FAQ, Team
### Refine and Improve
Continuously improve your blocks:
* **Add features** - New fields, variants, options
* **Improve styling** - Better responsive design, animations
* **Fix bugs** - Address issues as you find them
* **Optimize** - Better performance, smaller code
Your library grows with every project. Blocks you build for one site become reusable assets for future projects.
## Block Patterns
### Simple Content Blocks
Basic blocks for common content:
**Text Block:**
* Rich text content
* Alignment options
* Background color
**Image Block:**
* Image upload
* Alt text
* Caption
* Full-width option
**Video Block:**
* Video upload or embed
* Autoplay options
* Caption
### Layout Blocks
Blocks for organizing content:
**Card Grid:**
* Repeater for cards
* Configurable columns
* Responsive layout
**Columns:**
* 2, 3, or 4 column layouts
* Equal or custom widths
* Responsive stacking
**Container:**
* Max-width container
* Padding options
* Background styling
### Interactive Blocks
Blocks with user interaction:
**Call-to-Action:**
* Headline and description
* CTA button
* Background styling
**Form:**
* Form fields (repeater)
* Submit action
* Success message
**Modal:**
* Trigger button
* Modal content
* Close options
## Sharing Blocks
### Exporting Blocks
Export blocks to share or backup:
Open the block you want to export.
Click "Export" to download the block as a file.
Share the file with others or keep it as a backup.
### Importing Blocks
Import blocks from others:
Click "Import Block" in the Component Library.
Choose the block file to import.
Review the block code and fields, then import.
Only import blocks from trusted sources. Review the code before importing to ensure it's safe.
## Marketplace (Future Feature)
Primo will include a marketplace for:
* **Starter sites** - Complete site templates
* **Block packs** - Collections of related blocks
* **Individual blocks** - Single blocks from the community
* **Themes** - Design system templates
The marketplace will let you discover blocks built by the community and share your own creations.
## Best Practices
### 1. Build Reusable Blocks
Design blocks to work in multiple contexts by choosing field names carefully:
```svelte theme={null}
{headline}
{homepage_headline}
```
Use generic field names like `headline`, `content`, `image` instead of context-specific names like `homepage_headline` or `about_page_content`.
### 2. Document Your Blocks
Add clear descriptions:
**Hero Block**
A flexible hero section with headline, subheadline, CTA, and optional background image. Supports multiple variants (default, large, minimal) and works on any page type.
### 3. Version Carefully
Test updates before saving:
1. Make changes in the editor
2. Test with preview
3. Check on multiple sites if possible
4. Save when confident
### 4. Organize Early
Start organizing from the beginning:
* Use clear naming conventions
* Add descriptions immediately
* Group related blocks
* Keep a consistent structure
### 5. Reuse and Refine
Don't recreate blocks:
* Check your library first
* Adapt existing blocks when possible
* Improve blocks over time
* Remove unused blocks
## Common Workflows
### Creating a New Site
Review existing blocks before creating new ones.
Add existing blocks to your new site's page types.
Build new blocks only for site-specific needs.
### Updating a Block
Find the block in your library.
Check which sites use this block.
Make changes and test thoroughly.
Save changes and verify they work on all sites.
## Next Steps
Learn how to build effective reusable components.
Add fields to make blocks editable.
See how to build a library of blocks for a complete site.
Use library blocks in your page types.
Your Component Library is an investment. Every block you create saves time on future projects. Build thoughtfully, and your library becomes a valuable asset.
# Working with Fields
Source: https://docs.primo.build/building-sites/working-with-fields
Add content fields to blocks and pages to make them editable.
Fields are what transform your components into editable blocks. You write the component code, then attach fields to make specific parts editable by content editors. This guide shows you how to add and configure fields effectively.
## Understanding Fields
Fields bridge the gap between code and content:
* **Component code** defines structure and styling
* **Fields** define what content editors can edit
* **Together** they create blocks that are both powerful and editable
Fields can be added to both blocks (for component-level editing) and page types (for page-level metadata). The process is the same for both.
## Adding Fields to a Block
In the Component Library, create a new block or open an existing one.
Build your component. Fields are globally available—no need to declare them:
```svelte theme={null}
{headline}
{description}
{#if image?.url}
{/if}
```
Fields are automatically available in your component code. Just reference them directly like `{headline}` or `{image.url}`.
Click "Add Field" or the fields panel in the component editor.
Configure the field:
* **Field name**: Must match your prop name (e.g., `headline`)
* **Field type**: Choose the appropriate type (e.g., Text, Rich Text, Image)
* **Label**: Display name for editors (e.g., "Headline")
* **Default value**: Optional pre-filled value
The field name must exactly match the prop name in your component code.
Repeat for each prop you want to make editable:
* `description` → Rich Text field
* `image` → Image field
You don't need to create fields for every prop. Only create fields for content you want editors to control.
Click "Save Block" to save your component with its fields.
## Field Naming
When you create a field, the field name determines how you reference it in your component:
```svelte theme={null}
```
**Field configuration:**
* **Field name**: `cta_text` (how you reference it in code)
* **Label**: "CTA Text" (what editors see in the UI)
Field names are case-sensitive. `cta_text` and `ctaText` are different variables in your component.
## Common Field Patterns
### Hero Block
A typical hero block might use these fields in the template:
```svelte theme={null}
{headline}
{subheadline}
{#if cta_link?.url}
{cta_link.label}
{/if}
```
**Fields to create:**
* `headline` (Text) - Main headline
* `subheadline` (Text) - Supporting text
* `cta_link` (Link) - Button link with label
* `background_image` (Image, optional) - Background image
### Text Block
A simple text block:
```svelte theme={null}
{@html content}
```
**Fields to create:**
* `content` (Rich Text) - The main content
* `alignment` (Select) - Options: "left", "center", "right"
### Image Block
An image with caption:
```svelte theme={null}
{#if image?.url}
{/if}
{#if caption}
{caption}
{/if}
```
**Fields to create:**
* `image` (Image) - The image (includes URL and alt text)
* `caption` (Text, optional) - Image caption
### Card Grid Block
A grid of cards using a repeater:
```svelte theme={null}
```
**Fields to create:**
* `cards` (Repeater) with nested fields:
* `title` (Text)
* `description` (Text)
* `image` (Image, optional)
* `link` (Link, optional)
* `columns` (Number) - Number of columns
## Template Syntax Guide
Primo blocks use Svelte 5 syntax. Here's the essential syntax you'll use most often when working with fields.
### Basic Interpolation
Display field values in your template:
```svelte theme={null}
{headline}
{description}
```
### Fallback Values
Provide default values when fields are empty:
```svelte theme={null}
{headline || "Welcome"}
{description ?? "No description available"}
```
### Conditionals
Show or hide elements based on field values:
```svelte theme={null}
{#if image?.url}
{/if}
{#if featured}
Featured
{:else}
Standard
{/if}
{#if variant === "primary"}
{:else if variant === "secondary"}
{:else}
{/if}
```
### Loops (Repeater Fields)
Iterate over repeater field arrays:
```svelte theme={null}
{#each items as item}
{item.title}
{item.description}
{/each}
{#each features as feature, index}
{index + 1}
{feature.title}
{/each}
{#each items as item}
{item.name}
{:else}
No items to display
{/each}
```
### HTML Rendering
Render HTML from rich text or markdown fields:
```svelte theme={null}
{@html content}
{@html markdown_content}
```
Rich text and markdown fields render HTML using `{@html}`. Since content comes from trusted editors (not site visitors), this is safe to use.
### Reactive Statements
Compute derived values from fields:
```svelte theme={null}
Welcome, {fullName}
{#each featuredItems as item}
{item.title}
{/each}
```
### Event Handlers
Handle user interactions:
```svelte theme={null}
```
### Class Directives
Apply classes conditionally:
```svelte theme={null}
```
### Snippets (Reusable Templates)
Create reusable template chunks within a block:
```svelte theme={null}
{#snippet card(title, description)}
{title}
{description}
{/snippet}
{@render card("First Card", "Description here")}
{@render card("Second Card", "Another description")}
{#each items as item}
{@render card(item.title, item.description)}
{/each}
```
### What You Won't Need
These Svelte features are **not relevant** for Primo blocks:
* **Stores** - No need for cross-component state; fields are globally available within each block
* **Context API** - Use relational fields instead (Page Field, Site Field, Page, Page List) to reference data from elsewhere
* **\$bindable** - Only for component authoring, not block templates
* **Custom elements** - Blocks compile to standard components
* **SvelteKit features** - Routing, load functions, etc. (Primo handles this)
* **Component slots** - Blocks don't accept slotted content
### Learn More
For advanced Svelte features and patterns, see the [Svelte 5 documentation](https://svelte.dev/docs/svelte/overview).
## Field Configuration
### Required Fields
Mark fields as required when they're essential:
A Hero block's `headline` field should be required—a hero without a headline doesn't make sense.
**When to require:**
* Essential content (headlines, titles)
* Fields that affect functionality
* Fields needed for proper display
**When not to require:**
* Optional enhancements (images, captions)
* Fields with sensible defaults
* Decorative elements
### Default Values
You can set default values in the field configuration, or handle empty values in your template:
```svelte theme={null}
{headline}
{headline || "Welcome"}
{#if headline}
{headline}
{/if}
```
**Best practices:**
* Set sensible defaults in field configuration
* Handle empty/missing values gracefully
* Make components functional even with minimal content
### Help Text
Add help text to guide editors:
**Field**: `background_image`
**Help text**: "Optional background image. Recommended size: 1920x1080px. Will be used as a fallback if no image is provided."
**Good help text:**
* Explains what the field is for
* Provides guidance on content
* Mentions any requirements (size, format, etc.)
### Placeholders
Use placeholders to show example content:
* **Headline field**: "Enter your headline here"
* **Description field**: "Describe your product or service"
* **Link field**: "/about" or "[https://example.com](https://example.com)"
Placeholders help editors understand what to enter, especially for fields like URLs or slugs.
## Advanced Field Patterns
### Conditional Fields
Some fields should only appear when others have values:
A "Video" block might have:
* `video_type` (Select): "Upload" or "Embed"
* `video_file` (Video): Show only when `video_type` is "Upload"
* `video_url` (Text): Show only when `video_type` is "Embed"
**Setting up conditionals:**
1. Create the control field (e.g., `video_type`)
2. Configure dependent fields to show/hide based on the control field
3. Test with different values
### Repeater Fields
Repeaters let editors add multiple items:
Add a Repeater field (e.g., `features`).
Inside the repeater, add fields for each item:
* `title` (Text)
* `description` (Text)
* `icon` (Image, optional)
Reference the repeater field as an array:
```svelte theme={null}
{#each features as feature}
{feature.title}
{feature.description}
{/each}
```
### Group Fields
Groups organize related fields:
Add a Group field (e.g., `author`).
Inside the group, add related fields:
* `name` (Text)
* `email` (Email)
* `bio` (Textarea)
Reference the group field as an object:
```svelte theme={null}
```
## Field Validation
### Text Validation
Add validation rules to text fields:
* **Min length**: Minimum character count
* **Max length**: Maximum character count
* **Pattern**: Regex pattern for format validation
An email field might use pattern: `^[^\s@]+@[^\s@]+\.[^\s@]+$`
### Number Validation
For number fields:
* **Min value**: Minimum allowed number
* **Max value**: Maximum allowed number
* **Step**: Increment/decrement step
A "Price" field might have:
* Min: 0
* Max: 10000
* Step: 0.01 (for cents)
### File Validation
For image and file fields:
* **Allowed formats**: File extensions (jpg, png, pdf, etc.)
* **Max file size**: Maximum upload size in MB
* **Dimensions**: Min/max width and height (for images)
## Testing Fields
### Test with Empty Values
Always test your components with empty fields:
```svelte theme={null}
{#if headline}
{headline}
{/if}
{#if image?.url}
{/if}
```
### Test with Default Values
Verify defaults work (if you set them in field configuration):
1. Create a page with your block
2. Don't fill in any fields
3. Check that the component displays correctly with default values
### Test Edge Cases
Try:
* Very long text
* Special characters
* Empty arrays (for repeaters)
* Missing nested fields (for groups)
## Best Practices
### 1. Start Simple
Begin with essential fields, add more as needed:
```yaml theme={null}
Simple Hero:
Fields: headline, subheadline, cta_text
Enhanced Hero:
Fields: headline, subheadline, cta_text, background_image, overlay_color
```
### 2. Use Descriptive Labels
Labels are what editors see:
* ✅ "Featured Image"
* ✅ "Publication Date"
* ✅ "Author Name"
* ❌ "Field 1"
* ❌ "Data"
### 3. Provide Helpful Defaults
Set defaults in field configuration to make components work immediately:
* **Number fields**: Set sensible defaults (e.g., `columns` defaults to 3)
* **Boolean fields**: Set appropriate defaults (e.g., `show_title` defaults to true)
* **Select fields**: Pre-select the most common option (e.g., `alignment` defaults to "left")
### 4. Group Related Fields
Use Groups or organize fields logically:
* **Content fields**: headline, description, content
* **Media fields**: image, video, background
* **Settings fields**: alignment, columns, variant
### 5. Validate Input
Add validation to prevent errors:
* Required fields for essential content
* Format validation for emails, URLs
* Size limits for files and text
## Common Mistakes
### Mismatched Names
Field names must match how you reference them in code:
```svelte theme={null}
```
### Not Handling Empty Values
Always handle missing or empty field values:
```svelte theme={null}
{headline}
{#if headline}
{headline}
{/if}
{headline || "Welcome"}
```
### Over-Complicating
Start simple, add complexity later:
```yaml theme={null}
# Start here
Fields: title, description
# Add later if needed
Fields: title, description, image, link, category, tags, featured
```
## Next Steps
Complete reference of all available field types.
Learn how to write components that work well with fields.
See fields in action with a complete site example.
Add page-level fields to page types.
The best fields are the ones editors actually use. Start with essential fields, then add more based on feedback from content editors.
# Writing Components
Source: https://docs.primo.build/building-sites/writing-components
Build reusable components and turn them into content blocks.
Components are the foundation of Primo. You write components using Svelte, attach content fields to them, and they become blocks that editors can use to build pages. This guide covers everything you need to know about writing effective components.
## Components vs Blocks
Understanding the distinction helps you build better:
**Components** are pure Svelte code:
* Reusable UI elements (Button, Card, Hero)
* Accept props and handle logic
* No content fields attached
* Can be used anywhere in your Svelte code
**Blocks** are components with content fields:
* Same Svelte component code
* Plus editable content fields (text, images, links)
* Used specifically in page types and pages
* Editors can modify content, not code
When you create a block in Primo, you're writing a Svelte component and then attaching fields to it. The component code stays the same; fields make it editable.
## Component Structure
Every Primo component follows this structure:
```svelte theme={null}
```
## Writing Your First Component
Let's build a simple Button component step by step:
Think about what data your component needs. For a button block:
* `text`: The button text
* `href`: The link destination
* `variant`: The button style (primary, secondary, outline)
Fields will be automatically available in your component code.
Plan your fields first, then reference them in your template. You can set default values in the field configuration.
Write the HTML structure:
```svelte theme={null}
{text}
```
Add scoped styles:
```svelte theme={null}
```
## Best Practices
### 1. Handle Empty Values
Components should work gracefully even with empty fields:
```svelte theme={null}
{headline || "Welcome"}
{subheadline || "Default description"}
{#if headline}
{headline}
{/if}
{#if subheadline}
{subheadline}
{/if}
```
### 2. Use Conditional Rendering
Only show elements when content exists:
```svelte theme={null}
{#if image_url}
{#if caption}
{caption}
{/if}
{/if}
```
### 3. Handle Arrays Safely
When working with repeater fields, always check if the array has items:
```svelte theme={null}
{#if items && items.length > 0}
{#each items as item}
{item.name}
{/each}
{:else}
No items to display
{/if}
```
### 4. Use Scoped Styles
Primo components use scoped styles by default. Use `:global()` when you need to style child elements:
```svelte theme={null}
```
### 5. Make Components Responsive
Always consider mobile devices:
```svelte theme={null}
```
### 6. Create Component Variations
Instead of creating many similar blocks, use fields to create variations of a single component:
**Layout variations:**
```svelte theme={null}
```
**Size variations:**
```svelte theme={null}
```
One flexible block with variant options is easier to maintain than multiple similar blocks. Editors can adjust layouts without developers creating new components.
## Working with Rich Content
### Rich Text Fields
When using rich text fields, use `{@html}` to render HTML:
```svelte theme={null}
{@html content}
```
Only use `{@html}` with trusted content. Primo sanitizes rich text fields, but be cautious with user-generated content.
### Markdown Content
If you're using markdown fields, Primo processes them automatically. But you can also use a markdown processor for additional control:
```svelte theme={null}
{@html html_content}
```
## Advanced Patterns
### Reactive Statements
Use reactive statements (`$:`) for computed values based on fields:
```svelte theme={null}
{#each displayed_items as item}
{item.name}
{/each}
{#if has_more}
And {items.length - max_items} more...
{/if}
```
### Event Handling
Handle user interactions:
```svelte theme={null}
{cta_text}
```
### Conditional Classes
Use dynamic classes for variants:
```svelte theme={null}
```
In Primo's component editor, you write single-file components. For composition, you can create separate blocks and reference them, or write everything in one component file.
## Performance Considerations
### Lazy Loading Images
For image-heavy components:
```svelte theme={null}
```
### Optimize Re-renders
Use reactive statements efficiently:
```svelte theme={null}
{#each sorted_items as item}
{item.name}
{/each}
```
## Testing Your Components
### Use the Preview
Primo's in-browser editor provides instant preview:
1. Write your component code
2. Add test content in fields
3. See changes update in real-time
Test with empty fields, long text, special characters, and edge cases to ensure your component is robust.
### Test Different Screen Sizes
Use browser dev tools in the preview to test:
* Mobile (320px - 768px)
* Tablet (768px - 1024px)
* Desktop (1024px+)
## Common Patterns
### Card Component
```svelte theme={null}
{#if image_url}
{/if}
```
## Next Steps
Learn how to attach content fields to your components.
Organize and reuse components across sites.
Complete reference of all available field types.
See components in action with a complete site example.
The best components are simple, focused, and reusable. Start with basic functionality, then add complexity only when needed.
# Building from Scratch
Source: https://docs.primo.build/building-sites/your-first-site
Build a complete Primo site from the ground up and understand the full workflow.
This guide walks you through building a complete Primo site from scratch. You'll create multiple page types, build several blocks, and set up a site that's ready for content editors to use.
If you're looking for a faster introduction, check out the [Quickstart](/getting-started/quickstart) guide first.
## Overview
By the end of this guide, you'll have:
* A site with three page types (Homepage, Blog Post, About)
* Five reusable blocks (Hero, Text, Image, Card Grid, Footer)
* A working navigation system
* Content ready for editors to manage
## Prerequisites
* Primo installed and running ([Installation guide](/getting-started/installation))
* Basic familiarity with HTML, CSS, and JavaScript
* Understanding of [core concepts](/getting-started/core-concepts)
## Step 1: Create Your Site
When you first access Primo, create a new site. Give it a descriptive name like "My First Site".
The site name is for internal organization. You can change it later in settings.
Navigate to Site Settings and configure:
* **Site Name**: The name that appears in navigation and metadata
* **Domain**: Your site's domain (or subdomain for Cloud users)
* **Default Language**: Set to your primary language
You can always update these settings later. For now, focus on getting the structure in place.
## Step 2: Build Your Component Library
Before creating page types, let's build the blocks that will power your pages. We'll create five essential blocks.
### Block 1: Hero
The Hero block will be the first thing visitors see on your homepage.
Go to Component Library → Create Block. Name it "Hero".
```svelte theme={null}
```
Add these content fields:
* `headline` (Text) - Main headline
* `subheadline` (Text) - Supporting text
* `cta_text` (Text) - Button text
* `cta_link` (Text) - Button destination URL
* `background_image` (Image, optional) - Background image URL
The component is already set up to use these fields. Just add them in the field editor.
Your Hero block is complete and saved to your component library. It's ready to use on any page type.
### Block 2: Text
A simple text block for content sections.
Create a new block called "Text".
```svelte theme={null}
{@html content}
```
Add these fields:
* `content` (Rich Text) - The main content
* `alignment` (Select) - Options: "left", "center", "right"
Your Text block is complete. It's perfect for articles, descriptions, and long-form content.
### Block 3: Image
A block for displaying images with optional captions.
Create a new block called "Image".
```svelte theme={null}
{#if image?.url}
{/if}
{#if caption}
{caption}
{/if}
```
Add these fields:
* `image` (Image) - The image to display (includes URL and alt text)
* `alt_text` (Text, optional) - Override alt text if needed
* `caption` (Text, optional) - Image caption
* `full_width` (Switch/Toggle) - Whether to use full width
Your Image block is complete. It handles images with captions and full-width options.
### Block 4: Card Grid
A flexible grid for displaying cards (useful for features, team members, etc.).
Create a new block called "Card Grid".
```svelte theme={null}
{#each cards as card}
{/each}
```
Add these fields:
* `cards` (Repeater) - Array of card objects with:
* `title` (Text)
* `description` (Text, optional)
* `image` (Image, optional)
* `link` (Text, optional)
* `columns` (Number) - Number of columns (default: 3)
Your Card Grid block is complete. It's perfect for displaying features, team members, or any repeating content in a grid layout.
### Block 5: Footer
A site-wide footer block.
Create a new block called "Footer".
```svelte theme={null}
```
Add these fields:
* `copyright_text` (Text) - Copyright notice
* `links` (Repeater) - Footer links with:
* `label` (Text)
* `url` (Text)
Your Footer block is complete. It's ready to be used as a site-wide footer.
You now have five reusable blocks in your component library. These can be used across all your sites!
## Step 3: Define Page Types
Now let's create three page types that use these blocks.
### Page Type 1: Homepage
Go to Page Types → Create Page Type. Name it "Homepage" with slug "homepage".
Add these blocks as available for the body slot:
* Hero
* Text
* Image
* Card Grid
These blocks will be available for editors to add in the body slot. We'll add the Footer to the footer slot later.
Add these page-level fields:
* `meta_title` (Text) - SEO title
* `meta_description` (Text) - SEO description
### Page Type 2: Blog Post
Create a new page type called "Blog Post" with slug "blog".
Add these blocks:
* Text
* Image
Blog posts typically have simpler layouts. You can always add more blocks later.
Add these fields:
* `title` (Text, required) - Post title
* `author` (Text) - Author name
* `publish_date` (Date) - Publication date
* `featured_image` (Image) - Featured image
* `excerpt` (Text) - Short description
* `meta_title` (Text) - SEO title
* `meta_description` (Text) - SEO description
The Date field outputs YYYY-MM-DD format. Use `new Date(publish_date).toLocaleDateString()` in your components to format it for display.
### Page Type 3: About
Create a new page type called "About" with slug "about".
Add these blocks:
* Hero
* Text
* Image
* Card Grid
Add these fields:
* `meta_title` (Text) - SEO title
* `meta_description` (Text) - SEO description
## Step 4: Create Pages
Now let's create actual pages using these page types.
### Create the Homepage
Go to Pages → Create Page. Select "Homepage" as the page type.
Add blocks in this order:
1. **Hero** - Add compelling headline and CTA
2. **Text** - Add an introduction section
3. **Card Grid** - Add 3 feature cards
4. **Text** - Add a closing section
You can reorder blocks by dragging them in the editor.
Edit each block with your content:
* **Hero**: Welcome message and primary CTA
* **Text**: Introduction to your site
* **Card Grid**: Three feature highlights
* **Text**: Call to action or closing message
Fill in the page fields:
* Meta title: "Home - My Site"
* Meta description: "Welcome to my site built with Primo"
### Create a Blog Post
Create a new page with "Blog Post" page type.
Set:
* Title: "Getting Started with Primo"
* Author: Your name
* Publish date: Select a date from the date picker
* Excerpt: A short description
Add:
1. **Text** block with your blog post content
2. **Image** block for illustrations
You can add multiple Text and Image blocks to create rich blog post layouts.
### Create the About Page
Create a new page with "About" page type.
Add:
1. **Hero** - "About Us" headline
2. **Text** - Your story
3. **Card Grid** - Team members or values
4. **Image** - Team photo or office image
## Step 5: Add Footer to Page Types
Add the Footer block to each page type so it appears on every page:
Open the Homepage page type editor and add the Footer block to the **Footer slot**.
Blocks in the footer slot appear on every page of this type and can't be removed by editors.
Repeat for Blog Post and About page types. Add the Footer block to each footer slot.
You can configure the Footer block content once, and it will be consistent across all page types that use it.
Edit the Footer block to add:
* Copyright text
* Footer links (Privacy Policy, Terms, Contact, etc.)
## Step 6: Publish Your Site
Use the preview feature to check each page before publishing.
Publish your Homepage, About page, and Blog Post.
Your site is now live! Visit your domain to see it in action.
## What You've Built
You now have:
* ✅ A complete site structure with three page types
* ✅ Five reusable blocks in your component library
* ✅ Multiple published pages with real content
* ✅ Navigation and footer configured
* ✅ A site ready for content editors
## Next Steps
Learn advanced techniques for building reusable components.
Explore all field types and how to use them effectively.
Master page type configuration for complex content structures.
Learn how content editors use Primo to manage pages.
All the blocks you created are saved in your component library. You can reuse them in future sites, saving time on every new project.
# Deploy with the CLI
Source: https://docs.primo.build/cli/deploy
Ship a workspace as an editable CMS (primo deploy) or static site (primo build).
The CLI offers two deployment paths depending on whether you want the deployed site to be editable:
| You want… | Use |
| --------------------------------------- | -------------- |
| A CMS UI collaborators can edit through | `primo deploy` |
| A static site on any static host | `primo build` |
## `primo deploy` — Editable CMS
Deploy your entire workspace (every site under `sites/`, plus `library/` and `server.yaml`) as one editable Primo instance:
```bash theme={null}
primo deploy # Interactive provider selection
primo deploy -p railway # Deploy to Railway
primo deploy -p fly # Deploy to Fly.io
primo deploy --dry-run # Show what would be deployed
```
**Run from the workspace root** (the directory containing `server.yaml`).
### Railway
Railway is the easiest path — one command, automatic SSL, persistent volume included.
**Prerequisites:**
* [Railway CLI](https://docs.railway.com/guides/cli) installed and logged in (`railway login`)
* A Railway account (\$5/month minimum)
```bash theme={null}
primo deploy -p railway
```
The CLI provisions a Railway project, attaches a persistent volume for `/app/pb_data`, and prints the deployed URL.
### Fly.io
Fly.io has a generous free tier and global edge regions.
**Prerequisites:**
* [Fly CLI](https://fly.io/docs/flyctl/install/) installed (`fly auth login`)
* A Fly.io account
```bash theme={null}
primo deploy -p fly
```
### Updating a Deployed Workspace
Once deployed, push subsequent changes with `primo push`:
```bash theme={null}
primo push https://my-app.up.railway.app
```
See [Push and Pull](/cli/sync) for the full sync workflow.
## `primo build` — Static Site
Build a single site as static HTML/CSS/JS for any static host:
```bash theme={null}
primo build # Output to ./dist
primo build -o ./public # Custom output directory
```
The output is a self-contained folder you can deploy with any static-host CLI:
```bash theme={null}
# Netlify
npx netlify deploy --prod --dir=dist
# Vercel
npx vercel dist
# Cloudflare Pages
npx wrangler pages deploy dist
# Or push to a repo connected to GitHub Pages, Netlify, etc.
```
Static builds have no CMS — collaborators can't edit content through a UI. Use `primo deploy` if you need editor access.
## Choosing a Host
* One-command deploy from the CLI
* Automatic SSL and persistent storage
* From \$5/month
* Best for: production sites with collaborators who edit content
* Free tier with 3GB persistent volume
* Global edge deployment
* Best for: prototypes, low-traffic sites, regional latency requirements
* Full control, lowest cost at scale
* Manual setup via Docker
* See the [Deployment Guide](/reference/deployment) for instructions
* Best for: teams comfortable with Linux and Docker
* Free tiers, instant deploys, global CDN
* **No CMS** — use `primo build` and deploy the static output
* Best for: marketing sites where content is edited locally and shipped via Git
## Backups
Hosted Primo uses PocketBase's built-in backup system. Configure scheduled backups from `https://your-server.com/_/` under **Settings → Backups**. See the [Deployment Guide](/reference/deployment#database-backups) for details.
For Git-based backups, `primo pull` produces a full snapshot:
```bash theme={null}
primo pull https://my-app.up.railway.app -o ./backup-$(date +%Y%m%d)
```
## Next Steps
Sync changes between local and deployed servers
Self-host Primo on a VPS or custom infrastructure
# Local Development
Source: https://docs.primo.build/cli/local-development
Run the Primo CMS locally with primo dev and sync changes between files and the editor.
`primo dev` runs the full Primo CMS on your machine and keeps it in sync with your local files. You write blocks and content in your editor; the CMS picks up the changes. You make edits in the CMS; they're written back to disk.
## Starting the Dev Server
From a workspace root (the directory containing `server.yaml`):
```bash theme={null}
primo dev
```
Each site is served on its own subdomain:
* `http://my-site.localhost:3000` — the published site
* `http://my-site.localhost:3000/admin` — the CMS editor
The dev server listens on all interfaces, so you can hit `http://:3000` from a phone on the same network to test mobile layouts.
### Options
```bash theme={null}
primo dev -p 8080 # Use a custom port
primo dev --author files # Default: files are source of truth
primo dev --author cms # CMS is source of truth
primo dev --author both # Bidirectional (beta)
```
## Author Modes
`--author` controls which side wins when files and the CMS disagree. This is the most important concept in local dev — pick a mode that matches how you're working.
**Files are source of truth.**
* Edits to local files are imported into the CMS on save.
* The CMS UI is **read-only** for content that's tracked in files — you'll see a banner.
* Use this mode when you're building blocks or page types in your editor.
```bash theme={null}
primo dev --author files
```
**CMS is source of truth.**
* Edits in the CMS are written to local files.
* Edits to local files get reverted on the next CMS write.
* Use this mode when you want to do most of your work in the browser editor and just want files for Git history.
```bash theme={null}
primo dev --author cms
```
**Bidirectional sync.**
* Both sides can write.
* On conflict, CMS edits are often lost in favor of file edits — treat this as preview-quality.
```bash theme={null}
primo dev --author both
```
## Site Layout
Each site folder follows this layout:
```
sites/my-site/
├── site.yaml # Site name, hostname, group
├── site/
│ ├── fields.yaml # Site-wide fields (logo, nav, etc.)
│ ├── content.yaml # Values for site fields
│ ├── head.svelte # Custom content
│ └── foot.svelte # Custom end-of-body content
├── blocks/
│ └── hero/
│ ├── component.svelte
│ ├── fields.yaml
│ └── content.yaml
├── page-types/
│ └── blog-post/
│ └── config.yaml
├── pages/
│ └── index.yaml
└── uploads/ # Images and files (see below)
```
See [Architecture](/reference/architecture) for the underlying data model.
## Uploads
Files dropped into `sites/my-site/uploads/` are automatically imported into the CMS as `site_uploads` records:
```
sites/my-site/uploads/
├── hero.jpg
└── logo.svg
```
* On import, new files become uploads with stable IDs.
* Hash comparison skips re-upload when content is unchanged.
* A file removed from `uploads/` becomes an orphan upload warning — clean it up in the CMS or remove the record from the file system to delete it.
This means you can drop an image into `uploads/` and reference it from an image field without ever opening the CMS.
## site.yaml and Hostname
`site.yaml` holds the site name, hostname, and group. These round-trip between the CMS and files — editing the name in the dashboard updates `site.yaml`, and editing `site.yaml` updates the CMS.
`host` (the deployed hostname for a hosted server) is per-environment and is **not** overwritten by file sync. This prevents `primo push` from pointing your production site at `localhost`.
## Avatar Uploads
iPhone HEIC photos are automatically converted to JPEG when uploaded as avatars — no manual conversion required.
## Validating Your Work
Run `primo validate` from the workspace root to check site structure:
```bash theme={null}
primo validate
primo validate --strict # Treat warnings as errors
```
For AI-assisted development, the [MCP server](/reference/mcp-server) exposes per-file validators (`validate_block`, `validate_page`, `validate_site`) that agents call automatically.
## Next Steps
Pull and push changes against a deployed Primo
Ship your workspace to production
# CLI Overview
Source: https://docs.primo.build/cli/overview
Develop Primo sites locally with code-first workflows using primo-cli.
The Primo CLI (`primo-cli`) lets you build and edit Primo sites from local files using your own editor and version control. It runs a local CMS server, syncs file changes both directions, and ships your workspace to production.
The CLI is optional. You can still build everything in the in-browser editor — the CLI is for developers who prefer working in files and Git.
## Installation
```bash theme={null}
npm install -g primo-cli
```
**Requirements:**
* Node.js 18+
* For `primo deploy`: [Railway CLI](https://docs.railway.com/guides/cli) or [Fly.io CLI](https://fly.io/docs/flyctl/install/)
## Quick Start
```bash theme={null}
# Create a new site and start the local CMS
primo new my-site
```
This scaffolds a workspace and starts `primo dev` on `http://my-site.localhost:3000`.
## Workspace Structure
A Primo workspace contains one or more sites and a shared block library:
```
workspace/
├── server.yaml # Port, site groups, server-level config
├── library/ # Shared blocks reusable across sites
│ ├── marketing/
│ │ └── hero/
│ └── shared/
│ └── footer/
└── sites/
├── site-one/
│ ├── site.yaml # Site name, hostname, group
│ ├── blocks/ # Site-specific blocks
│ ├── pages/
│ ├── page-types/
│ ├── site/ # Site-wide fields, head, foot
│ └── uploads/ # Images and files synced to the CMS
└── site-two/
└── site.yaml
```
Each site is served on its own subdomain in dev: `site-one.localhost:3000`, `site-two.localhost:3000`.
## Command Summary
| Command | What it does |
| ------------------------- | ----------------------------------------- |
| `primo init [name]` | Initialize a new workspace |
| `primo new [name]` | Create a new site and start dev |
| `primo dev` | Run the local CMS with file/CMS sync |
| `primo pull ` | Pull a hosted server into local files |
| `primo push ` | Push local changes to a hosted server |
| `primo library pull/push` | Sync just the shared block library |
| `primo login ` | Authenticate with a hosted instance |
| `primo deploy` | Deploy the workspace to Railway or Fly.io |
| `primo build` | Build a static site for any host |
| `primo validate` | Check site structure for errors |
## When to Use Which Command
| You want to… | Use |
| ----------------------------------------------- | -------------- |
| Let collaborators edit content from a CMS UI | `primo deploy` |
| Ship a static site to Netlify/Vercel/Cloudflare | `primo build` |
| Sync local edits to an existing hosted server | `primo push` |
| Pull a hosted server's content into Git | `primo pull` |
## Next Steps
Run the local CMS and learn the file/CMS sync model
Use `primo push` and `primo pull` against a deployed instance
Deploy your workspace to Railway, Fly.io, or any static host
Let AI agents work inside your Primo workspace safely
# Push and Pull
Source: https://docs.primo.build/cli/sync
Sync between local files and a hosted Primo server.
`primo push` and `primo pull` move content and code between your local workspace and an already-deployed Primo server. Use them to keep production in sync with Git, or to grab a copy of a remote site for local development.
## Authenticate First
Before pushing or pulling against a hosted server, log in:
```bash theme={null}
primo login https://cms.example.com
primo login https://cms.example.com -e user@example.com
```
The CLI stores credentials per server and reuses them on subsequent commands.
## `primo pull` — Hosted → Local
Pull an entire server (every site plus the shared library) into local files:
```bash theme={null}
primo pull https://cms.example.com
primo pull https://cms.example.com -o ./my-workspace
```
**Options:**
* `-o, --output ` — Output directory (defaults to `./`)
* `-t, --token ` — Auth token (overrides stored credentials)
The result is a complete workspace ready for `primo dev`. Image files in `uploads/` are pulled as real binaries — the directory is self-contained.
## `primo push` — Local → Hosted
Push local changes to a hosted server:
```bash theme={null}
primo push https://cms.example.com
primo push https://cms.example.com --site abc123
primo push --only my-site # Push just one site folder
primo push --preview # Preview changes without applying
primo push --dry-run # Show what would be sent
```
**Options:**
* `-s, --server ` — Server URL
* `--site ` — Push to a specific site ID
* `--only ` — Only push the named folder under `sites/` (skips `library/`)
* `-d, --dir ` — Workspace directory (default: `.`)
* `-t, --token ` — Auth token
* `--preview` — Preview-only push (doesn't publish)
* `--dry-run` — Show planned operations without sending requests
Always run `primo push --dry-run` first when pushing to production. It surfaces import warnings (orphaned uploads, unresolved block references) before they hit the server.
## Library-Only Sync
To move just the shared block library between workspaces and servers:
```bash theme={null}
# Pull library into a workspace
primo library pull https://cms.example.com
primo library pull -o ./my-workspace
# Push library to a server
primo library push https://cms.example.com
primo library push https://cms.example.com -d ./my-workspace
```
This is useful when curating a block library independently of any particular site.
## What Gets Synced
| Synced | Not synced |
| ------------------------------------- | ----------------------------------------- |
| Blocks (code, fields, content) | The deployed `host` for a site |
| Pages and page types | Authentication users and sessions |
| Site fields and head/foot code | Server admin settings (set in PocketBase) |
| Uploads in `uploads/` | Local-only `.env` files |
| `library/` (with `library push/pull`) | Database backups |
`site.yaml` metadata (name, group) round-trips. `host` is treated as per-environment routing config and is never overwritten by file sync.
## Common Workflows
### Edit in production, snapshot to Git
```bash theme={null}
primo pull https://cms.example.com -o ./prod-backup
cd prod-backup
git add . && git commit -m "Snapshot $(date)"
```
### Edit locally, push to production
```bash theme={null}
primo dev # Build and edit locally
primo push --dry-run https://... # Preview the push
primo push https://cms.example.com # Apply
```
### Move a site between servers
```bash theme={null}
primo pull https://old-server.com -o ./workspace
primo push https://new-server.com -d ./workspace
```
## Next Steps
Deploy a workspace as an editable CMS or static site
Run the local CMS with file/CMS sync
# Inviting Collaborators
Source: https://docs.primo.build/collaboration/inviting-collaborators
Add team members and content editors to your Primo sites.
Once your site is set up with page types and blocks, it's time to hand it off to content editors. This guide shows you how to invite collaborators, manage permissions, and ensure a smooth handoff.
## The Collaboration Model
Primo enables clean separation between developers and editors:
* **Developers** control structure, design, and available options
* **Editors** manage content, create pages, and publish
* **Both** work independently without breaking each other's work
The goal: Mutual autonomy. Developers can update structure anytime, and editors can manage content without waiting for developers.
Collaborators are available on any deployed Primo instance. They're hidden in local dev mode (`primo dev`) since there's no shared server to invite people to.
## Inviting Collaborators
Navigate to your site and click "Settings" in the sidebar.
Click on "Team" or "Collaborators" to see current team members.
Click the "Invite Collaborator" button.
Fill in the invitation form:
* **Email**: The collaborator's email address
* **Role**: Choose their permission level (Content Editor or Developer)
* **Message** (optional): Add a personal note to the invitation
Include a message explaining what you want them to do: "I've set up the blog structure. You can start creating posts!"
Click "Send Invitation". The collaborator receives an email with a link to accept.
The invitation is sent! Your collaborator will receive an email with instructions to get started.
## Roles and Permissions
Primo has two primary roles:
### Content Editor
**What they can do:**
* Create, edit, and delete pages
* Add and remove blocks (from available options)
* Edit block content
* Upload images and media
* Publish and unpublish pages
* View site analytics (if enabled)
**What they can't do:**
* Modify page types
* Change available blocks
* Edit component code
* Change site settings
* Invite other collaborators
* Access developer tools
Use the Content Editor role for content managers, copywriters, and anyone who only needs to manage pages.
### Developer
**What they can do:**
* Everything Content Editors can do, plus:
* Create and modify page types
* Add and configure blocks
* Change site settings
* Invite collaborators
* Manage team members
* Access developer tools
* Deploy and manage hosting
Developer access gives full control over the site, including the ability to break things. Only give Developer access to trusted developers or technical staff.
### Choosing the Right Role
**Use Content Editor role for:**
* Content writers
* Marketing team members
* Client staff managing their own content
* Anyone who only needs to create/edit pages
**Use Developer role for:**
* Developers
* Technical leads
* Agency staff managing client sites
* Anyone who needs to modify structure
## The Handoff Process
Follow these steps for a smooth developer-to-editor handoff:
### 1. Before Inviting
Prepare the site for editors:
Create all necessary page types with:
* Available blocks configured
* Page fields defined
* Header and footer slots set up
* Helpful field labels and descriptions
Build 1-2 example pages for each page type:
* Shows editors what's possible
* Demonstrates best practices
* Serves as templates to duplicate
Editors often duplicate example pages instead of starting from scratch. Make them good!
Put yourself in the editor's shoes:
* Can you build complete pages with available blocks?
* Are field labels clear?
* Is anything confusing or missing?
* Do example pages look good?
Create a quick guide covering:
* How to create each page type
* Which blocks to use for what
* Any specific guidelines (brand, tone, etc.)
* Common tasks (publishing, adding images, etc.)
You can share the [Using the Editor](/content-management/using-the-editor) guide from these docs as a starting point.
### 2. During the Invitation
Make the invitation clear and helpful:
**Good invitation message:**
"Hey Sarah! I've set up the blog structure for our site. You can start creating blog posts right away. I've added a few examples to show you what's possible. Check out the 'Blog Post' page type when creating new pages. Let me know if you need anything added!"
**Bad invitation message:**
"You're invited to edit the site."
### 3. After They Accept
Follow up to ensure they're successful:
Spend 15-30 minutes showing them:
* How to create their first page
* Where to find example pages
* How to add and edit blocks
* How to publish when ready
The first few days, be responsive to questions:
* Answer quickly to avoid blockers
* Note repeated questions (might indicate unclear design)
* Offer to add features if they're struggling
After they've created a few pages, ask:
* What was confusing?
* What blocks are you wishing you had?
* What feels limiting?
* What's working well?
Use this feedback to improve page types and blocks.
## Managing Team Members
### Viewing Team Members
To see all collaborators on a site:
1. Open site settings
2. Go to Team section
3. View list of all members with their roles
### Changing Roles
To change a collaborator's role:
1. Find the team member in the list
2. Click on their role dropdown
3. Select the new role (Content Editor or Developer)
4. Confirm the change
Changing someone from Developer to Content Editor immediately removes their access to developer tools and settings.
### Removing Collaborators
To remove someone from the site:
1. Find them in the team list
2. Click the remove/delete icon
3. Confirm removal
Removed collaborators lose all access to the site immediately. Their created content remains on the site.
## Multi-Site Access
If you manage multiple sites, you can invite the same person to different sites:
* **Same role across sites**: Invite them with the same email to each site
* **Different roles per site**: They can be a Content Editor on one site and Developer on another
* **Separate permissions**: Access to one site doesn't grant access to others
This is useful for agencies managing client sites. You might be a Developer on all sites, but clients are Content Editors on only their own site.
## Best Practices
### 1. Start with Content Editor Role
When in doubt, start with the Content Editor role:
* Gives enough access for content work
* Protects site structure from accidents
* Can always upgrade to Developer later if needed
### 2. Create Clear Examples
Good example pages teach without documentation:
* Show all available blocks in use
* Demonstrate best practices
* Include helpful placeholder content
* Make them easy to duplicate
### 3. Provide Context in Invitations
A good invitation message includes:
* What you want them to do
* Where to start (which page types, examples)
* Who to ask if they have questions
* Any relevant guidelines or docs
### 4. Set Expectations Early
Be clear about:
* **What they can change**: Content, images, page structure (within page types)
* **What they can't change**: Design, colors, fonts, available blocks
* **How to request changes**: Who to ask for new features or blocks
### 5. Iterate Based on Usage
Watch how editors use the site:
* What do they struggle with?
* What do they ask for repeatedly?
* What blocks go unused?
* What workarounds are they creating?
Use this to improve page types and add helpful blocks.
## Common Scenarios
### Client Handoff
**Scenario**: Building a site for a client who will manage their own content
**Best approach:**
1. Build and configure everything as Developer
2. Create comprehensive examples for each page type
3. Invite client as Content Editor
4. Walk them through the basics (15-30 min)
5. Share documentation link
6. Be available for first few days
7. Schedule check-in after 1-2 weeks
### Team Collaboration
**Scenario**: Multiple content writers on your team
**Best approach:**
1. Invite all as Content Editors
2. Create clear examples for each content type
3. Establish content guidelines (tone, style, formatting)
4. Set up review process if needed
5. Use page status/drafts for collaboration
### Agency Multi-Site
**Scenario**: Managing many client sites
**Best approach:**
1. You're Developer on all sites
2. Each client is Content Editor on their own site only
3. Use consistent page type patterns across sites
4. Create a reusable library of blocks
5. Document common processes for efficiency
### Developer Team
**Scenario**: Multiple developers working on one site
**Best approach:**
1. All developers have Developer role
2. Communicate changes to page types/structure
3. Test thoroughly before deploying
4. Use version control for code
5. Coordinate on block library changes
## Troubleshooting
### Invitation Not Received
**Problem**: Collaborator didn't get the email
**Solutions**:
* Check spam/junk folder
* Verify email address is correct
* Resend the invitation
* Check if email service is blocking
### Content Editor Can't Find Blocks
**Problem**: "I need X block but I don't see it"
**Solution**:
* Add the block to the page type's available blocks
* Or explain which available block to use instead
### Content Editor Wants to Change Design
**Problem**: "Can I change the colors/fonts?"
**Solution**:
* Explain that design is controlled by you (developer)
* Offer to make the change for them
* If it's a common request, consider adding it as a configurable option (site field or block field)
### Too Many Requests
**Problem**: Content editor constantly asking for changes
**Solutions**:
* Review their requests for patterns
* Add commonly requested blocks to page types
* Make fields more flexible if appropriate
* Set expectations about what's configurable vs. fixed
### Accidental Deletion
**Problem**: Content editor deleted something important
**Solution**:
* Check if there's a backup or revision history (if enabled)
* Recreate from example page if available
* For future: educate on being careful with deletions
* Consider more restrictive permissions if repeated issue
## Next Steps
Configure page types before inviting collaborators.
Share this guide with your content editors.
The best collaborations start with clear communication. Explain what you've built, what editors can do, and how to ask for help. A good 15-minute walkthrough saves hours of confusion later.
# Setting Up Page Types for Editors
Source: https://docs.primo.build/collaboration/setting-up-page-types-for-editors
Configure page types with the right guardrails for content editors.
Page types are how you define what content editors can and can't do. When configured well, they give editors complete freedom to manage content while keeping your design and structure intact. This guide shows you how to set up page types that work for both developers and editors.
## The Role of Page Types
Page types serve as templates and guardrails:
* **Templates**: Define structure and available options for pages
* **Guardrails**: Control what editors can modify
* **Freedom within bounds**: Editors get content autonomy without breaking design
Think of page types like form templates. You define the structure (fields, available sections), and editors fill in the content. They can't change the form itself, just the answers.
## Core Configuration
### Available Blocks
The most important decision: which blocks can editors use in the body slot?
Navigate to your site, go to Page Types, and open the page type you want to configure.
Click on the "Blocks" tab to see which blocks are available for this page type.
Click "Add Block" to select which blocks editors can use:
* **Be selective**: Only include blocks that make sense for this page type
* **Consider use cases**: What will editors actually need?
* **Test the flow**: Can editors build complete pages with available blocks?
**Blog Post page type:**
* Text (for article content)
* Image (for illustrations)
* Quote (for pull quotes)
* Video (for embedded videos)
**Default page type:**
* Hero (for page header)
* Text (for descriptions)
* Card Grid (for features)
* CTA (for conversion actions)
* Image (for visuals)
If a block doesn't fit, remove it. Fewer, focused options are better than overwhelming choices.
Start minimal. You can always add more blocks later based on editor feedback.
### Header and Footer Slots
Header and footer slots contain blocks that appear on every page of this type. Editors can't add, remove, or reorder these blocks.
Add blocks that should appear at the top of every page:
* Navigation
* Site header
* Breadcrumbs
* Page-specific header
Header blocks appear in the same order on every page. Editors can't change the structure, only edit the content within fields.
Add blocks that should appear at the bottom of every page:
* Site footer
* Newsletter signup
* Related content
* Copyright notice
Don't add too many required blocks. Heavy headers and footers can make pages feel rigid.
Consider whether blocks truly need to be in header/footer:
* **Header/Footer**: Navigation, branding, site-wide elements
* **Body**: Everything else that might vary by page
### Page Fields
Page fields are metadata that applies to the entire page. Use them for information that doesn't fit in individual blocks.
Go to the Fields tab and add page-level fields:
**Common page fields:**
* `meta_title` (Text) - SEO title
* `meta_description` (Text) - SEO description
* `featured_image` (Image) - Social media image
* `publish_date` (Date) - Publication date
* `author` (Page, Person type) - Article author
* `category` (Select) - Content category
* `featured` (Toggle) - Mark as featured
Use the right field type for each purpose:
* **Text**: Titles, short descriptions
* **Rich Text**: Long descriptions, excerpts
* **Image**: Featured images, thumbnails
* **Date**: Publication dates, event dates
* **Select**: Categories, status options
* **Page**: Authors, related pages
* **Toggle**: Featured status, visibility flags
See the [Field Types reference](/reference/field-types) for all available types.
Make fields editor-friendly:
**Good field configuration:**
* **Field name**: `featured_image`
* **Label**: "Featured Image"
* **Help text**: "This image appears in social media shares and at the top of the post. Recommended size: 1200x630px."
**Bad field configuration:**
* **Field name**: `img_1`
* **Label**: "Image"
* **Help text**: *(empty)*
## Best Practices
### 1. Start with User Needs
Think about what editors will actually create:
**Blog:**
* Text for articles
* Images for illustrations
* Quotes for pull quotes
* Code blocks for tutorials
**Default Page:**
* Hero for impact
* Text for content
* Images for visuals
* Card grids for features
**Event Page:**
* Date/time/location
* Description
* Registration form
* Speaker bios
Talk to your content editors before configuring page types. What do they need to create?
### 2. Limit Choices to Useful Options
More options ≠ better. Too many blocks overwhelm editors and dilute your design.
**Good: Focused options**
```yaml theme={null}
Blog Post page type:
Available blocks:
- Text
- Image
- Quote
- Video
```
**Bad: Too many options**
```yaml theme={null}
Blog Post page type:
Available blocks:
- Text
- Rich Text
- Markdown
- Image
- Image Gallery
- Video
- YouTube Embed
- Vimeo Embed
- Quote
- Blockquote
- Pull Quote
- (15 more...)
```
### 3. Use Descriptive Names
Page type names help editors choose the right template:
* ✅ "Blog Post"
* ✅ "Default"
* ✅ "Team Member"
* ✅ "Case Study"
* ❌ "Type 1"
* ❌ "Template A"
* ❌ "Generic"
### 4. Create Reusable Patterns
Build page types that work across similar content:
**Person page type** (reusable for):
* Team members
* Authors
* Speakers
* Advisors
**Article page type** (reusable for):
* Blog posts
* News articles
* Case studies
* Documentation pages
### 5. Test with Real Content
Before handing off to editors:
1. Create a test page using the page type
2. Try to build a realistic page with available blocks
3. Look for gaps: What's missing? What's unnecessary?
4. Refine based on what you learn
## Common Page Type Patterns
### Blog Post
**Purpose**: Article content with metadata
**Available blocks:**
* Text
* Image
* Quote
* Video
**Page fields:**
* `title` (Text, required)
* `excerpt` (Text)
* `author` (Page, Person type)
* `publish_date` (Date)
* `featured_image` (Image)
* `category` (Select)
* `tags` (Repeater with Text)
* `meta_title` (Text)
* `meta_description` (Text)
**Header slot:**
* Site Navigation
* Breadcrumbs
**Footer slot:**
* Site Footer
### Team Member
**Purpose**: Individual profiles
**Available blocks:**
* Text (for bio)
* Image (for additional photos)
**Page fields:**
* `name` (Text, required)
* `title` (Text, required) - Job title
* `photo` (Image, required)
* `email` (Text)
* `bio` (Rich Text)
* `linkedin` (URL)
* `twitter` (URL)
**Header slot:**
* Site Navigation
**Footer slot:**
* Site Footer
### Event
**Purpose**: Event listings with date/location
**Available blocks:**
* Text (for description)
* Image (for photos)
* Map (for location)
**Page fields:**
* `title` (Text, required) - Event name
* `date` (Date, required)
* `time` (Text)
* `location` (Text, required)
* `address` (Text)
* `registration_link` (Link)
* `featured_image` (Image)
* `description` (Rich Text)
**Header slot:**
* Site Navigation
**Footer slot:**
* Site Footer
## Iterating Based on Feedback
Page types aren't set in stone. Improve them based on editor needs:
### Signs You Need to Adjust
**Editors keep asking for a specific block:**
→ Add it to the page type
**A block is never used:**
→ Consider removing it
**Editors work around limitations:**
→ Add the missing capability
**Editors are confused by options:**
→ Reduce choices or improve labels
**Editors duplicate pages instead of creating new ones:**
→ The page type might be too restrictive
### Making Changes
Ask editors what's working and what's not:
* What blocks do you wish you had?
* Which blocks do you never use?
* What feels limiting?
* What feels overwhelming?
Before making changes live:
* Create a test page type with proposed changes
* Have an editor test it
* Verify it solves the problem
When making changes:
* Update the page type
* Let editors know what changed
* Consider creating a quick guide for new features
## Advanced Patterns
Use conditional fields to show/hide fields based on other selections:
**Event page type with virtual/in-person toggle:**
Fields:
* `event_type` (Select) - Options: "In Person", "Virtual"
* `venue` (Text) - Show only when `event_type` is "In Person"
* `address` (Text) - Show only when `event_type` is "In Person"
* `meeting_link` (URL) - Show only when `event_type` is "Virtual"
This creates a dynamic form where editors only see relevant fields based on their selection.
Allow blocks to reference page-level data:
**Blog Post header block** that shows the featured image from page fields:
Block fields:
* `featured_image` (Page Field) - References the page's `featured_image` field
This way, editors set the featured image once in page fields, and it appears automatically in the header block.
**Benefits:**
* Single source of truth for page-level data
* Editors update one field, it appears everywhere
* Reduces duplication and inconsistency
Use Page List fields to automatically include content:
**Team page with automatic team member list:**
Available blocks:
* Text (for introduction)
* Team Grid (uses Page List field)
The Team Grid block has:
* `members` (Page List, Person type) - Automatically shows all Person pages
Editors don't select team members individually—the block shows everyone automatically.
**Use cases:**
* Team directory pages
* Blog post listings
* Product catalogs
* Event calendars
## Troubleshooting
### Editors Can't Find Blocks They Need
**Problem**: "I need a gallery block but it's not available"
**Solution**: Add the block to the page type's available blocks list
### Editors Break Layout
**Problem**: Editors add too many CTAs or use blocks incorrectly
**Solutions**:
* Remove problematic blocks from available options
* Create more specific blocks with built-in constraints
* Add Info fields with usage guidelines
* Provide examples of well-built pages
### Pages Look Inconsistent
**Problem**: Every page looks different
**Solutions**:
* Use header/footer slots for consistent structure
* Limit block options to maintain cohesion
* Create example pages editors can duplicate
* Add visual guidelines (style guide)
### Editors Want More Flexibility
**Problem**: "This feels too restrictive"
**Solutions**:
* Identify specific pain points
* Add necessary blocks thoughtfully
* Consider if constraints are serving design goals
* Balance freedom with consistency
## Next Steps
Add editors to your site and manage permissions.
Deep dive into page type configuration.
Share this guide with your content editors.
Complete reference of all available field types.
The best page type configurations come from iteration. Start with a solid foundation, then refine based on real-world usage and editor feedback.
# Publishing
Source: https://docs.primo.build/content-management/publishing
Deploy your site changes to production.
Publishing compiles your blocks into static HTML, CSS, and JavaScript and deploys them to your domain.
## How It Works
When you publish, Primo:
1. Compiles all blocks to optimized JavaScript
2. Generates static HTML for every page
3. Copies assets to the output directory
4. Deploys files to your domain
Your site is accessible at `https://yourdomain.com/` while the admin stays at `https://yourdomain.com/admin/`.
## Publishing a Site
In the toolbar, click "Publish" (or press `Cmd/Ctrl + P`).
In the dialog, click "Publish Changes" to confirm.
Publishing typically takes 10-30 seconds. You'll see a success message when done.
Your site is now live at your domain.
## What Gets Published
Every publish includes:
* All pages
* All blocks (recompiled)
* All assets (images, files)
* Site configuration
Even if you only change one thing, Primo rebuilds the entire site to ensure consistency.
## Auto-Save Behavior
Most changes auto-save:
* Page content edits
* Adding/removing blocks
* Reordering blocks
**Manual save required for:**
* Block editor (code/fields)
* Section editor
* Site settings editor
Always save in the block/section/site editor before publishing.
## Troubleshooting
**Publish fails with an error:**
* Check the error message for the specific block or page causing the issue
* Review recent changes
* Fix the issue and try again
**Changes don't appear on live site:**
* Hard refresh your browser (`Cmd/Ctrl + Shift + R`)
* Wait 1-2 minutes for propagation
* Verify you clicked "Publish Changes" in the dialog
**Images not loading:**
* Verify images were uploaded (not just linked externally)
* Republish to ensure assets were copied
## Next Steps
Learn to create and edit pages.
Configure domains and settings.
# Using the Editor
Source: https://docs.primo.build/content-management/using-the-editor
Guide for content editors on managing pages and blocks in Primo.
This guide is for content editors who create and manage pages in Primo. You'll learn how to create pages, add blocks, edit content, and publish your work.
If you're a developer setting up Primo, share this guide with your content editors.
## The Editor Interface
The Primo editor has three main areas:
1. **Sidebar** - Draggable blocks and library
2. **Canvas** - Visual page editor
3. **Toolbar** - Site navigation, Pages, and Publish button
## Creating Pages
Click "Pages" in the toolbar.
Click "Create Page".
Select a page type (e.g., "Blog Post", "Default").
Page types determine which blocks you can use. Your developer configures these.
Fill in:
* **Name**: Page title
* **Slug**: URL path (auto-generated)
* Any other fields the page type requires
Your page is created. Now add blocks to build content.
## Working with Blocks
Blocks are the building blocks of your pages.
### Adding Blocks
Browse available blocks in the left sidebar.
Available blocks depend on what your developer enabled for this page type.
Drag the block from the sidebar onto your page. Drop it where you want it.
Click the block to open its editor and fill in the fields.
### Editing Blocks
1. **Hover over a block** to see the block toolbar
2. **Click Edit** to open the editor
3. Fill in text, images, links, etc.
4. Changes auto-save
You can also double-click a block to open its editor.
### Reordering Blocks
Drag blocks to reorder them:
1. Hover over a block
2. Use the drag handle (appears in toolbar)
3. Drag up or down to new position
### Deleting Blocks
1. Hover over a block
2. Click the trash icon in the block toolbar
3. Confirm deletion
## Publishing
When your page is ready:
1. Click "Publish" in the toolbar (or press `Cmd/Ctrl + P`)
2. Click "Publish Changes" in the dialog
3. Wait for completion (\~10-30 seconds)
Your changes are now live!
Changes auto-save as you work, but aren't live until you publish.
## Keyboard Shortcuts
* `Cmd/Ctrl + P`: Publish
* `Cmd/Ctrl + ↑/↓`: Navigate between pages
* `Cmd/Ctrl + E`: Toggle to code tab (in block/section editors)
* `Cmd/Ctrl + S`: Save (in block/section editors)
## Next Steps
Learn the publishing workflow.
Understand how Primo organizes content.
# Visual vs Form Editing
Source: https://docs.primo.build/content-management/visual-vs-form
Understand the difference between viewing blocks on-page and editing in the form dialog.
Primo offers two ways to work with content: viewing blocks visually on the page, or opening them in the editor dialog to edit fields.
## Visual View (On-Page)
The visual view shows your page as it will appear to visitors.
**What you see:**
* All blocks stacked vertically
* How content looks in context
* Spacing, layout, and design
**What you can do:**
* Hover over blocks to see the toolbar
* Drag blocks to reorder them
* Click "Edit" to open the form dialog
* Delete blocks
* Add new blocks via drag-and-drop from sidebar
Use the visual view to see how your page looks and to rearrange blocks.
## Form Editor (Dialog)
Click "Edit" in the block toolbar to open the editor dialog.
**Content Tab:**
* Shows all fields in a structured form
* Edit text, images, links, etc.
* Scroll through all fields
* See what's filled in and what's empty
**Code Tab** (developers only):
* Edit HTML, CSS, and JavaScript
* See live preview
* Toggle with `Cmd/Ctrl + E`
Content editors only see the Content tab. Developers can switch between Content and Code tabs.
## When to Use Each
**Use Visual View When:**
* Building page layouts
* Rearranging blocks
* Checking how content looks
* Adding or removing blocks
**Open Form Editor When:**
* Editing block content
* Filling in multiple fields
* Updating text, images, links
* Need to see all fields at once
## Workflow
Typical editing flow:
1. **Start in visual view** - See the full page
2. **Hover over a block** - Toolbar appears
3. **Click "Edit"** - Form dialog opens
4. **Fill in fields** - Edit content in Content tab
5. **Save** - Changes appear on page
6. **Repeat** - Edit more blocks as needed
You can also press `Cmd/Ctrl + E` while hovering over a block to quickly open the editor dialog.
## Next Steps
Learn to create and edit pages.
Publish your changes.
# Block Library
Source: https://docs.primo.build/dashboard/block-library
Manage your server library of reusable blocks.
Your personal block library contains reusable blocks that you can use across all your sites.
## Overview
The block library is your collection of components you've built. Unlike site-specific blocks, library blocks can be added to any site you manage.
This page is under construction. More documentation coming soon.
## Next Steps
Learn how to create blocks
Work with library blocks in your sites
# Managing Sites
Source: https://docs.primo.build/dashboard/managing-sites
Organize and manage multiple sites with site groups.
The Sites dashboard helps you organize and manage all your Primo sites in one place.
## Creating Your First Site
When you first access Primo from a domain, you'll be prompted to create a site for that hostname. The site is automatically tied to the domain you accessed it from.
## Creating Additional Sites
To create additional sites, you need to connect them to new domain names:
Point your domain's DNS records to your Primo server, or configure your hosting provider to route traffic to the server.
Visit your new domain in a web browser (e.g., `https://newsite.com`).
You'll be shown the site creation screen. Complete the setup and your site will be automatically connected to the domain.
Each site is tied to a specific hostname/domain. You cannot have multiple sites on the same domain.
## Site Groups
**Site Groups** help you organize multiple sites into folders. They're purely organizational - they don't affect how sites function.
### Common Use Cases
* **Client Organization**: Group sites by client (e.g., "Client A Sites", "Client B Sites")
* **Project Type**: Organize by type (e.g., "Blogs", "Portfolios", "E-commerce")
* **Work Status**: Separate sites by stage (e.g., "In Development", "Live Sites", "Personal Projects")
### Managing Site Groups
From the Sites dashboard, site groups are created automatically when you first access the dashboard. You can rename them as needed.
Click the three-dot menu on any site → "Move" → Select the destination group.
Click the dropdown next to the group name → "Rename".
Click the dropdown next to the group name → "Delete".
Deleting a site group deletes all sites within it. Move sites out of a group before deleting it if you want to keep them.
## Managing Individual Sites
Each site card in the dashboard provides quick actions:
**Rename**: Change the site's display name (doesn't affect the domain)
**Move**: Move the site to a different group
**Download**: Export the site's data (coming soon)
**Delete**: Permanently delete the site and all its content
Deleting a site is permanent and cannot be undone. All pages, blocks, and content will be lost.
## Next Steps
Start building your first Primo site
Configure site-wide settings
# Marketplace
Source: https://docs.primo.build/dashboard/marketplace
Browse and use starter sites and blocks from the community.
The marketplace provides starter sites and blocks built by the Primo community.
## Overview
Discover pre-built components and complete starter sites to accelerate your development.
This page is under construction. More documentation coming soon.
## Next Steps
Start building from scratch
Learn about using blocks
# Core Concepts
Source: https://docs.primo.build/getting-started/core-concepts
Learn the fundamental concepts behind Primo: sites, page types, pages, blocks, and sections.
Understanding Primo's core concepts helps you build sites more effectively. Everything in Primo follows a clear hierarchy that separates structure from content, giving you control while empowering editors.
## The Content Hierarchy
Primo organizes content in three levels:
```
Site → Pages → Sections
```
This is your actual content structure - what lives in your database and what gets published.
**Page Types** and **Blocks** aren't part of this hierarchy - they're blueprints that define structure and behavior for pages and sections, respectively.
### Understanding the Structure
Let's break down how everything fits together with three focused diagrams:
**1. Content Hierarchy** - Your actual content structure:
```mermaid theme={null}
graph TB
Site[Site Your website]
Pages[Pages Individual pages]
Sections[Sections Content blocks on pages]
Site --> Pages
Pages --> Sections
style Site fill:#ff6b35,stroke:#333,stroke-width:2px,color:#fff
style Pages fill:#4a90e2,stroke:#333,stroke-width:2px,color:#fff
style Sections fill:#27ae60,stroke:#333,stroke-width:2px,color:#fff
```
**2. Templates & Structure** - How structure is defined:
```mermaid theme={null}
graph LR
PageTypes[Page Types Templates for pages]
Blocks[Blocks Reusable components]
PageTypes -.defines structure.-> Pages[Pages]
Blocks -.instantiated as.-> Sections[Sections]
PageTypes --> PTFields[Page Fields title, description, etc.]
PageTypes --> Slots[Fixed Sections Header, Body, Footer]
PageTypes --> Available[Available Blocks Which blocks editors can use]
PageTypes --> HTML[Head HTML & Body Footer HTML Scripts, styles, meta tags]
Blocks --> Code[Code HTML, CSS, JS]
Blocks --> BlockFields[Block Fields headline, image, etc.]
style PageTypes fill:#004e89,stroke:#333,stroke-width:2px,color:#fff
style Blocks fill:#f39c12,stroke:#333,stroke-width:2px,color:#fff
```
**3. Data Model** - How content is stored:
```mermaid theme={null}
graph TB
Fields[Fields Content schemas]
Entries[Entries Actual values]
Fields -.defines.-> Entries
Fields --> SiteFields[Site Fields logo, nav, etc.]
Fields --> PageFields[Page Fields title, description, etc.]
Fields --> BlockFields[Block Fields headline, image, etc.]
Entries --> SiteEntries[Site Entries Global content]
Entries --> PageEntries[Page Entries Page content]
Entries --> SectionEntries[Section Entries Block content]
style Fields fill:#9b59b6,stroke:#333,stroke-width:2px,color:#fff
style Entries fill:#1abc9c,stroke:#333,stroke-width:2px,color:#fff
```
**Key concepts:** **Blocks** are self-contained full-width components written in Svelte. They define a content schema using fields and are instantiated as sections when added to pages. **Page Types** define which blocks can be used, fixed sections (header/body/footer), and page-level fields. **Fields** define data schemas, **Entries** store the actual values.
## Sites
A **site** is your top-level container. Each site is completely independent with its own:
* **Site-wide content** - Navigation menus, logo, footer content, social links
* **Design system** - CSS variables for colors, typography, spacing
* **Page types** - Templates that define page structure
* **Site-specific blocks** - Blocks available only to this site
* **Domain** - Each site is tied to a specific hostname
When you create a new site, you choose a starter:
* Pick one of your existing sites as a template
* Choose a starter from the marketplace (blog, portfolio, restaurant, etc.)
* Start with a blank starter to build from scratch
## Page Types
**Page types** are templates that define the structure and capabilities of pages. To create one, you simply give it a name, icon, and color. Then you configure:
* **Page fields** - Content like title, description, publish date, or relational fields (e.g. author, location, etc.)
* **Available blocks** - Which blocks editors can add to pages
* **Fixed sections** - Static header/body/footer sections that appear on every page of this type (also called "Static page types" in the UI when body sections are fixed)
### Example Page Type Uses
Page types are completely customizable. Here are common examples:
**Content Types:**
* **Blog Post** - For individual blog articles
* **Person** - For team member profiles
* **Event** - For event listings
* **Case Study** - For portfolio pieces
* **Location** - For store/office locations
* **Product** - For e-commerce items
* **Documentation** - For help articles
**Layout Types:**
* **Blank** - No fixed sections, all blocks available
* **Standard** - Header/footer with standard blocks
* **Editorial** - Reading-focused layout with limited blocks
Everything in Primo is a page. Whether it's a content entity (blog post, team member) or a flexible layout page (homepage, about page), so long as it has a common structure you create a page type for it.
A "Blog Post" page type might include:
* **Fixed header sections** - Site navigation (static content across all blog posts) + page header (pulls title from page field)
* **Page fields**: Title, Description, Author, Publish Date, Featured Image
* **Available blocks**: Hero, Text, Image, Code Block, Call-to-Action, Video
* **Fixed footer sections** - Newsletter signup form (static content across all blog posts) + site footer
Blocks unavailable might include those specific to other page types, like "Person Bio" blocks that depend on "Person" page fields.
### About Fixed Sections
Fixed sections (ie sections within the header/footer slots) contain static content that stays the same across all pages of that type. If you want dynamic content in a fixed section (like showing the page title), you should connect it to a page field:
```svelte theme={null}
{title}
```
### Block Availability
Primo doesn't differentiate between types of blocks - you control which blocks are available per page type. For one-time-use blocks (like a homepage hero), toggle it on, place it on the page, then toggle it off. Toggling it off doesn't remove it from existing pages, just prevents editors from adding it again.
**Cross-page type dependencies:** When a block references fields from a different page type, Primo shows a broken link icon with "References Page Fields from \[Page Type]". You can still add the block, but those fields won't have data. To use a block across multiple page types, give each field the same key (e.g., a header block with `title` fields connected to both "Blank" and "Standard" page types, both referenced as `{title}` in the code).
## Pages
**Pages** are instances of page types. In Primo, everything is a page - whether it's content (blog post, team member) or layout (homepage, about page).
### Creating Pages
To create a page:
1. **Open the page list** - Click "Pages" in the toolbar
2. **Choose creation method:**
* Click "Create Page" for a top-level page
* Click "Create Subpage" next to a page to create a child page (or click the three-dot menu on a child-less page)
3. **Configure the page:**
* **Name** - Internal identifier (or external if using `_meta.name`)
* **Slug** - URL path segment
* **Page Type** - Template to use (hidden if only one exists)
4. **Add content:**
* Fill out page fields
* Add sections from available blocks (or edit existing default sections)
* Fixed sections (if any) are already in place and can't be removed
5. **Publish when ready**
Pages are built by stacking sections vertically. Each page follows the structure defined by its page type, ensuring consistency while allowing flexibility in content and layout.
## Blocks
**Blocks** are self-contained full-width components written in Svelte. They define a content schema using fields and are instantiated as sections when added to pages.
### Two Types of Blocks
**Library Blocks:**
* Stored in your server library
* Reusable across all your sites
* Build once, use everywhere
**Site-Specific Blocks:**
* Available only to one site
* Useful for site-specific components
* Don't clutter your server library
### Block Structure
Each block contains:
* **Component code** - Svelte component with HTML, CSS, and JavaScript
* **Content fields** - Editable data that editors can modify
* **Field configuration** - Define what content can be edited and how
Blocks stack vertically to create page layouts through sections (instances of blocks with actual content).
## Sections
**Sections** are instances of blocks placed on pages. When you add a block to a page, you're actually creating a section - an instance of that block with its own content.
Think of it this way:
* **Block** = template/blueprint (the code and schema)
* **Section** = instance (the actual content on a page)
You have a "Hero" block in your library. When editors add it to the homepage, they create a section. That section has its own content (headline, image, etc.) separate from other Hero sections on different pages.
This separation allows:
* The same block to be used multiple times with different content
* Block code updates to apply to all sections using that block
* Each section to maintain its own content independently
## The Collaboration Model
This hierarchy enables a powerful collaboration model:
**Developer Role:**
* Define page types with available blocks
* Set required blocks and constraints
* Build and style components
* Configure content fields
**Editor Role:**
* Create pages from page types
* Add and arrange sections from blocks
* Edit content within fields
* Publish when ready
**Result:** Developers control structure and design. Editors control content and layout. Both work independently without breaking each other's work.
Editors cannot modify page types, edit component code, or change design settings (unless pre-configured, like block variations). They work within the guardrails you define, ensuring designs stay intact while content remains flexible.
## Putting It Together
Here's how it all works in practice:
1. **You create a site** and choose a starter
2. **You define page types** like "Blog Post" with available blocks and fixed sections
3. **You build blocks** (Hero, Text, Image) and add them to your library
4. **Editors create pages** using the "Blog Post" page type
5. **Editors add sections** (instances of blocks) from the available options
6. **Editors edit content** within each section's fields
7. **Editors publish** when ready
8. **Repeat** from step 2 as required or desired
The hierarchy ensures consistency while maintaining flexibility. Every page follows its page type's structure, but editors have full control over which sections to add and what content to include.
## Next Steps
Build your first site and see these concepts in action.
Step-by-step guide to creating a complete site.
Learn how Primo works under the hood.
Set up page types to control structure and available blocks.
# Installation
Source: https://docs.primo.build/getting-started/installation
Self-host Primo on your own infrastructure or wait for Primo Cloud.
Primo offers two installation options: self-hosting for full control over your infrastructure, or our upcoming managed cloud platform.
## Self-Hosted Installation
Run Primo on your own infrastructure for complete control. Ideal for teams with specific security, compliance, or customization requirements.
Primo's architecture is intentionally simple—a single monolithic application with a SQLite database. This means self-hosting requires minimal operational overhead. No complex microservices, separate database servers, or distributed systems to manage.
**Persistent Storage Required**: Primo stores uses a SQLite database that requires persistent disk storage. It will NOT work on some serverless platforms like Vercel, Netlify, or Cloudflare Pages.
### Choose Your Hosting Path
Select the approach that matches your technical comfort level and requirements. All options can run Primo for **\$5-7/month**.
**New to self-hosting?** Start with [Railway](https://railway.com/?referralCode=RCPU7k) or [Fly.io](https://fly.io) for the easiest setup. If you need more control or lower costs at scale, consider VPS hosting.
**What you get:** One-click deployment, automatic SSL, managed infrastructure. No server management required.
**[Railway](https://railway.com/deploy/primo?referralCode=RCPU7k)** (Recommended)
* Click deploy button, done in 5 minutes
* From \$5/month with persistent volumes included
* Automatic deployments and SSL certificates
* **What you need:** Just a Railway account
**[Fly.io](https://fly.io)**
* Free tier includes 3GB persistent volume
* Global edge deployment
* Good for testing or small production sites
* **What you need:** [Fly.io](https://fly.io) account + CLI installation
**What you get:** Full server access, complete customization, lowest cost at scale. Manage your own infrastructure.
**[Hetzner Cloud](https://cloud.hetzner.com)**
* From €4/month (CX22: 2 vCPU, 4GB RAM, 40GB disk)
* Best price/performance ratio
* European data centers (GDPR-friendly)
* **What you need:** Docker, SSH, and Linux knowledge
**[DigitalOcean](https://www.digitalocean.com)**
* From \$6/month for basic droplets
* Excellent documentation and tutorials
* One-click Docker droplet available
* **What you need:** Basic Docker and command line knowledge
**Quick start:** If you just want to try Primo, click the [Railway deploy button](https://railway.com/deploy/primo?referralCode=RCPU7k) for instant setup. For production sites or when you need full control, choose the VPS path.
For contributing to Primo or testing locally:
* **devenv** ([install here](https://devenv.sh/)) or **Dev Container** compatible environment (VS Code)
* **Git**
**Local Development Steps:**
```bash theme={null}
git clone https://github.com/primocms/primo.git
cd primo
```
```bash theme={null}
devenv shell
```
If you have configured direnvto automatically open the devenv shell, you may ignore this step.
```bash theme={null}
npm install
```
```bash theme={null}
npm run build
```
Initial build is required before starting the dev server.
```bash theme={null}
npm run dev
```
* Main app (dev): [http://localhost:5173](http://localhost:5173)
* Built app: [http://localhost:8090](http://localhost:8090)
* PocketBase Admin: [http://localhost:8090/\_](http://localhost:8090/_)
Local development is for testing only. All code and content is stored in local database under `pb_data` directory.
### Manual Deployment (VPS & Custom Infrastructure)
**Using [Railway](https://railway.com/?referralCode=RCPU7k) or [Fly.io](https://fly.io)?** You can skip this section - those platforms handle Docker deployment automatically.
For VPS hosting (Hetzner, DigitalOcean, etc.) or custom infrastructure, deploy Primo as a Docker container using Docker Compose or by running Docker commands directly. See the [PocketBase production deployment guide](https://pocketbase.io/docs/going-to-production/) for additional information.
Configure your DNS to point your domain name to the server where you'll host Primo.
Use Docker Compose (recommended) or run Docker commands directly. Use `ghcr.io/primocms/primo:latest` as the image.
**Required volume**: Add a volume for the path `/app/pb_data`. This is where all files and the database will be stored.
**Optional environment variables** (only take effect on first start):
* `PRIMO_APP_URL` - Sets base URL used for generating links in the backend (e.g., emails)
* `PRIMO_SUPERUSER_EMAIL` + `PRIMO_SUPERUSER_PASSWORD` - Creates initial superuser account for accessing PocketBase dashboard.
* `PRIMO_USER_EMAIL` + `PRIMO_USER_PASSWORD` - Creates initial user account for accessing Primo.
```yaml compose.yaml theme={null}
services:
primo:
image: ghcr.io/primocms/primo:latest
restart: always
ports:
- 8080:8080
volumes:
- primo-data:/app/pb_data
# Optional environment variables:
# environment:
# - PRIMO_APP_URL=https://example.com
# - PRIMO_SUPERUSER_EMAIL=admin@example.com
# - PRIMO_SUPERUSER_PASSWORD=secure-password
# - PRIMO_USER_EMAIL=user@example.com
# - PRIMO_USER_PASSWORD=secure-password
volumes:
primo-data:
```
```bash docker run theme={null}
docker run -d \
--name primo \
--restart always \
-p 8080:8080 \
-v primo-data:/app/pb_data \
ghcr.io/primocms/primo:latest
```
Navigate to your domain name to complete setup. Setup only starts if a superuser was not created using environment variables in the previous step.
Configure PocketBase settings by navigating to `https://your-domain.com/_/` (replace with your domain).
Create your first site by navigating to `https://your-domain.com/admin` (replace with your domain). Note that the site is created using the domain name used to access the CMS.
## Primo Cloud
**Coming Soon** — Primo Cloud is currently in development. We recommend [self-hosting](#self-hosted) for now, which takes just a few minutes with platforms like Railway or Fly.io.
Primo Cloud will be a fully managed hosting solution that handles all the infrastructure for you—no installation, setup, or server management required.
### Planned Features
* Dedicated server instance for your account
* Automatic SSL certificates
* Global CDN hosting
* Automatic deployments when you publish changes
* Built-in collaboration tools
* Direct support for setup and troubleshooting
## Next Steps
Learn the fundamental concepts behind Primo
Build your first site in minutes
# Quickstart
Source: https://docs.primo.build/getting-started/quickstart
Build your first Primo site in 5 minutes — locally with the CLI or deployed and pulled down.
Primo has two starting points. Pick the one that matches how you work:
Run `primo new` to create a workspace on your machine. Build the site locally, then deploy when ready.
Deploy Primo to Railway or a VPS first, then pull it down to your laptop with `primo pull` for local edits.
Both paths land at the same place: a workspace on disk that you can edit with code, AI agents, or the visual CMS.
## Path 1: Build locally, deploy later
Best for: starting from scratch, prototyping, or experimenting before committing to a host.
```bash theme={null}
npm install -g primo-cli
```
Requires Node.js 18+.
```bash theme={null}
primo new my-site
```
This scaffolds a workspace and starts the local CMS at `http://my-site.localhost:3000`.
Open `http://my-site.localhost:3000/admin` to use the visual editor. Edits round-trip to your files in real time.
Open the workspace in your editor:
```
my-site/
├── server.yaml
├── library/
└── sites/my-site/
├── site.yaml
├── blocks/
├── pages/
├── page-types/
└── site/
```
Edit `blocks//component.svelte` in your editor — the CMS reflects changes immediately. See [Local Development](/cli/local-development) for the full workflow including `--author` modes.
For Claude Code, Cursor, or other agents, install the MCP server so they can validate edits and scaffold blocks correctly:
```bash theme={null}
npm install -g primo-mcp
claude mcp add --transport stdio primo -- primo-mcp
```
See the [MCP server reference](/reference/mcp-server) for details.
```bash theme={null}
primo deploy
```
Interactive picker for Railway or Fly.io. See [Deploy with the CLI](/cli/deploy) for other options.
## Path 2: Deploy first, pull down to edit
Best for: when you want a shared server from day one — collaborators on a CMS UI, agents pulling/pushing files, your team editing visually.
Pick one:
* **[Railway](https://railway.com/deploy/primo?referralCode=RCPU7k)** — one-click deploy with persistent storage
* **[Fly.io](https://fly.io)** — generous free tier
* **Docker / VPS** — `docker run -d -p 8080:8080 ghcr.io/primocms/primo:latest`
See the [Installation Guide](/getting-started/installation) for full instructions.
Visit your deployed URL and follow the site creation wizard. Pick a starter (Blog, Portfolio, Restaurant, Business, or Blank) or clone an existing site.
You can build the entire site in the browser without ever installing the CLI. The CLI is optional.
Open `https://your-domain.com/admin` and edit pages by clicking on them. See [Using the Editor](/content-management/using-the-editor) for the full visual workflow.
The admin panel is always at `/admin/` on your domain. Your published site lives at the root.
Once you want to edit blocks in code or hand things off to AI agents:
```bash theme={null}
npm install -g primo-cli
primo login https://your-domain.com
primo pull https://your-domain.com
```
You'll get a full workspace on disk. Push changes back with `primo push`. See [Push and Pull](/cli/sync) for the sync model.
Browser edits auto-save. To publish, click **Publish** (or press ⌘P / Ctrl+P). Primo compiles your blocks, generates static HTML, and serves it from your domain.
## What's Next
Sites, page types, pages, blocks, fields
Walk through building a complete site end-to-end
`primo dev`, author modes, uploads folder
Hand the site off to non-technical editors
# Introduction
Source: https://docs.primo.build/index
Primo is a visual CMS where AI agents edit files and humans edit visually — same source of truth.
Primo is a visual CMS that represents your entire site as files. Pull it down, edit with Claude Code, Cursor, or any agent, push it back. Your team manages content visually in the browser.
```bash theme={null}
docker run -d -p 8080:8080 ghcr.io/primocms/primo:latest
```
## What Makes Primo Different
Most web tools force a tradeoff: code control *or* visual editing. Primo keeps both in the same source of truth.
Blocks, pages, fields, and content all live as YAML and Svelte on disk. Version them in Git, edit them in any editor.
Any tool that reads files works — Claude Code, Cursor, Cline. The [official MCP server](/reference/mcp-server) gives agents live schema validation and scaffolding.
Non-technical teammates open the site in the browser, click anything, and edit it. No code, no broken designs.
Run it on Railway, Fly, a VPS, or your laptop. MIT license, single Docker container, SQLite under the hood.
## How It Works
1. **Build with code.** Write Svelte components and attach content fields with YAML. The [CLI](/cli/overview) runs the full CMS locally and watches your files.
2. **Hand off visually.** Invite [collaborators](/collaboration/inviting-collaborators) — they edit content in the browser without touching code.
3. **Edit anywhere, anytime.** Pull the site to your laptop, let an agent change a layout, push it back. Or stay in the browser. Same files either way.
## Who It's For
Primo is built for **developers who build sites for other people** — freelancers, agencies, and small teams who want full code control without leaving clients stranded.
## Quick Start
Build your first site in 5 minutes — local or deployed
Self-host with Docker, Railway, or Fly.io
Develop sites locally with `primo dev` and files as the source of truth
Wire up Claude Code, Cursor, or any MCP client
## Built on Svelte
Primo blocks are written in Svelte 5. If you're new to Svelte, the [official tutorial](https://svelte.dev/tutorial) covers what you need: scoped styles, simple HTML-based syntax, and reactive runes (\$state, \$derived, \$effect).
# Architecture
Source: https://docs.primo.build/reference/architecture
How Primo works under the hood.
Understanding Primo's architecture helps with self-hosting, troubleshooting, and contributing to the project.
## System Overview
**Single Application:**
* SvelteKit frontend + PocketBase backend, all in one binary
* SQLite database stores code and content
* Files (images, uploads) stored in filesystem (or optional S3)
* No microservices, no separate database server
* Minimal operational overhead
**What This Means:**
* One process to run and monitor
* SQLite file contains code/content, filesystem has assets
* Simple backups (database + storage in `pb_data` directory)
* Easy to reason about and debug
## Data Flow
```mermaid theme={null}
flowchart TB
a["1\. Developer writes Svelte block code in CodeMirror"] -->
b["2\. Code saved to SQLite (site_symbols table)"] -->
c["3\. Live preview compiles code in browser"] -->
d["4\. Fields configured and saved to database"] -->
f["Block available to editors"]
```
```mermaid theme={null}
flowchart TB
a["1\. User clicks Publish"] -->
b["2\. Svelte compiler processes all blocks"] -->
c["3\. Generates static HTML/CSS/JS for each page"] -->
d["4\. Files written to pb_data/storage/sites/[hostname]/"] -->
f["Site served as static files"]
```
## Key Components
### PocketBase
**Role:** Backend, database, auth, file storage, realtime
* SQLite database management
* User authentication and permissions
* File uploads and serving (local filesystem and optinally S3)
* Realtime subscriptions for live updates
* Custom API routes
**Files:**
* Database: `pb_data/data.db`
* Storage: `pb_data/storage/`
* Backups: `pb_data/backups/`
PocketBase can be configured to use S3-compatible storage (AWS S3, Cloudflare R2, etc.) instead of local filesystem for uploaded files. This is useful for scaling.
### Svelte Compiler
**Role:** Transforms block code into production JavaScript
**Where it runs:**
* **In the browser** using Web Workers (not server-side)
* Rollup worker for JavaScript bundling
* PostCSS worker for CSS processing
**When it runs:**
* On publish: Compiles all blocks to static files
* In preview: Compiles blocks for live preview
* Not during content edits (content changes don't trigger compilation)
**Process:**
1. Fetches block HTML/CSS/JS from database
2. Injects content variables from fields
3. Compiles in browser worker (parallel processing)
4. Generates static HTML for each page
Browser-based compilation means no server CPU usage during builds. All compilation happens client-side, making the server lightweight and scalable.
### CodeMirror
**Role:** Code editor for developers
**Features:**
* Syntax highlighting for HTML, CSS, JavaScript
* Autocompletion for field variables
* Error detection
* Keyboard shortcuts (`Cmd/Ctrl + 1/2/3` to switch panes)
### TipTap
**Role:** Rich text editor for content
**Features:**
* WYSIWYG editing with formatting toolbar
* Markdown shortcuts
* Paste from Word/Google Docs
* Link editing
## File Structure
```
pb_data/
├── data.db # SQLite database (code, content, config)
├── storage/
│ ├── sites/
│ │ └── [hostname]/ # Published static sites
│ │ ├── index.html # Home page
│ │ ├── _symbols/ # Compiled JavaScript bundles
│ │ ├── _uploads/ # Media files
│ │ └── [page]/ # Individual page HTML files
│ └── _pbc_xxxxxxxxxx/ # Uploaded files managed per collection
└── backups/ # Backups
```
## Database Schema
**Key Collections:**
* `sites`: Site configuration and settings
* `site_symbols`: Block code (HTML, CSS, JS)
* `site_symbol_fields`: Field definitions for blocks
* `site_symbol_entries`: Field values/content
* `page_types`: Page type configurations
* `pages`: Individual pages
* `page_sections`: Block instances on pages
* `page_section_entries`: Content for block instances
**Why This Structure:**
* Blocks (symbols) are reusable across pages
* Content (entries) is separate from code (symbols)
* Pages reference blocks via sections
* Supports multi-locale content (entries have locale field)
## How Publishing Works
### Static Site Generation
**Step 1: Compile blocks**
* Fetch all blocks used on the site
* Compile each block's Svelte code
* Generate optimized JavaScript bundles
* Upload bundles
**Step 2: Generate pages**
* For each page, fetch content from database
* Inject content into compiled blocks
* Render to static HTML
* Upload pages
**Step 3: Update static site**
* Copy new and updated JavaScript bundles to `_symbols/`
* Copy new and updated media files to `_uploads/`
* Copy new and updated pages to `[page]/index.html`
* Clean up unused files
**Result:**
* Static HTML files for every page
* Shared JavaScript bundles
* Fast, CDN-friendly output
* No database queries on page load
## Preview vs Published
**Preview (in editor):**
* Compiles blocks on-demand
* Shows unsaved changes
* Uses iframe for isolation
* Hot module replacement for fast updates
**Published (live site):**
* Pre-compiled static files
* No build step on request
* Served directly from file system
* Fast, cacheable responses
## Realtime Updates
Primo uses PocketBase's realtime subscriptions for live collaboration:
**What syncs in realtime:**
* Block code/content changes
* Page structure updates
* User activity indicators
* Field value updates
**How it works:**
* SSE connection to PocketBase
* Receive updates as they happen
* UI updates automatically
## Performance Characteristics
**Database:**
* SQLite is fast for read-heavy workloads
* Single-file makes backups trivial
* No network latency (in-process database)
* Scales to millions of rows
**Publishing:**
* Compile time grows linearly with page count
* \~3-20 seconds for most sites
* Compilation is parallelized in browser workers
* Limited by user's browser/device, not server
* Static output is extremely fast to serve
**Editor:**
* CodeMirror handles large files well
* Preview compilation is cached
* Realtime updates are throttled
* UI stays responsive during saves
## Self-Hosting Considerations
**System Requirements:**
* Docker
* 1GB RAM minimum
* Persistent disk storage
**Scaling:**
* Compilation happens client-side (no server CPU for builds)
* Server only needs to serve API requests and static files
* Horizontal scaling not needed (static output can be served from CDN)
* Database size grows with content (\~1MB per 50 pages)
* Use S3 storage to better integrate with content delivery networks
**Backups:**
* SQLite file contains code/content (`pb_data/data.db`)
* Storage directory contains uploaded files (`pb_data/storage/`)
* Backup both database + storage directory
* Or create and download backup from PocketBase
* If using S3, you only need to backup the database
## Technology Stack Summary
**Frontend:**
* SvelteKit 2
* Svelte 5 (with runes)
* TailwindCSS
* CodeMirror 6
* TipTap 2
**Backend:**
* Golang
* PocketBase
* SQLite 3
**Build Tools:**
* Vite
## Next Steps
Set up Primo for development or production.
Understand Primo's mental model.
# Deployment Guide
Source: https://docs.primo.build/reference/deployment
Deploy Primo to production on Railway, VPS, or custom infrastructure.
This guide covers deploying Primo to production. Choose the deployment method that matches your needs and technical expertise.
**Persistent Storage Required**: Primo stores all data in a SQLite database and needs persistent disk storage to function. Traditional serverless platforms (Vercel, Netlify, Cloudflare Pages) have ephemeral file systems and won't work.
## Quick Deploy: Railway
The fastest way to deploy Primo to production.
### Prerequisites
* [Railway account](https://railway.com/?referralCode=RCPU7k) (free to start)
* Domain name (optional, Railway provides a free subdomain)
### Deployment Steps
[](https://railway.com/deploy/primo?referralCode=RCPU7k)
Set environment variables if you want automated setup:
* `PRIMO_SUPERUSER_EMAIL`
* `PRIMO_SUPERUSER_PASSWORD`
* `PRIMO_APP_URL` (optional, auto-detected)
Railway will:
* Pull the Docker image
* Create a persistent volume
* Deploy the container
* Generate a URL
Click the generated URL to access Primo. If you didn't set environment variables, you'll see the setup screen.
Railway Settings → Domains → Add custom domain
**Cost**: From \$5/month (includes persistent volume)
Railway automatically handles SSL certificates, deployments, and scaling. Perfect for production use.
## VPS Deployment
Deploy to any VPS for full control over your infrastructure.
### Prerequisites
* VPS with Docker installed ([Hetzner](https://www.hetzner.com), [DigitalOcean](https://www.digitalocean.com), etc.)
* SSH access to your server
* Domain name pointed to your server IP
* Basic command line knowledge
### Option 1: Docker Compose (Recommended)
```bash theme={null}
ssh user@your-server-ip
```
```bash theme={null}
mkdir primo
cd primo
```
```yaml theme={null}
services:
primo:
image: ghcr.io/primocms/primo:latest
restart: always
ports:
- "8080:8080"
volumes:
- primo-data:/app/pb_data
environment:
- PRIMO_APP_URL=https://your-domain.com
# Optional: automated setup
# - PRIMO_SUPERUSER_EMAIL=admin@example.com
# - PRIMO_SUPERUSER_PASSWORD=
volumes:
primo-data:
```
```bash theme={null}
docker-compose up -d
```
```bash theme={null}
docker-compose logs -f
```
You should see logs indicating the server started on port 8080.
### Option 2: Docker Run
For simpler setups without compose:
```bash theme={null}
docker run -d \
--name primo \
--restart always \
-p 8080:8080 \
-v primo-data:/app/pb_data \
-e PRIMO_APP_URL=https://your-domain.com \
ghcr.io/primocms/primo:latest
```
### Configure Reverse Proxy
Primo runs on port 8080. Use a reverse proxy for SSL and domain routing:
```nginx theme={null}
server {
listen 80;
server_name your-domain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name your-domain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
Get SSL certificates with [Certbot](https://certbot.eff.org/):
```bash theme={null}
sudo certbot --nginx -d your-domain.com
```
Caddy automatically handles SSL certificates:
```caddy theme={null}
your-domain.com {
reverse_proxy localhost:8080
}
```
Start Caddy:
```bash theme={null}
caddy run --config Caddyfile
```
Add labels to docker-compose.yml:
```yaml theme={null}
services:
primo:
image: ghcr.io/primocms/primo:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.primo.rule=Host(`your-domain.com`)"
- "traefik.http.routers.primo.entrypoints=websecure"
- "traefik.http.routers.primo.tls.certresolver=letsencrypt"
- "traefik.http.services.primo.loadbalancer.server.port=8080"
```
## Platform-Specific Guides
### Hetzner Cloud
[Hetzner](https://www.hetzner.com) offers affordable VPS hosting in Europe and the US.
1. Create a VPS (CX22 recommended: €4/month)
2. Install Docker:
```bash theme={null}
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
```
3. Follow VPS deployment steps above
4. Configure firewall to allow ports 80, 443, and 22
### DigitalOcean
[DigitalOcean](https://www.digitalocean.com) provides simple VPS hosting with excellent documentation.
1. Create a Droplet (\$6/month minimum)
2. Choose "Docker" from one-click apps (or install manually)
3. Follow VPS deployment steps above
4. Use DigitalOcean's cloud firewalls for security
### Fly.io
[Fly.io](https://fly.io) offers a free tier with persistent volumes:
```bash theme={null}
curl -L https://fly.io/install.sh | sh
```
```bash theme={null}
fly auth login
```
```toml theme={null}
app = "your-app-name"
primary_region = "sjc"
[build]
image = "ghcr.io/primocms/primo:latest"
[http_service]
internal_port = 8080
force_https = true
[[mounts]]
source = "primo_data"
destination = "/app/pb_data"
[env]
PRIMO_APP_URL = "https://your-app-name.fly.dev"
```
```bash theme={null}
fly volumes create primo_data --size 3
```
```bash theme={null}
fly deploy
```
## Database Backups
Primo uses SQLite, stored in `/app/pb_data`. Back up this directory regularly.
**Cloud users:** Backups are handled automatically. Backup frequency depends on your plan.
### PocketBase Automated Backups (Recommended)
PocketBase has built-in automated backups that you can configure through the admin UI.
Navigate to `https://your-domain.com/_/` and log in with your admin credentials.
Click on Settings in the sidebar, then select the Backups tab.
Set a cron expression for your backup schedule:
* **Daily at 2 AM**: `0 2 * * *`
* **Every 6 hours**: `0 */6 * * *`
* **Hourly**: `0 * * * *`
* **Every minute** (not recommended): `* * * * *`
* **Local storage**: Backups saved to `/app/pb_data/backups`
* **S3-compatible storage**: Configure [AWS S3](https://aws.amazon.com/s3/), [Backblaze B2](https://www.backblaze.com/b2/cloud-storage.html), or similar for off-site backups
Configure how many backups to keep (e.g., keep last 7 days of backups).
For production sites, configure S3-compatible storage for off-site backups. This protects against server failures.
### Manual Backups with Docker
```bash theme={null}
#!/bin/bash
# backup-primo.sh
BACKUP_DIR="/backups/primo"
DATE=$(date +%Y%m%d_%H%M%S)
# Create backup directory
mkdir -p $BACKUP_DIR
# Copy pb_data
docker cp primo:/app/pb_data $BACKUP_DIR/pb_data_$DATE
# Keep only last 7 days
find $BACKUP_DIR -type d -mtime +7 -exec rm -rf {} +
```
Add to crontab:
```bash theme={null}
0 2 * * * /path/to/backup-primo.sh
```
### Restore from Backup
```bash theme={null}
# Stop container
docker-compose down
# Restore backup
docker cp pb_data_20250101_020000 primo:/app/pb_data
# Start container
docker-compose up -d
```
Test your backups regularly. A backup you haven't tested is not a backup.
## Updating Primo
### Docker Compose
```bash theme={null}
# Pull latest image
docker-compose pull
# Restart with new image
docker-compose up -d
```
### Docker Run
```bash theme={null}
# Stop and remove old container
docker stop primo
docker rm primo
# Pull latest image
docker pull ghcr.io/primocms/primo:latest
# Start new container (same command as initial deploy)
docker run -d --name primo ...
```
Always backup before updating, especially for major version changes.
## Performance Optimization
### Enable Gzip Compression
In your reverse proxy:
**Nginx:**
```nginx theme={null}
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;
```
**Caddy:**
```caddy theme={null}
encode gzip
```
### Add Caching Headers
```nginx theme={null}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
```
### Use a CDN
Point your domain through Cloudflare (free tier) for:
* Global CDN
* DDoS protection
* SSL certificates
* Caching
## Security Checklist
Generate random passwords for all accounts. Never use default or simple passwords.
Only allow ports 80, 443, and 22 (SSH). Block all other inbound traffic.
Regularly update Docker and the Primo image for security patches.
Never serve Primo over HTTP in production. Use Let's Encrypt for free SSL certificates.
* Disable password authentication
* Use SSH keys only
* Change default SSH port
* Use fail2ban to block brute force attempts
Automate daily backups and store them off-server (S3, Backblaze, etc.).
Check Docker logs regularly for suspicious activity:
```bash theme={null}
docker-compose logs -f --tail=100
```
## Troubleshooting
See the [Troubleshooting Guide](/reference/troubleshooting) for common deployment issues and solutions.
## Next Steps
Configure Primo with environment variables
Solve common deployment issues
# Environment Variables
Source: https://docs.primo.build/reference/environment-variables
Configure Primo using environment variables for deployment.
Environment variables let you configure Primo for different deployment environments. These are particularly useful for Docker deployments and automated setup.
Legacy `PALA_*` variable names are still accepted as fallbacks, but new deployments should use the `PRIMO_*` names documented below.
## Initial Setup Variables
These variables only take effect on **first startup** when initializing a new instance:
### `PRIMO_SUPERUSER_EMAIL`
Creates an initial superuser account (admin) with full access.
* **Type**: String (email address)
* **Required**: No
* **Default**: None
* **When to use**: Automated deployments, skipping manual setup
**Example:**
```bash theme={null}
PRIMO_SUPERUSER_EMAIL=admin@example.com
```
### `PRIMO_SUPERUSER_PASSWORD`
Password for the initial superuser account.
* **Type**: String
* **Required**: Only if `PRIMO_SUPERUSER_EMAIL` is set
* **Default**: None
* **Minimum**: 8 characters
**Example:**
```bash theme={null}
PRIMO_SUPERUSER_PASSWORD=secure-password-123
```
If both `PRIMO_SUPERUSER_EMAIL` and `PRIMO_SUPERUSER_PASSWORD` are set, the setup screen will be skipped and you'll go straight to the sign-in page.
### `PRIMO_USER_EMAIL`
Creates an initial regular user account.
* **Type**: String (email address)
* **Required**: No
* **Default**: None
* **When to use**: Pre-populate with a content editor account
**Example:**
```bash theme={null}
PRIMO_USER_EMAIL=editor@example.com
```
### `PRIMO_USER_PASSWORD`
Password for the initial regular user account.
* **Type**: String
* **Required**: Only if `PRIMO_USER_EMAIL` is set
* **Default**: None
* **Minimum**: 8 characters
**Example:**
```bash theme={null}
PRIMO_USER_PASSWORD=editor-password-123
```
### `PRIMO_APP_URL`
Sets the base URL for the application. Used for generating links in emails and backend processes.
* **Type**: String (URL)
* **Required**: No
* **Default**: Auto-detected from request
* **When to use**: Email notifications, API callbacks
**Example:**
```bash theme={null}
PRIMO_APP_URL=https://cms.example.com
```
## Common Configurations
### Development Setup
```bash theme={null}
# No environment variables needed
# Just run: docker-compose up
```
The setup screen will guide you through creating your first account.
### Production with Manual Setup
```bash theme={null}
PRIMO_APP_URL=https://cms.example.com
```
Access the setup screen to create your admin account manually.
### Production with Automated Setup
```bash theme={null}
PRIMO_APP_URL=https://cms.example.com
PRIMO_SUPERUSER_EMAIL=admin@example.com
PRIMO_SUPERUSER_PASSWORD=strong-random-password
PRIMO_USER_EMAIL=editor@example.com
PRIMO_USER_PASSWORD=another-strong-password
```
No setup screen - go straight to sign-in.
## Docker Compose Example
```yaml theme={null}
services:
primo:
image: ghcr.io/primocms/primo:latest
restart: always
ports:
- 8080:8080
volumes:
- primo-data:/app/pb_data
environment:
- PRIMO_APP_URL=https://cms.example.com
- PRIMO_SUPERUSER_EMAIL=admin@example.com
- PRIMO_SUPERUSER_PASSWORD=${SUPERUSER_PASSWORD}
- PRIMO_USER_EMAIL=editor@example.com
- PRIMO_USER_PASSWORD=${USER_PASSWORD}
volumes:
primo-data:
```
Use `.env` files or secrets management to avoid hardcoding passwords in docker-compose.yml
## Railway Template
[Railway](https://railway.com/?referralCode=RCPU7k) automatically handles environment variables through their UI:
1. Deploy using the Railway button
2. Set environment variables in Railway dashboard
3. Railway restarts the service automatically
```bash theme={null}
# Railway environment variables
PRIMO_APP_URL=${RAILWAY_STATIC_URL}
PRIMO_SUPERUSER_EMAIL=admin@example.com
PRIMO_SUPERUSER_PASSWORD=
```
## Security Best Practices
Use environment variables, secrets management, or encrypted files. Never hardcode passwords in `docker-compose.yml` or other config files.
Generate random passwords with at least 16 characters:
```bash theme={null}
# Generate a random password
openssl rand -base64 24
```
Change initial passwords after setup, especially for production environments.
For production deployments, use:
* **[Railway](https://railway.com/?referralCode=RCPU7k)**: Built-in secrets
* **Docker Swarm**: Docker secrets
* **Kubernetes**: Kubernetes secrets
* **Vault**: HashiCorp Vault
Environment variables contain sensitive information. Restrict access to deployment configurations.
## Troubleshooting
### Setup screen appears even with PRIMO\_SUPERUSER\_EMAIL set
**Cause**: Environment variables not loaded, or database already initialized
**Solution**:
1. Check environment variables are correctly set
2. Verify Docker/Railway configuration
3. If database exists, delete `/app/pb_data` volume and restart
### Can't sign in after setting environment variables
**Cause**: Wrong password, or user wasn't created
**Solution**:
1. Check logs for errors during startup
2. Verify password meets minimum requirements (8 characters)
3. Check that both EMAIL and PASSWORD variables are set
### Variables not taking effect
**Cause**: Variables only work on first startup
**Solution**:
Environment variables for user creation only work when the database is first initialized. To reset:
1. Stop the container
2. Delete the volume: `docker volume rm primo-data`
3. Restart with new environment variables
Deleting the volume deletes all your data. Only do this for fresh installs or if you have backups.
## Next Steps
Learn how to deploy Primo to production
Solve common deployment issues
# Field Types
Source: https://docs.primo.build/reference/field-types
Complete reference of all available field types for blocks and pages.
Fields are what make blocks editable. You attach fields to your blocks, and editors can fill them in without touching code. This reference covers all available field types in Primo.
Fields can be added to three places:
* **Sites** - Site-wide content (logo, navigation, footer, etc.)
* **Page Types** - Page-level metadata (title, author, publish date, etc.)
* **Blocks** - Section-level content (headlines, images, descriptions, etc.)
Most field types work the same in all contexts, with some exceptions noted below.
## Text Fields
Text input that automatically grows to accommodate content.
**Use for:**
* Headlines and titles
* Button labels
* Short descriptions
* Names
**Component usage:**
```svelte theme={null}
{headline}
{description}
```
WYSIWYG editor for formatted text with HTML output.
**Use for:**
* Article content
* Descriptions with formatting
* Long-form content
* Any text needing formatting
**Component usage:**
```svelte theme={null}
{@html content}
```
Rich text fields output HTML. Always use `{@html}` in your component to render it. To style the HTML content, use `:global()` in your CSS to target elements inside the rendered HTML. Use a unique class name on the wrapper to avoid affecting other styles on the page.
Markdown editor for formatted text.
**Use for:**
* Documentation
* Blog posts
* README content
* Developer-friendly content
**Component usage:**
```svelte theme={null}
{@html content}
```
## Number Fields
Numeric input for integers.
**Use for:**
* Quantities
* Counts
* Whole number ratings
* Integer values
**Component usage:**
```svelte theme={null}
Quantity: {quantity}
Rating: {rating}/5
```
Date picker that outputs dates in YYYY-MM-DD format.
**Use for:**
* Publication dates
* Event dates
* Deadlines
* Any calendar dates
**Component usage:**
```svelte theme={null}
```
The Date field outputs a string in YYYY-MM-DD format (e.g., "2024-01-15"). Use JavaScript's `new Date()` constructor to parse and format it for display. The raw value is perfect for the `datetime` attribute in `
## Selection Fields
Dropdown menu for selecting one option from a list.
**Use for:**
* Categories
* Variants
* Status options
* Single-choice selections
**Component usage:**
```svelte theme={null}
{category}
```
**Options:**
Each option in a Select field has three properties:
* **value**: The actual value stored (used in your code)
* **label**: Display text shown to editors
* **icon**: Optional icon identifier (from Iconify)
The first option in the list is used as the default value.
Boolean toggle switch for on/off options.
**Use for:**
* Feature toggles
* Boolean settings
* Show/hide options
* Enable/disable states
**Component usage:**
```svelte theme={null}
{#if show_cta}
{/if}
```
Slider for selecting numeric values.
**Use for:**
* Opacity values
* Percentage selections
* Adjustable numeric settings
**Component usage:**
```svelte theme={null}
Content with adjustable opacity
```
## Media Fields
Image upload and selection field. Returns an object with `url` and `alt` properties.
**Use for:**
* Photos
* Graphics
* Custom icons
* Any visual content
**Component usage:**
```svelte theme={null}
{#if image}
{/if}
```
**Options (both optional):**
* **Max Size MB**: Maximum file size in megabytes
* **Max Width or Height**: Maximum dimension (width or height) in pixels
The image field includes a Description input for alt text. Editors should fill this out, and developers should use `image.alt` in their components for accessibility.
Icon picker for selecting icons from [Iconify](https://icon-sets.iconify.design/). Returns an SVG string.
**Use for:**
* Feature icons
* UI elements
* Decorative icons
* Navigation icons
**Component usage:**
```svelte theme={null}
{@html icon}
```
For UI icons that should stay consistent across your site, consider creating a site field containing icon values. This lets you update icons site-wide from one place instead of editing individual blocks.
## Link Fields
Link field for both internal and external links. Returns an object with `url` and `label` properties.
**Use for:**
* Navigation items
* Call-to-action buttons
* Any clickable links
**Component usage:**
```svelte theme={null}
{link.label}
```
Simple URL input field.
**Use for:**
* External URLs
* Web addresses
* API endpoints
* Any URL string
**Component usage:**
```svelte theme={null}
Visit Website
```
## Relational Fields
Reference site-wide content fields. The site field references the complete value of the site field, regardless of its type.
**Use for:**
* Displaying site-wide content in blocks
* Reusing global content across pages
* Referencing branding elements
**Component usage:**
```svelte theme={null}
{company_name}
```
Available in page types and blocks to reference site-wide content.
When you edit a site field from the block form view, it updates the value site-wide across all blocks that reference it. Site fields cannot be edited inline on the page yet.
Reference page-level fields from the current page. The page field references the complete value of the page field, regardless of its type.
**Use for:**
* Displaying page title in blocks
* Showing page metadata in blocks
**Component usage:**
```svelte theme={null}
{title}
```
Available in blocks to reference page-level content.
When you edit a page field from the block form view, it updates the value for that specific page across all blocks that reference it. For example, if a blog post has a `featured_image` page field, changing it might update the image in the page's header block, the meta tags in the head, and on the articles list page—all from one edit (if integrated in those places).
Page fields cannot be edited inline on the page yet.
Select a specific page from a list. You must specify a page type to limit which pages appear in the selection list.
**Use for:**
* Article author (Person page type)
* Featured article (Blog Post page type)
* Related pages
**Component usage:**
```svelte theme={null}
```
The `_meta` property provides:
* `_meta.url`: Page URL for linking
* `_meta.slug`: Page slug
* `_meta.name`: Page name
**Options:**
* **Page Type**: Required. Specifies which page type to select from (e.g., "Person", "Blog Post")
Automatically outputs all pages of a specified page type. Editors cannot select which pages to include—it returns all pages of the given type.
**Use for:**
* Team member listings (all Person pages)
* Blog index (all Blog Post pages)
* Automatic page lists
**Component usage:**
```svelte theme={null}
```
**Options:**
* **Page Type**: Required. Specifies which page type to output (e.g., "Person", "Blog Post")
If you want editors to choose specific pages, use a **Repeater field** with nested **Page fields** instead. For selection by exclusion, combine a Page List with a Repeater > Page field.
## Structured Fields
Repeatable group of fields.
**Use for:**
* Feature lists
* Testimonials
* FAQ items
* Image galleries
* Card grids
**Component usage:**
```svelte theme={null}
{#each features as feature}
{feature.title}
{feature.description}
{/each}
{#each features as { icon, title, description }}
{title}
{description}
{/each}
```
Avoid naming your loop variable the same as a nested field. For example, if you have a nested `link` field, use `{#each links as item}` instead of `{#each links as link}` to prevent confusion when accessing `item.link.url`.
A "Features" block might use a repeater named `features` with fields: `title` (Text), `description` (Text), and `icon` (Image). Editors can add multiple features, and each gets the same set of fields.
Group of related fields displayed together.
**Use for:**
* Contact form (inputs + endpoint + submit\_label)
* Card content (title + description + cta\_link)
* Author info (name + bio + photo)
**Component usage:**
```svelte theme={null}
{author.name}
{author.bio}
```
## Utility Fields
Display-only field for showing helpful information to editors. Supports Markdown formatting and does not store any data—it only displays content in the editor interface.
**Use for:**
* Instructions for complex fields
* Explanatory notes about content requirements
* Documentation for editors
Consider combining Info fields with conditions to show context-specific help. For example, add an Info field that only appears when "Custom Layout" is selected from a Select field, explaining the custom layout options.
## Field Configuration
All fields share these base properties:
* **Type**: The field type (text, image, select, etc.)
* **Label**: Display name shown to editors
* **Key**: The variable name used in your component code
### Field-Specific Options
Some field types have additional configuration options:
**Select:**
* **Options**: Array of choices, each with value, label, and optional icon
**Image:**
* **Max Size (MB)**: Maximum file size in megabytes
* **Max Dimension (px)**: Maximum dimension (width or height) in pixels
**Page / Page List:**
* **Page Type**: Restrict selection to a specific page type
### Conditional Display
Fields can be conditionally shown or hidden based on other field values. Conditions are primarily designed for Select and Toggle fields, but also work with Text, URL, and Number fields.
When you add a condition ("Show if"):
* **Field**: Select which field to check (must be a preceding field at the same level)
* **Comparison**: Choose "Equals" or "Doesn't equal" (≠)
* **Value**: The value to compare against
Conditional fields help create dynamic editing interfaces that show relevant fields based on other selections. For example, to show an image field only when a "Show Image" toggle is enabled, the "Show Image" toggle must come first in the field list, then the image field below it with the condition. Fields can only reference fields that appear before them in the list.
## Best Practices
### 1. Choose the Right Field Type
Match field types to their purpose:
* ✅ Text for headlines
* ✅ Rich Text for formatted content
* ✅ Image for photos
* ✅ Select for categories
* ❌ Text for long articles (use Rich Text)
* ❌ Number for phone numbers (use Text)
### 2. Provide Defaults
Fields in Primo are globally available in your block code, but you should always handle empty/missing values:
```svelte theme={null}
{#if headline}
{headline}
{/if}
{#if show_cta}
{/if}
{headline || "Welcome"}
```
### 3. Use Helpful Labels
Clear labels help editors understand fields:
* ✅ "Featured Image"
* ✅ "Publication Date"
* ✅ "Author Name"
* ❌ "Field 1"
* ❌ "Data"
### 4. Add Help Text
Help text guides editors:
**Field**: "Featured Image"
**Help text**: "This image appears at the top of the post and in social media shares. Recommended size: 1200x630px."
## Field Combinations
### Common Patterns
**Hero Block:**
* `headline` (Text)
* `subheadline` (Text)
* `cta` (Link)
* `background_image` (Image, optional)
```svelte theme={null}
{#if background_image?.url}
{/if}
```
Start with simple field types (Text, Image, Link) and add complexity (Repeaters, Groups) as you need it. Most blocks only need a few well-chosen fields.
## Next Steps
Learn how to add fields to your components.
See how fields work in component code.
Use fields in page type configuration.
See fields in action with real examples.
# Keyboard Shortcuts
Source: https://docs.primo.build/reference/keyboard-shortcuts
Speed up your workflow with keyboard shortcuts for common actions.
Primo's keyboard shortcuts are built around the modifier key (⌘ on Mac, Ctrl on Windows/Linux). When you hold the modifier key, relevant UI elements appear showing you what other keys to press.
Throughout this page, we use ⌘ to represent Cmd on Mac or Ctrl on Windows/Linux.
## Global Shortcuts
These shortcuts work anywhere in Primo, **except when a dialog is open**:
| Shortcut | Action |
| -------- | -------------------------------------------- |
| ⌘P | Open publish dialog (press again to publish) |
| ⌘↑ | Navigate to previous page (same level) |
| ⌘↓ | Navigate to next page (same level) |
⌘P is your quickest way to publish. Press once to open the dialog, press again to confirm and publish.
## Page View Shortcuts
While viewing a page in the editor:
| Shortcut | Action |
| -------- | ------------------------------------------------ |
| ⌘E | Open section dialog (when hovering over a block) |
## Section & Block Editor Dialog
While editing a section or block in the dialog:
| Shortcut | Action |
| -------- | ------------------------------------ |
| ⌘S | Save changes |
| ⌘E | Toggle between Code and Content tabs |
| Esc | Close dialog |
**Code Editor** (when in the Code tab):
| Shortcut | Action |
| -------- | ------------------------- |
| ⌘1 | Switch to HTML pane |
| ⌘2 | Switch to CSS pane |
| ⌘3 | Switch to JavaScript pane |
| ⌘Enter | Format active code pane |
Primo uses CodeMirror for code editing. Some standard code editor shortcuts may work, but they're not guaranteed.
## Site Editor Dialog
While editing site configuration:
| Shortcut | Action |
| -------- | ------------ |
| ⌘S | Save changes |
## Tips for Efficiency
When you hold ⌘/Ctrl, Primo shows you contextual hints for what shortcuts are available in your current view.
Instead of clicking each pane tab individually, use ⌘1 for HTML, ⌘2 for CSS, and ⌘3 for JavaScript. This makes editing much faster.
When editing blocks, quickly switch between writing code and previewing content without reaching for the mouse.
Make it a habit to publish frequently. Press once to open the dialog, press again to publish. Quick iterations help catch issues early.
When managing many pages, use ⌘↑/↓ to move through them quickly.
## Next Steps
Learn about the publishing workflow
Master the page editor interface
# MCP Server
Source: https://docs.primo.build/reference/mcp-server
Let AI agents work inside Primo workspaces safely with the official primo-mcp server.
`primo-mcp` is the official [Model Context Protocol](https://modelcontextprotocol.io/) server for Primo. It gives AI coding agents (Claude Code, Cursor, Cline, etc.) live access to Primo's docs, schemas, and validators so they can produce correct block and page files on the first try.
The MCP server is optional. It's most useful when an agent is editing files in a Primo workspace export (a directory created by `primo new` or `primo pull`).
## What It Does
When an agent edits a `component.svelte`, `fields.yaml`, or page YAML, the MCP server:
* Validates the change against Primo's schema and reports specific errors
* Scaffolds new blocks and page types that are guaranteed to validate
* Resolves raw field values to the canonical shape Primo expects
* Surfaces the right docs section on demand instead of forcing the agent to grep
* Builds a preview so the agent can verify a change rendered before reporting done
## Installation
```bash theme={null}
npm install -g primo-mcp
```
## Configure with Claude Code
Register `primo-mcp` with the Claude Code CLI:
```bash theme={null}
claude mcp add --transport stdio primo -- primo-mcp
```
By default this adds the server to your current project. Use `--scope user` to add it globally, or `--scope project` to commit the config to `.mcp.json` so your team picks it up.
**Manual config (fallback):** add to `.mcp.json` (project) or `~/.claude.json` (user):
```json theme={null}
{
"mcpServers": {
"primo": {
"type": "stdio",
"command": "primo-mcp"
}
}
}
```
For other clients (Cursor, Cline, etc.), see the [MCP client docs](https://modelcontextprotocol.io/clients).
## Available Tools
| Tool | When the agent calls it |
| --------------------- | ------------------------------------------------------------ |
| `list_docs` | Discover available docs sections |
| `get_docs` | Read a specific docs section |
| `validate_block` | After editing any file in a `blocks//` folder |
| `validate_page` | After editing a `pages/*.yaml` or `page-types/*/config.yaml` |
| `validate_site` | After editing `site/head.svelte` or `site/foot.svelte` |
| `resolve_field_value` | When given a raw value that needs canonical shape |
| `scaffold_block` | Creating a new block from scratch |
| `scaffold_page_type` | Creating a new page type from scratch |
| `build_preview` | Verifying a change rendered visually |
The server ships with built-in instructions that tell the agent to run validators after every edit, so this happens automatically in well-behaved clients.
## Typical Agent Workflow
1. **Discover.** Agent calls `list_docs`, then `get_docs` for the section it needs.
2. **Scaffold.** Agent calls `scaffold_block` or `scaffold_page_type` to produce a known-valid skeleton.
3. **Edit.** Agent edits files in the workspace.
4. **Validate.** Agent calls `validate_block` / `validate_page` / `validate_site` after each edit and addresses every error before moving on.
5. **Preview.** Agent calls `build_preview` and loads the returned URL to verify visually.
## Folder Conventions It Enforces
The validators assume the standard workspace layout:
```
sites/my-site/
├── site.yaml
├── site/
│ ├── head.svelte
│ └── foot.svelte
├── blocks/
│ └── /
│ ├── component.svelte
│ ├── fields.yaml
│ └── content.yaml
├── page-types/
│ └── /
│ └── config.yaml
└── pages/
└── .yaml
```
A few invariants worth knowing:
* **Folder names are stable keys.** Renaming `blocks/hero/` to `blocks/banner/` breaks every page that references `hero`. Use a CMS rename or update references manually.
* **`site/head.svelte` is injected into ``.** Do **not** wrap its contents in `` yourself — the validator rejects this.
* **Field values have canonical shapes.** Image fields are `{ url, alt }`, links are `{ url, label }`, icons are SVG strings. Call `resolve_field_value` when in doubt.
## Without an Agent
You can also drive the same validators from `primo validate`:
```bash theme={null}
primo validate
primo validate --strict
```
This catches the same class of errors as the MCP validators, just at the workspace level instead of file by file.
## Next Steps
Run the CMS and sync files while you (or an agent) edit
Reference for the canonical field shapes the MCP validates
# Site Configuration
Source: https://docs.primo.build/reference/site-configuration
Configure site-wide settings, design systems, and global content.
Site configuration controls global settings that apply across your entire site. This includes design variables, global content fields, and custom HTML for scripts and analytics.
## Site Fields
**Site Fields** are global content that appears across multiple pages. They're perfect for content that needs to be consistent site-wide but may change over time.
### Common Use Cases
* **Logo**: Site logo that appears in the header
* **Navigation**: Main navigation menu items
* **Footer Content**: Footer links, copyright, social media
* **Contact Information**: Phone, email, address
* **Social Links**: Twitter, Facebook, LinkedIn profiles
* **SEO Defaults**: Default meta description, Open Graph image
### Creating Site Fields
Navigate to your site and click "Site" in the toolbar
Click "Add Field" and choose your field type
* **Name**: Internal identifier (e.g., `logo`, `nav_items`)
* **Label**: Display name for editors
* **Type**: Field type (text, image, repeater, etc.)
Reference site fields in your blocks using Site Field fields:
1. Add a Site Field field to your block
2. Set the field key (e.g., `logo`)
3. Select which site field to reference
```svelte theme={null}
{#if logo?.url}
{/if}
```
Site fields are reactive. When you update them, all blocks using them update automatically across your entire site.
## Head HTML
The **Head HTML** section lets you add custom HTML to the `` of every page on your site. Use this for analytics, fonts, meta tags, and other site-wide scripts.
Head HTML supports Svelte syntax and has access to site fields, allowing you to dynamically generate content based on your site configuration.
### Common Use Cases
**CSS Variables (Design System):**
Define a design system for your site using CSS Custom Properties. Define colors, typography, spacing, and other design tokens once, then use them across all your blocks.
```html theme={null}
```
Then reference these variables in your block CSS:
```svelte theme={null}
```
Use semantic naming (e.g., `--color-primary`) rather than descriptive names (e.g., `--color-orange`) so you can change values without renaming variables.
**Using Svelte:**
You can use Svelte syntax to dynamically inject content based on your site configuration.
```html theme={null}
{#if logo?.url}
{/if}
{#if site_name}
{/if}
{#if ga_id}
{/if}
```
To use site fields in Head HTML, first create the fields in Site → Fields, then reference them by name in your Head HTML code.
**Analytics & Tracking:**
```html theme={null}
```
**Filter admin routes from analytics:** Prevent `/admin` routes from being tracked:
```html theme={null}
{#if !window.location.pathname.startsWith('/admin')}
{/if}
```
**Custom Fonts:**
```html theme={null}
```
**Meta Tags:**
```html theme={null}
```
**Favicon:**
```html theme={null}
```
Head HTML is added to every page. Keep it minimal to avoid slowing down your site. Non-critical scripts (analytics, chat widgets, etc.) should be placed in Body Footer HTML instead to improve page load speed.
## Body Footer HTML
The **Body Footer HTML** section lets you add custom HTML before the closing `` tag of every page. Use this for scripts that should load after the page content.
Body Footer HTML only supports static HTML - it does not support Svelte syntax or access to site fields. If you need dynamic content, use the Head HTML section instead.
### Common Use Cases
**Chat Widgets:**
```html theme={null}
```
**Third-party Tools:**
```html theme={null}
```
Body Footer HTML loads after page content, making it better for non-critical scripts. This improves perceived page load speed.
## Next Steps
Learn how to create and use fields in blocks
Master page type slots for consistent layouts
# Troubleshooting
Source: https://docs.primo.build/reference/troubleshooting
Common issues and solutions for deploying and running Primo.
This guide helps you solve common issues when deploying or running Primo.
## Installation & Setup Issues
### Setup screen doesn't appear
**Symptoms:** Going to `/admin` redirects to sign-in instead of setup
**Causes:**
1. Superuser already exists (from environment variables or previous setup)
2. Volume persisted from previous deployment
**Solutions:**
If you set `PRIMO_SUPERUSER_EMAIL` and `PRIMO_SUPERUSER_PASSWORD`, a superuser was created automatically. Use those credentials to sign in.
```bash theme={null}
# Stop container
docker-compose down
# Check volume
docker volume inspect primo-data
# If you want to start fresh, delete the volume
docker volume rm primo-data
# Restart
docker-compose up -d
```
Deleting the volume deletes all your data. Only do this for fresh installs.
### Can't access after deployment
**Symptoms:** Browser can't reach your Primo instance
**Diagnostics:**
```bash theme={null}
# Check if container is running
docker ps | grep primo
# Check logs
docker logs primo
# Check if port is accessible
curl http://localhost:8080
```
**Solutions:**
```bash theme={null}
# Check why it stopped
docker logs primo
# Restart it
docker start primo
```
1. Check firewall allows port 8080 (or your configured port)
2. Verify port mapping in docker-compose.yml: `- "8080:8080"`
3. Check if another service is using port 8080:
```bash theme={null}
sudo lsof -i :8080
```
1. Check DNS settings point to your server IP
2. Wait for DNS propagation (up to 48 hours)
3. Test with server IP directly: `http://YOUR_IP:8080`
## Runtime Issues
### "No space left on device" error
**Cause:** Docker volume or host system out of disk space
**Solutions:**
```bash theme={null}
# Check Docker disk usage
docker system df
# Clean up unused images/containers
docker system prune -a
# Check host disk space
df -h
# If needed, resize volume or upgrade server
```
### Database locked errors
**Symptoms:** Errors mentioning "database is locked"
**Cause:** Multiple processes trying to write to SQLite simultaneously
**Solutions:**
1. Ensure only one Primo container is running:
```bash theme={null}
docker ps | grep primo
```
2. Restart the container:
```bash theme={null}
docker-compose restart
```
3. If persists, check for file permissions:
```bash theme={null}
docker exec primo ls -la /app/pb_data
```
### Publishing fails or times out
**Symptoms:** Publish button hangs or shows error
**Diagnostics:**
```bash theme={null}
# Check container logs during publish
docker logs -f primo
```
**Common Causes:**
Check browser console for JavaScript errors. Fix any Svelte compilation errors in your blocks.
Increase Docker memory limit:
```yaml theme={null}
services:
primo:
# ... other config
deploy:
resources:
limits:
memory: 2G
```
Check if your server can reach external CDNs (for Svelte compiler):
```bash theme={null}
docker exec primo curl -I https://esm.sh
```
## Performance Issues
### Slow editor loading
**Causes:**
* Large blocks with heavy dependencies
* Slow network connection
* Server resource constraints
**Solutions:**
* Remove unused dependencies
* Split large blocks into smaller ones
* Use lazy loading for images
Upgrade to a larger VPS or increase [Railway](https://railway.com/?referralCode=RCPU7k)/[Fly.io](https://fly.io) resources.
Configure gzip in your reverse proxy (see Deployment Guide).
Point your domain through Cloudflare for caching and compression.
### Slow published site
**Diagnostics:**
* Test site speed: [https://pagespeed.web.dev/](https://pagespeed.web.dev/)
* Check network waterfall in browser DevTools
**Common Fixes:**
* Optimize images (compress, use WebP)
* Minimize JavaScript bundle size
* Enable caching headers in reverse proxy
* Use a CDN
## Docker Issues
### Image pull fails
**Error:** `Error response from daemon: manifest for ghcr.io/primocms/primo:latest not found`
**Solutions:**
1. Check if you're spelling the image name correctly
2. Check your internet connection
3. Try pulling manually:
```bash theme={null}
docker pull ghcr.io/primocms/primo:latest
```
### Permission errors with volume
**Error:** `permission denied` when accessing `/app/pb_data`
**Solution:**
```bash theme={null}
# Check volume permissions
docker exec primo ls -la /app
# Fix permissions
docker exec primo chown -R 1000:1000 /app/pb_data
```
### Container keeps restarting
**Diagnostics:**
```bash theme={null}
# Check why it's restarting
docker logs primo --tail=50
# Check restart policy
docker inspect primo | grep -A 5 RestartPolicy
```
**Common Causes:**
* Application crash (check logs)
* Port already in use
* Out of memory
* Volume mount issues
## Authentication Issues
### Can't sign in
**Symptoms:** "Invalid credentials" error
**Checks:**
1. Verify email/password are correct
2. Check if account exists:
```bash theme={null}
docker exec primo ./primo admin ls
```
3. Reset password via PocketBase admin:
* Go to `https://your-domain.com/_/`
* Sign in with superuser
* Reset user password
### Locked out of admin
**Solution:** Create new superuser via command line:
```bash theme={null}
docker exec -it primo ./primo admin create \
admin@example.com \
new-secure-password
```
## Migration & Upgrade Issues
### Data loss after upgrade
**Prevention:**
Always backup before upgrading:
```bash theme={null}
# Backup pb_data
docker cp primo:/app/pb_data ./backup_$(date +%Y%m%d)
# Then upgrade
docker-compose pull && docker-compose up -d
```
**Recovery:**
Restore from backup:
```bash theme={null}
# Stop container
docker-compose down
# Restore backup
docker cp ./backup_20250101 primo:/app/pb_data
# Restart
docker-compose up -d
```
### Blocks not working after upgrade
**Cause:** Svelte version incompatibility
**Solution:**
1. Check release notes for breaking changes
2. Update block code to match new Svelte version
3. Re-publish all sites
## Network & SSL Issues
### SSL certificate errors
**Common Causes:**
Certbot should auto-renew. If not:
```bash theme={null}
sudo certbot renew
sudo systemctl restart nginx
```
Ensure all assets load via HTTPS. Check:
* Image URLs in blocks
* External scripts in head/foot HTML
* CSS file references
Make sure you're using certificates from a trusted CA (Let's Encrypt, not self-signed).
### CORS errors
**Symptoms:** Browser console shows CORS errors
**Cause:** Accessing Primo from a different domain than configured
**Solution:**
Ensure `PRIMO_APP_URL` matches the domain you're accessing from.
## Getting Help
If you're still stuck:
1. **Check the logs:**
```bash theme={null}
docker logs primo --tail=100 -f
```
2. **Search existing issues:**
* [GitHub Issues](https://github.com/primocms/primo/issues)
3. **Ask for help:**
* [GitHub Discussions](https://github.com/primocms/primo/discussions)
4. **File a bug report:**
* Include: OS, Docker version, Primo version, full error logs
* [Create Issue](https://github.com/primocms/primo/issues/new)
## Next Steps
Review deployment best practices
Configure Primo properly