Site themes

Site themes

A theme is “just a folder” in resources/scaffold/themes/ — adding it to the catalog does not require registering its name anywhere. Checked against Niang\Core\Console\ProjectScaffolder and ThemePackageInstaller.

Catalog

Eight themes ship with the framework, plus minimal (the demo skeleton, always last and the default). Their labels are in French, as the CLI displays them:

SlugLabelMeaning
vitrineSite vitrine / informationnelShowcase / informational site
ecommerceBoutique en ligneOnline store
blogBlog / magazineBlog / magazine
portfolioPortfolioPortfolio
landingLanding page one-pageOne-page landing page
authApplication avec comptes (inscription, connexion, espace membre)Application with user accounts — see “auth” starter
apiAPI REST (JSON, jetons, CORS, OpenAPI)JSON REST API — see “api” starter
saasSaaS (organisations, membres, abonnements)SaaS (organizations, members, subscriptions) — see “saas” starter
minimalMinimal — squelette de démonstration (défaut)Minimal — demo skeleton (default)

See Getting started for the question asked during installation, --type= and NIANG_SITE_TYPE.

Structure of a theme

A theme mirrors a project's directory tree — only the files actually provided are copied:

text
resources/scaffold/themes/vitrine/
├── app/Controllers/
├── config/site.php
├── public/css/theme.css       # builds on niang.css variables, never reinvents them
├── resources/views/
│   ├── layouts/
│   └── pages/
├── routes/web.php             # fully replaces the target project's one
├── tests/Feature/             # the theme's tests, shipped with it
└── theme.json                 # optional

theme.json (every field optional):

KeyPurpose
labelLabel shown in the catalog (default: the capitalized slug).
orderPosition in the catalog, ascending (default: 100).
modulesModules from resources/scaffold/modules/ to install with this theme — see below.
removePaths of the target project to delete before copying (e.g. the demo that a real theme replaces).
setupniang commands run automatically when the project is created — see below.
next_stepsCommands suggested after installation; those already run successfully by setup are removed from the list.
notesInformation shown after those commands (e.g. the credentials of a test admin account).

How shared/ and the theme are merged

resources/scaffold/shared/ follows the same tree and provides what every theme has in common (the niang.css/niang.js design system, components, base layout, error pages) — never duplicated in each theme. ProjectScaffolder::install() does, in order:

  1. Reads the remove of shared/theme.json, then that of each declared module, then that of the theme, and deletes those paths from the target project (the demos this theme replaces — views, tests specific to the minimal skeleton).
  2. Copies shared/ over the target project.
  3. Copies each module declared in modules (in the order listed), over shared/.
  4. Copies the theme's folder over everything, last — its files win on collision, including over a module's.

app/Middleware/, app/Models/ and database/migrations/ are part of neither shared/ nor a theme: they remain those of the base skeleton copied by niang new, available to every theme that needs them (authentication, CSRF...).

Installing a theme replaces, it does not merge: that is intentional, it only happens once, when the project is created, before anyone has touched anything — merging would be more fragile for no real benefit.

Modules: building blocks shared between themes

A module (resources/scaffold/modules/<name>/, same tree as a theme) is a building block reused by several themes without being a site type itself — the admin area, for example, shared by the store and the blog. It is declared in theme.json and copied after shared/ but before the theme (which can therefore replace any of its files):

resources/scaffold/themes/ecommerce/theme.json
{
    "label": "Boutique en ligne",
    "order": 20,
    "modules": ["admin"],
    "next_steps": ["./bin/niang migrate", "./bin/niang db:seed", "./bin/niang serve"],
    "notes": [
        "Compte administrateur de test (créé par db:seed) : admin@example.com / admin1234",
        "Connectez-vous sur /login : vous arrivez sur le tableau de bord /admin."
    ]
}

The admin module (shipped with the framework) brings the controllers, views and migrations of an admin dashboard; the ecommerce and blog themes both declare it rather than duplicating that code. A declared module that does not exist under resources/scaffold/modules/ makes the installation fail with an explicit error rather than a half-built project.

“auth” starter: an application with accounts

The starting point for a product where users have an account. No demo content to remove: a home page, then a member's whole journey.

bash
composer create-project niangpro/niangpro my-app
# choice: auth (or NIANG_SITE_TYPE=auth)
cd my-app && ./bin/niang serve
PagePurpose
/register, /login, /logoutsign-up, login (“remember me”, throttled attempts), logout
/forgot-password, /verify-email/{id}signed, time-limited links sent by email
/auth/google/redirect, /auth/github/redirectOAuth login, active as soon as the provider is configured in .env
/tableau-de-bordthe member area, where your product starts (resources/views/pages/dashboard.php)
/compteprofile (a new address becomes “unverified” again), password, two-factor authentication, deleting the account and its API tokens

