DEV Community

tani
tani

Posted on

How I Built a Free Online Time Toolkit with Next.js 16 (PWA, i18n, Web Audio)

A story about building Clock-Tani — 9 free time-management tools in a single PWA, written with Next.js 16 App Router. Live at https://clock-tani.com

Why I Built This

I needed a pomodoro timer at work, but every option was either ad-heavy, app-only, or just ugly. Same story for world clocks, countdown timers, and interval timers. So I built them all into one place.

That place is now Clock-Tani — 9 small but useful tools, no ads in the core experience, no signup required.

What's Inside

  • World Clock — 70+ cities, drag to reorder
  • Timer / Pomodoro / Interval (Tabata, HIIT) / Multi-Timer
  • Stopwatch with lap times
  • Alarm Clock with 15 sounds
  • Server Time (NTP) — for ticketing precision
  • D-Day Counter — supports the Korean lunar calendar

Tech Stack

Next.js 16 (App Router) · React 19 · TypeScript · Tailwind CSS 4 · next-intl (KR/EN) · PWA (Service Worker) · @dnd-kit · korean-lunar-calendar

Five Implementation Highlights

1. App Router i18n with next-intl

I used the /[locale]/[tool] structure with localePrefix: 'always' so even /clock redirects to /ko/clock. This kept canonical URLs clean.

// src/middleware.ts
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
  locales: ['ko', 'en'],
  defaultLocale: 'ko',
  localePrefix: 'always',
});
Enter fullscreen mode Exit fullscreen mode

2. Making it Installable as a PWA

Just public/manifest.json + public/sw.js. Next.js doesn't need extra config. The service worker uses stale-while-revalidate so tools work offline.

<link rel="manifest" href="/manifest.json" />
Enter fullscreen mode Exit fullscreen mode

3. Web Audio API for Reliable Alarm Sounds

The classic <audio> tag often gets blocked by mobile autoplay policies. After the user has clicked once, Web Audio API plays consistently:

const audioCtx = new AudioContext();
async function playSound(url: string) {
  const res = await fetch(url);
  const buffer = await audioCtx.decodeAudioData(await res.arrayBuffer());
  const source = audioCtx.createBufferSource();
  source.buffer = buffer;
  source.connect(audioCtx.destination);
  source.start();
}
Enter fullscreen mode Exit fullscreen mode

4. Wake Lock API to Keep the Screen On

If the screen sleeps mid-timer, alarms get delayed. Wake Lock fixes it:

const wakeLock = await navigator.wakeLock.request('screen');
// release when timer ends
Enter fullscreen mode Exit fullscreen mode

iOS Safari has supported it since 16.4, so this works almost everywhere now.

5. Auto-Generated OG Images for SEO

Each tool page has its own opengraph-image.tsx. Next.js generates the OG image at build time, which I reuse for Pinterest and social cards.

Lessons Learned

  • There's a real demand for ad-free utility tools — organic traffic grows steadily.
  • Almost no one installs the PWA. Most people just bookmark, so I now nudge "save to bookmarks" instead of "install app".
  • i18n is much easier when designed up front than retrofitted.

Closing

It's funny — building time-management tools doesn't actually make me less distracted. But it has made me more thoughtful about how a small piece of software can quietly support someone's day.

Try it: clock-tani.com/en
Pomodoro: clock-tani.com/en/pomodoro

Originally posted on Medium and velog. Feedback welcome — especially from fellow indie makers building small useful things.

Top comments (0)