Search by

rajtik76 / nette-tailwind-starter-kit

rajtik76

Nette starter kit with Tailwind CSS: authentication, dashboard and settings

Package info

github.com/rajtik76/nette-tailwind-starter-kit

Type:project

pkg:composer/rajtik76/nette-tailwind-starter-kit

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-11-03 16:32 UTC

This package is auto-updated.

Last update: 2026-11-03 16:34:46 UTC


README

CI

A Nette counterpart of the Laravel Livewire Starter Kit: authentication, dashboard and account settings on Latte templates, Tailwind CSS 4 and Naja for AJAX. Built on the rajtik76/nette-tailwind skeleton.

  • PHP 8.4+, Nette 3.2, Latte 3
  • Tailwind CSS 4 via @tailwindcss/vite, dark mode via the .dark class
  • Vite 8 with @nette/vite-plugin, assets served by nette/assets
  • Naja for AJAX forms (the Livewire counterpart), Alpine.js for small UI behaviour
  • Route middleware from rajtik76/nette-middleware
  • Nette Database Explorer, SQLite by default
  • Migrations, seeders and factories from rajtik76/nette-migrations
  • Mail through nette/mail with Latte templates, written to log/mail until SMTP is set

Features

  • Log in with remember me, registration, logout
  • Forgotten and reset password, email verification, password confirmation
  • Login and mail rate limiting, signed verification links
  • Dashboard in an application layout with a sidebar, mobile menu and user menu
  • Settings: profile (a new email is verified again), password, appearance (light, dark, system), account deletion
  • Every auth feature can be switched off in config/common.neon

Two-factor authentication and API tokens are not part of the kit yet.

Requirements

  • PHP 8.4 or newer
  • Composer
  • Node.js 22.12+ and npm (.nvmrc pins the version used in CI)

Installation

composer create-project rajtik76/nette-tailwind-starter-kit my-app
cd my-app
npm install
npm run build
php nette migrate --seed

create-project runs php nette key:generate, which writes appKey into config/local.neon. From a clone of the repository, run it yourself:

git clone https://github.com/rajtik76/nette-tailwind-starter-kit.git my-app
cd my-app
composer install
php nette key:generate
npm install
npm run build
php nette migrate --seed

Log in as test@example.com with the password password. Make sure the temp/ and log/ directories are writable.

npm run build is required. Vite is enabled in config/common.neon, so the page fails with "Failed to read Vite manifest" until the assets are built.

Database

The default connection is SQLite in database/database.sqlite (ignored by git). The file is created on the first connection, so php nette migrate is all it takes. To use MySQL, MariaDB or PostgreSQL, override the connection in config/local.neon:

database:
    dsn: 'mysql:host=127.0.0.1;dbname=app'
    user: root
    password: secret

php nette migrate --seed creates the user test@example.com with the password password.

php nette migrate:fresh --seed      # drop all tables, migrate and seed again
php nette make:migration create_posts_table
php nette make:factory Post
php nette make:seeder Post

Migrations live in database/migrations, factories and seeders in database/factories and database/seeders. See rajtik76/nette-migrations.

Authentication

The auth core in app/Auth is the counterpart of Laravel Fortify. It knows nothing about presenters or forms, so a Latte UI and a JSON API can share it. It stays self-contained to be extracted into a package later.

Actions (App\Auth\Actions) take plain input arrays and throw App\Auth\Validation\ValidationException, whose errors hold field => message:

Action Input
RegisterUser name, email, password, password_confirmation; logs in, sends the verification link
AuthenticateUser email, password, remember; 5 attempts per minute per email + IP
LogoutUser
SendPasswordResetLink email; one link per minute
ResetPassword token, email, password, password_confirmation; tokens expire in 60 minutes
SendEmailVerification user; 6 per minute, throws TooManyAttemptsException
VerifyEmail user + id, hash, expires, signature from the signed link (60 minutes)
ConfirmPassword user + password; valid for 3 hours (PasswordConfirmation::isRecent())
UpdateProfile user + name, email; a new email must be verified again
UpdatePassword user + current_password, password, password_confirmation
DeleteUser user + password; logs out

