Simple Website Engine

Simple Yet Powerful Website Generator

Markdown Blog Example

The Markdown blog example is a little bit more complex than the other examples, but it demonstrates how to create a simple blog using Markdown files as the content source. Each blog post is stored as a separate Markdown file in the articles/ directory. The example includes an action handler to read the Markdown files, convert them to HTML, and render them using a template.

There is already a plugin for that: Blog Plugin

Prerequisites

Installing Parsedown and Yaml-Front-Matter

For this example an external libraries are used called Parsedown and YAML Front Matter are used and has to be installed using composer.

composer require erusev/parsedown
composer require spatie/yaml-front-matter

This would create vendor/ directory in the project root directory. The vendor/autoload.php would be automatically included if it's present.

URL and File Structure

To create a blog functionality into an existing website with the following URLs:

https://example.com/blog/           # would map to /blog/index.php
https://example.com/blog/article    # would map to /blog/article.php

The following file structure is needed:

/var/www/project-root
|- /actions/blog
│- |- /index.php          # Handle article list requests (with pagination)
│- |- /article.php        # Handle single article requests
|- /views/blog
│- |- /index.html         # Template for article list
│- |- /article.html       # Template for single article
|- /articles              # Markdown files for blog posts

Actions

blog/index.php

<?php
class Index {
    public function get() {
        //Get pagination parameter
        $currentPage = getQuery('page', 1);

        // Get the markdown file directory
        $filesdir = ROOT_DIR . "/articles";

        // get the files in the directory
        $files = scandir($filesdir);
        
        // Subtracting 2 to account for "." and ".." 
        // this would be used for pagination total
        $fileCount = count($files) - 2;

        // Sort the files by modified time so new files would be first
        $filesList = [];
        foreach ($files as $file) {
            // Ignore . and ..
            if ($file === '.' || $file === '..') {
                continue;
            }
            //Make an array with filenames and their modified time for sorting
            $path = $filesdir . "/" . $file;
            if (is_file($path)) {
                // Store filename as key and MTime as value
                $filesList[$file] = filemtime($path);
            }
        }

        // Sort by modified time (arsort for newest first, asort for oldest first)
        arsort($filesList);

        // Get just the filenames (the keys)
        $sortedFilenames = array_keys($filesList);

        // Slice the array for the pagination
        $articles = array_slice($sortedFilenames, ($currentPage - 1) * $_ENV['PAGINATION'], $_ENV['PAGINATION']);

        // Loop through the slice of the files and construct the articles array
        foreach ($articles as $key => $article) {
            // Parse the markdown file for YAML front matter and content
            $object = Spatie\YamlFrontMatter\YamlFrontMatter::parse(file_get_contents($filesdir . "/" . $article));    

            // Create the limited description displayed in the articles list the max length is 400 characters.
            $description = substr(strip_tags((new Parsedown())->text($object->body())), 0, 400);
            $description = substr($description, 0, strrpos($description, ' ')) . '...';

            // Construc the final article data
            $articles[$key] = [
                'url' => pathinfo($article, PATHINFO_FILENAME), // the filename without extension for url
                'title' => $object->title, // This is the article title
                'image' => $object->featured, //article featured image
                'description' => $description // limited description for the articles list
            ];
        }
        //reindex the array to have sequential keys
        $articles = array_values($articles);
        
        return [
            'totalRecords' => $fileCount, //Used for pagination
            'articles' => $articles, // Articles data
        ];
    }
}

blog/article.php

<?php
class Article
{
    public function get()
    {
        // Get the markdown file name from the url query parameter
        $url = getQuery('url'); // get article name (md file name without extension)
        
        // ... more validation can be done on the $url here ...

        // $url is missing or empty
        if (!$url) {
            redirect('/404', 301); // Redirect to 404 page
        }

        // Construct the file path where the markdown file is located
        $filepath = ROOT_DIR . "/articles/" . $url . ".md";

        // The file is missing
        if (!file_exists($filepath)) {
            redirect('/404', 301); // Redirect to 404 page
        }

        // Parse the markdown file for YAML front matter and content
        $object = Spatie\YamlFrontMatter\YamlFrontMatter::parse(file_get_contents($filepath));    

        // Return data to be used in the template
        return [
            'title' => $object->title, // This is the article title
            'image' => $object->image, // This is the article featured image
            'content' => (new Parsedown())->text($object->body()) // This is the article content in HTML format
        ];
    }
}

Views

blog/index.htlm

<!--layout:/blog-layout.html-->
<section>
    <!--foreach($articles as $article)-->
        <article>
            <img src="<!--$article['image']-->" alt="<!--$article['title']-->">
            <h2><a href="/blog/article?url=<!--$article['url']-->"><!--$article['title']--></a></h2>
            <p><!--$article['description']--></p>
            <a href="/blog/article?url=<!--$article['url']-->">Read more</a>
        </article>  
    <!--endforeach-->
    <!--pagination-->
</section>
    

blog/article.html


<!--layout:/blog-layout.html-->
<article>
    <h1><!--$title--></h1>
    <!--($content)-->
    
    <a href="/blog/">Back to Articles list</a>
</article>
    

What a markdown file would look like?

The markdown file should have the following structure to allow extracting the page title and image from the file for the article list.

---
title: "My First Blog Post"
image: "/assets/blog/img/my-first-blog-post-featured.jpg"
---
# This is the content of my first blog post
Welcome to my blog! This is where I share my thoughts and experiences.
![An image in the blog post](/assets/blog/img/my-first-blog-post-image.jpg)
Here is some more content in **Markdown** format.