Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Front Matter Laravel Package

webuni/front-matter

webuni/front-matter is a PHP library for extracting and parsing front matter from files. It separates metadata from content, supports YAML-style headers, and makes it easy to read Markdown front matter in PHP applications. Simple, lightweight, and framework-agnostic.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require webuni/front-matter
composer show webuni/front-matter   # confirm the major version you got

Where to look first

Open vendor/webuni/front-matter/src/ and skim, in this order: FrontMatterInterface.php, DocumentInterface.php, FrontMatter.php. The whole surface is parse(string): DocumentInterface and usually parseFile(string): DocumentInterface; the returned document exposes getData() (the YAML metadata as an array) and getContent() (the body). Type-hint FrontMatterInterface everywhere rather than the concrete class — 1.x and 2.x differ on whether the class is FrontMatter or YamlFrontMatter, but the interface is stable.

Bind it once

// app/Providers/AppServiceProvider.php
use Webuni\FrontMatter\FrontMatterInterface;
use Webuni\FrontMatter\FrontMatter;

public function register(): void
{
    // Use a closure if your version's constructor expects a YAML parser argument.
    $this->app->singleton(FrontMatterInterface::class, fn () => new FrontMatter());
}

First use case: an artisan import command that turns storage/app/content/*.md into Post rows — front matter becomes columns, body becomes the markdown column.

$document = app(FrontMatterInterface::class)->parse($markdown);

$document->getData();    // ['title' => 'Hello', 'tags' => ['php', 'laravel']]
$document->getContent(); // "Hello world\n"

Implementation Patterns

Import command with validation (the bread-and-butter pattern)

class ImportContent extends Command
{
    protected $signature = 'content:import {dir=content}';

    public function handle(FrontMatterInterface $frontMatter): int
    {
        foreach (File::files(storage_path('app/'.$this->argument('dir'))) as $file) {
            $document = $frontMatter->parseFile($file->getPathname());
            $data = $document->getData();

            Validator::make($data, [
                'title'        => 'required|string|max:200',
                'slug'         => 'nullable|string|alpha_dash',
                'published_at' => 'nullable|date',
                'tags'         => 'nullable|array',
                'tags.*'       => 'string',
            ])->validate();

            Post::updateOrCreate(
                ['slug' => $data['slug'] ?? Str::slug($data['title'])],
                [
                    'title'        => $data['title'],
                    'body'         => $document->getContent(),
                    'meta'         => Arr::except($data, ['title', 'slug', 'published_at']),
                    'published_at' => isset($data['published_at'])
                        ? Carbon::parse($data['published_at'])
                        : null,
                ]
            );
        }

        return self::SUCCESS;
    }
}

Treat getData() as untrusted input. It is author-supplied YAML; validate it exactly like a request payload, and never extract() it into local scope.

Render pipeline

$document = $frontMatter->parse(File::get($path));

return view('post', [
    'meta' => $document->getData(),
    'html' => Str::markdown($document->getContent(), ['html_input' => 'escape']),
]);

Keep parsing and rendering as two steps — that way the same parsed document feeds both the <head> (title, description, og: tags) and the body.

Cache the parsed result, not the file read

$payload = Cache::remember(
    "content:{$slug}:".File::lastModified($path),
    now()->addHour(),
    fn () => tap($frontMatter->parseFile($path), null)
        ->pipe(fn ($d) => ['data' => $d->getData(), 'content' => $d->getContent()])
);

Cache the plain ['data' => ..., 'content' => ...] array rather than the document object — it survives serialization drivers cleanly and is obvious in redis-cli.

Custom delimiters / other formats

Implement FrontMatterInterface yourself (TOML +++ blocks, JSON header for API-driven content) and swap the container binding. Alternatively decorate: a class that takes the package parser, strips a leading comment block, and delegates. Consumers never change.

Testing

Storage::fake('content') + $this->artisan('content:import') + assertDatabaseHas. For the parser itself use assertSame on getContent() — whitespace differences are the whole bug surface here, and assertEquals will hide them.

Gotchas and Tips

  • Delimiter must be the very first bytes. A UTF-8 BOM, a leading blank line, or an editor-inserted \r\n before --- makes the parser silently return the whole file as body and [] as data. Normalize on read: $raw = ltrim($raw, "\xEF\xBB\xBF");
  • Only the first pair counts. A --- horizontal rule later in the markdown body is safe — but a document whose body starts with --- will be misread as an opening fence with no metadata.
  • Body whitespace is not normalized. Depending on version the body may keep a leading newline after the closing ---. Never compare rendered HTML in tests; compare getContent() with assertSame and pin the exact expected string.
  • Empty front matter (---\n---\n) legitimately yields [] plus the full body — same as no front matter at all. If you need to distinguish "no metadata" from "declared empty", check the raw file yourself.
  • YAML type coercion will bite your schema. published_at: 2025-02-08 may arrive as a DateTimeImmutable (Symfony YAML PARSE_DATETIME) or a string, depending on the parser wired in; yes/no/on/off become booleans; 1.10 becomes the float 1.1; 0123 becomes octal-ish ints. If a column is a string, cast defensively — (string) on a bool is "1", not "yes".
  • Quote anything with a colon. title: Laravel: The Good Parts is a YAML parse error. Same for values starting with @, `, *, !, %, {. This is the #1 author error in a content repo; surface the parser exception with the filename attached.
  • Errors throw. parseFile() raises on a missing file; malformed YAML raises from src/Exception (or the underlying parser). Wrap per-file work in try/catch inside the import loop so one bad post doesn't kill the run, and log with $file->getFilename() in context.
  • No round-trip fidelity. Comments, key ordering, and block-style formatting in the front matter are gone after parse. If you need to write files back, generate the YAML yourself and don't expect the original to survive.
  • Str::markdown() knows nothing about front matter. Strip with this package first, or you'll render --- as an <hr> and dump your metadata into the page. (If you'd rather do it in one pass, league/commonmark's FrontMatterExtension bundles both — but then you lose the clean data/body split this package gives you.)
  • Front matter is a place for secrets to leak. draft: true, internal author_email, reviewer: notes all end up in getData(). Filter explicitly before passing metadata to the view; don't @foreach ($meta as $k => $v) into <meta> tags.
  • Performance: parsing is cheap but not free, and it happens per request if you parse in a controller. Cache per file mtime, and for a small static site consider an artisan content:build step that pre-renders into a pages table or cache keys rather than parsing at request time.
  • Extension points: the container binding is your seam. Wrap FrontMatterInterface in a logging decorator to find which files are parsed on which routes, or in a memoizing decorator keyed by path+mtime, without touching call sites.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib