Things People Asked For In A Static Site Generator

These responses are from when I asked what folks would look for in a new static site generator. My thoughts are below each one.

  • {# type: standard #} {%- import "includes/theme-macros.neojinja" as theme -%} {% for span in span.content.spans %} {{- theme.output_spans(site, page_id, span) -}} {% endfor %}

    I spent a lot of time thinking about routing. Not just for Neopoligen, but for every site process I've used or built over the past two decades. My conclusion: let the computer do it. Give every page an ID and use that instead of spending time working on directory structures that never quite work right.

    Among other things, it helps avoid link rot. You can move to other systems and it's a lot easier to match the ID than try to map to some specific url structure or setting up redirects, or just letting the old urls break.

  • {# type: standard #} {%- import "includes/theme-macros.neojinja" as theme -%} {% for span in span.content.spans %} {{- theme.output_spans(site, page_id, span) -}} {% endfor %}

    This one got mentioned a few times. It is, of course, funny to me that no one said, "find something to replace markdown" seeing as that's key to everything here. Quite possible I'm tilting at a windmill, but someone's gotta do it every now and then.

  • {# type: standard #} {%- import "includes/theme-macros.neojinja" as theme -%} {% for span in span.content.spans %} {{- theme.output_spans(site, page_id, span) -}} {% endfor %}

    Continuing from above. I'm taking directory structure off the table. Files can be stored locally in any structure that makes the source content easy for you to maintain. When they're published, the computer takes over. The local directory structure is ignored and everything is put in a place by IDcustom-urls.

    Incidentally, it's tough to explain how much of a delight it's been to be able to name, rename, and move files without having to worry about breaking links or having to hunt down ones that need changing.

    The template used to render the page is defined by a combination of two attributes in a -- metadata section. Specifically, -- type and -- status. I use the type for the primary definition (e.g. "post", "review", "quote") and then use one of three statuses "scratch", "draft", or "published". You end up with things like post-published, post-draft, quote-scratch

    Throw files anywhere you want and move them whenever works for you. It won't matter. The output will always go to the same URL and your links will keep working.

    Making custom urls/slugs is a built-in feature, but I find I use it very rarely. I'd really rather just not think about it.

  • {# type: standard #} {%- import "includes/theme-macros.neojinja" as theme -%} {% for span in span.content.spans %} {{- theme.output_spans(site, page_id, span) -}} {% endfor %}

    There's a -- metadata section in Neopolitan. It's what houses these stuff that would be in markdown front matter. I usually keep it at the end of my documents though since I like seeing the content first.

    The primary values are limited to key/value pairs. You can also flag the section as either JSON or YAML and throw data in those formats into it. (CVS is a possibility, but there's lots higher on the priority list)

  • {# type: standard #} {%- import "includes/theme-macros.neojinja" as theme -%} {% for span in span.content.spans %} {{- theme.output_spans(site, page_id, span) -}} {% endfor %}

    You can't use the same MiniJinja tags in the templates or the Neopolitan tags inside templates. Both sides are pretty flexible though. Everything starts with the content and the tools for making decisions based on it are pretty robust. I'm curious to hear how things in this approach stack up with expectations.

  • {# type: standard #} {%- import "includes/theme-macros.neojinja" as theme -%} {% for span in span.content.spans %} {{- theme.output_spans(site, page_id, span) -}} {% endfor %}

    The way I view the project is with a strong separation of concerns where the content is separate from the rendering. I see this point more on the content side. I've been considering what it would look like to just call out to other processes and have them return JSON that gets fed to the templates along with the rest of the content.

    I really like the idea in terms of keeping things generic. It's also in line with the unix philosophy of do one thing and do it well. But, there's some security concerns there that I don't know enough about yet. I'll be reaching out to folks who know more about it than me.

  • {# type: standard #} {%- import "includes/theme-macros.neojinja" as theme -%} {% for span in span.content.spans %} {{- theme.output_spans(site, page_id, span) -}} {% endfor %}

    This is similar to the one before it. I was thinking specifically of making a call out to curl for this, but lots of APIs require some sort of key and I'm not at a point where I want to get into that mix.

    That brings me back to the idea of calling out to external process that are basically black boxes. The page asks for some data, the process does something, then some JSON comes back and you get to use it in the template for rendering.

  • {# type: standard #} {%- import "includes/theme-macros.neojinja" as theme -%} {% for span in span.content.spans %} {{- theme.output_spans(site, page_id, span) -}} {% endfor %}

    Oh yeah, I should point out that page title style slugs are still in the URLs. They're in the query string portion of the URL and can change to whatever because the pages are static and the server doesn't care about query strings. They're just there to make things easier to look at for us humans.

    (All the references I've seen say they don't think URLs have much of an influence over SEO. Even if they have a little, I'm not worried about it making a significant difference. Not having to worry about directory structures and linking is well worth it to me even if there is a little hit.)

  • {# type: standard #} {%- import "includes/theme-macros.neojinja" as theme -%} {% for span in span.content.spans %} {{- theme.output_spans(site, page_id, span) -}} {% endfor %}

    Page types are defined in a config file, you can have as many as you want. In the past I would have called the difference between static and blog posts the difference between URL styles where static would be like /neopoligen and blot posts would be like /2024/01/18/some-title/.

    You can still make those URLs, and I do them for landing pages (like /neopoligen, but the other pages are based off ID).

    I've found the overall approach freeing. I no longer think about if a page is a post static. I just think about what type I want it to be (e.g. example, quote, post) and then add that to the configconfig that controls which ones are available.

    As for ordering, that's built in with a few different approaches. You can currently call by page type either inclusive or exclusive. So, you can make a list of just "example", and "post" pages, or a list of everything except those two. In either case, you can sort alphabetically or chronologically.

    Those same lists will be able to power "previous/next" links for pagination as well.

  • {# type: standard #} {%- import "includes/theme-macros.neojinja" as theme -%} {% for span in span.content.spans %} {{- theme.output_spans(site, page_id, span) -}} {% endfor %}

    I'm addressing this on two fronts. The first one is the page lists. Template can be made that limit the responses to the most recent 10 or whatever to limit nav. I'm also making menus that can be defined by JSON in content files and then used anywhere on the site. There's more work to do with this.

    The good news is my site has 2k+ posts on it, so I've got a big one to play with.

  • {# type: standard #} {%- import "includes/theme-macros.neojinja" as theme -%} {% for span in span.content.spans %} {{- theme.output_spans(site, page_id, span) -}} {% endfor %}

    I'm still thinking about this, but I'm pretty sure it'll be a combination of the page lists and javascript. You'll request the page set you want and then use JS in query strings to navigate around. Still more research to do on this one though.

  • {# type: standard #} {%- import "includes/theme-macros.neojinja" as theme -%} {% for span in span.content.spans %} {{- theme.output_spans(site, page_id, span) -}} {% endfor %}

    An example site ships with Neopoligen with an expansive set of templates. All of them have lots of IDs and classes. And the templates are straight forward to edit. So, any style could be added and adapted to.

  • {# type: standard #} {%- import "includes/theme-macros.neojinja" as theme -%} {% for span in span.content.spans %} {{- theme.output_spans(site, page_id, span) -}} {% endfor %}

    This is a phase two thing for me. It's high on the priority list tough. I used to be a photographer and one of the things I want to solve for is image so I can enjoy posting them instead of fighting with the software to do so.

    One thing I'm looking to add is the ability to pull alt text directly from the metadata of an image. I really like the idea of writing in the alt text one time and then having it be applied wherever I use the image.

  • {# type: standard #} {%- import "includes/theme-macros.neojinja" as theme -%} {% for span in span.content.spans %} {{- theme.output_spans(site, page_id, span) -}} {% endfor %}

    I'm using the syntect highlighter. It's set up to add classes instead of inline styles for each token. Six style sheets are included. I'm not sure how they were made yet, but I'm sure others could be generated from existing style sets.

  • {# type: standard #} {%- import "includes/theme-macros.neojinja" as theme -%} {% for span in span.content.spans %} {{- theme.output_spans(site, page_id, span) -}} {% endfor %}

    This goes back to the external process calls. I expect

  • {# type: standard #} {%- import "includes/theme-macros.neojinja" as theme -%} {% for span in span.content.spans %} {{- theme.output_spans(site, page_id, span) -}} {% endfor %}

    Totally doable. I'd approach this by adding an author name to the metadata and then matching on it for the template to call an image. That would just be another template in the mix to create the URL.

  • {# type: standard #} {%- import "includes/theme-macros.neojinja" as theme -%} {% for span in span.content.spans %} {{- theme.output_spans(site, page_id, span) -}} {% endfor %}

    This will be a later phase thing, but I'm into the idea. For me, for now, my publishing is simply a matter of pushing main up to GitHub and letting Netlify pick it up and deploy it.

  • {# type: standard #} {%- import "includes/theme-macros.neojinja" as theme -%} {% for span in span.content.spans %} {{- theme.output_spans(site, page_id, span) -}} {% endfor %}

    Yep. That's built in. In fact, you can add multiple JSON sections with different content and you'll also be able to centralize JSON in one file and include it in others.

Closing

There's lots more to cover. I'll be pushing up a site with examples in the next week or so to give a better idea of how things work.

Footnotes

  • You can also define explicit url/slug paths when you want. I do this for landing pages for different projects, but only for the those. Any other page for a project, I let the computer handle the location and linking for me.

  • More docs coming on the config and what you can do with it.