develate/blogframe

A static Markdown blog framework for Laravel applications

Maintainers

Package info

github.com/develate/blogframe

pkg:composer/develate/blogframe

Transparency log

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0 2026-07-20 18:21 UTC

This package is auto-updated.

Last update: 2026-07-20 18:46:51 UTC


README

Blogframe is a Laravel package for blogs whose articles live in version-controlled Markdown files. It discovers translated articles, parses YAML front matter, renders CommonMark with runtime custom tags, and serves local images through signed responsive-image URLs.

Blogframe is deliberately a content package: your application keeps control of article routes, controllers, Blade views, feeds, and presentation.

Requirements

  • PHP 8.2 or newer
  • Laravel 12 or 13
  • GD or Imagick for responsive image generation

Installation

Install the package with Composer:

composer require develate/blogframe

Laravel discovers BlogframeServiceProvider and the Blogframe facade automatically. Publish the configuration when you need to change the defaults:

php artisan vendor:publish --tag=blogframe-config

Article structure

Articles belong in the host application's resources/blog/articles directory. The directory name supplies the positive numeric ID and name; the Markdown filename supplies the language:

resources/
└── blog/
    ├── articles/
    │   └── 22-beginners-guide-to-seo/
    │       ├── de.md
    │       └── en.md
    └── images/
        ├── 1.png
        └── content/
            └── audit.webp

Every translation requires YAML front matter with a non-empty title and image. Other attributes are preserved:

---
title: Einsteigerleitfaden für SEO
image: 1.png
description: Die wichtigsten Grundlagen für bessere Suchergebnisse.
published_at: 2026-07-20
tags:
  - seo
  - marketing
---

# Willkommen

This body is rendered with CommonMark.

{{image src=content/audit.webp alt="Screenshot of the SEO audit" class="rounded-xl"}}

The following front-matter keys are reserved because Blogframe derives them: id, name, language, image_url, image_srcset, image_sizes, markdown, html, and source_path.

Invalid filenames, duplicate IDs or translations, malformed YAML, missing required attributes, and missing images throw a descriptive Blogframe exception instead of silently hiding content.

Querying articles

Use the facade from controllers, view models, commands, or application code:

use Develate\Blogframe\Facades\Blogframe;

$articles = Blogframe::all('de');
$article = Blogframe::find(22, 'de');
$article = Blogframe::findOrFail(22, 'de');

all() returns an Illuminate\Support\Collection ordered by descending article ID. If no language is supplied, Blogframe uses Laravel's current locale. When a translation is unavailable it tries the configured fallback language, which defaults to app.fallback_locale.

An article is an immutable object with these public properties:

$article->id;
$article->name;
$article->language;
$article->title;
$article->image;
$article->imageUrl;
$article->imageSrcset;
$article->imageSizes;
$article->attributes;
$article->markdown;
$article->html;
$article->sourcePath;

$description = $article->attribute('description');
$html = $article->toHtml();

markdown contains only the body after front matter has been removed. html and toHtml() contain the rendered CommonMark output.

Header and OG images

imageUrl is the signed original image URL and is suitable for Open Graph metadata:

<meta property="og:image" content="{{ $article->imageUrl }}">

For a responsive header, add the generated candidate list and sizes:

<img
    src="{{ $article->imageUrl }}"
    @if ($article->imageSrcset) srcset="{{ $article->imageSrcset }}" @endif
    @if ($article->imageSizes) sizes="{{ $article->imageSizes }}" @endif
    alt="{{ $article->title }}"
>

You can also generate image values directly:

$originalUrl = Blogframe::imageUrl('1.png');
$srcset = Blogframe::imageSrcset('1.png');

The image custom tag

The built-in image tag resolves images relative to resources/blog/images:

{{image src=1.png alt="Homepage preview" class="rounded shadow"}}

With local image serving enabled, the result contains:

  • a signed fallback src near the configured fallback width;
  • a signed srcset containing only candidates that do not upscale the source;
  • sizes="100vw" by default, overridable with the tag's sizes attribute;
  • intrinsic width and height, unless explicitly provided.

The tag accepts src, alt, class, id, title, width, height, loading, decoding, fetchpriority, role, sizes, data-*, and aria-*. Blogframe generates srcset; an authored srcset, positional argument, event handler, style, or unknown attribute is rejected. Attribute values are HTML-escaped.

