Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"port": 3000,
"autoPort": true
}
]
}
13 changes: 13 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
# two trailing spaces are a line break in Markdown
trim_trailing_whitespace = false
30 changes: 30 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Dependency currency is automated, not remembered. Groups take minor and
# patch updates together; a MAJOR arrives as its own pull request — a major
# is a decision with a migration story, not a batch item, and a closed one
# isn't recreated. GitHub Actions are grouped whole (CI proves them).
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
groups:
# Next.js and its ESLint config are released together; move them together.
nextjs:
patterns: ["next", "eslint-config-next"]
update-types: [minor, patch]
dependencies:
dependency-type: production
update-types: [minor, patch]
dev-dependencies:
dependency-type: development
update-types: [minor, patch]

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
groups:
actions:
patterns: ["*"]
55 changes: 55 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Every push to main and every pull request: the same checks as
# `npm run check`, then the production build — the static export Cloudflare
# deploys — with a guard that every product card made it in.
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
cache: npm

- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm run format:check
- run: npm test

- name: Build (static export)
shell: bash
env:
# Product cards come from the GitHub API at build time; the job
# token lifts the anonymous rate limit (60 requests an hour per IP).
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -o pipefail
npm run build 2>&1 | tee build.log
# A failed product fetch only warns (the site still builds), so
# CI is where a missing card turns into a failure.
if grep -q "\[products\]" build.log; then
echo "::error::Some product cards failed to load — see [products] lines above."
exit 1
fi

- name: Check the static output
run: |
for f in index.html 404.html about.html writings.html sitemap.xml robots.txt feed.xml og.png favicon.ico; do
test -f "out/$f" || { echo "::error::out/$f is missing"; exit 1; }
done
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ yarn-error.log*
# vercel
.vercel

# cloudflare
.wrangler/

# local agent settings (the shared .claude/launch.json is committed)
.claude/settings.local.json

# typescript
*.tsbuildinfo
next-env.d.ts
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24
8 changes: 8 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.next
out
node_modules
package-lock.json
next-env.d.ts
public
# Essays are prose: the author's line breaks stay the author's.
content
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<!-- BEGIN:nextjs-agent-rules -->

# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
Expand Down
166 changes: 130 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,63 +1,157 @@
# omercelik.dev

Personal website and writing — a quiet home for open-source products and essays.

Built with **Next.js 16** (App Router), **React 19**, **Tailwind CSS v4**,
**next-intl** and **MDX**. Design language ported from the Mockifyr design system
("quiet frame, lively content").

## Features

- **i18n** — English (default, no URL prefix) plus Arabic (RTL), German, French,
Chinese and Japanese. UI strings are translated in `src/i18n/messages/*.json`;
articles stay in the language they were written in.
- **Theme** — light by default, dark toggle, no flash on load. All colours are
design tokens in one file (`src/app/theme.css`) — swap them to re-skin.
- **Products** — pulled live from the GitHub API and cached hourly. Curate the
list in `src/config/products.ts`.
- **Writing** — MDX files in `content/writings/`. Frontmatter drives the title,
date, language badge, tags and reading time. Code is highlighted with Shiki
(dual light/dark theme).
- **Contact** — dependency-free: the form composes a pre-filled message in the
visitor's mail client (no backend, no email service). Swap in an API route +
provider later if you want server-side sending.
Personal site and writing of Ömer Çelik — open-source products and essays.

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

## Develop

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

## Add a post
## 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.
- **Products** — read from the GitHub API at build time; curate the list in
`src/config/products.ts`. Set `GITHUB_TOKEN` wherever the build runs:
unauthenticated requests are limited to 60 an hour per IP, and a failed
request drops that card from the build (with a `[products]` warning in the
build log).
- **Contact** — no backend: the form opens the visitor's own mail app with the
message filled in, and says so.

## Writing

Create `content/writings/<slug>.mdx`:

```mdx
---
title: "My post"
description: "One-line summary."
date: "2026-07-09"
lang: "en"
tags: ["tag"]
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:

```mdx
<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.
- **Comments** — Giscus (GitHub Discussions), one thread per post.

## Configure

| What | Where |
| --- | --- |
| Name, email, social links | `src/config/site.ts` |
| Which repos show as products | `src/config/products.ts` |
| Design tokens (colours, radius, shadows) | `src/app/theme.css` |
| UI translations | `src/i18n/messages/*.json` |
| Supported locales | `src/i18n/routing.ts` |
| What | Where |
| ----------------------------------- | --------------------------- |
| Name, email, social links, comments | `src/config/site.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.

## After the first deploy — search engines

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

Deploys to **Vercel** out of the box. Set `GITHUB_TOKEN` in the project's
environment variables (optional but recommended).
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.
3 changes: 3 additions & 0 deletions content/writings/golden-paths-1.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ lang: "en"
tags: ["golden-paths", "architecture", "example"]
series: "Golden paths"
seriesOrder: 1
draft: true
---

> This is an example post to show how a **series** looks. Replace it with the
Expand All @@ -15,6 +16,8 @@ A **framework** imposes structure: you build inside its box. A **golden path**
does the opposite — it paves the road you were already walking, with enterprise
opinion on top of the platform you already use.

<PullQuote>A framework says "do it my way". A golden path says "here's the fast, correct way — step off it any time".</PullQuote>

## The difference in one line

- Framework: "do it my way."
Expand Down
8 changes: 8 additions & 0 deletions content/writings/golden-paths-2.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ lang: "en"
tags: ["golden-paths", "spec-driven", "example"]
series: "Golden paths"
seriesOrder: 2
draft: true
---

> Example post, part two of the series.
Expand All @@ -28,4 +29,11 @@ invariants:
- every endpoint has an idempotency key
```

<LayerStack title="Where each guarantee lives">
<Layer label="Spec" detail="manifest.yaml" note="What must be true." />
<Layer label="Contracts" detail="OpenAPI" note="How it is spoken." />
<Layer label="Code" detail="generated" note="What actually runs." />
<Layer label="Verify" detail="specdrift" note="Proof the three agree." />
</LayerStack>

Humans approve, guardrails verify, drift gets caught early. That's the whole idea.
8 changes: 4 additions & 4 deletions content/writings/hello-world.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ is that place.

A home for two things:

- **Products** — the open-source tools I ship, pulled live from GitHub.
- **Products** — the open-source tools I ship, with details pulled from GitHub.
- **Writing** — notes on software, systems, and the craft of building things.

Each piece stays in the language I wrote it in. The interface speaks several
languages; the essays speak mine.
The site itself is in English; each piece stays in the language I wrote it in,
so now and then you'll find one in Turkish.

## A small taste of code

Here's the shape of the thing that renders this very page:

```ts
```ts title="lib/writings.ts"
export async function getWritingBySlug(slug: string) {
const raw = await fs.readFile(`content/writings/${slug}.mdx`, "utf-8");
const { data, content } = matter(raw);
Expand Down
Loading