After sign-up or login, a member lands on User::homePath(), i.e. /tableau-de-bord. The account pages come from the account module, which other themes can declare to get the same authentication screens. The shipped tests (tests/Feature/AccountTest.php, AuthStarterTest.php) cover the whole journey, CSRF included.

“api” starter: a JSON REST API

For a mobile app or SPA backend: no HTML page, everything answers in JSON.

RoutePurpose
POST /api/v1/registercreates an account, returns its first token
POST /api/v1/tokensemail, password, device_name (and code when two-factor is on): a token
DELETE /api/v1/tokens/currentrevokes the token used (logs the device out)
GET /api/v1/methe token's account
/api/v1/notes, /api/v1/notes/{id}sample resource: paginated CRUD; another account's note answers 404
bash
curl -X POST http://127.0.0.1:8000/api/v1/register -H 'Content-Type: application/json' \
  -d '{"name":"Awa","email":"awa@example.com","password":"secret123","device_name":"curl"}'
# {"token":"ed9c...","user":{...}}
curl http://127.0.0.1:8000/api/v1/notes -H 'Authorization: Bearer ed9c...'

Validation through FormRequests (app/Requests/), responses through JsonResources (app/Resources/), CORS with an OPTIONS route per path, rate limiting on sign-up and login. The OpenAPI description is generated into public/openapi.json when the project is created; run ./bin/niang openapi again after every route change.

“saas” starter: organizations, members, subscriptions

The base of online software sold to teams. Each organization is a tenant: its pages live under /o/<slug> and its data (projects, members, invitations) is never visible to others — another organization answers 404. The account pages come from the account module, as in the “auth” starter.

PageWhoPurpose
/organisationsany logged-in membertheir organizations, creating a new one (they become its owner)
/o/<slug>, /o/<slug>/projetsmembersdashboard, sample resource; deletion for admins only
/o/<slug>/membresmembers (management: admins)roles, email invitations, removal, leaving the organization
/o/<slug>/abonnementadmins (changes: owners)plans, subscribing, cancelling
/invitations/{token}the invited person, logged in with the invited addressjoining the organization; single-use link, valid 7 days

Three roles: owner, admin, member. A route requires a minimum role with EnsureTeamMember::class . ':admin'. There is always at least one owner: the last one cannot be demoted, removed, leave, or delete their account while the organization has other members.

Plans and payment

config/billing.php
'provider' => env('BILLING_PROVIDER', \App\Billing\FakeBillingProvider::class),
'plans' => [
    'free' => ['name' => 'Gratuit', 'price' => 0, 'limits' => ['projects' => 3, 'members' => 2]],
    'pro' => ['name' => 'Pro', 'price' => 29, 'limits' => ['projects' => null, 'members' => 25]],
],

Limits are enforced (creating a project, inviting and accepting an invitation) with Billing::allows('projects'). Payment goes through the App\Billing\BillingProvider interface: checkout() (URL of the provider's payment page), cancel(), and webhook() for confirmations received on POST /billing/webhook, which apply the change with Billing::apply(). FakeBillingProvider activates a plan without payment; it is refused with APP_ENV=production. For Stripe, Paddle, PayDunya, CinetPay..., write your class and set it in BILLING_PROVIDER.

Creating a new theme

Adding a theme = adding a folder under resources/scaffold/themes/<slug>/ — it shows up in the catalog on its own, nothing to register anywhere else:

bash
mkdir -p resources/scaffold/themes/magazine/{app/Controllers,resources/views,routes}
# ... build the theme ...
./bin/niang new test-magazine --type=magazine   # check that it installs correctly

The slug must match ^[a-z0-9][a-z0-9_-]*$ and differ from minimal (reserved, always the skeleton as-is).

Publishing a theme as a Composer package

To distribute a theme separately from the framework (the “theme extensibility through separate Composer packages” milestone): an ordinary Composer package, with an extra.niangpro-theme key pointing to the theme's folder (already named after the desired slug — its basename becomes the slug):

composer.json of the theme package
{
    "name": "acme/niangpro-theme-magazine",
    "extra": {
        "niangpro-theme": "resources/magazine"
    }
}
bash
./bin/niang theme:add acme/niangpro-theme-magazine

The command runs composer require --dev, then ThemePackageInstaller copies vendor/acme/niangpro-theme-magazine/resources/magazine/ to resources/scaffold/themes/magazine/ — after that, the theme is an ordinary local folder, visible in niang new --type=magazine exactly like the five themes shipped originally. No change to ProjectScaffolder is required: “a theme is just a folder” stays true wherever it comes from.

⏱ 7.90 ms 🗄 0 requête(s) SQL 🧠 4.00 MB ↩ 200