PHP
Article
By Wern Ancheta

Sculpin Extended: Customizing Your Static Site Blog

By Wern Ancheta
Help us help you! You'll get a... FREE 6-Month Subscription to SitePoint Premium Plus you'll go in the draw to WIN a new Macbook SitePoint 2017 Survey Yes, let's Do this It only takes 5 min

If you’re a PHP developer and currently running a blog with a static site generator such as Octopress or Jekyll, wouldn’t it be great if you could use your primary language for it? Yes, it’s healthy for us developers to use more than one language, but let’s be honest – we often want to add some functionality to our blogs, but it’s difficult to accomplish in unfamiliar syntax. In this article, we’ll set up Sculpin, a static site generator for PHP. Just like any other static site generator, it uses markdown files and HTML templates to generate your blog, so the transition should be easy.

Sculpin logo

Installing Sculpin

Installing requires use of the command line and an installed modern version of PHP (5.6 onwards preferably).

wget https://download.sculpin.io/sculpin.phar

Once it’s done downloading, we make it executable:

chmod +x sculpin.phar

Then we move it to the executable directory of our operating system:

sudo mv sculpin.phar /usr/local/bin/sculpin

Alternatively, you can just copy it to any directory where you want to use it.

Executing sculpin will list the commands that you can use:

sculpin commmands

Creating a Blog

Now that you have Sculpin installed, we can create a new blog. We’ll be using the Sculpin Blog Skeleton provided by the Sculpin team. Clone the repository and download it into myblog.

git clone https://github.com/sculpin/sculpin-blog-skeleton.git myblog

Once it’s done cloning, navigate inside myblog and execute sculpin install. This will install all the dependencies needed by Sculpin to run.

cd myblog
sculpin install

This command uses Composer behind the scenes (you can open the sculpin.json file to see the similarity). Sculpin actually bundles an alternative version of Composer, so Composer isn’t needed outside it. If you need third-party packages to be installed just add them inside the require object then execute sculpin install to install them. By default, the packages are installed inside the source/components directory.

Once Sculpin is done installing the packages, execute the following command to generate the site. The watch option means that the site will be automatically re-generated once you change any of the configuration files or the files inside the source directory. The server option runs a server which will serve the generated site. By default, this can be accessed at http://localhost:8000, but if you’re using a good VM environment, the setup will vary depending on your open ports. See this post about Spress, a Sculpin alternative to learn how to set it up in a VM.

sculpin generate --watch --server

The generated site will be stored in the output_dev folder. This is only used for development purposes. If you’re already going to deploy your blog over to something like Github pages, you have to specify the env option:

sculpin generate --env prod

This will create an output_prod directory which you can then push to Github pages. We’ll go through this in a later section.

Configuring Sculpin

We can configure Sculpin by snooping around the two files inside the app/config directory:

  • sculpin_site.yml allows us to edit global options such as the title of the blog, and our disqus username.
  • sculpin_kernel.yml allows us to specify the theme to be used by Sculpin, and the permalinks format.

Other configuration files, such as the one for Amazon S3 bucket deployment, are in the root of the directory. We won’t really be using Amazon S3 in this tutorial, so we can just leave it as it is.

You can find more information about how to configure Sculpin by looking at the configuration documentation.

--ADVERTISEMENT--

Blogging Basics

If you’re coming from Octopress or Jekyll, you’ll feel right at home with Sculpin because it uses markdown files for blog posts, only instead of .markdown you’ll have to use the more standard .md as the file extension. Another thing to note is that the Sculpin CLI doesn’t have a command for generating a new markdown file for housing a blog post. This means that you’ll have to use the touch command to create a new file:

touch 2016-05-17-getting-started-with-sculpin.md

Typing this out is really a pain though, so here’s a touch.php file which you can put in the root of your blog’s directory:

<?php
$date = date('Y-m-d');
if(!empty($argv[2])){
    $date = $argv[2];
}
$file = 'source/_posts/' . $date . '-' . str_replace('_', '-', $argv[1]) . '.md';

$title = ucwords(str_replace('_', ' ', $argv[1]));
$handle = fopen($file, 'w');
$data = "---
title: {$title}
tags: []
categories: []

