Admin Template Migration

Verze:

05. 04. 2026

Zodpovědná osoba:

Rafael Quinteros

Poslední aktualizace:

05. 04. 2026, rafael.quinterosv@gmail.com

Introduction

Step-by-step guide for migrating legacy Nette Framework projects to the Demoweb template services architecture (Nette 3.1, PHP 8.1). Follow steps in order. Do not skip steps.

Prerequisites

Requirement Details
Demoweb template base Available at ~/projects/81/Demoweb/
PHP 8.1 Target runtime for all migrated projects
Database access Local MySQL with dev_rafa credentials
Reference project SynekAuto or penzionusynka — already migrated
GitLab access Read access to old repo, write to feature branch
Node.js 18 Required for npm install && npx mix

Migration Overview

Step Name Output
0 Project Setup Project cloned to correct folder
1 Backup {ProjectName}_old copy exists
2 Copy Demoweb New base installed, .git restored
3 Database DB structure aligned with demoweb_services
4 Supplement IDs IDs verified, ???: applied
5 Front Presenters BasePresenter + HomepagePresenter migrated
6 Latte Templates Templates copied, Latte 2.11 fixes applied
7 LESS → SASS All styles converted to SCSS
8 webpack.mix.js Assets compile successfully
9 config.neon Services and DB configured
10 GitLab CI Deploy config verified
11 Testing All pages return HTTP 200, no Tracy errors
12 Commit & Push Feature branch pushed, deploy verified

Step 0 — Project Setup

Clone the project and determine the correct target folder based on PHP version.

Ask the user

  1. What is the Git clone URL of the project?
  2. What is the project name? (e.g., autosynek, penzionusynka)

Folder selection

Check composer.jsonrequire.php. Target is always ~/projects/81/ regardless of current version.

PHP version in composer.json Current folder Target folder
>= 8.1 ~/projects/81/ ~/projects/81/{ProjectName}/
>= 7.4 ~/projects/74/ ~/projects/81/{ProjectName}/
>= 7.1 ~/projects/71/ ~/projects/81/{ProjectName}/

If the project already exists in a non-81 folder, that becomes the "old" reference used in Step 1.

Step 1 — Backup

Create a clean copy of the old project before overwriting anything.

cp -r ~/projects/{version}/{ProjectName} ~/projects/{version}/{ProjectName}_old
rm -rf ~/projects/{version}/{ProjectName}_old/.git
rm -rf ~/projects/{version}/{ProjectName}_old/.idea
rm -rf ~/projects/{version}/{ProjectName}_old/node_modules

Verification

  • {ProjectName}_old/ exists and contains source files
  • .git, .idea, node_modules are NOT present in the backup

Step 2 — Erase and Copy Demoweb

Replace project contents with the Demoweb template base, then restore Git history and local config.

Before erasing — save these files

  • .git/ directory
  • app/config/config.local.neon
  • .gitlab-ci.yml
cp -r ~/projects/81/Demoweb/* ~/projects/81/{ProjectName}/
cp ~/projects/81/Demoweb/.htaccess ~/projects/81/{ProjectName}/
cp ~/projects/81/Demoweb/.gitignore ~/projects/81/{ProjectName}/
cp ~/projects/81/Demoweb/.deploy_ignore ~/projects/81/{ProjectName}/

mkdir -p temp/cache && chmod 777 temp temp/cache

Do NOT copy .git, .idea, or node_modules from Demoweb. Restore the project's own .git and .gitlab-ci.yml after copying.

Step 3 — Connect Database and Compare Structure

Align the project database with the demoweb_services schema.

Local database connection

database:
    dsn: 'mysql:host=mysql-rafa;dbname={ProjectName}'
    user: dev_rafa
    password: 'WEKdHaYVsUyO/09aTSZKQQ=='

Passwords containing =, :, or special characters must be wrapped in quotes.

Compare tables

diff \
  <(mysql -u dev_rafa -p {ProjectName} -e "SHOW TABLES" | sort) \
  <(mysql -u dev_rafa -p demoweb_services -e "SHOW TABLES" | sort)

What to import

  • Missing tables from demoweb_services (use --single-transaction, SET FOREIGN_KEY_CHECKS=0)
  • Missing columns via ALTER TABLE
  • Missing settings_config rows: ADMIN_TAB_*, ADMIN_LOGO_SRC, IS_ESHOP
  • Missing settings_translation rows
  • Missing module rows

Step 4 — Check MenuRepository Supplement IDs

Verify supplement ID mapping and fix the null coalescing operator issue.

The menuRepository contains a $supp_x_config array mapping supplement names to IDs. These must match the actual supplement table in the database.

CRITICAL: Old databases store empty strings "" instead of null. Replace ?? with ?: (elvis operator) in:

  • app/presenters/Admin/ApiPresenter.php — all $names[...] ?? ... patterns
  • All front filters that access $item['supplements'][...]
Wrong Correct
$item['supplements']['Name'] ?? 'fallback' $item['supplements']['Name'] ?: 'fallback'

Step 5 — Move Front Presenters

Do NOT replace Demoweb's BasePresenter with the old one. Start from Demoweb's version and add project-specific logic on top.

BasePresenter — what to modify

  • Modify $menus and $pages static arrays to match old project values
  • Add project-specific injected services (filters, repositories)
  • Replace beforeRender() content with project-specific logic
  • Add createComponentContact() and createComponentSearch() if needed
  • Add paginate(), createComponentVisualPaginator(), lang() methods
  • Keep the Translator trait from Demoweb
  • Remove unused Demoweb dependencies: WebMutations, CookieSettings, Instagram, etc.

Presenters to remove (unless used by the project)

  • AccountPresenter
  • EshopPresenter
  • GoPayPresenter
  • InstagramPresenter
  • PohodaPresenter
  • TypeaheadPresenter

RegisterAndLoginPresenter

Use Demoweb's version. Override beforeRender() to NOT call parent::beforeRender() — set $web directly from settings instead.

Step 6 — Move Front Latte Templates

rm -rf app/presenters/Front/templates
cp -r {ProjectName}_old/app/presenters/Front/templates app/presenters/Front/templates

cp ~/projects/81/Demoweb/app/presenters/Front/templates/RegisterAndLogin/*.latte \
   app/presenters/Front/templates/RegisterAndLogin/

Latte 2.11 Compatibility Fixes

Apply to ALL .latte files in app/presenters/Front/templates/:

Old syntax New syntax
{!$var} {$var|noescape}
{!$var|filter} {$var|filter|noescape}
{? expr} {do expr}
{$template->elixir('path')} {='path'|elixir}
{$template->imageExist('path', 'default')} {('path'|imageExist:'default')}

Use a PHP script for replacements — sed/perl often breaks with quotes in Latte syntax.

<?php
$files = glob('app/presenters/Front/templates/**/*.latte');
foreach ($files as $file) {
    $c = file_get_contents($file);
    $c = preg_replace('/\{!\$([^}|]+)\|([^}]+)\}/', '{$$1|$2|noescape}', $c);
    $c = preg_replace('/\{!\$([^}]+)\}/', '{$$1|noescape}', $c);
    $c = str_replace("{? ", "{do ", $c);
    file_put_contents($file, $c);
}

Front Filters — required fixes

Old New
extends Nette\Object use Nette\SmartObject; trait
$item['supplements']['X'] ?? $fb $item['supplements']['X'] ?: $fb
Html::add() Html::addHtml()
->class('x') Html::el('div class="x"')
->href('x') ->setAttribute('href', 'x')

Step 7 — Convert LESS to SASS

  1. Copy old LESS files to resources/sass/{project}-front/
  2. Rename .less.scss, add _ prefix for partials
  3. Install bootstrap-sass@3.3.7
  4. Replace Bootstrap LESS with: @import "~bootstrap-sass/assets/stylesheets/bootstrap";

Syntax conversion table

LESS SCSS
@variable $variable (not @media, @import, @keyframes)
@{variable} #{$variable}
~"string" string
.mixin-name() { } @mixin mixin-name() { }
.mixin-name; @include mixin-name;
spin() adjust-hue()
@import (inline) "file" @import "file"
@import "file.less" @import "file"

Step 8 — Update webpack.mix.js

// front - {ProjectName}
mix.sass('resources/sass/{project}-front/app.scss', 'assets/css/front.css')
    .combine([
        'resources/js/jquery.js',
        'resources/js/bootstrap.js',
        'resources/js/netteForms.js',
        // add project-specific JS from old Gulpfile.js
    ], 'www/assets/js/front.js');

// admin (keep Demoweb config)
mix.sass('resources/sass/admin/app.scss', 'assets/admin/app.css');
fnm use 18 && npm install && npx mix --production

Step 9 — Update config.neon

  • Use Demoweb's config.neon as base
  • Update database credentials for production
  • Quote passwords with special characters
  • Add project-specific Front\Filter\* services
  • Keep all Demoweb admin services

