Custom Components
Native Webflow elements with Client-First classes applied.
This is some text inside of a div block.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique. Duis cursus, mi quis viverra ornare, eros dolor interdum nulla, ut commodo diam libero vitae erat. Aenean faucibus nibh et justo cursus id rutrum lorem imperdiet. Nunc ut sem vitae risus tristique posuere.
Media Cards
For use in posts and collections
Media Card Horizontal - Master Component
Lego Media Cards Master
App /Shortcut Media Card Master
Product Media Card Master
CMS Master: App Shortcut
CMS Master: Product Card
CMS Master: Lego
Old Card Style
Structure Classes
Defined and flexible core structure we can use on all or most pages.
Structure Classes
Defined and flexible core structure we can use on all or most pages.
HTML Heading Tags
HTML tags define default Heading styles.
Sample text helps you understand how real text may look. Sample text is being used as a placeholder.
Sample text helps you understand how real text may look on your website. Sample text is being used as a placeholder for real text that is normally present.
Sample text helps you understand how real text may look on your website. Sample text is being used as a placeholder for real text that is normally present.
Sample text is being used as a placeholder. Sample text helps you understand how real text may look. Sample text is being used as a placeholder for real text that is normally present.
Sample text is being used as a placeholder. Sample text helps you understand how real text may look. Sample text is being used as a placeholder for real text that is normally present. Sample text helps you understand how real text may look.
Sample text is being used as a placeholder for real text that is normally present. Sample text helps you understand how real text may look. Sample text is being used as a placeholder for real text that is normally present. Sample text helps you understand how real text may look.
Other HTML Tags
HTML tags define default text styles.
Sample text is being used as a placeholder for real text that is normally present. Sample text helps you understand how real text may look on your website. Sample text is being used as a placeholder for real text.
Sample text is being used as a placeholder for real text that is normally present. Sample text helps you understand how real text may look on your website.
- Sample text is being used as a placeholder for real text that is normally present.
- Sample text is being used as a placeholder for real text that is normally present.
- Sample text is being used as a placeholder for real text that is normally present.
- Sample text is being used as a placeholder for real text that is normally present.
- Sample text is being used as a placeholder for real text that is normally present.
- Sample text is being used as a placeholder for real text that is normally present.
Heading Styles
Heading classes when typography style doesn't match the default HTML tag.
Sample text helps you understand how real text may look.
Sample text is being used as a placeholder. Sample text helps you understand how real text may look.
Sample text helps you understand how real text may look on your website. Sample text is being used as a placeholder for real text that is normally present.
Sample text is being used as a placeholder. Sample text helps you understand how real text may look. Sample text is being used as a placeholder for real text that is normally present.
Sample text is being used as a placeholder. Sample text helps you understand how real text may look. Sample text is being used as a placeholder for real text that is normally present. Sample text helps you understand how real text may look.
Sample text is being used as a placeholder for real text that is normally present. Sample text helps you understand how real text may look. Sample text is being used as a placeholder for real text that is normally present. Sample text helps you understand how real text may look.
Text Classes
Text classes when typography style doesn't match the default HTML tag.
Text Sizes
Sample text is being used as a placeholder for real text that is normally present.
Sample text is being used as a placeholder for real text that is normally present on your website.
Sample text is being used as a placeholder for real text that is normally present. Sample text helps you understand how real text may look on your website.
Sample text is being used as a placeholder for real text that is normally present. Sample text helps you understand how real text may look on your website.
Sample text is being used as a placeholder for real text that is normally present on your website. Sample text helps you understand how real text may look on your website.
Sample text is being used as a placeholder for real text that is normally present on your website. Sample text helps you understand how real text may look on your website.
Text Styles
text-style-strikethrough
text-style-italic
text-style-muted
text-style-allcaps
text-style-nowrap
text-style-link
Sample text is being used as a placeholder.
Sample text is being used as a placeholder for real text that is normally present. Sample text helps you understand how real text may look on your website. Sample text is being used as a placeholder for real text text-style-2lines
Sample text is being used as a placeholder for real text that is normally present. Sample text helps you understand how real text may look on your website. Sample text is being used as a placeholder for real text text-style-2lines
Sample text is being used as a placeholder for real text that is normally present. Sample text helps you understand how real text may look on your website. Sample text is being used as a placeholder for real text. Sample text is being used as a placeholder for real text that is normally present. Sample text helps you understand how real text may look on your website. Sample text is being used as a placeholder for real text.
Text Weights
Text Alignments
Buttons
Button combo class system.
Colors
Manage recurring text and background colors.
Color Palette
Text Colors
Background Colors
Max widths
Use the max-width CSS property to contain inner content to a maximum width.
Border Radius
Border Radius
Paddings
Utility spacing system - padding classes. [padding-direction] + [padding-size].
Direction Classes
Size Classes
Margins
Utility spacing system - padding classes. [margin-direction] + [margin-size].
Direction Classes
Size Classes
Icons
Unify icons sizes. icon-height sets height of icons. icon-1x1 sets both height and width of icons.
Useful utility systems
Utility classes we like to use in most of our projects to build faster.
Webflow elements
Native Webflow elements with Client-First classes applied.
Example of a form component using Folders
Heading 1
Heading 2
Heading 3
Heading 4
Heading 5
Heading 6
Sample text with a link is being used as a placeholder for real text that is normally present. Sample text helps you understand how real text may look on your website. Sample text is being used as a placeholder for real text. Sample text is being used as a placeholder for real text. Sample text is being used as a placeholder for real text.
Ideas and goals
Most of the time, I'm just hacking on my site for fun and not-profit. But while building this latest version, I did manage to have a few key ideas and goals in mind:
Reduce friction for iteration and writing
Last year I wrote about why you're not updating your personal website. One of the key reasons I mentioned is that it's too hard to add and edit content for most people who roll their own codebase. If you have to open a pull request every time you want to write a note or make a small edit, you've lost.
So this new version of my website was built with the idea that I should be able to add, edit, and delete content directly from the front-end. This means that everything needs to be backed by a database or CMS, which quickly adds complexity. But at the end of the day, adding a bookmark should be a matter of pasting a URL and clicking save. Writing a blog post should be a matter of typing some markdown and clicking publish.
Extra friction on these processes would make me less likely to keep things up to date or share new things.
A playground for ideas
I've been collecting a list of frameworks and tools to try someday on Github, ranging from front-end JavaScript frameworks to small utilities for writing better CSS. I want my website to be a playground where it's safe to try new technologies and packages in a way that can be easily isolated and easily deleted. This requirement made using Next.js an easy decision because of way it supports hybrid page rendering strategies — static, server-rendered, or client-rendered. More on this below.
Fast
As noted by The Zen of GitHub, "it's not fully shipped until it's fast." While my site isn't the fastest website out there, it's still pretty fast. It turns out it's really hard to make a fast website in 2021, and between caching, preloading, and async computation, I think I've managed to make things feel snappy enough.
This is particularly important because of the layout I chose: a list-detail multi-column layout affords clicking between lots of different links in a single session, and if those links have just the slightest delay, the whole site can feel sluggish.
Social
One of my goals for this iteration was to make my website feel more social. Specifically, I thought it'd be fun to roll my own comment system so that people could share ideas, leave feedback, or point out any mistakes in my work. In the past, I had an email form at the bottom of each blog post that would send a private email, but making this social and public is more fun and adds real value to future readers.
Ideas and goals
Social
One of my goals for this iteration was to make my website feel more social. Specifically, I thought it'd be fun to roll my own comment system so that people could share ideas, leave feedback, or point out any mistakes in my work. In the past, I had an email form at the bottom of each blog post that would send a private email, but making this social and public is more fun and adds real value to future readers.
I wanted this version of my site to feel like a web application, mostly because it sounded fun! What would it look like to have a "personal application" that houses all of the things I've made? How would navigation work when I have lots of loosely related projects and content? How could I stretch beyond the single-column personal site design in a way that adds utility and creates creative design and programming problems to learn from?
- Automerge — tells Dependabot updates to automatically merge package updates as long as all of my tests are passing.
- CodeQL Analysis — uses GitHub CodeQL to detect vulnerabilities in my code.
- GraphCDN — keeps my schema in sync with GraphCDN.
- Hacker News Daily Digest — a cron job that triggers one of my API routes to send a daily digest of the top HN posts.
- Compress images — looks for any images committed to my source files, then compresses and commits the smaller files.
- End-to-end tests — this is the big Action that triggers tests on all new PRs, ensuring that Vercel is able to deploy my code and that I haven't broken core pages on the site.
- Dependabot — keeps my packages up to date, ensuring that bug fixes in my dependencies are patched and merged automatically.
The result is obviously inspired by macOS and iPadOS, with a global sidebar navigation and a fully responsive multi-column layout. This design makes it easy to jump from anywhere to anywhere, and I love the way the the list-detail layout makes it easier to quickly browse through lots of related content without needing constant back-and-forward navigation.
However, this layout is a pain in the ass to build, for a few reasons:
- Multi-column layouts need to persist many scroll positions as users move between pages.
- Each column needs to maintain its own scroll area, which means having to override the body scrolling behavior. This makes mobile views a huge pain, especially on iOS where you can't rely on having a fixed viewport height. It also breaks helpful shortcuts like tapping on the status bar (on iOS) to quickly jump back to the top of the page. TODO: Figure this out.
- My content is a mix of static (e.g. it lives alongside the code, like the Security Checklist page or the App Dissections), third-party (e.g. Hacker News, and first-party (e.g. anything I store in my own database, like my Bookmarks or Stack). Each of these views fetches data in different ways, wants a slightly different layout, and should present a different set of controls. This meant having to think long and hard about component abstractions for things like title bars and action bars.
Next.js
Next.js is my front-end React framework of choice. I particularly enjoy using it because of the file-system based router — it's an intuitive and powerful abstraction for constructing route hierarchy. Next.js also has a huge community of people who have really put the framework through the grinder, sanding down the edge cases and coming up with creative solutions to common problems with React, data fetching, and performance. Whenever I'm stuck, I head to the Next.js Discussions on GitHub and start searching — almost every time there are others before me who have found creative solutions to hard problems.
Next.js is also fast. They do so much optimization for me, either by making my local builds faster, automatically compressing static assets, or making my deployment times blazing fast. The project's regular cadence of updates means my site gets faster over time — for free!
Persisting layouts
One of the absolute hardest problems to solve with this new design was figuring out how to persist each column's scroll position while navigating between pages. For example, if you're viewing the bookmarks list view, and you scroll down, then click an individual bookmark, the list of bookmarks should stay in the same position despite loading an entirely new route and changing the URL.
Another challenge was having the multi-column layout behave correctly on mobile. For example, if someone is viewing the site on a phone, the /bookmarks route should only show a list of bookmarks, and /bookmarks/[id] should only show the bookmark detail. However, on a larger viewport, /bookmarks should show the list of bookmarks with an empty third column placeholder, and /bookmarks/[id] should show both the list of bookmarks and the bookmark detail.
Okay, so how to solve these two problems...
For the scroll persistence, I wish I could take more credit here for figuring out some beautiful abstraction, but I actually picked up most of the hacks from Adam Wathan's blog post, Persistent Layout Patterns in Next.js.
Here's roughly how it works:
- Every page has a static method called getLayout that does some hackery to wrap itself in some global service providers (like the Apollo cache, or Fathom's page view tracking). Let's look at the getLayout method for rendering a bookmark detail:
Then my global _app.tsx file either executes that getLayout method, or falls back to wrapping the page with providers and the site layout:
Okay, the really weird thing about the code above that broke my brain was how we pass the entire page as a component down to the detail view with getLayout(<Component {...pageProps} />).
This means that on the /bookmarks/[id] page, the primary export is just this:
Note that this page doesn't render any of the providers, or care about where the component fits into the global multi-column layout — all of that context has been lifted up to the App component. I only care about the detail view itself, the third column, and so that's the only thing this page has to worry about rendering.
Frankly, I don't know exactly how to describe how all of this works without writing a full-on tutorial breaking down the code line by line (which I should probably do anyways, in a separate post!), but I show this code just to point out how weird you have to get with some of the page layout abstractions to "trick" Next.js into persisting a global layout with variable scroll positions across page changes.
As far as the solution for responsive columns: plain ol' CSS! It's all here and here if you want to poke around.
Styling
Tailwind
Tailwind is my favorite CSS-authoring tool...ever? It's really, really good. I wrote about my first impressions last year, and the library has held strong in real-world use. I regularly see threads on Twitter and HN with people arguing about why Tailwind is the best thing ever, or the worst thing ever, and I don't want to wade into that flame war here. I'll say this:
Tailwind is a toolkit that makes everything pretty damn good by default. The magic is in the token system, and the sensible defaults that the team built into the framework. Once I got the hang of Tailwind's semantics, I was really able to start styling my markup at the speed of thought.
Tailwind mostly comes with everything I could need out of the box, but I've slowly added a tiny bit of custom CSS to make things feel a bit more unique. TODO: Inline more of these custom styles using Tailwind's just-in-time mode.
Feel free to poke around here to see what's custom.
Markdown
I've given up on rich text editors on the web. They're so damn complicated, and every tool has their own bespoke abstractions for blocks and elements. We spent hours upon hours trying to solve weird edge cases with rich text while building Spectrum, and I never want to deal with that again.
MDX was a logical tool to reach for next — it cleverly allows for a Markdown-flavored authoring experience, but with the flexibility to embed interactive code, like React components. People like Josh Comeau have made great use of this to build interactive blog posts and engaging tutorials. But again, it just requires so much boilerplate to stand up, and once you start putting React into your Markdown, you're locked in until the next big refactor.
So in the spirit of trying to avoid having to do another huge rewrite in the next couple of years, I looked for the simplest, dumbest solution possible to publishing content on my site: plain text.
The post you're reading now is plain text stored in MySQL, rendered with react-markdown. You can see my custom element renderers here. I can enhance my Markdown using plugins like remark-gfm to add support for tables, strikethroughs, footnotes, and more.
Tradeoffs abound!
Headless UI
The last thing to call out for the front-end is Headless UI, a set of utilities created by the Tailwind crew to provide accessible components like dialogs, menus, switches, and popovers. I'm using it for my dialog component, and found it has the best balance of good-enough defaults with just the right API surface area for customization.
A great alternative to Headless UI is Radix UI.