---

";
fwrite($handle, $data);

With this. you can execute the following command to create a new blog post:

php touch.php getting_started_with_sculpin

And it will create a source/_posts/2016-05-17-getting-started-with-sculpin.md file.

If you want a postdated blog post, you can specify a date as the second argument:

php touch.php getting_started_with_sculpin 2017-01-01

This will also add in the boilerplate front-matter contents for you:

---
title: Getting Started With Sculpin
tags: []
categories: []

---

Customizing the Site

Now it’s time to customize the site so it looks exactly how we want it. Here’s what the final output will look like:

Sculpin blog sample output

We’ll be primarily working inside the source directory, so go ahead and enter it. Once there, open the index.html file. This is the file that Sculpin serves for the homepage of the site. In the first few lines, you’ll see the meta block which is used for specifying the layout, the title of the page, the generator used in the page, and how many posts to show per page.

---
layout: default
title: Home
generator: pagination
pagination:
    max_per_page: 3
---

If you’ve ever used Twig, Blade or any other templating engine before, layouts should be pretty familiar. index.html inherits from the default layout. Layouts are stored in the _layouts folder. Templates use html files, so the actual file would be _layouts/default.html. Open that file up, clear its contents and paste the following:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>{% block title %}{{ page.title }}{% endblock %} &mdash; {{ site.title }} &mdash; {{ site.subtitle }}</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/picnic/4.1.2/picnic.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/picnic/4.1.2/plugins.min.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.4.0/styles/solarized-dark.min.css">
    <link href="{{ site.url }}/css/style.css" rel="stylesheet" type="text/css" />

    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.4.0/highlight.min.js"></script>
    <script>hljs.initHighlightingOnLoad();</script>
</head>
<body>
    <header>
        <nav>
          <a href="/" class="brand">
            <img class="logo" src="{{ site.url }}/images/jackson/76x76.png" />
            <span>{{ site.title }}</span>
          </a>

          <input id="bmenub" type="checkbox" class="show">
          <label for="bmenub" class="burger pseudo button">menu</label>

          <div class="menu">
            <a href="{{ site.url }}/blog">Archives</a>
            <a href="{{ site.url }}/about">About</a>
          </div>
        </nav>
    </header>

    <div id="wrapper">    
        <main>
        {% block content_wrapper %}
            {% block content %}{% endblock %}
        {% endblock %}       
        </main>

        <footer class="container">
            &copy; {{ "now"|date("Y") }} {{ site.title }}
        </footer>
    </div>
</body>
</html>

This is the main layout we’ll be using. Just concentrate on the code inside the <main> tag for now. We’ll demistify the rest later.

Only the contents inside the <main> tag change. What it means is: render the block named content_wrapper, and inside it render the block named content. This is where Twig really shines because we can override the contents of an inner block (content) if we define an outer block (content_wrapper). We’ll see this in action when we look at the _views/post.html file later on. For now, just keep in mind that either the content_wrapper or the content block should be defined in the templates that will inherit from this template.

<main>
{% block content_wrapper %}
    {% block content %}{% endblock %}
{% endblock %}     
</main>

We’ve established earlier that index.html inherits from layouts/default.html. But if you open the index.html file, you’ll see that we haven’t actually defined a content block. This is because Sculpin considers anything you add after the meta block to be the content. There is default content in there, added by the skeleton site, but we’ll use the content below. Let’s replace the default with the following:

{% for post in page.pagination.items %}
    <article>
        <header>
            <h2><a href="{{ site.url }}{{ post.url }}">{{ post.title }}</a></h2>
            <span class="date">{{ post.date|date('M dS, Y') }}</span>
        </header>
        <div>

        {% set break_array = post.blocks.content|split('<!-- more -->', 2) %}

        {{ break_array[0]|raw }}

        {% if break_array|length > 1 %}
        <p>
            <a href="{{ site.url }}{{ post.url }}">
                Read more
            </a>
        </p>
        {% endif %}

        </div>
    </article>
{% endfor %}

