Home

Why I Built Neopoligen

Intro

Making your own website builder is generally considered a silly thing to do. There are a ton of great site generators out there. If you're goal is simply to build a website, spending time "reinventing the wheel" isn't worth it.

So, why did I build my own? Because, it wasn't about the websites. It was about replacing markdown.

Quick Note

If you want to see what my format looks like you can check out the source file for this page here↗

The Case Against Markdown

I started using Markdown 1 for my notes for as soon as learned about the format. I love writing notes in plain - text files. I also publish a huge chunk of my notes on my website. So, having a format that outputs to HTML is ideal.

There are things I don't like about it though :

I bumped up against those limitations enough that it sent me on the search for something else.

Aside

The Case Against MDX, Etc...

I started looking at MDX when I hit my frustration limit with Markdown. It looked promising, but after getting it to work I realized I don't really like working with the JSX.

I took a look at Markdoc and some other formats too. They offered different feature sets, but nothing quite did what I wanted. So, I decided to start from scratch.

Designing Neopolitan

My website is over twenty years old. I've moved content repeatedly over the decades. I've used plain HTML, wordpress, django databases, ruby on rails, custom databases, markdown, MDX, and probably a few other things I've forgotten.

I never want to have to migrate content again.

That meant coming up with a single format that could do everything I can think of at the moment and everything I can envision wanting to do in the future. All without relying on a framework or specific language.

I ended up with the format I'm calling Neopolitan. I tried to keep it as simple as possible.

Templates Not JavaScript

I'm tired of jumping JavaScript frameworks. One of the final straws that sent me down the path of making my own site generator was when I went fix a spelling error on a site I hadn't touched in a few months and everything broke because some dependency in the framework changed.

Neopoligen is pretty basic at it's core. It parses Neopolitan files to create ASTs that are sent to the template engine for rendering. There's still some things to add to the AST, but the goal is to get the point where any changes are additive so that existing templates will always continue to work with future updates.

Neopolitan Sections And Spans

You can read more details about how the format works on the home page , but it mainly breaks down like this :

The most important point about the sections and spans is that they are arbitrary. The default theme ships with several basic ones to get started, but you can make new ones on the fly.

There's not a lot to it, but that's kinda the point. Complexity is friction and friction keeps me from making things. I want to reduce it as much as possible.

Sections

Neopolitan sections open with a line that has two dashes followed by the kind of section. For example :

-- note

This is a note

-- aside

This is an aside

-- warning

This is a warning

Which outputs :

Note

This is a note

Aside
Warning

This is a warning

Section Attributes

Section attributes are created by adding lines started with two dashes directly below the opening section line. For example, here's a "class" attribute added to a "p" section to apply a CSS style to the section to change the of the text :

-- p
-- class: green

Pour the stew from the pot into the plate

Raise the sail and steer the ship northward

Which outputs :

Pour the stew from the pot into the plate

Raise the sail and steer the ship northward

Section Templates

Neopoligen works by mapping the different types of sections to corresponding MiniJinja 4 templates 5 . There are several templates built into the app, but you can also make your own. For example, here's a section type I made called "book - to - read - example" :

-- book-to-read-example
-- title: The Peripheral
-- author: William Gibson
-- image: /neo-images/the-peripheral-cover/100w.jpg

I've read a bunch of other William Gibson stuff and can't
wait to get to this one!

Which looks like this :

Book cover for The Peripheral by William Gibson

The Peripheral

By: William Gibson

I've read a bunch of other William Gibson stuff and can't wait to get to this one!

It's important to point out that there's no difference between the templates that ship with Neopoligen and the ones you make. You can make as many arbitrary sections as you want. They are all treated the same way.

Alternate Templates

Each section has a default template. Additional templates can be built for them as well. Switching to one of the alternates is done by adding a "template" attribute to the section.

Here's the same example as above but calling a template called "alt" :

-- book-to-read-example
-- title: The Peripheral
-- author: William Gibson
-- image: /neo-images/the-peripheral-cover/100w.jpg
-- template: alt

I've read a bunch of other William Gibson stuff and can't
wait to get to this one!

Which looks like this :

William Gibson

The Peripheral

I've read a bunch of other William Gibson stuff and can't wait to get to this one!

Book cover for The Peripheral by William Gibson

There's a clean separation of concerns between the content and the layout.

Nesting Sections

One of the biggest differences between Neopolitan and markdown is that sections can be nested. This is done by adding a "/" to the end of a section name where you want the nesting to start and then closing the nesting with a corresponding section that starts with a "/". For example :

-- div/
-- class: example-grid

    -- note

    Fly by night, and you waste little time

    -- note

    Pull the dart from the cork target

    -- note

    Pick a card and slip it under the pack

-- /div

Outputs :

Note

Fly by night, and you waste little time

Note

Pull the dart from the cork target

Note

Pick a card and slip it under the pack

