dlo.me

The Cult of Done Manifesto

I watched a short clip this morning on Twitter between Bryce Roberts and Soleio. Soleio would read the manifesto aloud to early Facebook employees over beers. Please do yourself a favor, and read it aloud this morning. If you're a builder, it should invigorate you.

From the Internet Archive (unfortunately, the original is no longer available):

Dear Members of the Cult of Done,

I present to you a manifesto of done. This was written in collaboration with Kio Stark in 20 minutes because we only had 20 minutes to get it done.

The Cult of Done Manifesto

  1. There are three states of being: not knowing, action, and completion.
  2. Accept that everything is a draft. It helps to get it done.
  3. There is no editing stage.
  4. Pretending you know what you're doing is almost the same as knowing what you are doing, so just accept that you know what you're doing even if you don't and do it.
  5. Banish procrastination. If you wait more than a week to get an idea done, abandon it.
  6. The point of being done is not to finish but to get other things done.
  7. Once you're done, you can throw it away.
  8. Laugh at perfection. It's boring and keeps you from being done.
  9. People without dirty hands are wrong. Doing something makes you right.
  10. Failure counts as done. So do mistakes.
  11. Destruction is a variant of done.
  12. If you have an idea and publish it on the internet, that counts as a ghost of done.
  13. Done is the engine of more.

I Ditched Jekyll and Built a Static Site Generator Based on Go and SQLite

I've got quite the yak shave to share (have you shaved a yak before? If not, I highly recommend it).

It goes like this:

You start with the desire to wax your car.

To wax your car, you need a water hose. Only, your water hose is busted so you need to go down to the hardware store to get a new hose.

To get to the hardware store, you have to drive across a bridge. The bridge requires a pass or ticket. You can't find your pass, but you know your neighbor has one.

However, your neighbor won't lend you his pass until you return a pillow that you borrowed. The reason you haven't returned it is because the pillow is missing some stuffing.

The pillow was originally stuffed with yak hair. In order to re-stuff the pillow you need to get some new yak hair.

And that's how you end up shaving a yak, when all you really wanted to do was wax your car.

In summary: I had an interesting idea I wanted to write in long form (not on Twitter!), but it had been quite a while since I wrote a blog post. Unfortunately, so many years had passed since my previous blog post that I couldn't even get Jekyll to work. So, naturally, I ended up building a static site generator.

But First, Some History

My first blog was based on WordPress. It served as my journal while I studied abroad in China in 2007.

It worked pretty well! However, it used a gnarly cPanel-based "infrastructure" and was hosted on a fairly iffy host called midPhase (which is still around, somehow?).

For one reason or another, I decided to not renew my hosting plan with them, so they shut down my account and deleted all of my content. Years later, I regretted this decision.

Fortunately, I had been syndicating all of the content of that blog through Feedburner, and I was able to extract all of the posts I made using their RSS feed. I lost my photos, though.

Aside: I'm not really sure what happened to Feedburner, but whatever it is now, was not what it used to be. I just logged in and don't really understand what it is.

Several years later (2009/2010), I started blogging again, albeit with some infrequency, on Blogger. I posted only a few times there, but it still lives. I haven't had the heart, or desire, to take it down.

The Age of Jekyll

At some point soon thereafter (2010-2011ish), I must have gotten annoyed with having all of my content owned and hosted by a third-party (having been burned by my experience with my original WordPress blog), and also wanted to try out a cool new static-site generator called Jekyll written by Tom Preston-Werner. This post from Chris Parsons is emblematic of the Jekyll zeitgeist in late 2009 (HN submission here).

At the time, Jekyll was also the only supported service for deploying static sites to GitHub Pages. This made the setup process very straightforward and easy.

For the next several years, writing new posts was a breeze; create new markdown document. Save. Commit. Push. Published.

About a year later, I decided to add a gem or two that wasn't compatible with GitHub Pages, so as a result, I migrated everything to CloudFront and S3. That was fun.

And then I took a little bit of a blogging break.

Oops.

What I hadn't considered was that Ruby is not a platform or language that cares too much about backwards-compatibility. I also didn't consider that Jekyll was early-stage software and would change a lot (anyone could have predicted this 😬).

To be fair, this wasn't a problem for quite a while. But as the years piled on, the Ruby ecosystem's penchant for breaking changes started to slowly bite me: Jekyll 4 was released in 2019. And then Ruby 3 came out in 2020. And soon enough I realized I literally couldn't publish a new blog post without upgrading >10 gems (iykyk).

