PHPackages                             bristol-digital/qwikblog - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Parsing &amp; Serialization](/categories/parsing)
4. /
5. bristol-digital/qwikblog

ActiveLibrary[Parsing &amp; Serialization](/categories/parsing)

bristol-digital/qwikblog
========================

A simple, file-based blog for Laravel — no database required. Markdown posts with YAML front matter, a Livewire admin with WYSIWYG editor and image gallery, full-text search, scheduling, RSS, sitemap, and SEO meta.

v1.1.7(1mo ago)062MITPHPPHP ^8.2

Since Nov 23Pushed 1mo agoCompare

[ Source](https://github.com/bristol-digital/qwikblog)[ Packagist](https://packagist.org/packages/bristol-digital/qwikblog)[ RSS](/packages/bristol-digital-qwikblog/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (12)Versions (19)Used By (0)

QwikBlog
========

[](#qwikblog)

A simple, file-based blog for Laravel — headless front-end + lightweight admin with a Livewire-powered image gallery and a WYSIWYG body editor. No database required. Posts live in `resources/posts/` as Markdown files with YAML front matter.

Installation
------------

[](#installation)

```
composer require bristol-digital/qwikblog
```

The package's service provider auto-registers via Laravel's package discovery. Then publish the config and the admin's JS entry point:

```
php artisan vendor:publish --tag=qwikblog-config
php artisan vendor:publish --tag=qwikblog-admin-js
```

The admin JS publish puts `resources/js/qwikblog-admin.js` in your host app — a tiny Vite entry that imports Toast UI Editor for the post form's body field. Wire it into your Vite build (`vite.config.js`):

```
input: [
  'resources/css/app.css',
  'resources/js/app.js',
  'resources/js/qwikblog-admin.js',  //  **Note:** the package can't auto-install its own npm dependencies because Composer and npm are separate ecosystems. Every Laravel package that uses JS libraries works this way — `composer require` for PHP, `npm install` for JS, no automatic bridge between them.

Set admin credentials in `.env` (see [Configuration](#configuration)):

```
ADMIN_USERNAME=your-username
ADMIN_PASSWORD=your-password
```

Make sure your host app's Tailwind setup includes the package's blade files in its content scanning (so admin styling Just Works) and has the typography plugin and Alpine for the public views. In `resources/css/app.css` (or your Tailwind config):

```
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@source "../../vendor/bristol-digital/qwikblog/resources/views/**/*.blade.php";
```

In `resources/js/app.js`:

```
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();
```

The package's public views (post index, single post, etc.) extend a layout in your host app. Which value of `QWIKBLOG_LAYOUT` to set in `.env`depends on what your project has:

- **If you have `resources/views/app.blade.php`** — `QWIKBLOG_LAYOUT=app`
- **If you have `resources/views/layouts/app.blade.php`** — `QWIKBLOG_LAYOUT=layouts.app`
- **If you have neither yet** (common on fresh Laravel 11+ installs) — publish the package's bundled starter layout and point at it:

    ```
    php artisan vendor:publish --tag=qwikblog-views
    ```

    Then in `.env`:

    ```
    QWIKBLOG_LAYOUT=vendor.qwikblog.app
    ```

    The bundled starter is clean and functional; you can edit it (`resources/views/vendor/qwikblog/app.blade.php`) or replace it later with your own layout.

Whichever layout you use, make sure it has `@stack('head')` inside ``so the package can inject SEO meta. To enable RSS autodiscovery, also add:

```

```

Finally:

```
npm run dev
php artisan serve
```

To populate with the bundled flamenco demo content (12 posts across 4 categories with images):

```
php artisan vendor:publish --tag=qwikblog-seeds
php artisan blog:examples flamenco
```

Visit `/` to see the blog. Visit `/admin` to log in.

Routes
------

[](#routes)

RouteDescription`/` and `/blog`Post index — both serve the same content`/blog/{slug}`Single post **or** category filter **or** tag filter (resolves in that order)`/blog/search?q=...`Full-text search results`/blog/author/{slug}`All posts by an author`/blog/{year}`All posts from a year`/blog/{year}/{month}`All posts from a month (e.g. `/blog/2024/11`)`/blog/category/{slug}`Category filter — explicit prefix, always works`/blog/tag/{slug}`Tag filter — explicit prefix, always works`/blog/feed.xml`RSS 2.0 feed`/sitemap.xml`XML sitemapPublic views live at `resources/views/blog/index.blade.php` and `show.blade.php`. Both extend the host site's `app.blade.php` layout. The package ships a starter `app.blade.php`; host sites typically have their own and only need `@stack('head')` somewhere in `` for SEO meta to inject correctly.

Post fields
-----------

[](#post-fields)

Posts are stored as `resources/posts/YYYY-MM-DD-slug.md`. Front matter:

```
---
title: My First Post
subtitle: An optional subtitle
summary: Short excerpt — used on listings.
categories: Announcements, News
tags: launch, hello-world
hero_image: /images/blog/my-first-post/1.jpg
author: Jane Editor
date: 2024-11-15 10:00:00
---

Markdown body goes here.
```

FieldRequiredNotes`title`yesDetermines the URL slug`subtitle`noShown on cards and post header`summary`yesShort excerpt for listings, OG description`categories`noComma-separated; multi-valued`tags`noComma-separated; multi-valued`hero_image`noPath under `public/`; falls back to first gallery image`author`yesByline; links to `/blog/author/{slug}``date`yes`YYYY-MM-DD` or `YYYY-MM-DD HH:MM:SS`; future = scheduledThe admin writes this format automatically. The legacy singular `category:`field is still read for back-compat with older posts.

> **List values — inline or multi-line.** Categories and tags can be written either inline (as above) or as multi-line YAML lists; both parse identically:
>
> ```
> categories:
>   - Announcements
>   - News
> tags:
>   - launch
>   - hello-world
> ```
>
>
>
> The admin always writes the inline form. The multi-line form is supported for hand-authored or pasted-in content — useful when working with AI-generated drafts or copying from snippets that use standard YAML list syntax.

Categories and tags
-------------------

[](#categories-and-tags)

Posts can have any number of each. Both render in the right-rail sidebar on the index page (categories as a vertical list, tags inline as `#hashtag` chips), each with a post count. Active filter is highlighted.

URLResult`/` or `/blog`All published posts`/blog/palos`Posts in **Palos** (flat URL style, the default)`/blog/category/palos`Same — explicit prefix; always works regardless of style`/blog/gitano`Posts tagged **gitano**`/blog/tag/gitano`Same — explicit prefixOne filter at a time — clicking a different chip replaces the active filter rather than stacking with AND. Each post's own categories and tags also appear in the show-page sidebar with counts.

If no published post has any categories, the categories section hides entirely. Same for tags. The view checks `count(...) > 0` before rendering.

### URL collision

[](#url-collision)

Posts win on slug collision. A post titled "Palos" would shadow the Palos category filter at `/blog/palos`. If you need strict separation, set `QWIKBLOG_TAXONOMY_URL_STYLE=prefixed` — chip links then go to `/blog/category/palos` etc. and never collide with post slugs.

Scheduling
----------

[](#scheduling)

Set `date` to a future timestamp. The post will:

- Show in the admin index with a yellow **Scheduled** badge and a relative countdown ("in 3 weeks") that updates on the 30-second `wire:poll`
- Be hidden from the public `/blog` until the date passes
- Return 404 at its slug to public visitors but render normally for logged-in admins (preview)
- Auto-publish silently when the date arrives — no cron job required

Search
------

[](#search)

`/blog/search?q=...` runs an AND match across title, subtitle, summary, author, categories, tags, and rendered body content. Multi-term queries are supported (`jerez bulerías` returns posts containing both terms). Pure-PHP `str_contains` — fine up to a few thousand posts; past that, swap in a real index.

Search results pages are `noindex` by default — every unique query is a unique URL and not worth indexing.

Author pages
------------

[](#author-pages)

`/blog/author/{slug}` lists every published post by an author. The author byline on each show page links here automatically.

Archive
-------

[](#archive)

`/blog/{year}` and `/blog/{year}/{month}` return posts from that period. The sidebar archive nav appears when at least 2 distinct months are represented; below that it stays hidden (a single-month archive is just the index).

Related posts
-------------

[](#related-posts)

Each post page shows up to 3 related posts at the bottom, scored by taxonomy overlap:

```
score = (shared_tags × 2) + (shared_categories × 1)

```

Posts scoring 0 are excluded entirely — better to hide the section than show weak recommendations. Weights and limit are configurable.

RSS, sitemap, SEO
-----------------

[](#rss-sitemap-seo)

FeatureWhereRSS 2.0 feed`/blog/feed.xml` (most recent 50 posts)Sitemap`/sitemap.xml` (every post + filter URL + author + archive)Autodiscovery ``In every page's ``Open Graph + Twitter CardOn every post pageCanonical URLEvery pageReading time"X min read" on post pages, computed at 200 wpm by defaultRobots noindexSearch results, scheduled-post admin previewsReference the sitemap from `public/robots.txt`:

```
Sitemap: https://your-site.test/sitemap.xml

```

Image gallery
-------------

[](#image-gallery)

Each post has its own folder at `public/images/blog/{slug}/`. Images are numbered `1.jpg`, `2.jpg`, … and ordering is set by the numeric filename.

The Livewire gallery (`/admin/posts/{slug}/images`) lets you:

- Drag-and-drop upload (multi-select, JPG/PNG/GIF, max 20 MB each)
- Resize and re-orient automatically (max 1600×1200, 85% quality JPG; GIFs pass through untouched so animations are preserved)
- Reorder images by drag
- Delete individual images
- Copy a path to the clipboard for pasting into the post's `hero_image` field

If `hero_image` is left blank in the front matter, the first numerically- named image is used automatically. Set it explicitly to override.

When a post is renamed, its image folder moves with it. When a post is deleted, its image folder is deleted too.

On the public show page, multiple images render as a crossfading carousel with prev/next, dot navigation, a counter, and a thumbnail strip.

Body editor (WYSIWYG)
---------------------

[](#body-editor-wysiwyg)

The admin's body field uses [Toast UI Editor](https://ui.toast.com/tui-editor), defaulting to WYSIWYG mode with a Markdown toggle in the toolbar. The editor is bundled via Vite from `resources/js/qwikblog-admin.js` (published into your host app on install — see [Installation](#installation)) — no CDN dependency at runtime. Posts are stored as plain CommonMark, so files remain hand-editable.

If you don't run the editor and just want plain markdown editing, delete `resources/js/qwikblog-admin.js` and remove `@vite(['resources/js/qwikblog-admin.js'])`from the admin layout (publish the views via `vendor:publish --tag=qwikblog-views`to override). The textarea behind the editor stays usable as a markdown fallback in any case.

Admin
-----

[](#admin)

A small admin lives at `/admin` (or whatever path you set via `QWIKBLOG_ADMIN_PATH`).

### Auth — three options

[](#auth--three-options)

The package's admin protection is configurable via `QWIKBLOG_ADMIN_MIDDLEWARE`. By default it uses the package's own self-contained auth (single shared username/password from `.env`). For sites with their own auth system, you can wire the blog admin into that instead so users only log in once.

**Option 1 — Self-contained auth (default)**

Set credentials in `.env`:

```
ADMIN_USERNAME=your-username
ADMIN_PASSWORD=your-password
```

If either is empty, login is refused — there is no default password. Visit `/admin/login` to log in. This is the right choice for static sites, no-database deployments, or any host that doesn't already have a login system.

**Option 2 — Integrate with Laravel's auth**

If your host app already has Laravel auth set up (Breeze, Jetstream, Fortify, Sanctum, or a starter kit's bundled login), tell the package to use that instead:

```
QWIKBLOG_ADMIN_MIDDLEWARE=auth
QWIKBLOG_ADMIN_LOGOUT_ROUTE=logout
```

Now any user logged in to your host app can access the blog admin at `/admin` (or `QWIKBLOG_ADMIN_PATH`). The logout button in the admin chrome calls your host's logout endpoint. You can ignore `ADMIN_USERNAME` / `ADMIN_PASSWORD` entirely.

To restrict the blog admin to specific users (not just any authenticated user), combine with a Laravel gate:

```
QWIKBLOG_ADMIN_MIDDLEWARE=auth,can:manage-blog
```

Then define the gate in your host app's `AppServiceProvider`:

```
Gate::define('manage-blog', fn(User $user) => $user->isAdmin());
```

**Option 3 — Custom middleware**

Any middleware alias your host registers will work. Examples:

```
# Filament panel auth
QWIKBLOG_ADMIN_MIDDLEWARE=panel.auth

# Multiple middlewares stacked
QWIKBLOG_ADMIN_MIDDLEWARE=auth,verified,role:editor

# Fully custom
QWIKBLOG_ADMIN_MIDDLEWARE=my-blog-gate
```

### Routes

[](#routes-1)

RouteDescription`/admin/login`Login form (used only with default `admin` middleware)`/admin` → `/admin/posts`Posts index (Livewire — search, filters, pagination, polls every 30s)`/admin/posts/create`New post`/admin/posts/{slug}/edit`Edit post`/admin/posts/{slug}/images`Manage images (Livewire)The package's `AdminAuth` middleware (alias `admin`) uses session auth, file-based by default (`SESSION_DRIVER=file`), so no database is required.

### Filtering

[](#filtering)

The admin posts index has four filters that combine with AND:

FilterWhat it matches**Search**Title, subtitle, summary, author. Debounced 300ms**Category**Single category — drop-down of every category across all posts**Tag**Single tag**Status**All / Published / ScheduledFilter state is preserved in the URL — `/admin/posts?status=scheduled&category=Palos`is bookmarkable and reload-safe. The header shows totals (`12 published · 3 scheduled · 15 total`); the **Status** dropdown shows the same counts inline so editors can find scheduled posts without first applying a filter.

Page paginates at 30 by default — useful when scheduling content months ahead. Override via `QWIKBLOG_ADMIN_PER_PAGE`.

Scheduled rows show their relative publish time underneath the date ("in 3 weeks"), updating on the 30-second `wire:poll`.

Bulk import
-----------

[](#bulk-import)

For migrating content or seeding test data:

```
php artisan blog:import path/to/manifest.php
```

The manifest is a `.php` file returning an array (recommended for long bodies — heredoc is friendlier than JSON-escaping) or `.json`.

```