Emails are lowercased, like Fortify's lowercase_usernames. The session stores only the user id; Authenticator reloads the identity on every request, so a deleted user is logged out.

Customise validation and persistence in app/Actions/Auth, the counterpart of app/Actions/Fortify. These classes implement the contracts in App\Auth\Contracts (CreatesNewUsers, ResetsUserPasswords, UpdatesUserPasswords, UpdatesUserProfileInformation, DeletesUsers) and are registered in services.neon. Shared rules live in ValidationRules. App\Mail\AuthMailer sends the auth mails.

Configuration in config/common.neon, the counterpart of config/fortify.php:

auth:
    secret: %appKey%                    # signs email verification links
    strictPasswords: %productionMode%   # min 12 chars, mixed case, numbers, symbols
    features:
        registration: true
        resetPasswords: true
        emailVerification: true
    links:                              # destinations used in mails
        verifyEmail: Auth:verifyEmail
        resetPassword: Auth:resetPassword
    rememberFor: 30 days                # login expiration with "remember me"
    expireAfter: 2 hours                # inactivity expiration without it
    passwordTimeout: 10800              # seconds a password confirmation lasts

php nette key:generate writes a random appKey into config/local.neon (it runs after composer create-project). Rewriting an existing local.neon drops its comments.

Pages and routes

Routes follow the Laravel starter kit and are listed one by one in app/Core/RouterFactory.php. There is no catch-all <presenter>/<action> route, so route middleware cannot be bypassed.

Route Presenter Middleware
/login, /register, /forgot-password, /reset-password/<token> Auth guest
/verify-email, /email/verify/<id>/<hash> Verification auth
/confirm-password, /logout (POST) Auth auth
/dashboard Dashboard auth, verified
/settings/profile Settings auth
/settings/appearance Settings auth, verified
/settings/security Settings auth, verified, password.confirm

Routes of disabled auth features are not registered. Middleware lives in app/Http/Middleware: Authenticate, RedirectIfAuthenticated, EnsureEmailIsVerified and RequirePassword. Authenticate and RequirePassword store the request and pass its key as backlink, so the user returns to the original page after logging in or confirming the password.

Auth pages are plain POST forms, like the Fortify views. Settings forms have the ajax class and go through Naja: validation errors and toasts appear without a reload. A redirect is always a full page load, because it usually leads to another layout.

Templates use Tailwind utilities directly. Shared pieces are Latte blocks in app/Presentation/@ui.latte (form field, checkbox, auth header, logo) and Heroicons in @icons.latte. The button() Latte function returns the classes of a button variant (primary, danger, filled, ghost). Dropdowns, the mobile sidebar, the delete-account dialog, password visibility and the appearance switch use Alpine.js.

Mail

Out of the box, mails are written to log/mail as .eml files (Nette\Mail\FileMailer), like Laravel's MAIL_MAILER=log, so nothing breaks without an SMTP server. To send them, override the mailer in config/local.neon, e.g. for Mailpit (UI on http://localhost:8025):

services:
    mail.mailer: Nette\Mail\SmtpMailer(host: 127.0.0.1, port: 1025, username: '', password: '')

docker run -d --name mailpit -p 1025:1025 -p 8025:8025 axllent/mailpit

In production, set your SMTP server and credentials the same way and change mailFrom and appName. A failing SMTP server is an error (HTTP 500), as in Laravel with synchronous notifications.

App\Mail\TemplateMailer renders app/Mail/templates/<name>.latte inside the shared @layout.latte; the <title> block becomes the subject.

Development

Run the Vite dev server and a PHP server side by side:

npm run dev
php -S localhost:8000 -t www

