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
6 changes: 6 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
"runtimeArgs": ["run", "dev"],
"port": 3000,
"autoPort": true
},
{
"name": "worker",
"runtimeExecutable": "npx",
"runtimeArgs": ["wrangler", "dev", "--port", "8787"],
"port": 8787
}
]
}
11 changes: 11 additions & 0 deletions .dev.vars.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Local secrets for `npm run preview` (wrangler dev). Copy to .dev.vars β€”
# which git ignores. Production secrets are set in Cloudflare, never here.

# Cloudflare's test secret: always passes, and pairs with the test site key
# (NEXT_PUBLIC_TURNSTILE_SITE_KEY in .env.local).
TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA

# Log the email instead of sending it, so no Resend key is needed locally.
# Remove it and set RESEND_API_KEY to send a real test email.
CONTACT_DRY_RUN=1
# RESEND_API_KEY=re_...
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# --- GitHub (products) ---
# Optional: a GitHub personal access token (no scopes needed for public repos).
# Lifts the unauthenticated API rate limit so product cards always load.
GITHUB_TOKEN=

# --- Contact form (local only) ---
# Cloudflare's test site key, which always passes. Production uses the real
# key in src/config/site.ts; don't set this in Cloudflare's build settings.
NEXT_PUBLIC_TURNSTILE_SITE_KEY=1x00000000000000000000AA
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ yarn-error.log*

# env files (can opt-in for committing if needed)
.env*
!.env.example

# vercel
.vercel

# cloudflare
# cloudflare β€” local Worker secrets live in .dev.vars
.wrangler/
.dev.vars*
!.dev.vars.example

# local agent settings (the shared .claude/launch.json is committed)
.claude/settings.local.json
Expand Down
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ node_modules
package-lock.json
next-env.d.ts
public
.wrangler
# Generated by `npm run cf-typegen`.
worker/worker-configuration.d.ts
# Essays are prose: the author's line breaks stay the author's.
content
45 changes: 43 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,13 @@ npm run format # apply Prettier
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.
- **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

Expand Down Expand Up @@ -123,6 +128,7 @@ What an article can use:
| What | Where |
| ----------------------------------- | --------------------------- |
| Name, email, social links, comments | `src/config/site.ts` |
| Contact form recipient and sender | `wrangler.jsonc` (`vars`) |
| 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` |
Expand All @@ -139,6 +145,41 @@ Worker static assets, with `404.html` for unknown paths.
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:

```bash
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,
Expand Down
3 changes: 3 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const eslintConfig = defineConfig([
"out/**",
"build/**",
"next-env.d.ts",
// Generated by `npm run cf-typegen`, and wrangler's local state.
"worker/worker-configuration.d.ts",
".wrangler/**",
]),
{
files: ["**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts}"],
Expand Down
Loading