Spans

Spans are sections of content that look like this :

This is <<strong|bold text>>

Which outputs :

This is bold text

They work similar to sections in the each one has a corresponding template. Spans default to putting their text inside the corresponding HTML tag (e.g. "strong", "em", etc...).

Span Attributes

Spans can take optional attributes. They can be used for things like setting a CSS class on them.

For example, this :

This is <<strong|green bold text|class: green>>

Turns into this :

This is green bold text

Span Templates

Span templates don't have to map directly to HTML tags. They can be used to create more complicated output. Take this "tablink" for example :

<<tablink|text to link|https://www.example.com>>

It's used to create a link to another page that opens in a different tab like this :

text to link↗

The template creates the link, points it to the " _ blank" target for the new tab, and then appends the character to identify that it's going to a new tab.

Built - In Test Suite

Each template can have a set of tests associated with it to make sure it's doing the right thing. All the tests are run each time a site is generated and a report shows any errors.

The tests themselves are done in Neopolitan files that look like this :

-- start-theme-test

-- p

Alfa bravo charlie 

Delta echo foxtrot

-- expected-output/

<section class="p-section flow">
<p>Alfa bravo charlie</p>
<p>Delta echo foxtrot</p>
</section>

-- /expected-output

Adding CSS

My website includes a lot of my notes. Lots of those have little pieces of CSS. To help accommodate this I use a -- css section type. The default template for the section type is empty. So, nothing renders directly on the page. Instead I added the content of the section to the < head > of the document at the page level (which is just another template).

Of course, since it's a section, we can create a new template that does output its contents and call it to show the code on the page too. For example, this adds css for the #count_btn to the page :

-- css
-- template: show

button {
    color: red;
    font-weight: bold;
}

And, it outputs this on the page :

CSS

button {
    color: red;
    font-weight: bold;
}

I get hugely frustrated when I'm reading a blog post where someone puts in CSS that they say does something but when you copy/paste it it doesn't work. This goes a long way to avoiding that since the code displayed on the page is literally the same code that's in the < head > of the document.

Adding HTML

Adding HTML on the page is done in a similar manner to CSS. The biggest difference is that it outputs where you put it instead of in the < head > of the document. So, this :

-- html

<button>This button does nothing</button>

Outputs :

Note

The red/bold font in the button is from the CSS that was added to the page in the example above.

Showing HTML

Like CSS, you can also make templates that show the source code along with the actual items in the DOM.

For example, this :

-- html
-- template: show-above

<button id="count_btn" data-count="0">
    Counter: 0
</button>

Outputs this :

HTML

<button id="count_btn" data-count="0">
    Counter: 0
</button>

Output

This helps make sure that any HTML samples in blog posts and tutorials actually work because the same code that's used for the DOM is what's actually displayed.

Adding JavaScript

You can add JavaScript to the < head > of a document the same way you can with CSS. By default, it doesn't show on the page, but you can use a template to show it as well.

For example :

-- javascript
-- template: show

function addToCounter() {
    const btn = document.querySelector("#count_btn")
    const count = parseInt(btn.dataset.count, 10)
    btn.dataset.count = count + 1
    btn.innerHTML = `Counter: ${btn.dataset.count}`
}

document.addEventListener("DOMContentLoaded", () => {
    const btn = document.querySelector("#count_btn")
    btn.addEventListener("click", addToCounter)
})

Adds the code to the < head > of the document and outputs this on the page :

JavaScript

function addToCounter() {
    const btn = document.querySelector("#count_btn")
    const count = parseInt(btn.dataset.count, 10)
    btn.dataset.count = count + 1
    btn.innerHTML = `Counter: ${btn.dataset.count}`
}

document.addEventListener("DOMContentLoaded", () => {
    const btn = document.querySelector("#count_btn")
    btn.addEventListener("click", addToCounter)
})

You can verify that the code works by clicking the button in the HTML section. (Again, this is in an effort to make sure that any samples I show actually work)

External Scripts And < head > Content

The -- head section is used to add things directly to the < head > of a page. This can be used to call external JS libraries like P5, D3, etc... or anything else that you need to load onto the page.

Section Syntax Highlighting

Neopoligen includes a built in syntax highlighter. I use it in -- code sections by passing the name of the language like this :

-- code
-- rust

fn main() {
    println!("Hello, world");
}

Which outputs :

fn main() {
    println!("Hello, world");
}

Syntax highlighting is done with classes instead of inline styles so things can be controlled from style sheets.

The highlighter also adds span at the start of each line that can be used as a target for adding CSS line numbers as seen above.

Note

Eventually, this will be a web component that has a copy button, displays the language, and has the ability to turn wrapping on and off. It's just a matter of updating the template to make that happen.

Span Syntax Highlighting

Language specific syntax highlighting can be done one inline code too.

Rust code: <<code|println!("hello");|rust>>

