lumberjack wharfie is on neocities

home now blog gallery source

source code

5 September 2025

My Neocities site is built by a simple static site generator that I wrote in Go. It's kinda the first real thing I've made in Go, and it is thus far in its early stages, so there are probably ways to make it more efficient. I'll probably set up a git repo at some point, but for now I'll just put the code for myneocities.go here.

This code currently assumes the following folder structure:

  • myneocities/
    • 0_templates/
      • layout.html
      • style.css
    • 1_content/
      • blog/
        • blog entries as html snippets
        • should probably be named by date YYYY-MM-DD so they sort properly
      • index.html
      • any other pages' body content as html snippets
      • just don't name one "blog.html" as that gets generated
    • 2_output/
      • rendered pages end up here
    • go.mod
    • myneocities.go

myneocities.go:




package main

import (
	"html/template"
	"os"
	"sort"
)

const (
	TemplatesDir = "0_templates/"
	LayoutHTML   = "layout.html"
	StyleCSS     = "style.css"
	InputDir     = "1_content/"
	OutputDir    = "2_output/"
)

type Page struct {
	PageFilename string
	PageContent  string //keep in mind that os.ReadFile productes []byte, template.Execute wants template.HTML
} //at the moment of rendering, an anonymous struct is created with PageContent as template.HTML instead of string

func ConstructPageFromFile(dir string, file string) Page {
	filecontent, err := os.ReadFile(dir + file)
	if err != nil {
		panic(err)
	}
	return Page{file, string(filecontent)}
}

func RenderPage(templ *template.Template, dir string, page Page) {
	outfile, err := os.Create(dir + page.PageFilename)
	if err != nil {
		panic(err)
	}

	pageEscaped := struct { //need to wrap the PageContent as a template.HTML instead of string
		PageFilename string
		PageContent  template.HTML
	}{
		PageFilename: page.PageFilename,
		PageContent:  template.HTML(page.PageContent), //wraps to template.HTML string instead of regular string
	}
	templ.Execute(outfile, pageEscaped)
	defer outfile.Close()
}

func CreateContentManifest(dir string) ([]Page, []Page) {
	// read the passed directory (will be InputDir)
	arrMainFiles, err := os.ReadDir(dir) //returns ([]os.DirEntry, error)
	if err != nil {
		panic(err)
	}
	// read the "blog" directory within the passed directory
	arrBlogFiles, err := os.ReadDir(dir + "/blog") //returns ([]os.DirEntry, error)
	if err != nil {
		panic(err)
	}

	// initialize two arrays of Page constructs - these will be output
	var arrMainManifest []Page
	var arrBlogManifest []Page

	// iterate thru the two directories and create Page constructs from each file
	// TODO -- handle other file types, like images, so they can be in the InputDir with the HTML snippets
	for i := range arrMainFiles {
		if !arrMainFiles[i].IsDir() {
			arrMainManifest = append(arrMainManifest, ConstructPageFromFile(InputDir, arrMainFiles[i].Name()))
		}
	}
	// (sort the blog files descending first)
	sort.Slice(arrBlogFiles, func(i, j int) bool {
		return arrBlogFiles[i].Name() > arrBlogFiles[j].Name()
	}) //then iterate thru like the main files
	for j := range arrBlogFiles {
		if !arrBlogFiles[j].IsDir() {
			arrBlogManifest = append(arrBlogManifest, ConstructPageFromFile(InputDir+"blog/", arrBlogFiles[j].Name()))
		}
	}

	return arrMainManifest, arrBlogManifest
}

func main() {
	// parse layout.html template
	LayoutHTMLTemplate := template.Must(template.ParseFiles(TemplatesDir + LayoutHTML)) //type *template.Template

	// create manifest of pages to generate
	PagesManifest, BlogsManifest := CreateContentManifest(InputDir)

	// copy stylesheet
	StyleCSSFile, err := os.ReadFile(TemplatesDir + StyleCSS)
	if err != nil {
		panic(err)
	}
	os.WriteFile(OutputDir+StyleCSS, StyleCSSFile, 0664)

	// generate main pages from the PagesManifest
	for i := range PagesManifest {
		RenderPage(LayoutHTMLTemplate, OutputDir, PagesManifest[i])
	}
	// generate blog post individual pages & overall blog page from the BlogsManifest
	var BlogPage = Page{"blog.html", ""}
	BlogPage.PageFilename = "blog.html"
	for j := range BlogsManifest {
		RenderPage(LayoutHTMLTemplate, OutputDir, BlogsManifest[j])
		BlogPage.PageContent += BlogsManifest[j].PageContent
	}
	RenderPage(LayoutHTMLTemplate, OutputDir, BlogPage) //render blog.html
}