Help & Documentation

Pubner gives you two ways to build a site: describe what you want and let the AI assistant build it, or open Expert mode and craft the templates yourself. This guide covers both — start with the basics, then move on to clean URLs, SEO and the data builders.

Building with AI

The fastest way to build with Pubner is to simply describe what you want. In your site panel, switch the editor to AI mode, type a request in plain language, and the assistant builds the page for you — showing a live preview next to the chat as it works.

The assistant can create and edit pages, layouts and partials, and wire up routes for you. Keep refining in conversation — “make it wider”, “add a testimonials section” — and nothing touches your live site until you press Save. At any point you can switch to Expert mode to fine-tune the generated code by hand.

Example prompts:

{# Describe a page the way you'd explain it to a designer #}
“Create a contact page with a form and a map.”
“Add a blog section with categories and pagination.”
“Make the header sticky and add a language switcher.”
Every change is previewed before it goes live, and earlier versions are kept — so you can experiment freely and roll back whenever you need to.

Expert mode: your theme files

A Pubner theme is just Twig templates and one YAML config — no PHP. There are four kinds of files: pages/ (one template per URL), partials/ (reusable chunks), layouts/ (the HTML shell around every page), and config.yml (which URL loads which page). You edit them directly in the panel and switch between Expert and AI mode whenever you like.

Because themes contain no PHP, a template can never run arbitrary code — the worst a mistake can do is render broken HTML. Every save is validated first, and previous versions are kept so you can always roll back.

How a theme is organized:

my-theme/
├─ config.yml {# routes #}
├─ layouts/
│  └─ layout.twig {# the HTML shell #}
├─ pages/
│  ├─ home.twig {# one file per page #}
│  └─ post.twig
└─ partials/
   ├─ header.twig {# reusable chunks #}
   └─ footer.twig

Pages

Pages are the most crucial elements of your Pubner website. Every URL route corresponds to a specific Page template. When a visitor requests a URL, the system loads the associated Page, processes its Twig code, and injects the output into the defined Layout.

Think of a Page as the "body" of your content. It contains only what is unique to that specific URL (like an article's text, or a specific form), while the global elements (like the navigation bar or footer) are handled by Layouts and Partials.

Render the current page content inside a layout:

{% page %}

Partials

Partials are reusable chunks of HTML and Twig logic. By breaking your design into partials, you keep your codebase DRY (Don't Repeat Yourself) and highly organized. If you need to update your site's footer, you only change one partial file, and it instantly updates across the entire site.

Common use cases for partials include headers, footers, sidebar widgets, product cards, or complex UI components that are used on multiple different pages.

Include a partial in your layout or page:

{% partial 'navigation.twig' %}

Pass data into a partial:

{% partial 'card.twig' product=item, featured=true %}
Named parameters become variables inside the partial — here product and featured are available directly in card.twig.

Layouts & Placeholders

Layouts define the core HTML wrapper of your site (the <html>, <head>, and <body> tags). A single project can have multiple layouts (e.g., one for the public site, another for a customer dashboard).

To make layouts flexible, Pubner uses Placeholders. Placeholders act as "hooks" where individual pages can inject custom content, such as specific SEO meta tags, extra CSS stylesheets, or tracking scripts.

Define a placeholder in your layout:

{% placeholder styles %}

Push content to a placeholder from a specific Page:

{% put styles %}
    <style> .custom-hero { background: blue; } </style>
{% endput %}
Content placed between the put tags will be automatically injected exactly where the placeholder is defined in your layout.

Variables & Control Logic

Pubner is powered by the modern Twig template engine. This allows you to write clean, secure, and highly dynamic templates without writing pure PHP code.

You can easily output variables, apply filters to transform text, loop through arrays of data (like blog posts or products), and use conditional logic to show or hide elements based on the current state or user session.

Output variables and use conditions:

{% if site().name %}
    <h1>Welcome to {{ site().name }}</h1>
{% endif %}

Loop over a collection:

{% for post in records({'area': 'blog'}).get() %}
    <article>{{ post.name }}</article>
{% else %}
    <p>No posts yet.</p>
{% endfor %}

Clean URLs & Route Binding

In config.yml, every route is built from segments instead of a hand-typed path. A segment is either a fixed word (a literal, like news) or a captured value (a param, like a slug). A segment can also be bound to your content — an area, a category or a record — which Pubner then looks up automatically by its slug.

When a segment is bound, Pubner loads that item for you and injects it into the page: the record is available as record, plus area and category if you bind them. Mark one binding as primary and its fields also spread to the top level, so you can write name on its own. If the slug matches nothing, Pubner returns a clean 404 automatically — no manual lookup, no error page to build.

A bound detail route in config.yml:

- page: post.twig
  layout: layout.twig
  segments:
    - { type: literal, value: news, bind: area }
    - { type: param, name: slug, bind: record, primary: true }
This serves /news/{slug}. The news area is validated, the record is loaded by slug, and a missing slug 404s on its own. Prefer not to touch YAML? The panel's visual URL builder writes these segments for you.

Use the bound model in post.twig:

{# record is injected — no query needed #}
<h1>{{ record.name }}</h1>
{{ record.content | raw }}

{# the primary binding also spreads to the root #}
<title>{{ name }}</title>
No set, no firstOrFail(). The bound record arrives ready to use — and it's the same model the SEO block uses for the page title.

Managed SEO

One function — seo_head() — renders your entire managed <head> SEO block: the <title>, the meta description, a robots tag when indexing is turned off, and your analytics snippet. Drop it into your layout's <head> once and it stays correct on every page.

Titles fill themselves in. On a bound detail page, seo_head() uses the record's meta_title and meta_description, falling back to its name and then the site name. To override on a specific page, call seo_title() / seo_description() from the page body. The brand suffix, indexing on/off and analytics ID are set once in Site settings (seo_title_suffix, seo_indexing, seo_ga). Need the pieces separately? seo('title'), seo('description'), seo('robots') and seo('analytics') each return one part.

Add the managed head to your layout:

<head>
    {% placeholder meta %} {# your charset / viewport block #}
    {{ seo_head() }}
</head>
One call renders the title, description, robots and analytics tags together.

Override title &amp; description on a page:

{# call these from the page body #}
{{ seo_title('Summer Sale — up to 50% off') }}
{{ seo_description('Our biggest discounts of the year, ending Sunday.') }}
Bound detail pages usually need none of this — the record's meta_title / meta_description are used automatically.

Redirects & Navigation

Navigating users around your application should be safe and predictable. Pubner provides a suite of Twig functions to handle internal and external redirects directly from your templates.

Additionally, you can use the request_is() helper to easily determine the active state of navigation menus, supporting wildcard patterns and localized routes.

Available Redirect Helpers:

{# Redirect to a specific URL #}
{{ redirect('/about') }}

{# Redirect to the previous page #}
{{ redirect_back() }}

{# Redirect only if the user is logged in (Guest protection) #}
{{ redirect_if_auth('/dashboard') }}

{# Safe redirect (Prevents external open-redirect vulnerabilities) #}
{{ redirect_safely(user_provided_url) }}

Check Active Route (Great for Navbars):

{% if request_is('about*', 'contact') %}
    {# This will be true for /about, /about/team, and /contact #}
    <a class="active">Company</a>
{% endif %}

Build Locale-Aware URLs with page_url():

{# Link to a record detail page (locale-aware) #}
<a href="{{ page_url('/news', record.slug) }}">Read more</a>

{# Link to a static page #}
<a href="{{ page_url('/about') }}">About Us</a>

{# Single-language → /news/my-post   Multilingual → /ua/news/my-post #}

Forms, Assets & UI Helpers

Building interactive elements like contact forms or loading static assets (CSS/JS) is streamlined through our built-in helpers. They automatically handle security (CSRF tokens), routing, and session state.

We also provide an automated paginator generator that will inject a clean, responsive pagination component wherever you need it, creating the necessary partial files on the fly if they don't exist.

Initialize a secure form:

{{ form({'action': 'contact.send', 'redirect': '/thanks'}).open() }}
    {# Form inputs go here #}
{{ form().close() }}

Add inputs to your form:

{{ form({'action': 'contact.send'}).open() }}
    {{ form().text('name') }}
    {{ form().email('email') }}
    {{ form().textarea('message') }}
    {{ form().select('topic', {'sales': 'Sales', 'support': 'Support'}) }}
    {{ form().submit('Send message') }}
{{ form().close() }}
Inputs repopulate from the last submission and show validation errors automatically. Also available: password(), file(), checkbox(), radio(), hidden(), label() and button().

Check for form success/fail alerts:

{% if success() %}
    <div class="alert-success">Message sent successfully!</div>
{% endif %}

Generate Pagination Links:

{{ paginator_links(records) }}
Pass a paginated collection here. Pubner will automatically generate a pagination.twig partial in your theme if it doesn't exist.

Localization & Languages

Pubner is built from the ground up for global audiences. The native localization system allows you to translate static text strings and build intelligent language switchers with minimal effort.

The lang() object gives you full control over the URL structure, automatically injecting or updating the language prefix in the current URL while preserving query parameters.

Translate text strings:

{{ t('btn_checkout', 'Proceed to Checkout') }}
Using a default fallback string ensures your layout doesn't break even if the translation key hasn't been saved in the database yet.

Language Switcher Logic:

{# Get current language code (e.g., 'en') #}
{{ lang().current() }}

{# Check if specific language is active #}
{% if lang().is('ua') %}...{% endif %}

{# Generate a URL for a different language (Keeps current path) #}
<a href="{{ lang().url('ua') }}">Українська</a>

Global Data Helpers

To build a truly dynamic platform, you need access to the environment. Pubner exposes global helpers to retrieve the current Site's configuration, authenticated user details, and URL payload data.

These helpers fetch data instantly without the need to write complex backend controllers for every single view.

Site Configuration Data:

Site Name: {{ site('name') }}
Primary Email: {{ site().email }}
Available Locales: {{ site().locales | join(', ') }}

Authenticated User &amp; Requests:

{# Get logged-in user name (Returns null if guest) #}
{{ auth('name') }}

{# Get current URL Path parameter #}
{{ path('slug') }}

{# Get $_GET or $_POST request data #}
{{ request('search_query') }}

The Record Builder

The true power of a CMS is querying data. The Builders allow you to query your database records, categories, and areas directly from the template layer. They are extremely fast and automatically respect your site's multi-tenant boundaries.

There are two ways to query. Pass a filter array — records({...}) — for quick, declarative lookups, or chain methods fluently for more control. Either way, finish with .get() (an array of items), .first(), .paginate() or .count().

Filter-array style (e.g., Blog Posts or Products):

{% set posts = records({
    'area': 'blog',
    'limit': 10,
    'sort': '-published_at'
}).get() %}
Pass 'area' with an area's slug to scope the query. For sort, prefix a field with - for descending order (e.g. -published_at) or omit it for ascending.

Fluent chaining for finer control:

{% set posts = records()
    .where('category', 'guides')
    .search(request('q'))
    .latest()
    .limit(6)
    .get() %}
Filter with .where() / .whereNot() / .whereIn() / .like() / .search(); order with .latest() / .oldest() / .sort('-field') / .rand(); and use .with() to eager-load related data.

Paginate results:

{% set page = records({'area': 'blog'}).paginate() %}
{% for post in page.data %} ... {% endfor %}
{{ paginator_links(page) }}
.paginate() returns the items in page.data plus paging metadata; pass the whole result to paginator_links() to render the controls.

Query Categories and Areas:

{# Fetch Categories for a specific Area #}
{% set blog_categories = categories({'area': 'blog'}).get() %}

{# Fetch a specific structural Area #}
{% set portfolio_area = areas({'slug': 'portfolio'}).first() %}

Rendering Record Content

Records and categories store their rich content as structured blocks, written in the panel's block editor — separately for each language. Your templates never touch the raw blocks: the platform renders them into clean HTML for the current locale.

Read the rendered HTML from the content field and output it with Twig's raw filter — the same for records and categories. On a record's own page (the record bound to the route) the short content variable is available directly, without a prefix.

Output a record's content:

{# Fetch the record and render its content #}
{% set post = records({'slug': path('slug')}).first() %}

<article>
    {{ post.content | raw }}
</article>
The HTML comes only from the editor's whitelisted blocks — headings, paragraphs, lists, images and links — so it is safe to output with raw.

Telegram assistant

Connect your Telegram once and the site starts coming to you: new inquiries arrive as messages, the weekly digest reports your numbers, and the AI assistant answers your requests right in the chat — it can write posts, translate, update content and draft replies to inquiries.

To connect: open Panel → Profile, press Connect next to Telegram and tap Start in the chat that opens. Every admin connects their own chat; what each person sees respects their panel permissions. Send /stop to the bot any time to unlink.

The assistant never acts silently: anything that changes your live site or emails a real person comes back as a preview with ✅ Confirm / ❌ Cancel buttons. Photos you send land in your site's Cloud; a voice note becomes a command.

AI chat widget for visitors

Your site can answer visitors itself. The AI chat widget knows your published content — services, prices, posts — and replies in the visitor's language, 24/7. It only ever sees what is already public on your site.

Turn it on in Settings → AI chat widget: pick the accent color, write a greeting per language, set a daily limit — and the widget appears on your site. It runs on your AI balance; if the balance is empty the widget simply hides.

Content autopilot

The autopilot writes posts on a schedule you set: “an article about coffee trends every Friday”, “news from our industry twice a month”. It can search the live web for fresh facts and picks matching photos.

Everything it writes is saved as a draft — nothing is published without you. Set it up in Settings → Content autopilot; you'll get a notification (and a Telegram ping, if connected) whenever a new draft is ready for review.

Site email

Your site sends email on your behalf — notifications about new inquiries and replies you approve. For this it uses your own email provider, so messages come from your address and land in inboxes, not spam.

Set it up in Settings → Mail config: pick a provider (any SMTP, Brevo, SendGrid, Mailgun), paste its credentials and hit Send test email. Without this, the site simply doesn't send mail — inquiries still arrive in the panel and Telegram.