wissel.net

Usability - Productivity - Business - The web - Singapore & Twins

From Blogsphere to a Static Site (Part 3) - Generating pages


The rendering engine I choose is mustache which describes itself as "logic-less templating engine". My main criteria was the availability on multiple platforms including Java and JavaScript (I might port the rendering part to NodeJS at some time in the future).
The only logic mustache supports is conditional rendering based on the presence or absence of an element. When an element is present and is an array (or a collection in Java) the body of the template gets repeated for each element in the array. A scalar value hence is treated as an array with one value only.

Mustache is simple to use. All you need is a data bean (in Java, a JSON structure for JavaScript) and a text file containing placeholders with the property names. E.g. <h1>{{title}}</h1> will render a headline with the title property of you data object. In Java that would be either a public variable or a call to getTitle according to the bean specification. The blog rendering code therefore is quite simple:

private void renderOneEntry(BlogEntry be, Mustache mustache) throws IOException {

        String location = this.config.destinationDirectory + be.getNewURL();
        String outDirs = location.substring(0, location.lastIndexOf("/"));
        File dirs = new File(outDirs);
        if (!dirs.exists()) {
            dirs.mkdirs();
        }
        // Set the current context
        for (LinkItem cat : be.getCategory()) {
            String c = cat.name;
            this.allCategories.get(c.toLowerCase()).active = true;
        }
        this.allDateCategories.get(be.getDateYear()).active = true;

        if (be.getSeries() != null) {
            String series = be.getSeries();
            if (this.allSeries.containsKey(series)) {
                this.allSeries.get(series).get(be.getNewURL()).active = true;
            }
        }

        // Prepare to write out
        ByteArrayOutputStream out = new ByteArrayOutputStream(102400);
        Writer pw = new PrintWriter(out);

        // This is where the magic happens
        mustache.execute(pw, be);
        pw.flush();
        pw.close();
        this.saveIfChanged(out.toByteArray(), location);

        // Cleanup
        for (LinkItem cat : be.getCategory()) {
            String c = cat.name;
            this.allCategories.get(c.toLowerCase()).active = false;
        }
        this.allDateCategories.get(be.getDateYear()).active = false;

        if (be.getSeries() != null) {
            String series = be.getSeries();
            if (this.allSeries.containsKey(series)) {
                this.allSeries.get(series).get(be.getNewURL()).active = false;
            }
        }
    }