So I just stopped!

Which, when you think about it, is kind of wild: I stopped blogging because I literally couldn't run bundle exec jekyll build. And it just wasn't going to be worth the time to migrate everything to the latest versions. I kept pushing it off, and, before you know it, several years had passed by.

Durable, Portable, and Easy-to-understand

In the last two weeks, I decided enough was enough. I needed to make the call on whether to nuke my entire setup, or try and figure out a migration strategy to a new system. Jekyll just wasn't going to cut it; even if I fixed the issues now, in just a few years this entire problem would resurface just like a game of whack-a-mole.

Not to mention, I also have a few other static sites running Jekyll, all with the same issues, and they would need to be moved too. But what system to migrate to? Obviously, it's never anyone's immediate thought to build their own static site generator, so I wasn't going to do that. I'm not that unhinged (or am I?).

So I came up with a list of good alternatives, and I literally ported my blog to each one of them. Aside: I still kind of can't believe I did that.

With some human assistance, ChatGPT threw together this feature matrix, which does a fairly good job of summarizing the pros/cons between the options I landed on:

FeatureHugoVite / React RouterAstro
Speed🟢 Fastest🟠 Moderate (JavaScript-heavy)🟢 Fast (optimized builds)
Mental Overhead🟢 Low (simple templates)🟠 Moderate (React concepts, routing)🟢 Low (modern, intuitive workflow)
Interactivity🔴 Limited🟢 Full React SPA/MPA🟢 Hybrid (JS islands)
Flexibility🟢 Content-focused🟢 Component-based SPA🟢 Best of both worlds
Community Support🟢 Mature🟢 Mature (React ecosystem)🟠 Growing
Future Proof🟢 Stable and reliable🟠 Moderate (npm/Node issues)🟠 Moderate (npm/Node issues)
SEO Optimization🟢 Excellent (purely static)🟠 Requires extra effort (SSR or hydration)🟢 Excellent (static-first with flexibility)

So that feature matrix is maybe helpful, but my actual experiences went like this:

Hugo

  • Converting templates was frustrating since error messages were hard to understand.
  • Documentation was extensive but poorly organized. I often found myself digging through pages of search results to uncover how to do simple things, like how to display an image.
  • Tons of mental overhead with respect to how templates are actually rendered. See their documentation on template lookup order as an example.
  • Unclear when to use shortcodes versus variables / other alternatives.
  • Tons of boilerplate to do simple things, such as including a CSS file. E.g.:
    {{ $opts := dict "transpiler" "libsass" "targetPath" "css/style.css" }}
    {{ with resources.Get "sass/main.scss" | toCSS $opts | minify | fingerprint }}
      <link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
    {{ end }}
    

Vite / React Router

Vite Logo React Router Logo

  • I'm super familiar with this stack, so getting set up was quick.
  • Had to roll my own post management system, and there wasn't anything off the shelf that would essentially work as a CMS.
  • The site was fast to compile and run.
  • Even though I could prerender all of the pages, my prediction is that SEO would take a hit due to the sloppy HTML.
  • Based on the Node stack so I would expect the entire thing to require an upgrade in a few years. :(

Astro

Astro Logo
  • Heard tons of good things about this setup lately so I decided why the heck not.
  • I thought the entire frontmatter architecture, where TypeScript code lives at the top of .astro files, was very weird.
  • Didn't love the custom file format (".astro"). Requires an editor plugin to read them correctly and none of my auto formatting tools worked out of the box.
  • It was fast-ish.
  • Also built on the Node stack, and a newer technology, so inevitably I'd run into some issues in the future.

Recap

In the end, Hugo's documentation and verbosity ultimately scared me away. Vite/React Router was nice, but it wouldn't be great SEO and I'd have to deal with Node.js. Astro was a nice experience, but I thought the entire .astro file format and the somewhat proprietary "feeling" of the stack to be a turnoff. And being JavaScript-ecosystem-based as well, I'd still have to deal with the future-proofing thing.

Maybe I'd have to settle. But there was something I realized in the process of trying all of these services out that led me down a unexpected path. First, let's review how I ported things over.

Porting

Trying out a new static site generator involved several steps:

  1. Read docs for 15m+ to learn the basics.
  2. Setup stack.
  3. Copy all markdown files from old Jekyll setup to new location.
  4. Copy all templates to from old Jekyll setup to new location.
  5. Copy static files.
  6. Run dev server and iterate for several hours until all posts rendered somewhat correctly.
    • This involved much searching and replacing, and repeated updates. Even with AI assistance, this was a slog.

So that was fun. At some point, while porting to Astro, I realized it could plug into a custom loader, and read from a database. "That's interesting", I thought. "Could I just port all of my blog posts to a database, and then make text edits simply by running a SQL query?" Well, yes. I could.

And then, if I ever needed to move to a different static-site generator in the future, I could just keep using the same database file to regenerate the files or read them from a loader. Sweet. Maybe Astro is the move!

So I wrote a little script that converted all of my markdown files to a SQLite database, and started writing some code in Astro to read those posts. But after about an hour of wrangling with the docs, I couldn't get it to work! It was just too confusing. At this point I realized this was not sustainable. If I took a month break from blogging this was not going to be something I was going to remember.

I felt lost and frustrated. What was I to do?

But then I stepped back, and I realized that my entire mental model of how a static site generator should work was off. I had been thinking about it completely wrong.

Logic != Data

Blog posts are data! Static pages are data! Templates, however, are code. They contain "business logic". Business logic belongs in code.

Data belongs in a database. Why am I storing my blog posts in files, and treating them like code? They should live in a database that any static site generator can read from. That way my data is permanent and not tied to my static site generator. That's how it should be.

Weirdly, though, not many (any?) popular static site generators use a database as a backend. Pretty much everyone uses flat files. Which is kinda nice (you can see diffs!) but honestly, I don't really care about diffs. I just want my blog to work, be stable, and my data to be portable.

At this point I realized where I was headed: I was going to need to build a static site generator.

It would need to be based on technology that would be permanently future proof, with a data backend that would never become outdated.

A Wild Static Site Generator Appears

A Wild Static Site Generator Appears

The fact that Hugo was built on Go gave me some hints about what direction to take this in, and I'd built much software in Go that still worked without changes for over a decade. So the programming language choice was clear: I would build it in Go.

For the data side, the choice was also quite clear since I already had a SQLite database from my Astro experiment with all of my blog posts and pages. I didn't even need to do any additional work there. There was some schema and data modification, sure, but it just involved a few SQL queries and I was set.

Some Notes on SQLite

First of all, SQLite is just awesome. It's incredible software. It can literally do anything. Ok, maybe not, but it's really flexible and solid.

One thing you may not know is that SQLite is a great "filesystem". It can store files super efficiently, and there's even a tool written by the SQLite authors called sqlar which is effectively equivalent to ZIP in terms of performance and space requirements, with the added benefit that you can treat the entire archive as a database!

You might be wondering: "Dan, can you really store a SQLite database in a Git repo?" The answer is yes.

I had an intuition that Git's delta compression would store SQLite quite efficiently, and based on my own research and that of others, it turns out that I was right: Git excels at storing SQLite databases in version control. It's on par, even, with plain text.

Add in a custom diff handler, specify which filetypes should get treatment with .gitattributes, and we now have a setup that shows diffs and is as space-efficient as storing raw markdown files.

Go

Go is remarkably solid. I've written programs in Go over a decade ago that run without modification to anything, even the build system.

And that's expected, as one of Go's core design principles is backwards compatibility. From the linked document:

In the quoted text from “Go 1 and the Future of Go Programs” at the top of this post, the ellipsis hid the following qualifier:

At some indefinite point, a Go 2 specification may arise, but until that time, [… all the compatibility details …].

That raises an obvious question: when should we expect the Go 2 specification that breaks old Go 1 programs?

The answer is never. Go 2, in the sense of breaking with the past and no longer compiling old programs, is never going to happen. Go 2 in the sense of being the major revision of Go 1 we started toward in 2017 has already happened.

There will not be a Go 2 that breaks Go 1 programs. Instead, we are going to double down on compatibility, which is far more valuable than any possible break with the past. In fact, we believe that prioritizing compatibility was the most important design decision we made for Go 1.

So what you will see over the next few years is plenty of new, exciting work, but done in a careful, compatible way, so that we can keep your upgrades from one toolchain to the next as boring as possible.

How fricking awesome is that? Whatever you write in Go, will continue to work, forever.

Go does have its own warts, but so does every language, and one must prioritize needs in order to make software design decisions, and in my case, reliability and future proofing trumped almost everything else.

Go is also very simple. Simple is good!

Doing The Thing

So yeah, I built a static site generator using Go, using SQLite as the database, stored in version control, and it's now running this very blog, right now. I wrote a GitHub action that generates the pages (takes <20s to build and deploy the entire site!) and it's now hosted on GitHub Pages.

GitHub Actions

I even wrote a little editor that runs when I run the site locally using go run, so I can edit posts in the browser instead of using a database management tool (I got tired of that quite quickly!).

I'm super happy with how everything turned out. Dependency management is a breeze, and the entire thing is blazing fast. It preprocesses SCSS and code blocks, and minifies images, CSS, HTML, and JavaScript.

There are obviously a lot of rough edges given that this is pre-alpha software, but those will be smoothed out soon enough as I port a few other websites over to the new system.

Some things I want to do next:

  • Allow an entire website to be bundled as a single executable file, with all static content included, so it can run as a full web server. Is there something like Cosmopolitan, but for Go?
  • Make it go installable so it can just work as a binary.
  • Write a standard spec for the database schema that will handle most static sites.
  • Turn it into a library so I can simply import it into another Go file to extend behavior.
  • Improve the editing experience, using something like Editor.js.
  • Open source it. 🎉

All in all, this was a fun learning experience and I think I built something pretty cool. I'm really excited to migrate my other websites and see my work pay off (I'm really hoping this will be the last time! 😅🤞🏻).