Then open http://localhost:8000. In debug mode, {asset 'main.js'} loads assets from the Vite dev server, so changes to templates and CSS show up immediately. Without the dev server running, the last npm run build output is used.

Laravel Herd / Valet need a custom driver that serves www/ as the document root. wwwDir is set explicitly in app/Bootstrap.php, because these servers boot index.php through their own router script.

Console

php nette is the Nette counterpart of php artisan, provided by rajtik76/nette-console:

php nette list                       # all commands
php nette make:command SendReport    # app/Console/SendReportCommand.php, runs as app:send-report
php nette cache:clear                # delete temp/cache
php nette package:discover           # extensions registered by installed packages

Packages declare their DI extensions in composer.json (extra.nette.extensions) and Bootstrap registers them automatically, so their commands work right after composer require. Classes ending with Command in app/ are registered by search in services.neon.

The console runs in debug mode, so config changes apply without clearing the cache. Set NETTE_DEBUG=0 in production.

Checks

composer run lint:check    # Latte templates (own engine, warnings fail) + NEON config
composer run type:check    # PHPStan level 8
composer run tester        # Nette Tester
composer run ci:check      # all of the above, same as GitHub CI

Tests render the layout, so run npm run build first. CI runs on PHP 8.4 and 8.5, Node version comes from .nvmrc.

Testing

tests/config.neon switches the container to an in-memory SQLite database (migrated by tests/bootstrap.php), an ArrayMailer that keeps sent mails, a FrozenClock and a memory cache. Helpers in tests/Support/functions.php:

$container = require __DIR__ . '/../../bootstrap.php';

actingAs($container, createUser($container, ['email_verified_at' => null]));
$response = request($container, 'POST', '/settings/profile?do=profileForm-submit', $post, ajax: true);

redirectPath($response);         // '/verify-email', without the flash id
html($response);                 // rendered page
mailLink(mailer($container)->last());

request() goes through the router and the route middleware, so tests cover access control too. tests/Auth tests the auth actions, tests/Presentation the pages.

How Tailwind is wired in

  • assets/main.css imports Tailwind and adds @source "../app". Vite's root is assets/, so without it Tailwind would not scan the Latte templates in app/.
  • assets/main.js is the Vite entry point and imports main.css.
  • vite.config.ts registers the tailwindcss() plugin next to the Nette plugin.
  • app/Presentation/@layout.latte renders {asset? 'main.js'}, which outputs both the <script> and the <link rel="stylesheet"> for the bundled CSS.
  • The dark: variant follows the .dark class on <html>. An inline script in @layout.latte sets it before the first paint from localStorage (appearance is light, dark or missing for system); assets/appearance.js changes it at runtime and the Appearance settings page switches it through the appearance Alpine component.

Use Tailwind classes directly in any .latte file under app/.

Welcome and error pages

  • Colors and the font are defined in @theme in assets/main.css. The font (Instrument Sans) is loaded from Bunny Fonts by the welcome page and the auth and app layouts, through the head block of @layout.latte. Error pages make no external requests and fall back to the system font.
  • The cube illustrations are inline SVG blocks in app/Presentation/@cubes.latte.
  • Error4xx/@layout.latte wraps every 4xx template in a shared frame, so 403.latte, 404.latte etc. stay plain markup.
  • 500.phtml and 503.phtml use neither Tailwind nor the layout, because they must render even when the app or the asset build is broken. Their CSS is inline, so when you change colors in main.css, update them there by hand.

Production

npm run build
php nette key:generate
NETTE_DEBUG=0 php nette migrate --force

Set the database, SMTP, mailFrom and appName in config/local.neon. strictPasswords follows production mode, so new passwords need 12 characters with mixed case, numbers and symbols.

Point the web server's document root to www/. Built assets go to www/assets/ (ignored by git, so build them on deploy).

Important: app/, config/, database/, log/ and temp/ must never be web-accessible. See the security warning.