How zanabler.com is built on Zana

Eight modules, two languages, a back office built from content types, forms that survive a mail failure, and one command to deploy to shared hosting. A tour of the site you are reading.

A web page in a browser window, connected to eight module tiles, with a check mark.

We tell clients that Zana is ready for real websites, so the honest test was to build our own on it. Zanabler.com is a bilingual site with a back office, case studies, articles, a project request form and a newsletter. It runs on shared cPanel hosting. This article walks through how it is put together.

Eight modules, one per part of the site

The application is a list of modules in config/modules.php:

return [
    Modules\Site\SiteModule::class,
    Modules\Insights\InsightsModule::class,
    Modules\Admin\AdminModule::class,
    Modules\Services\ServicesModule::class,
    Modules\Work\WorkModule::class,
    Modules\Products\ProductsModule::class,
    Modules\Pages\PagesModule::class,
    Modules\Contact\ContactModule::class,
];

Each module is a folder with its routes, controllers, templates, translations, database migrations and back-office screens. Insights holds the articles, including this one. Work holds the case studies, and Contact holds the project form and the newsletter. When we change how articles work, we open one folder.

Modules talk to each other only through small contracts. The home page shows the latest articles, but it does not query the articles table. It asks the Insights module for an ArticleCatalog, and Insights decides how to answer.

Two languages, in the URL

Every public page exists in English and French, with its own address in each language:

$router->localized(['en' => '/insights', 'fr' => '/articles'], [InsightsController::class, 'index'])->name('insights');
$router->localized(['en' => '/insights/{slug}', 'fr' => '/articles/{slug}'], [InsightsController::class, 'show'])->name('insights.show');

Content is stored the same way. An article is one record, with one translation per language, and each translation has its own title, summary, text and slug. The language switcher and the hreflang links come from those slugs. If an article has no French version yet, the switcher leads to the French list of articles instead of a missing page.

A back office made from content types

Most of the site's content is edited in the back office: services, case studies, articles, team members, pages and newsletters. We did not write a set of screens for each one. A content type describes the fields once:

return new ContentType(
    name: 'articles',
    table: 'articles',
    key: 'article_id',
    fields: [
        Field::select('category', self::CATEGORIES),
        Field::image('cover_image'),
        Field::text('author'),
        Field::checkbox('featured'),
        Field::text('title', translated: true, required: true),
        Field::text('slug', translated: true),
        Field::textarea('summary', translated: true),
        Field::markdown('body'),
    ],
    order: 'published_at DESC',
);

From that description, the back office builds the list, the forms (with one section per language), validation, drafts, signed preview links and publishing. Every change is recorded in an audit trail, so we can see who changed what, and when.

Articles are written in Markdown. Zana's Markdown renderer escapes raw HTML and removes unsafe links, so a pasted script stays text and never reaches a visitor's browser.

Forms that keep working

The project request form is where a new client first reaches us, so it must not lose a message. Requests are saved in the database before any email is sent. If the mail server is down, the visitor still sees a confirmation, the request is waiting in the back office, and the failure is written to the error log. The form is protected by CSRF tokens and rate limiting.

The newsletter respects the limits of shared hosting. A scheduled task sends queued newsletters in small batches every five minutes, and it never runs twice at the same time:

$schedule->call([NewsletterDispatcher::class, 'sendBatch'])->everyFiveMinutes()->name('newsletter batch')->withoutOverlapping();

On the server, a single cPanel cron job runs Zana's scheduler, and the scheduler takes care of the rest.

Tested like a visitor

Close to sixty tests open the site as a browser would. They follow links, submit forms and check what appears on the page:

$this->browser()->get('/fr/services/applications-mobiles')
    ->assertOk()
    ->assertSelector('h1', 'Applications mobiles')
    ->assertSelector('link[rel=alternate][hreflang=en][href="http://localhost:8000/en/services/mobile-apps"]');

The tests run in memory, with their own settings file. A developer's local configuration never leaks into them, and no real email is ever sent. A test can still check that the right message would have gone out.

Deployed to shared hosting

Many of our clients host on cPanel, often without SSH, so we wanted that path to be just as easy. One command prepares the upload:

php bin/console release:build

It writes two archives from the last commit, with the production dependencies already installed. One contains the application, placed beside public_html so that settings and code are never reachable from the web. The other goes into public_html. In production the container is compiled and the configuration cached. The application refuses to start if debug mode is on or a secret is still a placeholder.

The site also sends strict security headers, including a content security policy. Images are served in responsive sizes, and every page has structured data and an entry in the sitemap.

What we took from it

Building our own site on Zana improved the framework. When we needed this site to log server errors and keep its forms working through a mail failure, we did not keep that code here. It went into Zana 2.6, where every project can use it.

That is the point of using our own tool. If something is awkward for us, it is awkward for our clients, and we fix it in the framework rather than working around it in one project.

If you need a website or business application built this way, tell us about your project. If you want to build it yourself, Zana is open source.

Get new articles by email

One email a month. You confirm your address before anything is sent.