Adding SSG Support to Jasper

code jasper
May 25, 2021 / Robert Zehnder

Why should the NodeJS people have all the fun? Adding the ability to generate a static site with Jasper.

Cover

Yesterday, reading through LinkedIn, I had a notification that CF Mitrah had commented on one of my posts. He had asked a queestion about the simple CF blogging project I have been working on, Jasper. He wanted to know if there was any plan to have Jasper act as a static site generator.

Turns out the answer to that question is "yes".

Adding SSG capabilities to Jasper

If you are not familiar with Jasper, it is a simple blog written in ColdFusion. The first itertion of Jasper allows you to create content just by dropping a new markdown file in the /posts directory. ColdFusion handles indexing all the posts and displaying it. If ColdFusion was removed from the equation, the blog could be compiled to HTML and it could be served anywhere that hosts HTML. There are quite a few ways to solve the problem, but I wanted something simple.

Since I have methods to list all post and page markdown files and given a slug it is easy to calculate the URL. When I am writing a post like this, the commandbox server instance is running so I can guage the posts layout on the page. I decided to leverage this functionality to iterate through all the posts, use cfhttp to grab the HTML, and then write that HTML out to a file.

The first issue

The first issue is building out the static files and to do that I created Build.cfc, the ColdBox handler responsible for generating the files. The first step is to delete the /dist directory if it exists to ensure everything is built fresh. Once the required directories are created and the /assets folder is moved it is just a matter of iterating through the items.

component {
 
	property name = "PostService" inject;
	property name = "PageService" inject;
 
	function index (event, rc, prc) {
		var tags = ["code", "misc"]; // build these tag pages
 
		if(getSetting("environment") != "development") abort;
		directoryDelete(path = expandPath(".") & "/dist", recurse = true);
		directoryCreate(path = expandPath(".") & "/dist", ignoreExists = true);
		directoryCreate(path = expandPath(".") & "/dist/post", ignoreExists = true);
		directoryCreate(path = expandPath(".") & "/dist/page", ignoreExists = true);
		directoryCreate(path = expandPath(".") & "/dist/tag", ignoreExists = true);
 
		// move static assets
		directoryCopy(source = expandPath(".") & "/assets", destination = expandPath(".") & "/dist/assets", recurse = true);
		cfhttp(url = "127.0.0.1:" & cgi.SERVER_PORT & "/?build=true");
		fileWrite(expandPath(".") & "/dist/index.html", cfhttp.fileContent);
 
		// Generate posts
		var posts = PostService.list();
		posts.each((post) => {
			cfhttp(url = "127.0.0.1:" & cgi.SERVER_PORT & "/post/" & post.slug & "?build=true", result = "_" &lcase(hash(post.slug)).replace("-", "", "all"));
			fileWrite(expandPath(".") & "/dist/post/" & post.slug & ".html", variables["_" & lcase(hash(post.slug)).replace("-", "", "all")].fileContent);
		}, true);
 
		// Generate pages
		var pages = PageService.list();
		pages.each((page) => {
			cfhttp(url = "127.0.0.1:" & cgi.SERVER_PORT & "/page/" & page.slug & "?build=true", result = "_" &lcase(hash(page.slug)).replace("-", "", "all"));
			fileWrite(expandPath(".") & "/dist/page/" & page.slug & ".html", variables["_" & lcase(hash(page.slug)).replace("-", "", "all")].fileContent);
		}, true);
 
		// Generate Tags
		tags.each((tag) => {
			cfhttp(url = "127.0.0.1:" & cgi.SERVER_PORT & "/tag/" & tag & "?build=true", result = "_" &lcase(hash(tag)).replace("-", "", "all"));
			fileWrite(expandPath(".") & "/dist/tag/" & tag & ".html", variables["_" & lcase(hash(tag)).replace("-", "", "all")].fileContent);
		}, true);
 
		return true;
	}
 
}

Next, dealing with file extensions

The next issue to overcome is letting the layout know we are generating a static site instead of a dynamic one. When building the site the build URL parameter is passed. The request context decorator looks for this parameter to set a variable used in the layout and views to determine whether or not to add the .html extension to generated links.

Hopefully file extensions can be removed and handled with rewrite rules, but for the time being it is included for expediency.

component extends="coldbox.system.web.context.RequestContextDecorator" {
 
	property name="Controller" inject="coldbox";
 
	function configure(){
		var rc = getRequestContext().getCollection();
		var prc = getRequestContext().getCollection(private = true);
 
		...
 
		prc["buildExt"] = Controller.getSetting("environment") == "development" && rc.keyExists("build") ? ".html" : "";
 
		if(rc.keyExists("rb")) cacheClear();
 
		return this;
	}
 
}

Building the static content is as easy as hitting the build handler: http://127.0.0.1:57478/build

Once the request has finished the files in the dist folder are ready to be deployed to whichever service you choose.

And here is a copy of my blog hosted statically.

Edit: Originally this post included a link to the static version of the blog. Now the blog is presented fully static so no link is required!

Cover image by Casey Horner on Unsplash

About Robert Zehnder
Robert is a Senior Lead ColdFusion Engineer. In his spare time he enjoys hanging out with his family, his dog, and working on cool stuff.