Step 10 — Update .gitlab-ci.yml

Variable Value
PROJECT_NAME Must match the old project name exactly
DEVELOP_DOMAIN czechdevelo
DEVELOP_DIR /www/hosting/czechdevelo.cz
chown $DEVELOP_USER:$DEVELOP_USER and $PRODUCTION_USER:$PRODUCTION_USER

CRITICAL: Verify DEVELOP_DOMAIN and DEVELOP_DIR against the real server before the first push. Wrong values here caused 2 of the 3 fix commits in penzionusynka.

Step 11 — Testing

Verify all pages return HTTP 200 with no Tracy errors.

# Front pages
for page in "/" "cs/m-{id}-{name}"; do
    STATUS=$(curl -sk -o /dev/null -w "%{http_code}" \
        "https://{project}.rafa.lat04.vas-server.cz/$page" -u claude:radegast)
    echo "$STATUS → /$page"
done

# Admin pages
for page in "admin/login" "admin" "admin/menu" "admin/fotogalerie" \
            "admin/eshop" "admin/nastaveni" "admin/uzivatele"; do
    STATUS=$(curl -sk -o /dev/null -w "%{http_code}" -L \
        "https://{project}.rafa.lat04.vas-server.cz/$page" -u claude:radegast)
    echo "$STATUS → /$page"
done

# Check Tracy errors
curl -sk "URL" -u claude:radegast | grep -c "tracy-section--error"

Expected result: 0 Tracy errors on every page.

Step 12 — Commit and Push

feat({ticket-id}): migrate project to Demoweb template services

Steps 0–11 completed. Database aligned, templates converted to
Latte 2.11, LESS migrated to SCSS, CI deploy configured.

Refs: https://app.freelo.io/task/{ticket-id}

After pushing, verify the deploy to czechdevelo completes without errors and the site loads correctly.

Common Gotchas

1. Cache permissions

Apache runs as www-data, CLI as rafa. When cache breaks after deploy, recreate the temp directory:

mv temp temp_old && mkdir -p temp/cache && chmod 777 temp temp/cache

2. NEON password parsing

Passwords containing =, :, or special characters must be quoted in .neon files or the parser will fail silently.

3. Supplements empty strings vs null

Old databases store "" instead of null. Use ?: (elvis), never ?? (null coalescing) when accessing supplement values.

4. Html::add() removed in Nette 3.x

Use addHtml() or addText() instead. Html::add() will throw a fatal error.

5. Html magic methods

->class(), ->href() cause errors in Nette 3.x. Use Html::el('div class="x"') or ->setAttribute('href', 'x').

6. Latte {!} noescape

In Latte 2.11, {!} means boolean negation inside filters, NOT noescape. Always use |noescape explicitly.

7. $template->filter() in Latte 2.11

Does not work. Use {='value'|filter} or {$var|filter} syntax instead.

8. Route::$defaultFlags removed

Delete the line entirely — it does not exist in Nette 3.x.

9. Route constants renamed

Route::FILTER_IN/OUTRoute::FilterIn/FilterOut. Using RouteAlias::FILTER_IN also works.

10. Security classes renamed

Nette\Security\IdentitySimpleIdentity. IAuthenticatorAuthenticator. Passwords must be injected, not used statically.

Lessons from penzionusynka fix commits

  • temp/log dirs in CI deploy — Add mkdir -p temp/cache log && chmod 777 temp temp/cache log to the deploy job, not just locally.
  • chown must match reference project — Check SynekAuto's .gitlab-ci.yml for the exact chown syntax before writing your own.
  • DEVELOP_DOMAIN and DEVELOP_DIR — Verify against the real server before the first push.

Migration Checklist

Step 0 — Project Setup

  • Git clone URL obtained
  • Project name confirmed
  • PHP version checked in composer.json
  • Project placed in ~/projects/81/{ProjectName}/

Step 1 — Backup

  • {ProjectName}_old/ created
  • .git, .idea, node_modules removed from backup

Step 2 — Copy Demoweb

  • Demoweb copied to project folder
  • .git restored from original project
  • config.local.neon restored
  • .gitlab-ci.yml restored from old project
  • temp/cache created with chmod 777

Step 3 — Database

  • config.local.neon points to correct DB
  • Tables compared against demoweb_services
  • Missing tables imported
  • Missing columns added via ALTER TABLE
  • settings_config rows imported
  • settings_translation rows imported
  • module rows imported