The actual rendering is just the line mustache.execute(pw, be); The code around it prepares and resets the collections that might render on a page like categories, series or month and year. Also of interest is this.saveIfChanged(out.toByteArray(), location); which only saves results back to disk if it actually has changed. Don't be mistaken: any change in layout will lead to a newly rendered page, so this is quite important to save as needed and not more (you don't want to have tons of identical files that only differ in their time stamp)

    private void saveIfChanged(byte[] newData, String targetName) {

        boolean saveThis = false;

        File targetFile = new File(targetName);
        if (targetFile.isDirectory()) {
            System.out.println("Directory encountered!" + targetName);
        } else if (targetFile.exists()) {
            try {
                InputStream existing = new FileInputStream(targetFile);
                ByteArrayOutputStream compare = new ByteArrayOutputStream(102400); 
                ByteStreams.copy(existing, compare);
                // Save if they are not equal..
                saveThis = !Arrays.equals(newData, compare.toByteArray());
                Closeables.close(existing, true);
            } catch (FileNotFoundException e) {
                // Anything goes wrong -> we save the file
                e.printStackTrace();
                saveThis = true;
            } catch (IOException e) {
                // Anything goes wrong -> we save the file
                e.printStackTrace();
                saveThis = true;
            }

            // Now if it is there, get rid of it.
            if (saveThis) {
                targetFile.delete();
            }

        } else {
            saveThis = true;
        }

        if (saveThis) {
            OutputStream finalOut = null;
            try {
                finalOut = new FileOutputStream(targetFile);
                finalOut.write(newData);
                finalOut.flush();
                Closeables.close(finalOut, true);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("\n+" + targetFile);
        } else {
            System.out.print(".");
        }
    }

The template itself is broken into multiple pieces. Since I have different page styles: start page, article, categories, month and year overviews, I have several parts that repeat: headers, footers, side bar elements. Mustache makes that easy to deal with. The constructions I used most:

  • {{somevalue}}: Render somevalue, escape html
  • {{& body}}: Render body, don't escape html
  • {{> snippet}}: Pull in snippet.mustache from disk and use it at template at this position
  • {{#element}}: render if element exists, repeat once per member of collection. End with {{/element}}
  • {{^element}}: render if element doesn't exist or array is empty

With these the actual template becomes rather short and quite maintainable. Adding or removing a side block is just adding or removing a single statement in the main mustache file. As a result the mustache template for a blog entry is less than 70 lines, the home page less than 50

index.mustache

<!DOCTYPE html>
<html lang="en">
<head>
<title>NotesSensei's Blog</title>
{{> partial_analytics}}
{{> partial_meta}}
{{> partial_style}}
{{> partial_feeds}}
<meta name="Revisit-After" content="2 Days" />
</head>
<body>
    {{> partial_navbar}}
    <div class="container">
    {{> partial_header}}
    {{> partial_browserwarning}}
    <div class="row-fluid">
            <div class="span9">
                {{#topArticles}}
                <article class="well well-raised">
                    <h1><small><a href="/blog/{{newURL}}">{{& title}}</a></small></h1>
                    <hr />
                    <div>{{& mainBody}}</div>
                    <hr />
                    {{#moreBody}} <a href="/blog/{{newURL}}">Read more</a>
                    {{/moreBody}}
                    <p>{{> partial_authorentryinfo}}</p>
                </article>
                {{/topArticles}}
                <ul class="breadcrumb pull-centre">
                    <li><a href="/blog/all" title="That's a loooong list!">Older entries<i class="icon-hand-right"></i></a>
                    </li>
                </ul>
            </div>
            <div class="span3">
                {{> sidebar_reference}}
                {{> sidebar_categoriesArchive}}
                {{> sidebar_upgrade}}
                {{> sidebar_twitter}}
                {{> sidebar_languages}}
                {{> sidebar_visitors}}
                {{> sidebar_usefultools}}
            </div>
        </div>
    </div>
    {{> partial_footer}}
    {{> partial_bottomscripts}}
</body>
</html>

blogentry.mustache

<!DOCTYPE html>
<html lang="en">
<head>
<title>NotesSensei's Blog - {{title}}</title>
{{> partial_analytics}}
{{> partial_meta}}
{{> partial_style}}
{{> partial_feeds}}
<meta name="Revisit-After" content="90 Days" />
</head>
<body>
    {{> partial_navbar}}
    <div class="container">
    {{> partial_headersmall}}
    {{> partial_browserwarning}}
    {{> partial_breadcrumb}}
        <div class="row-fluid">
            <div class="span12">
                {{> partial_series}}
                {{> partial_previousnext}}
                <article class="well well-raised">
                    <h1><small>{{& title}}</small></h1>
                    <hr />
                    <div>{{& allBody}}</div>
                    <hr />
                    {{> partial_authorentryinfo}}
                </article>
                <a name="comments"></a>
                {{^commentsclosed}}
                <div class="well well-raised" style="text-align : center">
                <button class="btn btn-lg btn-info" data-toggle="collapse" data-target="#commentform_{{UNID}}" type="button">
                    Add your comment...  <span class="glyphicon glyphicon-comment"></span>
                </button>
                </div>
                <div id="commentform_{{UNID}}"  class="collapse"></div>
                {{/commentsclosed}}
                <div class="well well-raised">
                    <h4>Comments</h4>
                    <ol id="commentList">
                        {{#comments}}
                        <li>
                           {{#gravatarURL}}<img src="{{.}}" class="gravatarimg" />
                           {{/gravatarURL}}
                            posted by <b>{{author}}</b> on <i>{{createdString}}</i>:<br /> {{& comment}}
                            <hr style="clear : both" />
                        </li> {{/comments}}
                        {{^comments}}
                        <li id="nocomments">
                            <h5>No comments yet, be the first to comment</h5>
                        </li>
                        {{/comments}}
                    </ol>
                </div>
                {{> partial_previousnext}}
            </div>
        </div>
        {{> partial_breadcrumb}}
    </div>
{{> partial_footer}}
    <script type="text/javascript">
        var permaLink = "https://wissel.net/blog/{{newURL}}";
    </script>
    {{> partial_bottomscripts}}
    {{> partial_commentscripts}}
</body>
</html>

Next stop: a new comment engine


Posted by on 2017-05-01 11:23 | Comments (0) | categories: Blog

Comments

  1. No comments yet, be the first to comment