惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

腾讯CDC
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog
S
SegmentFault 最新的问题
WordPress大学
WordPress大学
P
Proofpoint News Feed
Hugging Face - Blog
Hugging Face - Blog
MyScale Blog
MyScale Blog
A
About on SuperTechFans
雷峰网
雷峰网
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
The Blog of Author Tim Ferriss
MongoDB | Blog
MongoDB | Blog
博客园 - 【当耐特】
The Cloudflare Blog
F
Fortinet All Blogs
小众软件
小众软件
博客园 - 三生石上(FineUI控件)
宝玉的分享
宝玉的分享
罗磊的独立博客
量子位
有赞技术团队
有赞技术团队
V
V2EX
Engineering at Meta
Engineering at Meta

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Building a Swedish Sudoku Site with Next.js 15, MDX & a P...
Evy Lundell · 2026-05-18 · via DEV Community

I recently launched sudokun.se — a free Swedish sudoku site with no sign-up required and no ads inside the puzzle. Here's a walkthrough of the interesting technical decisions behind it.

The stack at a glance

  • Next.js 15 App Router with React Server Components + selective client hydration
  • TypeScript in strict mode throughout
  • Tailwind CSS for styling
  • MDX for all content (guides, blog posts, landing pages)
  • schema-dts for typed JSON-LD schema builders
  • Vitest for the puzzle engine unit tests

The puzzle engine — pure TypeScript

The core of the site is a fully self-contained sudoku engine in lib/sudoku/. No external dependencies.

Deterministic puzzle generation via seeded PRNG

I wanted puzzles to be reproducible from a seed string (useful for daily puzzles and sharing). The engine uses a mulberry32 PRNG seeded with a FNV-1a hash of the seed string:

function hashSeed(seed: string): number {
  let h = 2166136261 >>> 0;
  for (let i = 0; i < seed.length; i++) {
    h ^= seed.charCodeAt(i);
    h = Math.imul(h, 16777619) >>> 0;
  }
  return h >>> 0;
}

function mulberry32(a: number): () => number {
  return function () {
    a |= 0;
    a = (a + 0x6d2b79f5) | 0;
    let t = a;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

Enter fullscreen mode Exit fullscreen mode

A solved board is generated using randomized backtracking with the seeded PRNG, then clues are removed down to the target count per difficulty level — while always verifying the puzzle has a unique solution.

Difficulty levels

Level Clues given
Barn 50–55
Lätt 38–45
Medel 28–35
Svår 22–27
Extrem 17–21

The solver is a pure constraint-propagation + backtracking solver. Unique-solution verification is done by running the solver and checking it finds exactly one result.


Selective client hydration

The game board is a 'use client' component, but the surrounding page shell — layout, header, footer, FAQ, quick-links — is fully server-rendered. This means:

  • First paint is fast — HTML arrives with all content already rendered
  • JS bundle only includes what the interactive board actually needs
  • The difficulty selector is the only stateful piece on the homepage

Switching difficulty remounts the board via React's key prop, which is simpler and more reliable than trying to imperatively reset game state:

<GameFullscreenWrapper key={difficulty} difficulty={difficulty} />

Enter fullscreen mode Exit fullscreen mode


MDX content pipeline

All guides, blog posts, and landing pages live as .mdx files under content/. Frontmatter is the source of truth for metadata and structured data.

---
title: "Hur spelar man sudoku?"
description: "Lär dig sudokuets grundregler på fem minuter."
publishedAt: "2025-03-01"
faq:
  - question: "Kan samma siffra finnas två gånger i en rad?"
    answer: "Nej, varje siffra 1–9 får bara förekomma en gång per rad, kolumn och 3×3-ruta."
howTo:
  steps:
    - text: "Identifiera tomma rutor"
    - text: "Använd eliminering för att hitta rätt siffra"
---

Enter fullscreen mode Exit fullscreen mode

The MDX renderer reads the frontmatter and automatically generates FAQPage, HowTo, and Article JSON-LD without any manual schema work on the content author's part.


Typed JSON-LD schema builders

Rather than writing raw JSON objects, I used schema-dts to get TypeScript type-checking on all structured data:

import type { FAQPage, WithContext } from 'schema-dts';

export function buildFAQ(
  items: { question: string; answer: string }[],
): WithContext<FAQPage> {
  return {
    '@context': 'https://schema.org',
    '@type': 'FAQPage',
    mainEntity: items.map((item) => ({
      '@type': 'Question',
      name: item.question,
      acceptedAnswer: { '@type': 'Answer', text: item.answer },
    })),
  };
}

Enter fullscreen mode Exit fullscreen mode

If I accidentally pass the wrong shape — TypeScript catches it at compile time. No more silent JSON-LD bugs that only surface in Google Search Console weeks later.


Security headers out of the box

Next.js makes it trivial to add security headers globally via next.config.mjs:

async headers() {
  return [
    {
      source: '/:path*',
      headers: [
        { key: 'X-Content-Type-Options',  value: 'nosniff' },
        { key: 'Referrer-Policy',         value: 'strict-origin-when-cross-origin' },
        { key: 'X-Frame-Options',         value: 'SAMEORIGIN' },
        { key: 'Permissions-Policy',      value: 'camera=(), microphone=(), geolocation=()' },
      ],
    },
  ];
}

Enter fullscreen mode Exit fullscreen mode


What I'd do differently

  1. Puzzle caching — currently every page load generates a fresh puzzle. A small in-memory cache of pre-generated puzzles per difficulty would eliminate the ~20ms generation cost on the first load.
  2. Daily puzzle with a stable URL — using new Date().toISOString().slice(0, 10) as the seed makes this trivially easy to add.
  3. Web Worker for generation — the engine is synchronous. On low-end devices, a 17-clue extreme puzzle can block the main thread for 30–80ms. Moving generation to a Web Worker would fix this.

Try it

The site is live at sudokun.se — free, no account, works on mobile and desktop.

If you're building something similar — puzzle sites, game sites, MDX-heavy content sites — happy to go deeper on any part of this in the comments.