{% if page.pagination.previous_page or page.pagination.next_page %}
    <div class="nav-container"> 
        {% if page.pagination.previous_page %}
        <a href="{{ site.url }}{{ page.pagination.previous_page.url }}" class="right">Newer Posts</a>
        {% endif %}
        {% if page.pagination.next_page %}
        <a href="{{ site.url }}{{ page.pagination.next_page.url }}" class="left">Older Posts</a>
        {% endif %}
    </div>
{% endif %}

This brings us to the next topic: generators. Out of the box, Sculpin has a generator called pagination, which allows us to easily paginate through a collection. In this case, the collection consists of the posts that we currently have in the source/_posts directory. The code below loops through the posts based on the current page number. On the index page, this will only return the last three posts (posts are ordered from newest to oldest).

{% for post in page.pagination.items %}
<article>
    <!-- contents for each post here -->
</article>
{% endfor %}

To show the pagination links:

{% if page.pagination.previous_page or page.pagination.next_page %}
    <div class="nav-container"> 
        <!-- pagination links here -->
    </div>
{% endif %}

The number of posts that are displayed per page can be specified by updating the value for the max_per_page field in the template that’s currently being used.

pagination:
    max_per_page: 3

Going back to the code that we we’re displaying for every iteration of the pagination loop, we have the following:

<header>
    <h2><a href="{{ site.url }}{{ post.url }}">{{ post.title }}</a></h2>
    <span class="date">{{ post.date|date('M dS, Y') }}</span>
</header>
<div>

{% set break_array = post.blocks.content|split('<!-- more -->', 2) %}

{{ break_array[0]|raw }}

{% if break_array|length > 1 %}
<p>
    <a href="{{ site.url }}{{ post.url }}">
        Read more
    </a>
</p>
{% endif %}

</div>

This displays the title of the post, its contents, and a read more link. The read more link is implemented by creating a variable called break_array which stores the first half and second half of the article as two separate array items. This looks for the <!-- more --> comment as the delimiter. HTML comments aren’t visible in the page, so it’s the perfect delimiter.

{% set break_array = post.blocks.content|split('<!-- more -->', 2) %}

Next, we render whatever it is that’s in the first index of the break_array. The raw filter is used so that HTML gets rendered instead of escaped.

{{ break_array[0]|raw }}

We then check if there is more than one item stored in break_array, and in that case we render a link for viewing the whole post.

{% if break_array|length > 1 %}
<p>
    <a href="{{ site.url }}{{ post.url }}">
        Read more
    </a>
</p>
{% endif %}

Now that we’ve figured out how the index page is being rendered, let’s return to the _layouts/default.html file. For the main stylesheet we use Picnic.css. We’re loading it from a CDN, but you can also download the files to your source/css directory and link to them.

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/picnic/4.1.2/picnic.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/picnic/4.1.2/plugins.min.css">

As developers, we mostly have code snippets in our blog posts so we’re linking the solarized dark theme for highlight.js to add syntax highlighting.

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.4.0/styles/solarized-dark.min.css">

Finally, we have our custom stylesheet saved under source/css.

<link href="{{ site.url }}/css/style.css" rel="stylesheet" type="text/css" />

Here are the contents of the style.css file:

#wrapper {
    width: 900px;
    margin: 0 auto;
}

nav .menu {
    float: left;
    margin-left: 20px;
}

nav .menu a {
    margin-left: 20px;
}

main {
    margin-top: 80px;
}

article {
    margin-top: 50px;
    border-bottom: 1px dashed #ccc;
    color: #1D1D1D;
}

footer {
    margin: 20px 0;
    text-align: center;
    color: #525252;
}

.right {
    float: right;
}

.left {
    float: left;
}

.nav-container {
    overflow: auto;
    padding: 30px 0;
}

article h2 {
    margin-bottom: 0;
    padding-bottom: 0;
}

.date {
    color: #565656;
    font-size: 15px;
}

pre {
    background: none;
}

@media screen and (max-width: 780px){
    #wrapper {
        width: 100%;
        padding: 10px;
    }
}

Right below the link to the custom stylesheet (css/style.css) we have the highlight.js script and the initialization code which will apply the syntax highlighting.

