add: image and blog import

fix: category field to Entry model

feat: EntriesTable with searchable and sortable columns

chore: update CHANGELOG with recent additions and API updates
This commit is contained in:
jon brookes 2026-01-18 18:02:42 +00:00
parent 9f77f8b8d3
commit ccf38ef6f9
5 changed files with 235 additions and 2 deletions

View file

@ -2,6 +2,18 @@
## 2026-01-08
added text widgets
added categories
updated api to access text widgets and categories
added url and call to action for entries for use in cards
added import image and blogs import commands
## 2026-01-08
added tags to entry model
added text widget and category

View file

@ -0,0 +1,144 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use App\Models\Entry;
class ImportBlogs extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:import-blogs';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle()
{
/*
{
"id": 7,
"title": "Low/No Code FlowiseAI",
"slug": "lowno-code-flowiseai",
"content": "<p><img alt=\"\" src=\"http://127.0.0.1:8000/storage/99/ZsTHWAQRbdvgRUEwGmCIsm4TyChIjBwiY71VmnnR.webp\"/></p>\n<p>In <a href=\"https://flowiseai.com/\">FlowiseAI</a>, applications based on the JavaScript fork of LangChain are modeled in a 'no/low-code' environment. If you are coming from a closed source world, yet trying to implement devops principles this may fill you with fear, dread and uncertainty. FlowiseAI offers the advantage of using plain-text JSON files to represent each workflow. These files are easy to understand, open, and readily backup-able, unlike opaque proprietary binary formats\nThe data used at runtime and other component prerequisites like credentials are stored in the FlowiseAI data volume, which looks like this</p>\n<p><code>bash\nMode LastWriteTime ..... 8ZS1C0ZBB.webp",
"created_at": "2024-12-17 11:24:59",
"updated_at": "2024-12-17 12:53:54",
"category_id": 1,
"blog_date": "2024-02-26 00:00",
"is_featured": 0,
"published": 1
},
*/
$filePath = '/home/user/projects/laravel/12/media_library/boring-astro-static/imported_database/blogs.json';
if (!file_exists($filePath)) {
$this->error("File not found: $filePath");
return 1;
}
$jsonContent = file_get_contents($filePath);
$blogs = json_decode($jsonContent, true);
if (!$blogs) {
$this->error("Could not parse JSON file: $filePath");
return 1;
}
foreach ($blogs as $blog) {
// Only process the blog with ID 51
if (($blog['id'] ?? null) !== 51) {
continue;
}
$slug = $blog['slug'] ?? null;
$this->info("Processing blog ID: {$blog['id']} with slug: {$slug}");
// Check if the entry already exists
$existingEntry = Entry::where('slug', $slug)->first();
if ($existingEntry) {
// Update existing entry with cleaned content
$existingEntry->update([
'content' => $this->cleanHtmlForFilament($blog['content'] ?? ''),
'description' => $this->extractPlainTextFromHtml($blog['content'] ?? ''),
]);
$this->info("Updated content for: {$existingEntry->title}");
} else {
// Create new entry
Entry::create([
'title' => $blog['title'] ?? null,
'slug' => $slug,
'description' => $this->extractPlainTextFromHtml($blog['content'] ?? ''),
'is_published' => $blog['published'] ?? false,
'is_featured' => $blog['is_featured'] ?? false,
'published_at' => $blog['blog_date'] ?? null,
'content' => $this->cleanHtmlForFilament($blog['content'] ?? ''),
'category_id' => 1, // Default category
]);
$this->info("Created new entry: " . ($blog['title'] ?? 'Untitled'));
}
}
$this->info('Blogs imported successfully.');
}
/**
* Extract plain text from HTML for description field
*/
private function extractPlainTextFromHtml(string $html): string
{
if (empty($html)) {
return '';
}
// Decode HTML entities and strip tags
$text = html_entity_decode(strip_tags($html));
// Clean up whitespace
$text = preg_replace('/\s+/', ' ', $text);
// Limit to reasonable length for description
return trim(substr($text, 0, 500));
}
/**
* Clean HTML content for Filament rich editor
*/
private function cleanHtmlForFilament(string $html): string
{
if (empty($html)) {
return '';
}
// Convert escaped newlines to actual newlines
$html = str_replace(['\\n', '\\r\\n', '\\r'], "\n", $html);
// Decode HTML entities
$html = html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8');
// Clean up excessive whitespace but preserve paragraph structure
$html = preg_replace('/\s*\n\s*/', ' ', $html);
$html = preg_replace('/[ \t]+/', ' ', $html);
// Ensure paragraphs have proper spacing
$html = str_replace('</p><p>', '</p>' . "\n" . '<p>', $html);
return trim($html);
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace App\Console\Commands;
use App\Models\Entry;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class ImportImages extends Command
{
protected $signature = 'app:import-images {--entry-id=1}';
protected $description = 'Import images into the media library';
public function handle(): void
{
$entryId = $this->option('entry-id');
$entry = Entry::find($entryId);
if (!$entry) {
$this->error("Entry with ID {$entryId} not found");
return;
}
$files = Storage::disk('public')->files('imported_images');
if (empty($files)) {
$this->info('No files found in storage/app/public/imported_images/');
return;
}
$this->info("Found " . count($files) . " files to import");
foreach ($files as $filePath) {
try {
$fullPath = storage_path('app/public/' . $filePath);
$fileName = pathinfo($fullPath, PATHINFO_BASENAME);
if (!file_exists($fullPath)) {
$this->error("File not found: {$fullPath}");
continue;
}
// Check if already exists
$existingMedia = $entry->getMedia()->where('file_name', $fileName)->first();
if ($existingMedia) {
$this->info("Skipping existing: {$fileName}");
continue;
}
$media = $entry->addMedia($fullPath)
->toMediaCollection('default');
$this->info("Imported: {$fileName}");
} catch (\Exception $e) {
$this->error("Failed to import {$filePath}: " . $e->getMessage());
}
}
$this->info('Import completed');
}
}

View file

@ -17,18 +17,33 @@ class EntriesTable
{
return $table
->columns([
TextColumn::make('id')
->label('ID')
->sortable(query: function ($query, $direction) {
$query->orderBy('id', $direction);
}),
SpatieMediaLibraryImageColumn::make('featured_image')
->collection('featured-image')
->label('Image')
->circular()
->stacked()
->limit(3),
TextColumn::make('title')
->searchable(),
TextColumn::make('slug')
->searchable()
->sortable(),
TextColumn::make('category.name')
->label('Category')
->sortable(query: function ($query, $direction) {
$query->join('categories', 'entries.category_id', '=', 'categories.id')
->orderBy('categories.name', $direction)
->select('entries.*');
})
->searchable(),
IconColumn::make('is_published')
->label('pub')
->boolean(),
IconColumn::make('is_featured')
->label('feat')
->boolean(),
TextColumn::make('published_at')
->date()

View file

@ -32,6 +32,7 @@ class Entry extends Model implements HasMedia, HasRichContent
'content',
'call_to_action_link',
'call_to_action_text',
'category_id',
];
/**