Step 4 — Supplement IDs

  • IDs in $supp_x_config verified against DB
  • All ?? replaced with ?: in ApiPresenter and filters

Step 5 — Front Presenters

  • BasePresenter adapted from Demoweb base
  • HomepagePresenter copied and updated
  • Unused Demoweb presenters removed
  • RegisterAndLoginPresenter uses Demoweb version

Step 6 — Latte Templates

  • Templates copied from old project
  • RegisterAndLogin templates from Demoweb
  • {!$var}|noescape applied
  • {? expr}{do expr} applied
  • Elixir and imageExist filter syntax updated
  • Front filters updated (SmartObject, Html methods)

Step 7 — LESS → SASS

  • LESS files copied to resources/sass/{project}-front/
  • Files renamed to .scss
  • bootstrap-sass@3.3.7 installed
  • LESS syntax converted to SCSS

Step 8 — webpack.mix.js

  • SASS entry point configured
  • JS files combined correctly
  • npx mix --production succeeds without errors

Step 9 — config.neon

  • Based on Demoweb config
  • Production DB credentials set
  • Project-specific filters registered

Step 10 — GitLab CI

  • PROJECT_NAME matches old project
  • DEVELOP_DOMAIN = czechdevelo
  • DEVELOP_DIR = /www/hosting/czechdevelo.cz
  • chown matches SynekAuto reference
  • mkdir -p temp/cache log in deploy job

Step 11 — Testing

  • All front pages return HTTP 200
  • All admin pages return HTTP 200
  • All zeroadmin pages return HTTP 200
  • Zero Tracy errors on all pages

Step 12 — Commit & Push

  • Conventional commit message with ticket ID
  • Pushed to feature branch
  • Deploy to czechdevelo verified
  • Site loads correctly on czechdevelo domain

Reference Projects

SynekAuto

First successfully migrated project. Primary reference for CI/CD configuration and server ownership.

  • .gitlab-ci.yml — exact chown syntax and deploy variables
  • File ownership on the server — match this exactly
  • app/presenters/Front/BasePresenter.php — structure reference

penzionusynka

Second migrated project. Useful for understanding what can go wrong in CI deploy (branch: 28992001-new-admin).

  • .gitlab-ci.ymlmkdir -p temp/cache in deploy job
  • app/model/Supplement.php?: operator usage
  • app/presenters/Admin/ApiPresenter.php — empty string guard pattern

Commit history reference (penzionusynka)

# Commit What it fixed
1 feat(28993490): migrate project to Demoweb template services Steps 0–11
2 fix(28993490): update develop deploy domain to czechdevelo Wrong DEVELOP_DOMAIN
3 fix(28993490): fix deploy config and ensure temp/log dirs exist Missing mkdir in CI
4 fix(28993490): fix file ownership in CI deploy to match SynekAuto Wrong chown

Claude Code Prompt

Copy and paste this prompt into Claude Code to start a new migration. The prompt will guide Claude through all 12 steps in order.

You are migrating a legacy Nette Framework project to the new Demoweb template services architecture (Nette 3.1, PHP 8.1).

Follow these steps IN ORDER. Do not skip steps. Ask the user before proceeding to the next major step.

## Step 0: Project Setup

Ask the user:
1. What is the Git clone URL of the project?
2. What is the project name? (e.g., autosynek, penzionusynka)

Then:
- Clone the project to a temporary location
- Check composer.json for the PHP version requirement (require.php)
- If PHP >= 8.1 → folder ~/projects/81/
- If PHP >= 7.4 → folder ~/projects/74/
- If PHP >= 7.1 → folder ~/projects/71/
- Check if the project already exists in any of these folders
- The project will be migrated to ~/projects/81/{ProjectName}/ regardless of its current PHP version
- If it exists in another folder (e.g., 74/), that becomes the "old" reference

## Step 1: Backup

- Copy the old project to {ProjectName}_old (without .git, .idea, .vscode, node_modules)

cp -r ~/projects/{version}/{ProjectName} ~/projects/{version}/{ProjectName}_old
rm -rf ~/projects/{version}/{ProjectName}_old/.git ~/projects/{version}/{ProjectName}_old/.idea ~/projects/{version}/{ProjectName}_old/node_modules

## Step 2: Erase and Copy Demoweb

- Save .git directory and app/config/config.local.neon from the project
- Erase everything in the project directory
- Copy Demoweb template services as the new base:

cp -r ~/projects/81/Demoweb/* ~/projects/81/{ProjectName}/
cp ~/projects/81/Demoweb/.htaccess ~/projects/81/{ProjectName}/
cp ~/projects/81/Demoweb/.gitignore ~/projects/81/{ProjectName}/
cp ~/projects/81/Demoweb/.deploy_ignore ~/projects/81/{ProjectName}/

- Restore .git and config.local.neon
- Do NOT copy .git, .idea, node_modules from Demoweb
- Create temp directory: mkdir -p temp/cache && chmod 777 temp temp/cache
- Restore the project's .gitlab-ci.yml from the old backup

## Step 3: Connect Database and Compare Structure

Local database connection — config.local.neon should point to:

database:
    dsn: 'mysql:host=mysql-rafa;dbname={ProjectName}'
    user: dev_rafa
    password: 'WEKdHaYVsUyO/09aTSZKQQ=='

IMPORTANT: If password contains special chars (=, :, etc.), wrap in quotes.

Compare project DB against demoweb_services:

diff <(mysql ... {ProjectName} -e "SHOW TABLES" | sort) <(mysql ... demoweb_services -e "SHOW TABLES" | sort)

- Import missing tables from demoweb_services (use --single-transaction, SET FOREIGN_KEY_CHECKS=0)
- ALTER TABLE to add missing columns
- Import missing settings_config rows (ADMIN_TAB_*, ADMIN_LOGO_SRC, IS_ESHOP, etc.)
- Import missing settings_translation rows
- Import missing module rows

## Step 4: Check MenuRepository Supplement IDs

- The menuRepository has a $supp_x_config array that maps supplement names to IDs
- Compare the old project's mapping with the actual supplement table in the database
- Verify that supplement IDs match between the code and the database
- CRITICAL: Demoweb code uses ?? (null coalescing) for supplement values, but old databases often have empty strings "" instead of null. Replace ?? with ?: (elvis operator) in:
  - app/presenters/Admin/ApiPresenter.php — all $names[...] ?? ... patterns
  - All front filters that access $item['supplements'][...]

## Step 5: Move Front Presenters (BasePresenter, HomepagePresenter)

DO NOT replace Demoweb's BasePresenter with the old one. Instead:

BasePresenter:
- Start from Demoweb's app/presenters/Front/BasePresenter.php
- Modify the $menus and $pages static arrays to match the old project's values
- Add the project-specific injected services (filters, repositories)
- Replace the beforeRender() method content with the project-specific logic
- Add createComponentContact() and createComponentSearch() if needed
- Add paginate(), createComponentVisualPaginator(), lang() methods
- Keep the Translator trait from Demoweb
- Remove Demoweb-specific dependencies the project doesn't need (WebMutations, CookieSettings, Instagram, etc.)

HomepagePresenter:
- Copy from old project
- Update self::$item references to self::$pages
- Verify all repository method calls exist in the current repositories

Other Front Presenters:
- Copy FeederPresenter.php, RegisterAndLoginPresenter.php from Demoweb
- Remove Demoweb-only presenters: AccountPresenter, EshopPresenter, GoPayPresenter, InstagramPresenter, PohodaPresenter, TypeaheadPresenter (unless the project uses them)

RegisterAndLoginPresenter:
- Use Demoweb's version (has proper traits)
- Override beforeRender() to NOT call parent::beforeRender() — instead set $web directly from settings

## Step 6: Move Front Latte Templates

rm -rf app/presenters/Front/templates
cp -r {ProjectName}_old/app/presenters/Front/templates app/presenters/Front/templates
cp ~/projects/81/Demoweb/app/presenters/Front/templates/RegisterAndLogin/*.latte app/presenters/Front/templates/RegisterAndLogin/

Fix Latte 2.11 Compatibility — apply to ALL .latte files:
1. Noescape output: {!$var} → {$var|noescape}, {!$var|filter} → {$var|filter|noescape}
2. Do macro: {? expr} → {do expr}
3. Elixir filter: {$template->elixir('path')} → {='path'|elixir}
4. imageExist filter: {$template->imageExist('path', 'default')} → {('path'|imageExist:'default')}

Use PHP script for reliable replacement:

<?php
$c = preg_replace('/\{!\$([^}|]+)\|([^}]+)\}/', '{$$1|$2|noescape}', $c);
$c = preg_replace('/\{!\$([^}]+)\}/', '{$$1|noescape}', $c);
$c = str_replace("{? ", "{do ", $c);

Fix Front Filters — all filters in app/presenters/Front/filters/ need:
- extends Nette\Object → use Nette\SmartObject; trait
- Guard clauses: if (!$item || !is_array($item)) return '';
- $item['supplements']['X'] ?: $fallback (NOT ??)
- Html::add() → Html::addHtml()
- ->class('x') → use Html::el('div class="x"') syntax
- ->href('x') → ->setAttribute('href', 'x')

## Step 7: Convert LESS to SASS

1. Copy old LESS files to resources/sass/{project}-front/
2. Rename .less → .scss, add _ prefix for partials
3. Install bootstrap-sass@3.3.7
4. Replace Bootstrap LESS with: @import "~bootstrap-sass/assets/stylesheets/bootstrap";
5. Convert custom/theme files:
   - @variable → $variable (but NOT @media, @import, @keyframes)
   - @{variable} → #{$variable}
   - ~"string" → string
   - .mixin-name() { } → @mixin mixin-name() { }
   - .mixin-name; → @include mixin-name;
   - spin() → adjust-hue()
   - @import (inline) "file" → @import "file"
   - @import "file.less" → @import "file"
6. CSS libs (.css) → rename to _name.scss
7. Update webpack.mix.js to compile the new SASS

## Step 8: Update webpack.mix.js

mix.sass('resources/sass/{project}-front/app.scss', 'assets/css/front.css')
    .combine([
        'resources/js/jquery.js',
        'resources/js/bootstrap.js',
        'resources/js/netteForms.js',
    ], 'www/assets/js/front.js');

mix.sass('resources/sass/admin/app.scss', 'assets/admin/app.css');

Run: fnm use 18 && npm install && npx mix --production

## Step 9: Update config.neon

- Use Demoweb's config.neon as base
- Update database credentials for production
- IMPORTANT: Quote passwords with special characters
- Add project-specific Front\Filter\* services
- Keep all Demoweb admin services

## Step 10: Update .gitlab-ci.yml

- Set correct PROJECT_NAME (must match the old project)
- Set DEVELOP_DOMAIN: "czechdevelo"
- Set DEVELOP_DIR: "/www/hosting/czechdevelo.cz"
- Use proper chown with $DEVELOP_USER:$DEVELOP_USER and $PRODUCTION_USER:$PRODUCTION_USER

## Step 11: Testing

for page in "/" "cs/m-{id}-{name}"; do
    STATUS=$(curl -sk -o /dev/null -w "%{http_code}" "https://{project}.rafa.lat04.vas-server.cz/$page" -u claude:radegast)
    echo "$STATUS → /$page"
done

for page in "admin/login" "admin" "admin/menu" "admin/fotogalerie" "admin/eshop" "admin/nastaveni" "admin/uzivatele"; do
    STATUS=$(curl -sk -o /dev/null -w "%{http_code}" -L "https://{project}.rafa.lat04.vas-server.cz/$page" -u claude:radegast)
    echo "$STATUS → /$page"
done

for page in "zeroadmin" "zeroadmin/user" "zeroadmin/lang" "zeroadmin/translation" "zeroadmin/config" "zeroadmin/supplement" "zeroadmin/form" "zeroadmin/image"; do
    STATUS=$(curl -sk -o /dev/null -w "%{http_code}" -L "https://{project}.rafa.lat04.vas-server.cz/$page" -u claude:radegast)
    echo "$STATUS → /$page"
done

Check Tracy errors: curl -sk "URL" -u claude:radegast | grep -c "tracy-section--error"

## Step 12: Commit and Push

- Use conventional commits with the ticket ID as scope
- Push to the feature branch
- Verify deploy to czechdevelo works

## Common Gotchas

1. Cache permissions: mv temp temp_old && mkdir -p temp/cache && chmod 777 temp temp/cache
2. NEON parsing: Passwords with =, : or special chars MUST be quoted
3. Supplements empty strings: Old databases have "" instead of null — use ?: not ??
4. Html::add() doesn't exist in Nette 3.x — use addHtml() or addText()
5. Html magic methods like ->class(), ->href() cause errors — use Html::el('tag class="x"') or setAttribute()
6. Latte {!} in Latte 2.11 means boolean negation inside filters, not noescape — always use |noescape
7. $template->filter() doesn't work in Latte 2.11 — use {='value'|filter} or {$var|filter}
8. Route::$defaultFlags removed in Nette 3.x — delete the line
9. Route::FILTER_IN/OUT renamed to Route::FilterIn/FilterOut — but using RouteAlias::FILTER_IN works too
10. Nette\Security\Identity → SimpleIdentity, IAuthenticator → Authenticator, Passwords must be injected (not static)

Additional Resources