Skip to content

About

Personal website and writing — omercelik.dev

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

omercelik.dev

Personal site of Ömer Çelik — software architect. Case studies, runnable demos and essays on AI-era enterprise .NET systems you can prove.

Built with Next.js 16 (App Router, static export), React 19, Tailwind CSS v4, next-intl and MDX.

Develop

npm install
cp .env.example .env.local   # optional: add a GITHUB_TOKEN
npm run dev                  # http://localhost:3000 (runs `gen` first)
npm run build                # static site in ./out
npm run check                # lint + typecheck + format check + tests (as CI)
npm run format               # apply Prettier

Quality gates

  • CI — .github/workflows/ci.yml, on every push to main and every pull request: lint, typecheck, format check, tests, then the static build. The build fails if any product card couldn't be fetched, and the job checks the key files in out/ exist.
  • Dependabot — .github/dependabot.yml, weekly. Minor and patch updates arrive grouped (Next.js and its ESLint config together); a major version arrives as its own pull request. GitHub Actions are grouped.
  • Tests — Vitest, src/**/*.test.ts: heading ids match rehype-slug, tag slugs, the SEO helpers, and the UI messages (no empty strings or lists).
  • Tooling — Node 24 (.nvmrc), Prettier defaults, .editorconfig. Essays under content/ are left out of formatting on purpose.

How it's put together

  • Static export — output: "export": every page is plain HTML generated at build time. There is no server and no proxy/middleware.
  • Language — the interface is English, with no locale prefix in URLs. UI copy lives in src/i18n/messages/en.json (next-intl, for ICU plurals and rich text). An article can be written in Turkish: its lang frontmatter sets the lang attribute on its text and the locale of its social card.
  • Theme — follows the operating system until the visitor picks light or dark with the toggle.
  • Design tokens — colours, radius and elevation live in src/app/theme.css; the type scale (text-meta … text-display) in src/app/globals.css. Use the scale rather than arbitrary sizes.
  • Motion — CSS first: .intro for page-load entrances, .reveal for scroll-linked ones (animation-timeline: view()). Content is visible without JavaScript, and prefers-reduced-motion switches it all off.
  • Signature diagram — src/components/diagram/: an isometric stack of architecture layers in CSS 3D (no WebGL), calm by design — nothing moves on its own. Pointing at a layer opens the stack there (the layers above lift and draw back, the focused one comes forward and lights up, its description shows below); the legend does the same on hover or keyboard focus and pins a layer on click; on touch, tapping a layer pins it. Invisible hit surfaces at the layers' resting positions decide which layer is under the pointer, so the focus doesn't jump while the stack opens. It opens a little as the page scrolls and never tilts, so the layers stay where the pointer expects them. Without JavaScript or with reduced motion it's a still diagram.
  • SEO — src/lib/seo.ts: every page gets its title, description, canonical, Open Graph and X card from pageMetadata / articleMetadata (Next merges metadata shallowly, so pages set the whole block). Structured data: Person + WebSite everywhere, ProfilePage on About, BlogPosting + BreadcrumbList on articles. Social cards are real .png routes (src/app/og.png, src/app/og/[slug]/card.png) so static hosts serve them as images. Tag pages are noindex, follow.
  • Type — Geist for the interface, Newsreader (serif) for essay titles and pull quotes.
  • Content is folders — content/writings/<slug>.mdx (essays), content/work/<slug>.mdx (case studies) and content/labs/<slug>/ (demos). Adding a file or folder is all it takes; lists, pages, the sitemap and the feed pick it up. Demos are code, so npm run gen (run before dev, build and check) writes the import table src/generated/labs.ts; it's committed, and a test fails when it's stale.
  • Feature flags — src/config/features.ts. qorpe shows the Qorpe band and product cards on Work and the home page; off until licensing and qorpe.com are settled. Product cards read from the GitHub API at build time (src/config/products.ts); set GITHUB_TOKEN wherever the build runs.
  • Profile facts — src/config/profile.ts: the proof tiles under the thesis, experience (one paragraph per role, never bullets) and the toolbox. Entries marked todo render in dev and are dropped from the production build.
  • Motion contract — one page-load entrance (.intro, staggered), one block-level scroll reveal per section (.reveal), hover changes colour only, numbers count up once (Counter), and the Flow diagram's signal is the only thing that travels. Reduced motion switches all of it off.
  • Contact — the form posts to /api/contact, the one piece of the site that isn't a static file: a small Worker script (worker/) that checks the request, verifies a Cloudflare Turnstile token (spam protection, invisible unless Cloudflare wants a click) and emails the message through Resend, with the visitor's address as Reply-To. A honeypot field drops simple bots. Until a Turnstile site key is set in src/config/site.ts, the page shows a plain email link instead of the form. Setup: see Contact form below.

Writing

Create content/writings/<slug>.mdx:

---
title: "My post"
description: "One-line summary."
date: "2026-09-11"
lang: "en" # en | tr — the language the piece is written in
tags: ["architecture"]
draft: true # optional: visible in `npm run dev` only
series: "Golden paths" # optional, with seriesOrder: 1
---

Your MDX content…

What an article can use:

  • Code — fenced blocks are highlighted with Shiki (light + dark). Add a file name and highlighted lines with ```ts title="lib/x.ts" {2-3}, and line numbers with showLineNumbers. Every block gets a copy button.

  • Callouts — <Callout type="note | tip | warning" title="…">…</Callout>.

  • Figures — <Figure src="/writings/diagram.png" alt="…" caption="…" />, with the image under public/writings/.

  • Pull quotes — <PullQuote cite="…">The line you want remembered.</PullQuote>, set large in the serif.

  • Layer diagrams — the hero's 3D stack, for your own systems:

    <LayerStack title="Where each guarantee lives">
      <Layer label="Spec" detail="manifest.yaml" note="What must be true." />
      <Layer label="Code" detail="generated" />
    </LayerStack>

    String props only — the MDX pipeline doesn't evaluate {…} expressions.

  • Featured — featured: true in the frontmatter leads the home page with that essay.

  • GFM — tables, task lists and footnotes ([^1]).

  • Table of contents — appears automatically from three h2/h3 headings up.

  • Diagrams — <Flow steps="Manifest: intent | Contracts: OpenAPI | Gate: drift checks" notes="… | … | …" title="…" /> animates a signal through the stages (plays once in view; Play / Step / Reset). <Compare before="a | b" after="a | b" />, <Matrix x="…|…" y="…|…" cells="a|b|c|d" highlight="4" /> and <Timeline items="2024: Title: detail | …" /> are still. Items are separated by |; string props only.

  • Demos — <Demo id="spec-drift" /> embeds a lab, closed until opened (its JavaScript loads then). A lab is content/labs/<slug>/lab.mdx (title, summary, date, topic, the essay it belongs to) next to Demo.tsx, a client component on mock data. It also gets its own page at /labs/<slug>.

  • Case studies — content/work/<slug>.mdx with sector, role, period, stack, outcomes (value + label, countTo animates) and labs (slugs to show under "Try it"). Domains, never client names.

  • Comments — Giscus (GitHub Discussions), one thread per post.

Configure

What Where
Name, email, social links, comments src/config/site.ts
Contact form recipient and sender wrangler.jsonc (vars)
Feature flags (Qorpe band) src/config/features.ts
Proof tiles, experience, toolbox src/config/profile.ts
Which repos show as products src/config/products.ts
Colours, radius, shadows src/app/theme.css
Type scale, motion, code styling src/app/globals.css
UI copy src/i18n/messages/en.json

Deploy — Cloudflare Workers Builds

Same setup as the *.qorpe.com sites: wrangler.jsonc serves ./out as Worker static assets, with 404.html for unknown paths.

  1. Connect the repository in Cloudflare Workers Builds. Build command: npm run build; deploy command: npx wrangler deploy.
  2. Add a GITHUB_TOKEN build variable — a token with no permissions is enough to read public repositories.
  3. Attach the omercelik.dev custom domain to the Worker.

Contact form — Resend and Turnstile

Both are free at this scale (Resend: 3,000 emails a month, 100 a day; Turnstile: unlimited). Mail for omercelik.dev stays on Google Workspace: Resend only sends, from the mail.omercelik.dev subdomain, so the root MX records are never touched.

  1. Resend — at resend.com, add the domain mail.omercelik.dev and add the DNS records it lists in Cloudflare DNS (SPF and DKIM TXT records and an MX record, all under mail.omercelik.dev). Wait for Verified, then create an API key with Sending access for that domain only.
  2. Turnstile — in the Cloudflare dashboard, Turnstile → Add widget: hostname omercelik.dev, mode Managed. Copy the site key into turnstileSiteKey in src/config/site.ts (it's public) and keep the secret key for the next step.
  3. Secrets — in the Worker's Settings → Variables and Secrets, add RESEND_API_KEY and TURNSTILE_SECRET_KEY as Secret (or run npx wrangler secret put RESEND_API_KEY). They never go in the repo.
  4. Deploy, send yourself a message from /contact, and check Resend's Emails log if it doesn't arrive.

Without the secrets the endpoint answers 503 and the form points visitors to the email address, so a half-finished setup never loses a message silently.

Try it locally — copy .env.example to .env.local and .dev.vars.example to .dev.vars (Cloudflare's test keys, and a dry run that logs the email instead of sending it), then:

npm run preview   # static build + wrangler dev → http://localhost:8787

npm run dev serves the pages but not the Worker, so the form can't send there. After changing wrangler.jsonc, run npm run cf-typegen.

After the first deploy — search engines

The site ships everything search engines read (sitemap, canonicals, structured data); these steps tell them it exists:

  1. Google Search Console — add a Domain property for omercelik.dev, verify with the DNS TXT record (in Cloudflare DNS), then submit https://omercelik.dev/sitemap.xml. Use URL inspection → Request indexing for the home page and each new essay.
  2. Bing Webmaster Tools — import the site from Search Console (Bing also feeds DuckDuckGo and others).
  3. Link back to the site from GitHub, LinkedIn and X profiles — the same URLs listed as sameAs in the Person structured data. Consistent profiles are what tie searches for the name to this site.
  4. Check a page with Google's Rich Results Test and share one link on LinkedIn/X to confirm the social card.

About

Personal website and writing — omercelik.dev

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages