Every few years, a website starts feeling like a coat that no longer fits. The seams are too tight in some places, baggy in others, and there’s always that one pocket you stopped using because it got too complicated. That’s where we were with the Tinkink portal.
We created this portal back in 2021 as a simple static HTML site. It was built in a terminal aesthetic way. This was a fun design choice at the time, and it served us well for a while. But as we added more content, more pages (like the blog pages), the page becomes increasingly difficult to expand.
So we rebuilt it — not as a patch job, but as a clean-room rewrite. And along the way, we made some deliberate choices about architecture that are worth talking about.
The Terminal Aesthetic
The visual design of the new site is deliberately terminal-flavored: monospace fonts, a dark background, cyan accents, scanline-style borders. The header reads tinkink > ~/current-path _, and the footer ends with EOF.
These stylistic choices keep the spirit of the original while giving us a consistent design language to work with. The terminal aesthetic also pairs well with the content.
A Note on Navigation
Since the site is a SPA after hydration, client-side navigation between pages is instant — but it’s not silent. When you click a link, lazily-loaded Vue chunks may need to fetch, or a component might be doing async work. Without any feedback, the UI feels frozen.
We added a slim loading bar at the top of the viewport that appears during in-flight navigations. It’s a small touch, but it makes the difference between a site that feels fast and one that just is fast.
Going Static with Vite-SSG
Let’s talk back to the architecture. We still wanted the site to be static — no server runtime, no API layer, just files that can be served from a CDN.
In the previous version, we had a custom build script that read templates and meta data, then generated static HTML files. It worked, but it was a bit of a black box — the logic was buried in a custom Node script that wasn’t really part of the development workflow.
vite-ssg (Vite Static Site Generation) slots into a Vite + Vue project almost transparently. You swap out the usual createApp call for ViteSSG, hand it a route config, and at build time the tool crawls your routes and pre-renders them to static HTML files. The JavaScript still ships and hydrates the page on load, so Vue’s reactivity kicks in after the initial paint — you get the best of both worlds.
The output lands in a public/ directory ready to be dropped onto a CDN or a static host like Netlify or Cloudflare Pages.
File-Based Routing: Convention Over Configuration
The routing setup is where things get interesting. Instead of writing a routes array by hand, we drop .vue files into a src/pages/ directory and the router figures out the structure automatically.
src/pages/
├── index.vue → /
├── blog/
├── index.vue → /blog
└── [slug].vue → /blog/:slug
Dynamic segments like [slug] map to named route parameters. Deeply nested paths just become nested folders. There’s no manifest, no central route registry to keep in sync with the file tree — the folder structure is the route map.
The plugin also generates a typed-router.d.ts file at build time that contains TypeScript types for every route in the app. If you try to navigate to a path that doesn’t exist, the compiler tells you before the browser ever does.
Markdown-Based Blog Content
For blog content, we love markdown, just like many developers do. It’s simple, human-readable, and has a huge ecosystem of tools.
We created a custom Vite plugin previously, it reads Markdown files and generated HTML at build time, which was a bit of a hack.
This time, we went with unplugin-vue-markdown, which compiles Markdown files directly into Vue components. Each blog post is a .md file in src/pages/blog/, and the plugin handles the rest — parsing the frontmatter, converting the body to render functions, and making it available as a Vue component.
Comparing with Nuxt.js
At this point you might be asking: this all sounds like Nuxt — why not just use Nuxt?
It’s a fair question, because the surface area really is similar. Nuxt is the dominant framework for Vue meta-apps, and it covers most of the above features out of the box. Let’s be honest about where the overlap lies.
File-Based Routing
Nuxt’s pages/ directory and our src/pages/ directory work almost identically. Dynamic segments are [param].vue in both. Nested folders become nested routes in both.
Static Output
Nuxt’s nuxt generate command does exactly what vite-ssg build does: pre-render every route to a static HTML file, ship a JS bundle for hydration. The dist/ output from nuxt generate and the public/ output from our Vite-SSG build look structurally identical. Per-page .html files, a _nuxt/ or assets/ folder with hashed chunks, no server runtime needed.
Layout System
Nuxt has layouts/default.vue and opt-in override via definePageMeta({ layout: 'custom' }). We also have layouts/default.vue and opt-in override. The concept is the same;.
Where They Diverge
The real difference is scope. Nuxt is a full framework with opinions about server middleware, API routes, modules, Nitro’s deployment adapters, and more. For a site this size, that’s a lot of machinery to configure and maintain when none of it gets used.
Nuxt do provides a nuxt generate command that outputs a static site, but the framework itself is designed to support much more than that.
The difference of the goals made the two codebases look very different under the hood. Nuxt’s configuration is sprawling, with dozens of possible keys in nuxt.config.ts that affect everything from build output to runtime behavior.
Our setup is the opposite: a minimal Vite config with a small set of precisely chosen plugins. The entire plugin configuration fits in a single file. There are no Nuxt modules to audit, no nuxt.config.ts with dozens of possible keys, no Nitro server to think about.
Wrapping Up
The rebuilt Tinkink portal is leaner, faster to build, and easier to understand than what it replaced. The combination of Vite-SSG, file-based routing, and a markdown content system gives us most of what a full framework like Nuxt offers, without the overhead of features we’ll never use.