Registering custom tags

Additional tags are registered at runtime, not in configuration. Register them in your application's AppServiceProvider::boot() method:

namespace App\Providers;

use App\Markdown\CalloutTag;
use Develate\Blogframe\Facades\Blogframe;
use Illuminate\Support\ServiceProvider;

final class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Blogframe::customtag(new CalloutTag());
    }
}

The identifier returned by the tag is its registry key. A later registration with the same identifier replaces the existing tag and invalidates rendered articles in the current lifecycle:

Blogframe::customtag(new FirstCalloutTag());  // identifier: callout
Blogframe::customtag(new NewCalloutTag());    // replaces FirstCalloutTag

The built-in image tag follows the same rule and can be replaced by registering another tag whose identifier is image.

Custom tags extend Develate\CommonmarkCustomtags\Customtag. See develate/commonmark-customtags for its argument syntax and interface.

Configuration

The published config/blogframe.php contains all supported settings:

use Intervention\Image\Drivers\Gd\Driver;

return [
    'paths' => [
        'articles' => resource_path('blog/articles'),
        'images' => resource_path('blog/images'),
    ],

    'languages' => [
        'default' => null,  // app locale
        'fallback' => null, // app fallback locale
    ],

    'commonmark' => [
        'options' => [
            'html_input' => 'strip',
            'allow_unsafe_links' => false,
        ],
        'extensions' => [],
    ],

    'images' => [
        'serve' => true,
        'base_url' => null,
        'route_prefix' => 'blogframe/images',
        'middleware' => [],
        'allowed_extensions' => ['png', 'jpg', 'jpeg', 'webp', 'gif', 'avif'],
        'widths' => [320, 640, 960, 1280, 1600],
        'fallback_width' => 960,
        'sizes' => '100vw',
        'driver' => Driver::class,
        'quality' => 82,
        'cache_path' => storage_path('framework/cache/blogframe/images'),
        'cache_control' => 'public, max-age=31536000, immutable',
    ],
];

Configured CommonMark extension classes are resolved through Laravel's container. Blogframe always owns the single CustomtagExtension; register individual tags with Blogframe::customtag() instead of configuring another custom-tag extension.

Use Intervention\Image\Drivers\Imagick\Driver when Imagick is installed and preferred. An allowed source format must also be supported by the selected driver. SVG is intentionally unsupported.

Signed responsive image routes

When images.serve is enabled, Blogframe registers this named route:

{route_prefix}/{source-fingerprint}/{width-or-original}/{image-path}

Every URL has a permanent relative Laravel signature. The source fingerprint, requested width, and path are covered by that signature. The route additionally rejects widths outside the configured allowlist, upscaling, stale fingerprints, unsupported files, and paths that leave the configured image directory.

Relative signature validation allows the generated URLs to pass through a proxy or CDN host without weakening path validation. Public URLs are absolute so they can be used for Open Graph metadata.

Derivatives are generated proportionally on their first request, retain the source format, and are cached beneath images.cache_path. A per-variant file lock and atomic rename protect concurrent first requests. The source fingerprint changes when the source contents change, giving new URLs and making the default one-year immutable cache header safe. Old derivative directories may be removed during normal deployment cleanup.

When images.serve is false, Blogframe registers no image route and generates no srcset. Set images.base_url to the public directory or CDN containing the same relative image paths:

'images' => [
    'serve' => false,
    'base_url' => 'https://cdn.example.com/blog',
    // ...
],

Exceptions

  • ArticleNotFoundException is thrown by findOrFail().
  • InvalidArticleException describes invalid article files or rendering failures.
  • InvalidImageException describes unsafe, missing, or unsupported images.
  • InvalidConfigurationException describes inconsistent package configuration.

The signed image route intentionally translates invalid image requests to HTTP 404 responses; Laravel's signature middleware returns HTTP 403 before image processing when a signature is missing or modified.

Testing

composer test

The test suite uses Orchestra Testbench and covers article discovery, translations, runtime tag replacement, responsive rendering, derivative caching, source invalidation, and signed-route security.

License

Blogframe is open-source software licensed under the MIT license.