SculpinPostsBundle

The default behavior of Sculpin is that no one source has any meaning over any other Source. PostsBundle allows for treating some sources as if they are Posts. For all intents and purposes, it adds blogging capabilities to Sculpin.

Installation

The bundle should be included in Sculpin and enabled by default.

If you are building your own custom Sculpin from the ground up, you need to require either sculpin/sculpin or sculpin/posts-bundle via Composer and you need to ensure that Sculpin\Bundle\PostsBundle\SculpinPostsBundle is registered.

Configuration

The default configuration is as follow:

sculpin_posts:
    paths: ["_posts"]
    publish_drafts: null
    permalink: pretty
    layout: post

paths

Paths is a list of directories in which sources should be considered Posts. default setting is ["_posts"] which means any source under sources/_posts will be considered a Post.

publish_drafts

Posts may be considered draft if they have a draft top level metadata key in their YAML frontmatter.

The default behavior is that if publish_drafts is not set (null) it will be set based on the Kernel's environment. If the environment is prod, the value is set to false. In all other cases, the value is set to true.

The end result is that in the default dev environment drafts will be published. If --env=prod is specified, drafts will not be published.

If drafts are set to be published, any Post that is marked as draft will automatically be tagged with the "drafts" tag.

The default permalink to use if one is not specified by the source itself. Defaults to pretty.

To use a common date based format, you could set the permalink to something like:

blog/:year/:month/:day/:filename/

layout

The default layout to use if one is not specified by the source itself. Defaults to post.

Post

A Post is a tiny wrapper over a ProxySource. A ProxySource is a more view-friendly interface around a SourceInterface. The Post wrapper provides a little bit of extra functionality specific to posts.

Drafts

A Post may be marked as draft by setting the drafts top level metadata key to true. A Post with this setting will be treated as a draft and subject to the publish_drafts rules listed above.

---
draft: true
title: This is a WIP
---
# More to come later

If a draft Post is published it will automatically have the "drafts" tag added to it.

Title

A Post has a title as a first class concept. It is set by the title top level metadata key. If a Post is used from a data provider, the title is available as post.title, which is a shortcut for post.meta.title.

Example:

---
title: The Best Title Ever
---
# Hello World

Date

A Post has a notion of a date. It is a "calculated" value based on either information in the filename ("YYYY-MM-DD-rest-of-filename.md") or read from the date top level metadata key. If a Post is used from a data provider, date can be easily found by requesting post.date.

Example of a date being specified by YAML frontmatter:

---
title: The Best Title Ever
date: 2013-09-29 20:05
---
# Hello World

Example of a date being specified by a filename (example, "2013-09-29-the-best-title-ever.md"):

---
title: The Best Title Ever
---
# Hello World

Tags and Categories

Posts have special support for tags and categories. These can be added to a post by including tags and categories top level metadata keys.

---
title: The Best Title Ever
tags: ["simple", "example", "php"]
categories: ["opinion", "software"]
---
# Hello World

Previous Post and Next Post

A Post has a concept of the Post that comes before and after it. These are set as a part of the Posts initialization. They are available as post.nextPost and post.previousPost. They will be null where appropriate.

These are also available via metadata by accessing post.meta.next_post and post.meta.previous_post.

Posts DataProvider

Posts can be injected into any source by using them with use: ["posts"].

---
use:
    - posts
---

DataProvider provided data behaves in a way that is similar to a standard source but there are a few differences.

First, standard YAML frontmatter is accessible via post.meta. Second, in order to get access to the content for the source, its appropriate block must be access like post.blocks.content.

Extending Posts Behavior with a Map

A custom Map can be applied to posts by registering a service that implements Sculpin\Core\Source\MapInterface that is tagged with sculpin_posts.posts_map.

A trivial example would be a map that looks at the title of every post and automatically tags important words. The use case would be that if you write about Silex very often and want to ensure that every post with the word Silex in the title has the silex tag even if you forget.

AutoTagFromTitle Class

This is a trivial implementation of a Sculpin Source Map. Every Source that is determined to be a post will be sent to this service.

namespace Acme\SculpinBlog;

use Sculpin\Core\Source\Map\MapInterface;
use Sculpin\Core\Source\SourceInterface;

class AutoTagFromTitleMap implements MapInterface
{
    private $tags;

    public function __construct(array $tags = [])
    {
        $this->tags = array_map(function ($value) {
            return strtolower($value);
        }, $tags);
    }

    public function process(SourceInterface $source)
    {
        $tags = [];

        // Break out the title into words.
        $nameParts = explode(' ', $source->data()->get('title'));

        foreach ($nameParts as $name) {
            if (in_array(strtolower($name), $this->tags)) {
                // If this is a word we care about, we should add
                // it to our tags list.
                $tags[] = strtolower($name);
            }
        }

        $existingTags = $source->data()->get('tags');
        if (null === $existingTags) {
            // The Post had no tags to begin with
            $existingTags = [];
        } else {
            if (!is_array($existingTags)) {
                if ($existingTags) {
                    // Normalize in case people did something
                    // not smart.
                    $existingTags = [$existingTags];
                } else {
                    // Just in case...
                    $existingTags = [];
                }
            }
        }

        // Combine the tags we added with the existing tags making
        // sure that we do not duplicate tags.
        $source->data()->set('tags', array_unique(array_merge(
            $tags,
            $existingTags
        )));
    }
}

Register the Service

This can be handled in any number of ways. The best way would probably be to publish it as a bundle and require it with sculpin.json or composer.json. For the sake of brevity, we'll show an example of loading this through sculpin_services.yml just to show how to wire up the service.

#
# app/config/sculpin_services.yml
#

parameters:
services:
    acme_sculpin_blog_auto_tag_from_title_map:
        class: Acme\SculpinBlog\AutoTagFromTitleMap
        arguments:
            - ["yolo", "silex", "symfony"]
        tags:
            - { name: sculpin_posts.posts_map }