['name' => 'Markup', 'flux_icon_name' => 'code-bracket', 'detail_view_route' => 'plugins.markup'], 'api' => ['name' => 'API', 'flux_icon_name' => 'braces', 'detail_view_route' => 'plugins.api'], ]; protected $rules = [ 'name' => 'required|string|max:255', 'data_stale_minutes' => 'required|integer|min:1', 'data_strategy' => 'required|string|in:polling,webhook,static', 'polling_url' => 'required_if:data_strategy,polling|nullable|url', 'polling_verb' => 'required|string|in:get,post', 'polling_header' => 'nullable|string|max:255', 'polling_body' => 'nullable|string', ]; private function refreshPlugins(): void { $userPlugins = auth()->user()?->plugins?->map(function ($plugin) { return $plugin->toArray(); })->toArray(); $this->plugins = array_merge($this->native_plugins, $userPlugins ?? []); } public function mount(): void { $this->refreshPlugins(); } public function addPlugin(): void { abort_unless(auth()->user() !== null, 403); $this->validate(); \App\Models\Plugin::create([ 'uuid' => Str::uuid(), 'user_id' => auth()->id(), 'name' => $this->name, 'data_stale_minutes' => $this->data_stale_minutes, 'data_strategy' => $this->data_strategy, 'polling_url' => $this->polling_url ?? null, 'polling_verb' => $this->polling_verb, 'polling_header' => $this->polling_header, 'polling_body' => $this->polling_body, ]); $this->reset(['name', 'data_stale_minutes', 'data_strategy', 'polling_url', 'polling_verb', 'polling_header', 'polling_body']); $this->refreshPlugins(); Flux::modal('add-plugin')->close(); } public function seedExamplePlugins(): void { Artisan::call(ExampleRecipesSeederCommand::class, ['user_id' => auth()->id()]); $this->refreshPlugins(); } public function importZip(PluginImportService $pluginImportService): void { abort_unless(auth()->user() !== null, 403); $this->validate([ 'zipFile' => 'required|file|mimes:zip|max:10240', // 10MB max ]); try { $plugin = $pluginImportService->importFromZip($this->zipFile, auth()->user()); $this->refreshPlugins(); $this->reset(['zipFile']); Flux::modal('import-zip')->close(); $this->dispatch('notify', ['type' => 'success', 'message' => 'Plugin imported successfully!']); } catch (\Exception $e) { $this->dispatch('notify', ['type' => 'error', 'message' => 'Error importing plugin: ' . $e->getMessage()]); } } }; ?>

Plugins & Recipes

Add Recipe Import Recipe Seed Example Recipes
Import Recipe Alpha Upload a ZIP archive containing a TRMNL recipe — either exported from the cloud service or structured using the trmnlp project structure.
The archive must at least contain settings.yml and full.liquid files. {{--

The ZIP file should contain the following structure:

--}} {{--
--}}
{{--.--}}
{{--├── src--}}
{{--│   ├── full.liquid (required)--}}
{{--│   ├── settings.yml (required)--}}
{{--│   └── ...--}}
{{--└── ...--}}
{{--                    
--}}
Limitations
  • Only full view will be imported; shared markup will be prepended
  • Some Liquid filters may be not supported or behave differently
  • API responses in formats other than JSON are not yet supported
  • {{--
      --}} {{--
    • date: "%N" is unsupported. Use date: "u" instead
    • --}} {{--
    --}}
Please report issues on GitHub. Include your example zip file.
@error('zipFile') {{ $message }} @enderror
Import
Add Recipe
@if($data_strategy === 'polling')
@if($polling_verb === 'post')
@endif
@endif
Create Recipe
@foreach($plugins as $plugin) @endforeach