From fcadc8d7a120a648c07a11d93a158bc2eff0963f Mon Sep 17 00:00:00 2001 From: lexx Date: Wed, 6 May 2026 23:42:32 +0300 Subject: [PATCH] doc update --- docs/Fields.md | 12 ++++- docs/Schemas.md | 20 ++++++- docs/Static Generator.md | 109 ++++++++++++++++++++++++++++--------- docs/Website Setup.md | 112 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 223 insertions(+), 30 deletions(-) diff --git a/docs/Fields.md b/docs/Fields.md index 3dd6392..b4e276b 100644 --- a/docs/Fields.md +++ b/docs/Fields.md @@ -165,9 +165,17 @@ Date and time selector. Stores as an ISO 8601 string. --- +### uuid + +UUID text field. Stores an arbitrary UUID string. Typically used as a read-only external reference ID. + +**Required:** `name`, `label` + +--- + ### json -Raw JSON data field. +Raw JSON data field. Accepts either a JSON string or a plain array — both are stored as JSON. **Required:** `name`, `label` @@ -175,7 +183,7 @@ Raw JSON data field. ### file -Upload or select files from a files schema. +Upload or select files from a files schema. Files can be uploaded directly or browsed from previously uploaded files. Image files (jpeg, png, webp, gif, tiff) automatically get a 300×300 thumbnail generated on upload. **Required:** `name`, `label`, `collections` diff --git a/docs/Schemas.md b/docs/Schemas.md index a0684d2..65dd4cd 100644 --- a/docs/Schemas.md +++ b/docs/Schemas.md @@ -40,8 +40,6 @@ There are 2 types of schemas: | `label` | yes | Friendly display name | | `type` | yes | Must be `"files"` | | `fields` | yes | Array of field definitions. See [Fields](Fields.md) | -| `disk` | no | Laravel disk name. Default: `"lucent"` | -| `path` | no | Subdirectory for uploads. Default: schema name | | `groups` | no | Group IDs to split fields into tabs | | `isEntry` | no | Show in sidebar. Default: `false` | | `sortBy` | no | Default sort in browser | @@ -51,6 +49,24 @@ There are 2 types of schemas: | `read` | no | Roles with read access | | `write` | no | Roles with write access | +### File Metadata + +Every uploaded file is automatically stored with the following metadata (stored in `lucent_files`): + +| Field | Description | +|---|---| +| `id` | Unique file ID | +| `recordId` | ID of the record this file belongs to | +| `originalName` | Original filename as uploaded | +| `mime` | MIME type e.g. `image/webp` | +| `path` | Storage path e.g. `files/{recordId}/{filename}` | +| `size` | File size in bytes | +| `width` | Image width in pixels (0 for non-images) | +| `height` | Image height in pixels (0 for non-images) | +| `checksum` | SHA-1 hash of the file contents | + +Image files (jpeg, png, webp, gif, tiff) also get a 300×300 thumbnail generated automatically at `thumbs/{path}`, plus any image filter presets configured in `lucent.imageFilters`. + ## System Fields diff --git a/docs/Static Generator.md b/docs/Static Generator.md index 98b34c7..cfd2185 100644 --- a/docs/Static Generator.md +++ b/docs/Static Generator.md @@ -5,51 +5,94 @@ To generate the static content of the website, lucent provides a helper class. ## Laravel Command -Just create a command and name it as you like. Make sure to inject the StaticGenerator class. +Create an Artisan command and inject `StaticGenerator`: ```php -public function __construct( - public StaticGenerator $staticGenerator, -) { - parent::__construct(); +class GenerateStatic extends Command +{ + protected $signature = 'generate:static'; + + public function __construct( + public StaticGenerator $staticGenerator, + public Context $ctx, + ) { + parent::__construct(); + } + + public function handle(): void + { + $this->staticGenerator->run('generate:static', function ($writer) { + $writer->save("/", $this->ctx->render("homepage")); + $writer->save("/about", $this->ctx->render("about")); + }); + } } ``` -## Redirect command +`run(string $signature, callable $callback)` — the first argument must match the command's artisan signature. This is used to stream live build logs in the Lucent UI. The callback receives a `Writer` instance. -There are cases which is useful to create a redirect like when you have a multilingual website: + +## Writer: save + +Writes an HTML file at the given path: ```php -$this->staticGenerator->run(function ($writer) { - $writer->createRedirect("/", "/el"); -}); +$writer->save("/blog/my-post", $html); +// writes to: storage/lucent/build/blog/my-post/index.html ``` -## The Writer save command +An optional third argument changes the file extension (default `"html"`). -In order to create an html file, you have to use the writer's save command -The first argument is the url and the second is the rendered HTML. That's where the page classes do come handy. +## Writer: createRedirect + +Generates an HTML meta-refresh redirect. Useful for root-level locale redirects: ```php -$this->staticGenerator->run(function ($writer) { - $writer->save("/", $this->ctx->render("homepage")); - $writer->save("/about", $this->ctx->render("about")); -}); +$writer->createRedirect("/", "/el"); +$writer->createRedirect("/", "/el", "Redirecting", "Please wait..."); ``` +Arguments: `from`, `to`, `title` (default `"Redirecting"`), `message` (default `"Redirecting Soon..."`). + + +## Writer: recordIterator + +Iterates over all records in paginated batches — useful when there are too many records to load at once: + +```php +$writer->recordIterator( + query: fn(int $limit, int $skip) => $query + ->filter(["schema" => "blogPosts"]) + ->onlyPublished() + ->limit($limit) + ->skip($skip) + ->tree(), + parser: function ($records) use ($writer) { + foreach ($records as $record) { + $writer->save( + "/blog/" . $record->data->slug, + $this->ctx->render("blogPost", $record), + ); + } + }, +); +``` + +Signature: `recordIterator(callable $query, callable $parser, int $skip = 0, int $limit = 100): int` + +The `$query` callable receives `($limit, $skip)` and must return a collection. Iteration stops automatically when a batch returns zero records. + + ## Storage and Permissions -All the generated html is stored on `storage/lucent/live` -In order to make it accessible you have to create symlink: +All generated HTML is written to `storage/lucent/build` during the run, then atomically swapped to `storage/lucent/live` on completion. Create the public symlink with: ```bash php artisan lucent:livelink ``` -Now your static website is accessible at `http://localhost:8000/live` - -But it would not be nice to have the **live** prefix everywhere. That's why you need to make the following tweak in the nginx vhost +The static site is then accessible at `http://localhost:8000/live`. To serve it without the `/live` prefix, add this to your nginx vhost: ```nginxconf location / { @@ -57,14 +100,30 @@ location / { } ``` + +## Artisan Commands + +| Command | Description | +|---|---| +| `lucent:setup-db` | Create all Lucent database tables | +| `lucent:schemas` | Compile schema JSON files | +| `lucent:livelink` | Create the `public/live` symlink | +| `lucent:rebuild:thumbnails` | Regenerate thumbnails for all uploaded images | +| `lucent:removeOrphanEdges` | Remove edges pointing to deleted records | +| `lucent:generate:collection {name}` | Scaffold a new collection schema JSON file | + + ## Build from the Lucent UI -In your lucent.php config file you can define your command that is used to generate the static files: +Register your generate command in `config/lucent.php` so admin users can trigger it from the UI: ```php -"generateCommand" => "generate:static" +"commands" => [ + "generate:static" => "Generate Static Site", +], ``` -That way, the users will be able to initiate the build command through the user interface. + +Only roles listed in `canBuild` can trigger commands from the UI. diff --git a/docs/Website Setup.md b/docs/Website Setup.md index f32192b..cf55bde 100644 --- a/docs/Website Setup.md +++ b/docs/Website Setup.md @@ -98,4 +98,114 @@ class Context } ``` -Add the middleware inside the HTTP Kernel file. \ No newline at end of file +Add the middleware inside the HTTP Kernel file. + +## Configuration + +After publishing the config (`php artisan vendor:publish --tag=lucent-config`), configure `config/lucent.php` via `.env`: + +| Key | Env var | Default | Description | +|---|---|---|---| +| `env` | `LUCENT_ENV` | `production` | `production` or `development` | +| `auth` | `LUCENT_AUTH` | `lucent` | Auth driver: `lucent` or `lunar` | +| `disk` | `LUCENT_DISK` | `public` | Laravel filesystem disk for uploads | +| `schemas_path` | `LUCENT_SCHEMAS_PATH` | `resources/lucent/schemas` | Where schema JSON files live | +| `database` | `LUCENT_DB_CONNECTION` | `DB_CONNECTION` | Database connection name | +| `name` | `LUCENT_NAME` | `Lucent` | CMS display name | +| `url` | `LUCENT_URL` | `APP_URL` | Base URL of the CMS | +| `previewTarget` | `LUCENT_PREVIEW_TARGET` | `previewTarget` | Preview route parameter | +| `commands` | — | `[]` | Artisan commands exposed to admin users (see [Static Generator](Static%20Generator.md)) | +| `imageFilters` | — | `[]` | Image filter presets applied to uploads (see below) | +| `canInvite` | — | `["admin"]` | Roles that can invite new users | +| `canBuild` | — | `["admin"]` | Roles that can trigger static builds | +| `systemUserId` | — | `""` | User ID used for console-initiated record writes | + + +## Image Filters + +Image filters are Intervention Image filter classes applied to every uploaded image, producing additional versions (e.g. a cropped or watermarked variant). Each preset is stored at `templates/{name}/{original-path}` on the configured disk. + +Define filters in `config/lucent.php`: + +```php +"imageFilters" => [ + "hero" => \App\ImageFilters\HeroFilter::class, + "thumb" => \App\ImageFilters\ThumbFilter::class, +], +``` + +A filter class must implement `Intervention\Image\Filters\FilterInterface`: + +```php +namespace App\ImageFilters; + +use Intervention\Image\Filters\FilterInterface; +use Intervention\Image\Image; + +class HeroFilter implements FilterInterface +{ + public function applyFilter(Image $image): Image + { + return $image->fit(1200, 600)->encode('webp', 80); + } +} +``` + +Every uploaded image automatically gets a 300×300 WebP thumbnail at `thumbs/{path}` in addition to any configured presets. + + +## Authentication + +Lucent uses **email-link (magic link) login** — no passwords. Users receive a time-limited link by email. + +### Auth modes + +Set `LUCENT_AUTH` to choose how users are stored: + +| Value | Description | +|---|---| +| `lucent` | Users stored in the `lucent_users` table. Supports roles. | +| `lunar` | Delegates to Lunar's `lunar_staff` table. Roles not supported. | + +### Login flow + +1. User submits their email at `/lucent/login` +2. A 32-character token is stored against the user with a timestamp +3. Lucent emails a login link containing the token +4. User clicks the link → token is validated (must be used within **1 hour**) +5. Session is established; token is cleared + +### First-time setup + +The `/lucent/register` route is only available when **no users exist** in the system. Once the first admin registers, the route returns a redirect to `/lucent/login`. Registration automatically assigns the `admin` role and sends a login link. + +### Middleware + +Two middleware aliases are available for your routes: + +| Alias | Description | +|---|---| +| `lucent.auth` | Requires an active Lucent session | +| `lucent.guest` | Redirects authenticated users away | + +### Roles + +Roles are defined in your channel config. Only roles listed there are valid — anything else is silently stripped on assignment. The `canInvite` and `canBuild` config keys control which roles can invite users and trigger builds respectively. + +On console commands and non-`/lucent` routes, `currentUserId()` returns `config("lucent.systemUserId")` instead of the session user. + + +## Database Tables + +All Lucent-managed tables use the `lucent_` prefix: + +| Table | Description | +|---|---| +| `lucent_records` | All collection and file schema records | +| `lucent_files` | Uploaded file metadata | +| `lucent_edges` | Relationships between records | +| `lucent_revisions` | Record revision history | +| `lucent_users` | Users (when using `lucent` auth mode) | +| `lucent_command_logs` | Background command execution logs | + +These are created automatically by running `php artisan lucent:setup`. \ No newline at end of file