Creativity Inc.

This is a list of my 7 favorite things about the book Creativity, Inc.: Overcoming the Unseen Forces That Stand in the Way of True Inspiration, by Ed Catmull, co-founder of Pixar Animation.

  1. Ed Catmull is a fantastic writer and it was a pleasure to read.
  2. There is a formula to encourage creativity in large organizations.
  3. The portrait of Steve Jobs was eye-opening, and Ed’s interactions with him reflected him in a more realistic light.
  4. Creative success can take decades.
  5. Creative organizations require constant conflict to succeed.
  6. What might have worked in the past will probably not work in the future.
  7. Creators rarely can predict what their inspiration will lead to.

Kerrville Tri 2017 Race Report

This race was a breakthrough for me. I dominated in the swim (3/22 in AG, only ~9s behind the leader), had a PR on the bike in terms of wattage and speed (although it was a flat course), and had my fastest run of any race, including the Splash and Dashes.

The weirdest part about all of it was that, franky, I felt like crap the night before and even the morning of—pretty sure this was the lingering cold I’ve had for a week. Not only that, but my right hip was super stiff. The night before when I went out for a quick grocery run, I was literally walking with a limp. Not good, I thought…

I had redfish and some veggies at around 6:30pm the night before. Prepped my transition bags, and finally got to sleep at around 10ish, a little later than I’d wanted to. I was also pretty stressed out. A big work thing is going awry and I was away from Rachel and Gideon, so that was tough mentally. Went to bed feeling really depressed and not feeling great. Popped two tylenols before falling asleep in an attempt to dull the pain.

I set the alarm for 5am and got right out of bed. Still felt sick and pretty yucky, so decided to take another two tylenol. I could only take 2-3 bites of the yogurt. Banana, nuts, and honey went down easily though. I then took a water bottle and drove to T2 to drop off the run bag and took the shuttle back to T1 at around 5:30. Dawdled a bit and got my bike setup after I got there, prepped a towel, got my shoes set up, etc. At ~6:30 I was starting to feel like I could eat a little more so I ate a clif bar pre-race oatmeal bar that I’d brought along just for kicks. I’d never had one before but I was so hungry and had nothing else, so I decided to risk it. It really didn’t go down easily. I was worried I'd get sick but I pretty much force fed it to myself, reminding myself of what happened at Jack’s Generic Tri in August.

My right hip was still really hurting, so I stretched for about 15 minutes. Still no good. I then massaged the muscle constantly up until literally right before the race started. I was super anxious and worried about the bike. At about 7:15 I had a honey stinger to eliminate any potential energy lapse. I'd rather overeat than under.

It was a TT start for the swim and my AG was the first to go. I was about the 6th person in the water. Right after I jumped in I just got into a flow. I felt fast and just kept pushing. Water was cool and I sighted well. I passed one two people too, which was crazy given how early I was in the water.