Which outputs :

Rust code : println!("hello, world");

Footnotes

I love footnotes. They make it easier for me to write at a higher level then point to more detail later which makes for better prose. There's a shorthand built into Neopoligen for adding the footnote numbers in the content that looks like this :

This is a footnote^^6^^

Which outputs :

This is a footnote 6

These correspond to a footnote section that has an id attribute with the same number. For example, this is the section that produces the footnote that's at the bottom of the page :

-- footnote
-- id: 6

This is the target footnote for the example in
the main content.

Responsive Image Optimization

I used to be a professional photographer. I don't shoot as much these days, but I still want it to be easy to work with photos. Neopoligen is set up so I can throw my full size images into a directory and it'll automatically make multiple sizes of them that can be used with srcset .

Calling Images By Name

Another image related thing I always found a pain was dealing with the paths for the images. Neopoligen deals with this by calling images by name regardless of where they are stored in the source images directory. The data returns to the template includes all the available responsive sizes for building a srcset attribute.

You can also skip the extension. This means if you ever decide to to something like upgrading all your .jpg files to .webp, they'll continue to work without having to go back and re - name everything.

The default way to call an image looks like this :

-- image
-- large-test-image
-- alt: The words 'Large Test jpg' are in dark blue
-- alt: text on a black background in the upper right.
-- alt: Most of a red circle shows up in the upper left,
-- alt: and most of the bottom is covered by a curved
-- alt: blue shape that just gives some shape for
-- alt: this test image.

The example also shows how alt text can be added to image including extended alt text that would otherwise end up in a single very long line in the content file.

Note

I'm refactoring the way images handling. The srcset and alt text might not show depending on when you see this. And, yes, if you have two files with the same name it creates a "race condition" where you don't know which image will show up. I'm adding a feature to warn you about that, but either way it's a minor issue compared to the convenience of not having to mess with file paths.

Automatic Image Alt Text Ingestion

The International Press Telecommunications Council 7 (which is responsible for setting standards for the new media) added Alt Text fields to their spec 8 . These have been included in Photo Mechanic, Photoshop, Brdige, and Lightroom (the tools I use for editing my photographs). This means you can store alt text directly in your images. Neopoligen pulls that metadata if it's available and uses it for image alt text.

Aside

Open Graph Image Generation

Neopoligen is set up to automatically generate Open Graph images that include the title of the page for use in previewing on other sites. You can also create custom ones for individual posts as well.

RSS Feeds

I love RSS Feeds. There's been a bit of a resurgence of them recently and I want to participate. I'm still figuring out the exact approach I want to use but Neopoligen will be able to provide multiple feeds that can be filtered based off different criteria (e.g. just posts, just photos, or everything)

RSS Timeline

I'm still working on this, but the basic idea is to create a list of other RSS feeds and then include links to the individual posts as a form of timeline.

Microblog

Microblog style posts can be made without having to worry about making a title.

DB Style Collections

This is still a work in progress, but the goal is to be able to query across all my content files to make collections out of them. For example, I'm making a book section type. By adding and -- read attribute to it I can create pages showing what I want to read and what I've read independently. When I read a book, I'll update the attribute for it and it'll move automatically.

This is actually one of the most exciting things for me. The idea that I can use flat files to store content and then query things like a database opens the door for so many possibilities.

Permanent URLs

I don't want to have to think about URLs. And, if I ever migrate to new software I want to make it as easy as possible to match the existing URls so I don't have to do a bunch of redirects. To address this I set up Neopoligen so that every page must have an ID and that ID is what's used by default for the URL. The base URL for any post is :

/LANGUAGE/ID

For example, this page is always available at :

/en/2kn3czfj

URLs are made more human readable to appending the title in a "query parameter" like this :

/en/2kn3czfj/?page=why-i-built-neopoligen

Because of the way the site is served the title can change and both the old and new URLs will continue to work without having to build a redirects setup.

They aren't the prettiest URLs. I'm also unsure of what the SEO impact of moving the titles to query strings is. But, I don't care. This is very much a case where I prefer function (designed to prevent link rot) over form.

Automatic Linking

Because URLs are ID based it's trivial to use them to create links between pages. Two built - in templates provide links with either custom text or the title of the page being linked to. For example :

This is a <<ilink|custom text link|2kiw8tvv>>

Link with title: <<tlink|2kiw8tvv>>

Outputs :

This is a custom text link

Link with title : TODO List

Wrapping Up

I've been working on Neopolitan and Neopoligen for a couple years at this point. The idea above are the refinement of all that time. My goal is to be able to use the format and the engine for the next twenty years. But, even if the engine doesn't last, I'm pretty confident at this point that the format will stick with me.

It's been a lot of work, but I couldn't be happier with the results.

If you've got any questions or just want to reach out, you can hit me up on Mastodon

~ fin ~

Footnotes