Cody Woolaver

Software engineer portfolio

This is my personal site: part portfolio, part project notebook, and part place to keep useful little web tools alive. I'm updating it into a clearer home for my work, my background, and the experiments I want to keep building over time.

Site Update · June 13, 2026

Making blog posts a real model

One of the quieter changes in this site refresh was moving blog posts out of one-off template edits and into a real BlogPost model. That sounds like a small model update, but it changes the way the site can grow. A post now has a title, slug, status, publish date, body, and timestamps that belong to the application instead of being scattered through static markup.

That move was smart because it gives the writing side of the site the same kind of shape as the rest of the project. Published posts can be queried, sorted, counted, previewed on the home page, and shown on their own stable URLs. Drafts can exist without being public. The admin dashboard can surface the latest publishing activity. Most importantly, adding a post no longer means inventing another little content path by hand.

I also like that the model gives future work somewhere obvious to land. Tags, RSS, better excerpts, scheduled publishing, search, or project-specific post collections can all build on the same table instead of needing a new static convention each time. Even if I never add every one of those ideas, the current shape keeps the door open without making the first version heavy.

The implementation stayed intentionally plain. Markdown is stored as source text, rendered through a CommonMark parser, and cleaned before it reaches the page. The public routes use date pieces plus a slug, so URLs are readable and stable. Publication fields are denormalized just enough to make uniqueness and routing straightforward: the date is available as a real date, and the published day key protects against accidentally publishing two public posts on the same day.

The admin flow was another practical part of the change. I can create, edit, preview, publish, and unpublish posts from the site itself, while older launch posts can still be seeded through migrations when that is the right tool. This entry is doing exactly that, which feels fitting: the blog system is now content, data, and deployment working together instead of a pile of special cases in templates.

Read full post

Site Update · June 7, 2026

Giving the pet archive a proper home

The pet archive had been one of those half-finished ideas I kept meaning to come back to. It started as a small personal page, then sat quietly while the rest of the site changed around it. When I returned to it, I wanted it to feel less like a folder of uploaded images and more like a real little corner of the site: organized by pet, easy to browse, and built with enough care that it could keep growing over time.

The data model now treats pets and photos as separate things. A pet can have life dates, a stable slug, a profile photo, and a display priority. A photo can be connected to more than one pet, carry a short description, and keep the date it was taken when that information is available. That shape matters because a lot of the best pictures are shared moments, and I did not want to duplicate the same file just because two names belonged beside it.

The upload path borrows from the ShareX storage work already in the site. Admin uploads go through the shared-file pipeline, validate that the file is an image, strip embedded EXIF data, and try to infer the photo date from the filename or image metadata before falling back to the upload time. The public pages then redirect image requests through the stored file variants, so the pet pages can use thumbnails, larger gallery images, and lightweight placeholders without each feature needing its own separate storage system.

On the visitor side, the goal is simple: make it pleasant to wander. The main page can be filtered by name or status, sorted by the order I choose, alphabetically, or by photo count, and each pet page can be searched, filtered by year, and opened into a lightbox. It is intentionally modest, but it gives the archive enough structure to feel like a project rather than a loose pile of files.

I also liked the excuse to go back through older pictures. Building the feature meant seeing faces from different parts of my life again: pets that are still here, pets I miss, and small ordinary photos that are worth keeping because of who was in them. That is the best kind of personal project for me, where the implementation is interesting but the reason for building it stays close to home.

View the pet archive

Read full post

Site Update · June 3, 2026

Building my own contact form spam check

While updating the contact form, I wanted a small barrier that fit the site instead of dropping in a large third-party widget. I could have used reCAPTCHA and moved on quickly, and for some products that would be the right call. For this site, part of the fun was treating the form as a small design problem: how far could I get with a focused, understandable solution that I owned end to end?

The weak spot was obvious. If the text is printed directly into the HTML, a basic script can collect it almost as well as a person can. So the server now creates a simple arithmetic check, breaks the characters into shuffled fragments, and stores those fragments in the markup in a lightly encoded form. The browser puts the text back together for visitors, while the raw page source is less useful to a drive-by bot.

The important part still happens on the server. The hidden token includes the issue time, a random nonce, and a keyed digest of the expected answer. When the form is submitted, the server checks the signature, rejects stale tokens, and compares the submitted value with the signed digest. That means the page can display the check without trusting the browser to decide whether it was solved.

This is not meant to be a universal anti-spam system. A determined attacker could inspect the JavaScript and automate around it. But for a personal contact form, the goal is more practical: avoid the simplest automated submissions, keep the experience quiet for real visitors, and learn from building the moving parts myself instead of immediately handing the job to another service.

Read full post

Site Update · June 2, 2026

Adding practical local services

I added a new page for local services outside of software: hauling, dump trailer work, flatbed trailer work, small-acreage tractor help, cleanup, and light chainsaw services around the Nashwaak Valley and Greater Fredericton area.

As much as I have a passion for building good technology, there is something deeply satisfying about using my hands to finish practical work. Moving material, clearing a property, preparing a garden area, or helping someone get a small job unstuck has a directness that I value. The work is visible, useful, and grounded in the place I live.

The new services page gives those jobs their own home on the site, with photos of the equipment, a clear description of what I can help with, and a simple way to call when someone wants to talk through whether a job is a good fit.

View local services

Read full post

Site Update · May 30, 2026

Taking image hosting back home

The ShareX project started from a practical itch: I wanted to stop leaning on public image-hosting services that I do not own. Those services are convenient until links rot, policies change, files get harder to move, or I simply want to know exactly where my screenshots and small uploads live. Wiring ShareX into this site gives me short public links on infrastructure I control.

The project ended up being more than an upload endpoint. It accepts screenshots, files, and text uploads behind a bearer token, writes incoming files through temporary storage, detects common image, video, and text types, and stores enough metadata to keep the original filename, size, media kind, and generated public path around. Public links serve images, videos, and text inline, while unknown files download instead.

Caching was one of the details that mattered. Public files get cache headers so repeated views are cheap, while responses also set nosniff and noindex so the links behave more like unlisted personal uploads than a searchable gallery. The admin side is where the system becomes manageable: I can upload manually, copy the custom ShareX config, sort by creation time, last view, or download count, see thumbnails and metadata, and mark files for deletion without immediately losing the bytes on disk.

The less glamorous parts were the real challenge: generating short URLs without collisions, keeping storage stable across Docker restarts, handling large uploads without reading everything into memory, tracking last-viewed and download counts without turning it into a full analytics system, and making deleted links stop resolving while still leaving room for recovery. It is small infrastructure, but it is mine, and that was the point.

Read full post