<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.4.0/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>

Individual Post Page

Now that we’re done with the index page, it’s time to proceed with the template for individual post pages. The file in question is _views/post.html. Let’s put in the following:

{% extends "default" %}

{% block content_wrapper %}
    <article>
        <header>
            <h2>{{ page.title }}</h2>
            <span class="date">{{ post.date|date('M dS, Y') }}</span>
        </header>
        <div class="contents">
            {{ page.blocks.content|raw }}
        </div>
        {% if page.tags %}
            <p class="tags">
            Tags:
            {% for tag in page.tags %}
            <a href="{{ site.url }}/blog/tags/{{ tag|url_encode(true) }}">{{ tag }}</a>{% if not loop.last %}, {% endif %}
            {% endfor %}
            </p>
        {% endif %}
    </article>
{% endblock %}

This is mostly based on the individual post template from the Sculpin Blog Skeleton, with added date, and removed categories and pagination links.

You may have noticed that we’re using a different syntax for extending the default template:

{% extends "default" %}

This is because post is considered to be a layout. In Sculpin, anything that you put inside these directories: _views, _layouts, _partials, includes is considered a layout. This means that any template can inherit from these files, or include the partials.

We mentioned content_wrapper; the block that we we’re trying to render in the _layout/default.html file. Inside it is the content block:

<main>
{% block content_wrapper %}
    {% block content %}{% endblock %}
{% endblock %}     
</main>

We already know that the content block doesn’t need to be explicitly defined because Twig assumes that anything below the meta block is the content. This means that we can actually have only the following code in _views/post.html and the post would still be rendered:

{% extends "default" %}

But this will only render the content of the post, so the title, the date and any other information about the post that we want included wouldn’t be.

However, if we try to override the contents of the content block like so:

{% extends "default" %}

{% block content %}
<h1>I'm trying to override the content!</h1>
{% endblock %}

… we would still see the content of the post.

This means that we can’t really override what’s inside the content block. This is where the content_wrapper block comes into play. By defining this block, we can override the content block. This is because the content_wrapper block is wrapping the content block. Now we can do things like output the title and the date of publication.

{% block content_wrapper %}
    <article>
        <header>
            <h2>{{ page.title }}</h2>
            <span class="date">{{ post.date|date('M dS, Y') }}</span>
        </header>
        <div class="contents">
            {{ page.blocks.content|raw }}
        </div>
        {% if page.tags %}
            <p class="tags">
            Tags:
            {% for tag in page.tags %}
            <a href="{{ site.url }}/blog/tags/{{ tag|url_encode(true) }}">{{ tag }}</a>{% if not loop.last %}, {% endif %}
            {% endfor %}
            </p>
        {% endif %}
    </article>
{% endblock %}

Adding Disqus

Adding Disqus for handling comments is very easy, as it comes built into the skeleton:

{% if site.disqus.shortname and site.disqus.shortname != '' %}
<div id="disqus_thread"></div>
<script type="text/javascript">
    /* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
    var disqus_shortname = '{{site.disqus.shortname}}'; // required: replace example with your forum shortname


    {% if page.disqus.identifier  %}var disqus_identifier = '{{page.disqus.identifier}}'; {% endif %}

    {% if page.disqus.title %}var disqus_title = '{{page.disqus.title}}';{% endif %}

    {% if page.disqus.url %}var disqus_url = '{{page.disqus.url}}';{% endif %}

    {% if page.disqus.category_id %}var disqus_category_id = '{{page.disqus.category_id}}';{% endif %}

    /* * * DON'T EDIT BELOW THIS LINE * * */
    (function () {
        var dsq = document.createElement('script');
        dsq.type = 'text/javascript';
        dsq.async = true;
        dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
        (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
    })();
</script>
<noscript>Please enable JavaScript to view the
    <a href="https://disqus.com/?ref_noscript" rel="nofollow">comments powered by Disqus.</a>
</noscript>
{% endif %}

For this to work we’ll have to update the app/config/sculpin_site.yml file with the Disqus shortname:

# Insert your disqus shortname
disqus:
   shortname: YOUR_DISQUS_USERNAME

Blog Archives

Just like every blog, ours also needs an archive page where readers can see all the previously published posts. In blog.html, add the following:

---
layout: default
title: Posts Archive
generator: pagination
use:
    - posts

---
{% set year = '0' %}
<h2>Posts Archive</h2>
{% for post in page.pagination.items %}
{% set this_year %}{{ post.date | date("Y") }}{% endset %}
{% if year != this_year %}
  {% set month = '0' %}
  {% set year = this_year %}
{% endif %}
{% set this_month %}{{ post.date | date("F") }}{% endset %}
{% if month != this_month %}
  {% set month = this_month %}
  <h3>{{ month }} {{ year }}</h3>
{% endif %}
  <div>
    <a href="{{ site.url }}{{ post.url }}">{{ post.title }}</a>
  </div>
{% endfor %}

<div>
{% if page.pagination.previous_page or page.pagination.next_page %}
    <div class="nav-container"> 
    {% if page.pagination.previous_page %}
    <a class="previous" href="{{ site.url }}{{ page.pagination.previous_page.url }}" title="Previous Page"><span class="title">Previous Page</span></a>
    {% endif %}
    {% if page.pagination.next_page %}
    <a class="next" href="{{ site.url }}{{ page.pagination.next_page.url }}" title="Next Page"><span class="title">Next Page</span></a>
    {% endif %}
    </div>
{% endif %}
</div>

Again we’re using a familiar syntax. Only this time we haven’t specified how many items we want per page. Sculpin will use the default: 10 items.

---
layout: default
title: Posts Archive
generator: pagination
use:
    - posts
---

Next, we break down the following code:

{% for post in page.pagination.items %}
{% set this_year %}{{ post.date | date("Y") }}{% endset %}
{% if year != this_year %}
  {% set month = '0' %}
  {% set year = this_year %}
{% endif %}
{% set this_month %}{{ post.date | date("F") }}{% endset %}
{% if month != this_month %}
  {% set month = this_month %}
  <h3>{{ month }} {{ year }}</h3>
{% endif %}
  <div>
    <a href="{{ site.url }}{{ post.url }}">{{ post.title }}</a>
  </div>
{% endfor %}

The code above does the same thing we did earlier in the index.html file. That is, it loops through all the items in the current page, only this time we’re grouping each post based on month and year.

Deploying to Github Pages

To deploy to Github pages, you’re going to need a Github account so go ahead and sign up of you haven’t already. Then, set up Git, generate an SSH key and add the SSH key to your Github account. If you’re new to all this, I recommend checking out Shaumik’s article on Git for Beginners.

Once you’ve set up a Github account, create a new repository by using the following format for the name:

your_github_username.github.io

If your username is captain then the repository name should be captain.github.io. This is pretty much the standard for Github pages, which also means that you can only create one Github page per account.

Next, go to the root directory of your blog and execute the following:

sculpin generate --env prod

This will generate a site for production. The generated site will be stored in the output_prod directory. Once generated, you can go ahead and delete the .git directory. If you remember from earlier, we have cloned the Sculpin Blog Skeleton. We’re not really going to contribute code to the repository so we can delete its local repo.

We now need to initialize a new git repository inside the output_prod folder. Navigate inside the directory and execute git init. Then, execute the following to commit the changes:

git add -A
git commit -m "create new blog"

Add the Github repository that you created earlier as a remote:

git remote add origin git@github.com:your_github_username/your_github_username.github.io.git

Finally, push to the repository:

git push origin master

Wait for a few minutes before your Sculpin blog becomes live at http://your_github_username.github.io. Congrats!

Conclusion

In this tutorial, we looked at Sculpin, a PHP-based static site generator. As you have seen, Sculpin is a really good option for your static site generation needs. You can access the code used in this tutorial in this Github repo.

Did you do any extensive customization on your Sculpin blog? Is there any other static site generator solution you’d prefer instead? Let us know!

Login or Create Account to Comment
Login Create Account
Recommended
Sponsors
Get the latest in PHP, once a week, for free.