Files
hwilliams-dev/data/content.ts
2026-10-31 03:13:41 +00:00

349 lines
16 KiB
TypeScript

export type MoreSection = {
title?: string;
text: string;
}
export type SocialLink = {
label: string;
url: string;
icon: "gh" | "li" | "x" | "dev";
};
export type ContactMethod = {
masked: boolean;
label: string;
icon: string;
}
/** A gallery image: either a bare url, or a url with metadata. */
export type GalleryImage = string | {
src: string;
/** Used as the image's alt text and tooltip. */
description?: string;
};
export function imageSrc(image: GalleryImage): string {
return typeof image === "string" ? image : image.src;
}
export function imageDescription(image: GalleryImage): string | undefined {
return typeof image === "string" ? undefined : image.description;
}
export type Project = {
id: number;
slug: string;
title: string;
description: string;
details?: string;
tags: string[];
images?: GalleryImage[];
videos?: string[];
link: string;
year: string;
};
export type BlogPost = {
id: number;
slug: string; // kebab-case, URL-safe
title: string;
description: string; // 1-2 sentence excerpt shown on cards
date: string; // display date, e.g. "2026-09-14"
tags: string[];
images?: GalleryImage[]; // optional, rendered via SimpleGallery
details: string; // markdown body
};
export type Mod = {
id: number;
title: string;
description: string;
link: string;
linkLabel?: string;
version?: string;
tags?: string[];
};
export type ModdedGame = {
id: number;
slug: string;
title: string;
platform?: string;
description?: string;
mods: Mod[];
};
export const PROFILE = {
name: "Hunter",
title: "Software Engineer",
email: "contact@hwilliams.dev",
portrait: "img/portrait.jpg",
bio: `Hi there. I consider myself a problem solver type and the adventurous sort. Lets take it apart, see how it works — then we have some options:
build something completely different out of it, take it to it's genuine limits, or just change a few things about it.
Looking for work. See below.`,
moreSections: [
{
title:"A little more about me",
text: `
I'm an associate level (~1yr) software engineer with full-stack development experience across a variety of frameworks and languages including Vue,
React, Next.JS, Nuxt, Node.JS, Express, Flask, FastAPI, Gunicorn, and more. Graduated with a B.S. in May of 2025. Last seen at USAN building AWS deployed apps. `
},
{
title:"What do I do right now?",
text: "I'm building Flowdesk! Check it out on my projects page. I also build mods for video games such as Palworld, Space Engineers and Rimworld, and run webservers for various modding tools — like the U4ESS Object Explorer. I don't post my projects on github as much anymore to avoid being free training for AI, but my old stuff is still there and you can find any new open source projects of mine at git.hwilliams.dev/hwilliams."
},
{
title:"My goals",
text: `Land the perfect user experience every time. \n \n Nothing makes me happier than seeing someone enjoy the experience they have using the things I make, and the way that happens is by practicing
development techniques that make a good user experience. I love to hear things like 'Your app makes it easy!', 'Your app feels good to use!', and 'Wow, it used to be difficult to do this!'.
`
},
{
title:"Footnote",
text: `If you've found this page trying to answer a question about my mods, please feel free to reach out.
`
}
] satisfies MoreSection[],
moreSectionsStart: 1,
links: [
{ label: "GitHub", url: "https://github.com/FerrenF", icon: "gh" },
{ label: "LinkedIn", url: "https://www.linkedin.com/in/hwilliamsf/", icon: "li" }
] satisfies SocialLink[],
contactMethods: [{
masked: true,
label: "contact@hwilliams.dev",
icon: "email"
}
] satisfies ContactMethod[]
};
export const SITE = {
title: "Hunter W. - Software Engineer",
description: "SWE Looking for work",
source: {
label: "Git",
link: "https://git.hwilliams.dev/hwilliams/hwilliams-dev"
}
};
export const PROJECTS: Project[] = [{
id: 1,
slug: "workflows-app",
title: "Flowdesk: Hit the ground running",
description: "Flowdesk is a microservices platform for breaking down complex business processes into guided visual workflows. Includes an SSO system, administration panel, database and API, and front-end.",
details: `# Flowdesk: A Flow-based support platform designed for event-based organizations.
<br/>
<blockquote>It's not just "make the UI simpler" but "make a day-one hire perform like a veteran on this task, every time."
<a target="_blank" href="/img/projects/fam/example-welcome.png">
<img src="/img/projects/fam/example-welcome.png" alt="Alt text" width="600"/>
</a>
</blockquote>
<br/>
## App Overview
Flowdesk is a set of microservices that acts as a platform for breaking down complex business processes into guided workflows. It's designed for event-based organizations, such as those that run
conferences, festivals, or other large scale events. Guest check-in/out, agent management, membership tracking, and many other critical tasks are given a consistent and streamlined workflow that
just feels good to use.
The approach that Flowdesk takes is trying to collapse the gap in time it takes to perform tasks between a novice agent and an expert one. Two agents doing the same check-in shouldn't take 90 seconds and 6 minutes. Flowdesk as a product seeks to compress that spread in variance.
As an objective it is measurable, demonstrable, and directly tied to money (labor cost, queue length, guest satisfaction, throughput per desk). **It's all about time-to-task and its variance - not just the average.**
It's not just "easy software. It's not just "make the UI simpler". It's "make a day-one hire perform like a veteran on this task, every time."
<a target="_blank" href="https://flowdesk.hwilliams.dev">Preview: Flowdesk Development Site</a>
## Private Repository
This is a real product that is in development. The source code is not public, but the platform may soon be available as a service or for licensing.
Please contact me for more information or exceptions to this.
A real-world solution for event staff and volunteer management.
`,
tags: ["Vue", "Node.JS", "Event-Management", "Real-Time", "Full-Stack", "Microservices", "SSO", "Rest API"],
link: "https://flowdesk.hwilliams.dev",
year: "2026",
images: [
{ src: "img/projects/flowdesk/flowdesk-preview-summary-1.png", description: "Flowdesk overview: your organization, your people, your process" },
{ src: "img/projects/flowdesk/prototype-live-screenshot.png", description: "Live registration flow: guest information and ID verification step" },
{ src: "img/projects/flowdesk/demo-screenshot-loggedout-flow.png", description: "Guided workflow as seen by a logged-out user" },
{ src: "img/projects/flowdesk/demo-screenshot-loggedin-flow.png", description: "Guided workflow as seen by a logged-in agent" },
{ src: "img/projects/flowdesk/demo-screenshot-editor-1.png", description: "Workflow editor" },
{ src: "img/projects/flowdesk/demo-screenshot-org-manage.png", description: "Organization management" },
{ src: "img/projects/flowdesk/demo-screenshot-org-details-sponsor.png", description: "Organization details: sponsorship" },
{ src: "img/projects/flowdesk/demo-screenshot-org-details-invite.png", description: "Organization details: member invites" },
{ src: "img/projects/flowdesk/demo-screenshot-message-inbox-empty.png", description: "Message inbox" },
{ src: "img/projects/flowdesk/demo-screenshot-admin-org-manage.png", description: "Admin panel: organization management" },
{ src: "img/projects/flowdesk/demo-screenshot-admin-tickets-manage.png", description: "Admin panel: ticket management" },
{ src: "img/projects/flowdesk/demo-screenshot-admin-message-manage.png", description: "Admin panel: message management" },
{ src: "img/projects/flowdesk/prototype-feedback-screenshot.png", description: "Event staff feedback after the prototype's first convention deployment" },
],
//videos: [ "img/projects/fam/runthrough.mp4" ]
},{
id: 2,
slug: "clean-space",
title: "Clean Space",
description: "Clean Space is a proof of concept and in-development tool for (Space Engineers) Torch server owners featuring client and server side plugin hashing, identification, and filtering controls.",
tags: ["C#", ".NET", "WPF", "Netcode", "Security"],
link: "https://github.com/FerrenF/CleanSpace",
year: "2025",
images: [
"img/projects/clean/cleanspace.png",
"img/projects/clean/ss1.png",
"img/projects/clean/ss2.png",
"img/projects/clean/ss3.png"],
},{
id: 3,
slug: "ue4ss-explorer",
title: "Unreal: UE4SS Object Explorer",
description: "The UE4SS Object explorer implements a byte-level parser to organize unreal engine object dumps into visual data structures.",
tags: ["Nuxt", "Vue", "TypeScript", "Node"],
link: "https://ue4ss-explorer.gimme.pet",
year: "2026",
images: [ "img/projects/uoe/example.png"],
},{
id: 4,
slug: "personal-portfolio",
title: "Personal Portfolio Website",
description: "This very website! Built with Next.JS, React, and TypeScript. Featuring a custom CMS and a lot of custom-built components and hooks.",
tags: ["Next.JS", "React", "TypeScript", "Vercel"],
link: "https://git.hwilliams.dev/hwilliams/hwilliams-dev",
year: "2026",
}];
export const BLOG_POSTS: BlogPost[] = [
{
id: 1,
slug: "adding-a-blog-tab",
title: "Adding a blog tab to a portfolio site",
description: "A portfolio is a static-ish site with a lot of moving parts. Here's how I added a fourth tab to this very site without adding a single dependency.",
date: "2026-10-26",
tags: ["next.js", "react", "typescript"],
details: `# Adding a blog tab to a portfolio site
This little corner of the site started as a question: **how does long-form content fit into a site that was built for short-form stuff?** Portfolio sites are grids of cards, and a blog is a column of words. The answer turned out to be \"don't treat them differently at all.\"
<blockquote>
A blog is just another collection of cards. The difference is what happens when you click one.
</blockquote>
## The shape of the problem
Every tab on this site follows the same recipe:
- A typed data array in a single content file, which acts as the whole CMS
- A shared layout that provides the nav chrome and the footer
- A thin client page that renders one list component
- A detail page that reuses the same layout, minus the nav, plus a context provider
A post that fits that shape is just another entry in an array. A post that doesn't fit it is a content management system in disguise, and that's a whole other project.
## Why the markdown was already there
The project detail pages render markdown through a small remark pipeline that has been sitting in the codebase for a while, used by the project pages. The blog needed exactly the same thing, which meant the data layer for a blog post is mostly just a type and a lookup function. The pipeline is shared, so a post and a project detail are rendered by the same code, with the same quirks, and the same fixes.
That is the whole trick. The feature felt like it would need infrastructure, but the infrastructure already existed for a different feature. When a new feature turns out to be a clone of an old one, the new feature is cheap, and the codebase gets a little more regular as a side effect.
## What actually changed
A new type, a new data array, a new lookup function, a new tab in the nav, two layouts, a list page, a card, and a detail page. No new dependencies, no new build steps, no new configuration. The tab order now reads profile, project, blog, mods, and the blog sits in the middle of the two portfolio tabs.
Editing a post from here on out is editing the array, which is the same as editing a project. That's the part I wanted.
`,
},
{
id: 2,
slug: "hello-world",
title: "Hello, world. This is the new blog.",
description: "A short note about why this site grew a fourth tab, how the posts are written, and what the rest of this page demonstrates.",
date: "2026-09-14",
tags: ["meta", "blog"],
details: `# Hello, world. This is the new blog.
This is a blog now. There's a fourth tab up top, and it works the same way the rest of the site works: content lives in the repository as plain data, the page renders it, and the markdown is processed by the same pipeline that renders the project detail pages.
<blockquote>
If a site is only ever going to have three tabs, it's a portfolio. The fourth tab is where it starts being a site.
</blockquote>
## Why it exists
The portfolio is for finished things, and the mods page is for the things I ship to other people's games. This is for the middle: write-ups, notes, and the occasional \"here's how I did that\" that doesn't belong anywhere else. It's also the first place on this site where I write in the first person at length, which is a change.
## How posts are written
Each post is a typed object in the content file, with a slug, a date, a short description for the card, and a markdown body. The body goes through the site's existing remark pipeline, so any markdown that works on the project pages works here, including raw HTML blocks when I want a specific layout.
- Headings render in the mono face, like the rest of the site
- Blockquotes get the raised, bordered treatment you can see above
- Images are kept out of this post on purpose. The pipeline supports them, and a future post will use one
## What this page is doing while you read it
This page is a dynamic route. The slug comes from the URL, the layout resolves it to rendered HTML on the server, and the page on the client side reads that and paints it in. That's the same architecture the project detail pages use, which is the point of reusing it: a new page type that can't do anything the old page types can't already do.
`,
}
];
export const MOD_GAMES: ModdedGame[] = [{
id: 1,
slug: "palworld",
title: "Palworld",
platform: "UE4SS / Dedicated server",
description: "Server-side tooling and quality-of-life changes for dedicated Palworld servers.",
mods: [{
id: 1,
title: "PalUI Extension",
description: "A collection of UI enhancements for Palworld, focused on manipulating the existing UI instead of adding complex new elements.",
link: "https://plz.gimme.pet/ferren/PalUIExtension",
linkLabel: "Git",
version: "0.0.3",
tags: ["Client", "Unreal Engine", "Palworld", "UE4SS", "C++", "UI", "Injection"],
}]
}
];
import { remark } from "remark";
import remarkParse from "remark-parse";
import rehypeRaw from "rehype-raw";
import rehypeStringify from "rehype-stringify";
import remarkRehype from "remark-rehype";
/**
* Shared remark -> rehype pipeline. Content is authored in-repo and reviewed
* via git, and allowDangerousHtml is intentional: post/project markdown may
* embed raw HTML blocks (see the blockquote-with-image idiom).
*/
async function renderMarkdown(source: string): Promise<string> {
const re = await remark()
.use(remarkParse)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeStringify)
.process(source);
return re?.toString() ?? "";
}
export async function getProjectDetails(slug: string) {
const p = PROJECTS.find((v)=>v.slug == slug);
if (!p || !p.details) return null;
return renderMarkdown(p.details);
}
export async function getBlogDetails(slug: string) {
const p = BLOG_POSTS.find((v) => v.slug == slug);
if (!p || !p.details) return null;
return renderMarkdown(p.details);
}