Stepping out of the water was right when I knew things were going to go well. I wasn’t tired at all.

It only got better. There was a huge hill to get into T1, and I was worried the day before about how rough that would be. But nope, ran like mad all the way to the top and still felt fresh and full of energy. Then I knew for sure I was going to rock it. Used a towel to wipe grass and dirt off my foot and then got going on the bike.

Before the race, I was thinking I’d be ecstatic if I hit 170W+ on the bike? Well…I don’t know how it happened but I blasted through that goal. 185W normalized for the full duration. I lost a little steam during the last 2 miles which brought my average down, but even for that period I was cruising at 176W. There was another guy who kept trading places with me. In the end, he won out, but the back and forth was great. Pushed myself much harder because of it. Shows you the power of competition I guess. :)

I got of the bike, ran to my run bag, swapped the shoes, and got going. The run course was flat and easy, so it friendly to a new runner like me. Like every other time, I think that I could have gone faster. Then again I had my fastest run ever at a 9:10 / mile pace, so whatever I did worked. My splits reflect my attitude…first kilometer I cruise, then the middle three I take too easy, and the last km I speed up again. If I can cruise longer and then pick of the final pace earlier, that alone I think could bring me to a sub 9” pace. Getting there!

In the end, I really do have no idea where today’s burst of energy and power came from. I’ve been sick all week (and still feel sick!), had a limp hip, and have been super tired…but it all worked out in the end. Also happy I skipped the Splash and Dash earlier this week. I guess my body needed the rest.

Jack's Generic Tri 2017 Race Report

Woke up at 5:10, a little earlier than I’d planned since my dad-in-law decided he was doing the race at the last minute, so I picked him up on the way. This actually sort of caused a cascade of issues since I didn’t have time to eat breakfast. The steel cut oats, banana, and maple syrup REALLY didn’t appeal to me unfortunately, and because I felt so rushed, I maybe got one or two bites in before I had to head out. I think this was a big mistake.

Transition setup went well, didn’t forget anything, plan was great.

Swim was fantastic (at least from what I know from the watch). My pace was 1:40/100m which was much better than the previous race. We’ll have to wait on the official results but I was really happy with this. For some reason I got it in my head that I needed to push myself in the swim but in the end I’m a little regretful about this. You'll see why.

T1 was BAD. I was totally out of breath when I got out of the water and essentially needed to stand still and put hands on knees to regroup myself. I probably lost a minute or two just getting back to the right mental & physical place. Now whether this had something to do with me not really having any breakfast…I guess there’s no way to know, but wouldn’t be surprised.

Bike started off OK I think. The whole time I kept wanting to push harder, but just didn’t have the energy in me. Again, think the lack of a “real” breakfast got me here. I also didn’t drink any water at all for the full 42 minutes. I need a better strategy there too, I just plain forgot and the aero bottle I’m using has a really rigid straw that makes it really tough to drink and ride at the same time without some facial acrobatics. I did have the clif blok earlier than planned because I felt REALLY hungry. Man, lack of breakfast really did not do good things for me. My average power was 152W and normalized was 156W. For comparison, Life Time average was 153W and normalized was 158W. So it is what it is. I do think the reason I didn’t perform as well as I thought I could was 1) pushing too hard in the swim and 2) no breakfast 3) having to pee like crazy from about 15 minutes on. The last issue really affected me more than I would have liked, it was just really hard to push myself.

T2 was much much better than T1. I felt good getting off the bike, got the helmet off, popped a honey stinger, had some water, put on the visor, and went on my way.

Like the swim, the run was my best run of the season so far. According to the watch, my average pace was 9:53 / mile which is ~40s / mile faster than Life Time in late May! I’m really happy with this and didn’t walk for a second! The last km was a sprint for me, I saw another guy with a 33 on his leg and made up my mind to beat him. So I picked up the pace to a 8 minute pace for the last 0.6 km, I kept it up and beat him!

All in all, I’m really happy with the race. At the results tent, my placing was 10/14 when I checked, I’m sure it could change as more people finished but the way I look at it, my swim and run were both MUCH better, and the bike was the same as last race. I would have liked to have hit my bike goals but I think this was more a matter of race day planning and nutrition than fitness. Always something. Onwards to Kerrville where maybe I can learn from today and rock the bike.