About a month ago, I launched UtilYard, a free collection of developer tools, text utilities, finance calculators, and image tools all in one place:
I had a simple goal for the site. Create a clean, fast site where people could find useful tools without bouncing between a dozen different websites, release it, and basically just see where it goes. While I do have more ideas for the site, I mostly have a "let's see where this goes" mentality towards the site
Building the tools was not the hard part. The hard part was making the site easy to scale with ever expanding tools. I did not want to have to rewrite the pages for the every tool, so I developed a routing for each tool into a standard layout. Every tool lives under /tools/[slug], and a single page.tsx handles all of them.
The foundation is a central routes.json file that describes each tool:
{
"path": "/tools/base64",
"name": "Base64 Encoder / Decoder",
"category": "developer",
"tags": ["base64", "encode", "decode"],
"faqNamespace": "Base64Tool"
}
During the build process, generateStaticParams() reads that registry and generates a static page for every tool. No request-time rendering, just static HTML that's fast to load and easy for search engines to crawl.
export async function generateStaticParams() {
return toolRoutes.flatMap(t =>
routing.locales.map(locale => ({
locale,
slug: t.path.replace('/tools/', '')
}))
)
}
Rendering the tools
Each tool is its own React component, and a simple registry maps slugs to components:
const registry: Record<string, ComponentType> = {
'base64': Base64Tool,
'lorem-ipsum-generator': LoremIpsumGenerator,
// ...
}
Adding a new tool is simple. You build the component, add it to the register, then add the entry to the routes.json. Everything needed for the tool) metadata, breadcrumbs, related tools, structured data) gets generated automatically.
The FAQ schema is generated from translation files. If a tool has FAQ entries in its translation namespace, they're automatically converted into JSON-LD.
for (let i = 1; i <= 8; i++) {
try {
const q = tf(`q${i}` as any)
const a = tf(`a${i}` as any)
if (q && a) faqs.push({ q, a })
} catch {
break
}
}
Early results for the site has been the following. Its been indexing for about three weeks now, and search impressions have been growing week over week. It's still early, but it's encouraging to see organic traffic starting to pick up. Still looking to expand the site further. It be frank, it's been fun building this site so I want to develop it even more and see where it goes.
Happy to answer questions about the architecture, SEO setup, or anything else related to the build.
Top comments (0)