.─[ matrix ]────────────.
┌─────────────────────┐
                     
                     
                     
                     
                     
                     
                     
                     
                     
                     
                     
                     
                     
└─────────────────────┘
'─[ asciiart.eu ]───────'
V - ZS Call Center Website
~ Erdem Hanay
──[ VOL.1 :: ZS ]────────────────────────────────────────────────────────────//─
──
    :..........................................................................:

[>] Project Link: https://zeynelsahin.com/

──[ 0x00 setup ]────────────────────────────────────────────────────────────//──
─

After SAGD and SAY, I wrote that
things were about to get a lot more interesting.
Then I did İdil's portfolio because I wanted a break.
Not a break from CS.
A break from making stuff for money.
Then money came back anyway, ngl.
This is that contract.

I wrote this document by strictly following
my internal architecture documents.
I made a lot of reductions for the sake of security.
And the cadence and wording is pragmatic and boring.
This is meant to be read by employers or people interested.
I dont recommend anyone else reading this post.

ZS Call Center.
Zeynel Sahin.
A Kayseri contact center that does nearshore BPO
for Europe and Turkey.
Voice, chat, email, back-office.
Turkish and German desks.
The kind of client where GDPR and KVKK
are not some footer you slap on in week four
because a lawyer mailed a PDF.
They actually talk like that.

Live site if you wanna click around while you read:
https://zeynelsahin.com/en

If you read the SAGD entry you already know
the punchline I was sitting on.
I knew Vue.
I had no time to learn React.
Next.js worked better with Vercel
and I still said no.
I wired @vue/server-renderer by hand
and told myself that was SSR.
It was.
For a five night NGO site and a three hour holiday site.
(Five Nights at Freddies style, again, I know.)
It was not the thing I wanted people to judge me on.

ZS is the one I wanted to be judged on.
And I wanted it on Next.js.
For real this time.

They needed a site that could do a couple of jobs
without looking like three websites glued together.
Tell people who they are.
Get B2B leads from companies that wanna outsource a queue.
Hire agents.
And quietly give staff a place to read the inbox
so I am not digging in the database at 1 AM
like some kind of gremlin.

──[ 0x01 architecture ]─────────────────────────────────────────────────────//──
─

First thing I did was the same first thing I always do.
I made an architecture.md.
I did not touch product code.
BSOYD started like that too.
Five iterations of a plan before I wrote Vue.
This time I already knew what a rushed plan looks like
because SAGD WAS a rushed plan that shipped.
I was exhausted, I needed money, I used AI as a junior
I couldnt fully trust.
I called those builds a stain on my portfolio
and I meant it.
Honesty is the best policy.

So I sat on this one longer.
First pass of the architecture file was just pages.
Home, about, projects, solutions, career, contact, a pile of legal.
I looked at it and went
this is a sitemap, not a system, what am I doing.

Second pass I wrote "Sanity for projects and jobs"
out of pure muscle memory.
I stared at that sentence and got annoyed at myself.
I used Sanity for over a year.
BSOYD needed it because they posted updates constantly.
SAGD copied that layer because I told a model
"copy this x system from BSOYD."
İdil I used it as a spatial database for SVG paths
which was fun and also not what a call center needs.
I was done using a headless CMS as a personality, ngl.

My thought process here was kinda mean.
Most of this copy is not gonna change on a Tuesday.
Mission sentence.
2006 timeline.
Inbound outbound technical back-office cards.
Nine legal documents.
Nav labels.
Form error strings.
If that lives in a CMS,
someone publishes a draft of the privacy policy
while I am asleep.
If it lives in git,
changing it is a pull request.
That friction is the feature, tbh.

But jobs DO change on a Tuesday.
Case studies might, later.
Leads and applications are not "content" at all.
They are just rows.
They should not show up on the homepage
as a testimonial I did not ask for.

──[ 0x02 buckets ]──────────────────────────────────────────────────────────//──
─

So I split the world into three buckets
and I refused to start the repo until I liked them.

Hardcoded.
Copy in the repo.
Deploy to change.
Staff cannot touch it.
I will repeat that because later me
will try to "just add a textarea."
No.

Dynamic.
Only two things.
${project} and ${openp}.
[Redacted Database schema architecture detail for sec reasons]
Public pages read them on the server.
I already did that crime
with meta tags in App.vue on BSOYD.
Google indexed a shell.
I was super proud of myself then.
Oh, how ignorant I was.

Transactional.
Information requests.
Contact inquiries.
Job applications.
Forms write them.
Staff read them.
Never marketing fields.

Once I had that, the stack actually had a job.
It was not a personality.
It was how those three buckets dont leak into each other.

──[ 0x03 i18n ]─────────────────────────────────────────────────────────────//──
─

Then languages.
The company is Turkish.
The work is Turkey plus Europe.
German is not a "nice to have" you add when you have time.
It is a desk.
English is for the brands evaluating them.

I set Turkish as default on purpose.
/ goes to /tr.
hreflang x-default is tr.
English speakers get /en.
German speakers get /de.
Every public route is /{locale}/…
I did not want some plugin that "detects browser language"
and yeets a Turkish applicant onto an English legal page
because their phone is in German.
Like come on.

Forms render in the active locale.
[Redacted Form locale architecture detail for sec reasons]
That sounds obvious.
It is the difference between a useful inbox
and a pile of anonymous submissions.

Then I argued with myself about slugs for a whole evening, ngl.
Localized slugs sound respectful.
/tr/projeler/foo versus /en/projects/foo
They also triple your redirect table
and break the idea that a project is one record.
I chose one slug per project, one slug per job,
shared across locales.
[Redacted Slug and translation architecture detail for sec reasons]

If the German translation does not exist yet,
/de/career/foo 404s.
It does not show the English body inside German chrome.
I have seen that.
It looks "complete."
It is a lie.
[Redacted hreflang architecture detail for sec reasons]
That 404 made me feel kinda cruel for a minute.
Then I thought about Google.
Then I kept it.

──[ 0x04 next ]──────────────────────────────────────────────────────────────//─
──

Now the stack question I had been postponing since March.
SAGD: I knew Vue, no time for React,
Next.js was out of the question,
Nuxt I also didnt have time for,
so @vue/server-renderer it was.

I thought about staying in Vue
because my hands already know it.
I like Vue.
I can still ship Vue.
This contract did not want Vue.
It wanted Next.js.
Like, actually Next.js.
Not a Vue SPA pretending.
Not a hand-rolled server renderer I was proud of for a week.

ZS had time.
ZS had three locales, forms, a staff area.
So I finally sat down and used Next.js heavily.
That was the whole point of the SAGD cliffhanger, ngl.

App Router, not the old pages thing.
[Redacted Locale routing architecture detail for sec reasons]
Server Components for the HTML Google actually sees.
[Redacted Metadata architecture detail for sec reasons]
[Redacted On-Demand ISR architecture detail for sec reasons]
[Redacted Sitemap architecture detail for sec reasons]
[Redacted Static params architecture detail for sec reasons]
[Redacted Tech stack architecture detail for sec reasons]
The site itself is Next.js all the way down.
I used it a lot.
I used it on purpose.
I used it until my hands stopped reaching for Vue files.

Controversial but I dont love using TypeScript for everything.
I wrote a whole rant about it in Volume 3.
If you are building a complex system, yes.
This was a complex system.
[Redacted Validation architecture detail for sec reasons]
This repo is me not repeating that sentence.

So I started a Next.js app.
[Redacted Tech stack architecture detail for sec reasons]
Then I deleted the blog demo like it was radioactive.
Then I made the folder structure match the architecture file
before I made a single pretty page.

The public marketing routes live under the locale prefix.
[Redacted Admin routing architecture detail for sec reasons]
[Redacted Data fetching architecture detail for sec reasons]
Message files empty on purpose.

[Redacted Environment variables architecture detail for sec reasons]
I am not listing secrets here.
They live in the host as [REDACTED].
They do not live in git.
BSOYD already had a chapter about keys in the wrong account.
I do not need a sequel.

npm run dev served a blank Next.js corpse.
I committed that corpse.
Boring commits are how I prove I was not drowning.

──[ 0x05 design ]───────────────────────────────────────────────────────────//──
─

Then I did something that would have felt like procrastination
on SAGD, when I had five nights and the heat was coming.
I built the design system first.

Because if I didnt,
I would copy a homepage glass div onto about, solutions, career, contact
and then I would have five slightly different border radii
and a client asking why the privacy policy looks like a nightclub.

Lora.
I wanted a serif that looks like a firm, not a startup sans.
Then I sat with a blue palette that the mock named yale_blue three times
with three different hexes.
I renamed them like an adult.
deepspaceblue in the basement.
Then the navy steps.
Cerulean for buttons and links.
Steel and sky for borders and the edge of glass.

Glass is a marketing trick.
Ice on dark, a little blur, a 1px border that is almost ashamed.
Legal body text does NOT get glass.
I put that in the spec like I was threatening myself.
A privacy policy with backdrop-filter
is how you fail contrast
and also how you look like you are hiding the words.

I made primitives so I would stop inventing them.
Buttons, fields, cards, headings.
And the dual CTA.
That last one is the whole business, ngl.
Left: talk to us about outsourcing, go to /solutions.
Right: come work here, go to /career.
A call center site that cannot decide if it is hiring or selling
will try to do both in every heading.
I made the indecision a component
so at least it would be consistent.

I installed Lenis and did not wire it yet.
I made a throwaway page, looked at it, hated one token, fixed it,
left a note to delete it before production.
Then I actually deleted it later.
SAGD me would have shipped the throwaway page.
I know because I almost did.

──[ 0x06 database ]─────────────────────────────────────────────────────────//──
─

Then the database.
Still no homepage.
[Redacted Database schema architecture detail for sec reasons]
No public sign-up.
I am not storing plaintext.
I am not putting [REDACTED] in Notion.

The B2B lead form on /solutions.
The contact form on /contact.
That is not a lead with a different title.
Sales and "hi I am press" should not share a tab.

The job application on /career/{slug}.
[Redacted Job application architecture detail for sec reasons]

[Redacted Resume storage architecture detail for sec reasons]
I wrote that down BEFORE I wrote the form UI
because the form UI will beg you to dump a file onto the app disk.

[Redacted Database seed architecture detail for sec reasons]
I knew the homepage would render
before any human had a case study to publish.
If empty crashes the page, the architecture is a toy.

I also wrote a contradiction into the spec like an idiot.
In one place I said the project grid could be fetched on the client.
In another place I said public HTML must come from the server.
I sat there with both sentences.
A spinner, then cards, very "app."
Google would have gotten an empty list
the way Google got empty meta on BSOYD.
I picked the server for that HTML.
[Redacted Public content API architecture detail for sec reasons]
If I catch myself fetching the grid after hydration
I am doing BSOYD meta tags again with extra steps.

──[ 0x07 copy ]─────────────────────────────────────────────────────────────//──
─

Then i18n.
Still no homepage. I swear.

I thought I could postpone the copy files until I "knew the copy."
Then I thought about BSOYD,
entering eight locales by hand after the UI existed,
and I did not want that homework at the end again.

[Redacted i18n architecture detail for sec reasons]
Skeletons first: common, nav, footer, meta.
Then I grew them whenever I needed a sentence on a page
instead of inventing a string in JSX
and promising I would extract it later.
I never extract it later.
Keys in all three files even when German was still a placeholder
that made me wince.
Missing keys are how you ship a page that says
home.header.title
in production and calls it a translation strategy.

The Next.js root page redirects / to /tr.
I sat on that redirect.
English as default would have been easier for me
because I write these case studies in English.
The company is Turkish.
The default is tr.
I am not the audience.

──[ 0x08 routing ]───────────────────────────────────────────────────────────//─
──

Then routing.
This is where I always get arrogant.

I wired the locale prefix
and I felt done.
[Redacted Admin host isolation architecture detail for sec reasons]
[Redacted Locale routing architecture detail for sec reasons]

[Redacted Local admin hostname architecture detail for sec reasons]
I would forget this later and remember the README
which is the only reason README exists, ngl.

Placeholder about. Placeholder home.
I hit /en/about /tr/about /de/about
and saw empty shells switch language.
That was the first time it felt like a product
and not a tutorial.

Then the data layer before pretty UI,
because pretty UI will invent fetch.

[Redacted Admin data auth architecture detail for sec reasons]
That was the point.
If I wired staff tools before auth,
I would "temporarily" leave it public.
I have met that person.
He is me on a deadline.

[Redacted Data layer architecture detail for sec reasons]
[Redacted Form rate limiting architecture detail for sec reasons]
Tired me always leaves the weak version.
The launch checklist exists to catch tired me.

[Redacted Public content API architecture detail for sec reasons]
I was weirdly proud of an empty result.
It meant the query did not explode.
It meant the content model was not a slide deck.

──[ 0x09 chrome ]───────────────────────────────────────────────────────────//──
─

Then, finally, a website.
I had been staring at copy files so long
that putting a navbar on the screen felt illegal.

The public layout with the dark gradient.
The logo pointing at /{locale}/
not at / because / is a redirect
and I am tired of bounce chains
that make analytics look like a bounce rate problem
when it is just me being cute with roots.

Then the navbar, sticky glass,
and I spent a stupid amount of time
on whether search belongs on mobile in the bar
or inside the hamburger.
I built a locale-aware href helper
after I concatenated a string in three files
and shipped /en/en/about to myself
and gaslit myself for ten minutes
about whether the i18n layer was broken.
It was not broken.
I was.

Language changer keeps you on the same page in another locale.
If you are on /en/career/foo and you click de,
you go to /de/career/foo
and if that translation does not exist you get the 404 I chose.
I thought about being "helpful" and falling back.
I even sketched a little "this role is not in German yet" banner.
Then I thought about a German applicant reading English requirements
inside a German header and applying to the wrong thing.
I deleted the sketch.
I kept the 404.

Accessibility settings went in the same week
because I opened BSOYD in another tab
and remembered the Low Stimulation Mode
and felt like a hypocrite.
Font size, contrast, reduce motion.
[Redacted Accessibility settings architecture detail for sec reasons]
I am not gonna ship that story in one volume
and then ignore motion in the next
because the industry is "call center" instead of "autism NGO."

Search I almost skipped.
Then I thought about nine legal pages
and a person who types "kvkk" into a box
instead of reading a footer.
Client modal.
[Redacted Search index architecture detail for sec reasons]
Google is not waiting on the search modal to rank /tr/career.
Google is not waiting on the search modal to rank /tr/career.

Footer I wrote when I was tired
which is why it has three rows and a shoutout.
Logo, socials, copyright, nine legal links.
website made by Erdem Hanay
It is on the live site.
It is on the other sites.
I like fingerprints.
I thought about removing it for "corporate."
Then I thought about SAGD being a stain
and I kept the name.
If the work is mine, the footer can say so.

Cookie FAB bottom left came after I drafted privacy
and realized an analytics tag in the layout "just for now"
would make the policy a fiction.
First visit, modal, blur the page.
[Redacted Cookie consent architecture detail for sec reasons]
Analytics does not load before that.
I grepped.
I grepped again.

To-the-top FAB bottom right, mobile only,
after you scrolled about 20 percent,
with a ring that fills as you go down.
I built that after İdil when my hands still wanted a toy.
Then I looked at it and I allowed myself this one.
A contact center homepage does not need a Drunkard's Walk.
It needs a form that works.

Then I wired Lenis in the locale layout,
watched it fight a hash link to the form,
fixed the hash link,
destroyed Lenis when reduced motion.
[Redacted Admin routing architecture detail for sec reasons]
Staff are trying to read an inbox
not experience butter.

──[ 0x0A home ]─────────────────────────────────────────────────────────────//──
─

Then I built the home page
the way a stranger would scroll it.

I thought, what does a procurement person need to believe
in the first screen
before they bounce to a competitor in Istanbul.
So the hero went first.
Call center outsourcing that scales with your business.
Nearshore from Turkey.
Pilot to full scale without the quality falling off a cliff.
I wrote the English like a capability deck
because that is what /en is for.
Then I copied the keys into tr and de
and I did not invent a second layout.
If the German hero needs different poetry,
that is a JSON problem, not a React problem.

Then I thought they will not believe poetry without numbers.
So stats.
10–18 weekday hours.
20+ years.
Two service languages, Turkish and German.
Pilot programs before ramp.
I almost animated the numbers because I like toys.
Then I remembered İdil's butterfly
and BSOYD's overstimulating NGO site
and I put the animation behind the fold
and behind reduced motion
and I still side-eye it.

Then services, which is just a funnel wearing a suit.
Four cards. CTA to /solutions.
Then industries. Same motion.
Retail, banking, healthcare, telecom, travel, utilities.
Trust chips for GDPR/KVKK
because this client will be asked about those
before they are asked about the serif.

Then I hit Home-Projects
and the empty database looked me in the eye.

I could have seeded Acme Bank and a stock photo
so the homepage would look expensive in a screenshot.
I did not.
The list came back empty.
The section still renders.
Case studies coming soon.
That sentence is on the live site.
It is not a failed query.
It is me refusing to use the admin to lie.

Home-Careers was the opposite.
When a real opening existed, the same pattern
grew a row.
Telephone agents, home agents,
Turkey - Germany, hybrid, remote, FULL_TIME.
So I knew the mixed-section idea worked
before I ever built the admin UI.
One list empty on purpose.
One list alive because a human needed hiring to work
more than they needed a portfolio of case studies.

Then the dual CTA
and I walked away from home long enough
to write Next.js metadata,
locale alternates,
x-default pointing at tr,
and JSON-LD Organization plus WebSite.
BSOYD me put tags in App.vue and prayed.
This me used Next.js metadata on the Server Component
and did not feel heroic about it.
It is just the thing that exists now.

──[ 0x0B pages ]────────────────────────────────────────────────────────────//──
─

About was a relief because nothing in it is a database.
I moved there next because I needed a win
that would not wait on the client to send case studies.

They have been alive since 2006
so I wrote a timeline like a person who had actually asked.
Founded with inbound retail.
Service lines in 2014.
German queues in 2018.
Cloud telephony in 2022.
Today.
Mission, values, leadership as role cards, not headshots.
I am not taking a new org chart in Word every quarter
and turning it into a database.
Integrity, excellence, empathy, accountability
sound like every BPO until you remember
they have to mean something under KVKK
or they are just glass cards.

Glass on the sections.
Not on the long prose.
I keep saying that because I will try to glass the prose
the minute I am bored.

Then I had to face /projects
which is a page that currently exists
to tell the truth about not existing yet.

I thought about hiding the route until we had case studies.
Then I thought about a nav link that 404s
and a stranger who assumes the company has no work.
So the page stays.
Chrome hardcoded.
[Redacted Data fetching architecture detail for sec reasons]
I wanted filters.
I thought about fetching on filter change.
Then I thought about that contradiction in the spec
and I filtered cards that were already on the page.

[Redacted Featured projects architecture detail for sec reasons]
If they want a carousel later, that is CSS.

Detail route /projects/{slug}
I wrote before we had a slug worth opening.
[Redacted Static params architecture detail for sec reasons]
[Redacted Missing translation architecture detail for sec reasons]
Related projects, CTA to /solutions,
JSON-LD CreativeWork waiting for a body.
The live page still has nothing to hang that JSON-LD on.
That is fine.
The route is not a 404.
The empty state is the product
until they send me real work I am allowed to name.

──[ 0x0C solutions ]────────────────────────────────────────────────────────//──
─

Then solutions,
which is where the site either makes money
or it is a brochure.

I did not want to start with the form.
A form at the top looks hungry.
So I built a conversation down the page.
What we deliver.
The four lines.
How an engagement actually goes
because "contact us" without a process
is how RFPs bounce.
Discovery. Solution design. Pilot and ramp. Optimize.
Why people stay.
Quotes I hardcoded
because we did not have a testimonial CMS
and I was not going to fake a carousel of names.
An FAQ I put on this page
even though career also wanted an FAQ.
I thought about one shared FAQ component.
Then I thought about a workforce question
showing up next to a GDPR question
and I kept two namespaces.

Then the lead form
and the first real write
and the first time I felt the old SAGD speed-urge
to just make the button work.

[Redacted Validation architecture detail for sec reasons]
[Redacted Lead form fields architecture detail for sec reasons]
Consent required, linked to /privacy.
[Redacted Form locale architecture detail for sec reasons]
[Redacted Lead form anti-abuse architecture detail for sec reasons]

[Redacted Transactional email architecture detail for sec reasons]
The inbox is the source of truth.
Email is a courtesy.

I submitted a fake company until a row appeared.
Same dumb joy as the first Sanity prototype on BSOYD
except this time I could not blame a SaaS.

Contact came next and I almost copied the lead form.
Then I remembered why I split them
and I made a separate contact form with a subject select
and a map embed from an i18n key
so a future human can change the iframe without me hunting JSX.

Career list felt like home's careers section
after it had gone to the gym.
I started with culture and benefits
because a job board with no company
is just a scraped XML feed.
Then I dropped the openings in from the server
and the same FULL_TIME row from home appeared
and I thought, ok, the teaser was not a dream.
Hiring steps.
FAQ as a client accordion.
If there are at least three items I emit FAQPage JSON-LD.
I thought about stuffing two questions in to hit the number.
Then I thought about lying to Google
and I left the count honest.

──[ 0x0D career ]───────────────────────────────────────────────────────────//──
─

Then the scary page.
/career/{slug}
I called it scary because this is where a stranger
puts their CV into my system
and I have to deserve that.

Role body from the server query.
[Redacted Missing translation architecture detail for sec reasons]
JobPosting JSON-LD
because Google likes jobs more than it likes my prose.
Related roles at the bottom
so a person who is almost right
does not bounce to indeed.

Then the form, and I sat on the resume flow
longer than the UI.
I have seen people upload to the app server.
I have seen public buckets with CVs in them.
I have opinions that are not cute.

Authentication I will not vibe code.
NEVER EVER EVER NEVER.
I wrote that in Volume 3 in capital letters
like a crazy person
and I still mean it.
Database schemas I also dont want AI inventing.
I actually did that once, checked it carefully,
wont recommend it tbh.

[Redacted Resume upload architecture detail for sec reasons]
I said that out loud to myself like a spell.

Project images I did the opposite on purpose
in the same week
so I would not copy the resume path
and accidentally make a CV public.
[Redacted Project image upload architecture detail for sec reasons]
Marketing images have to live in img tags.
CVs do not.

I refused to let a file touch the app disk.
[Redacted Upload validation architecture detail for sec reasons]

──[ 0x0E legal ]────────────────────────────────────────────────────────────//──
─

Legal was the opposite of cute
and I did it anyway because the footer already promised nine links
and a footer that 404s is a special kind of embarrassment.

I thought about putting legal in the admin.
Then I thought about a Tuesday, a phone, a "quick edit,"
and a privacy policy that no longer matches the German one.
So the legal layout is high contrast prose.
No glass on the text.
TR first, because default locale.
EN and DE keys existed even when the prose was still a skeleton.
KVKK, GDPR, BDSG, privacy, terms, modern slavery,
communication preferences, accessibility, a legal hub.
I wrote those names into the footer before I wrote the pages
which is how you force yourself to actually write the pages.
Lawyers send paragraphs.
I want that to be a code change.
WYSIWYG policies are how a Tuesday becomes an incident.

404 I built after I clicked a fake slug
and Next showed me a naked error
that looked like I got hit by a truck.
Navbar still on.
Go back.
Home CTA.
noindex.
[Redacted Not found architecture detail for sec reasons]
Invalid locale and invalid slug
are different ways to get lost
and I was tired of one file pretending to be both.

──[ 0x0F staff ]────────────────────────────────────────────────────────────//──
─

Then I was in this weird spot
where the public site could almost be a brochure
and the forms could write rows
and nobody without the staff tools could read them.
That is when I added auth.
Late on purpose.
If I had added it first I would have built the staff UI forever
and shipped no /solutions.

I thought about BSOYD again.
I did not trust myself to roll auth by hand then.
I still do not, not for a staff inbox full of CVs.
[Redacted Admin authentication architecture detail for sec reasons]

Then routing grew teeth
and I broke the site for an afternoon.
I fixed it.
I felt like the fifth iteration of BSOYD
when Vercel wanted physical .html files
and I did not know the name of the problem yet.
This time I knew the name.
I just typed it wrong.

[Redacted Admin session isolation architecture detail for sec reasons]
[Redacted Admin login architecture detail for sec reasons]
Staff are not here for glass.

Dashboard after that.
I opened the empty staff shell,
thought "I should make this look like Linear,"
and then I thought about a person who just wants to know
if anyone applied today.
[Redacted Admin dashboard architecture detail for sec reasons]
Traffic I left as a link out.
[Redacted Analytics architecture detail for sec reasons]
Two systems, two truths.

Then projects editing
because the empty homepage was staring at me
and I wanted the button that would someday fill it.
[Redacted Admin projects architecture detail for sec reasons]
I thought about a rich text war.
I thought about two extra days.
I shipped the simple version.
[Redacted Project image upload architecture detail for sec reasons]
I hovered over a hard delete
and I thought about a URL in an RFP PDF
and I made delete mean unpublish.

Open positions the same muscle
because hiring could not wait for my portfolio feelings.
[Redacted Admin open positions architecture detail for sec reasons]

Submissions last,
because it is the reason the staff area exists
and I kept putting it last anyway.
[Redacted Admin submissions architecture detail for sec reasons]
I thought about a Kanban.
phone screen, onsite, offer, ghosted.
Then I thought about building a second HR product
inside a marketing site
and I stopped.
This site is intake.
Their real HR tool can be their real HR tool.

Then I thought about a hiring manager who posted a role at 10:00
and a candidate who hit /career at 10:01
and saw last month.
[Redacted On-Demand ISR architecture detail for sec reasons]
I tested it with a fake job titled something embarrassing
so I would not forget it in production,
watched /en/career, watched it appear,
unpublished it, watched it go.
[Redacted Soft unpublish architecture detail for sec reasons]

Then I went back to the public site
like a person who had forgotten SEO
which I had not.
I had just been scared of it since BSOYD.

I thought, if I leave the sitemap for last I will ship without it.
I have done that kind of last.
So I wrote it while the staff UI was still ugly.
[Redacted Sitemap architecture detail for sec reasons]

[Redacted Robots architecture detail for sec reasons]
I generated metadata on every public page like a ritual
because the one time I got cute with App.vue
Google indexed a shell.
That is a Next.js job now.
[Redacted Metadata architecture detail for sec reasons]
Not a client island.

JSON-LD I added when the HTML already made sense.
Home: Organization plus WebSite.
Job detail: JobPosting.
Career FAQ: FAQPage if there are at least three items.
I thought about putting JobPosting on the list page
and then I thought about what a crawler does with a lie
and I put it on the slug instead.

I replaced leftover img tags after I caught one on the about timeline
that would have wrecked CLS for a year story
nobody even scrolls to.
[Redacted Image loading architecture detail for sec reasons]
I used Next.js for the heavy islands
because home LCP does not care about my command palette.
I gated analytics behind consent
and then I grepped for [REDACTED]
like I did not trust myself.
I did not trust myself.

Then headers.
[Redacted Security headers architecture detail for sec reasons]

I thought about BSOYD scoring 130/100 on a HTTPS test
and then spending iterations fighting my own CSP.
I was super proud of that score.
It was not a success.
Unneeded oppressive security just shows
how inexperienced a developer is.
Sometimes a site with 80/100 is better than 140/100
because you have to balance utility and velocity with security.
I wanted an A+ Observatory number.
I also wanted to ship a form.
I did not treat the score as a personality this time.

Lighthouse on home, solutions, career.
CLS fought me because glass and a cookie modal and FABs
and a sticky navbar all want to move.
I set image dimensions.
I still do not believe CLS 0.0 as a religion
while backdrop-filter is in the room.
INP on the forms mattered more to me.
A lead form that janks on submit is a business bug.

I put production on the host
and I sat there for a minute
the way I sat on BSOYD when the DNS was wrong
except this time I checked the records twice
like a person who has been humbled.
[Redacted Database seed architecture detail for sec reasons]
Then I changed [REDACTED]
because a seed value that lived in a log
is how horror stories start.

I walked /tr then /en then /de
and I watched the same hero put on different clothes.
I sent a test lead from /en/solutions
and I waited for the mail
and then I opened the staff inbox and the row was there
and I believed the staff story
in my body, not just in the spec.

I uploaded a test resume on staging
with a filename I would recognize in a panic.
[Redacted Resume signed URLs architecture detail for sec reasons]
I tabbed through the cookie modal until escape worked
because I have shipped a modal you cannot leave
without refreshing
and then I made fun of website builders
which is not a look.
I turned on reduced motion and watched Lenis die
and the count-up stay still
and the page still made sense.
I removed the throwaway page
and the console.logs I had used as emotional support.
I wrote the README in my own words
so tired me could run the setup
without asking past me what a local setup meant.

──[ 0x10 walk ]─────────────────────────────────────────────────────────────//──
─

Then I looked at the live English page
the way a stranger would,
not the way an architect would.

I skipped to main content because I had put that there
and I wanted to know if it was real.
I switched en to tr to de and back
and I did not get lost.
I read the hero.
I read the stats.
I scrolled services and industries
and I waited for the projects section
to embarrass me with lorem
and it did not.
Coming soon.
Then the trust band.
The spec had called it investors.
The site calls it accountability.
I changed the sentence in JSON
and I did not migrate anything
and that was the whole point of hardcoded copy
showing up in production as a feeling,
not as a lecture.

Then a job row.
Telephone agents. Home agents.
Turkey. Germany. Hybrid. Remote. FULL_TIME.
I clicked it.
The slug page was a page.
The apply form was a form.
I did not submit a real CV to production like a clown.
I already knew that path worked
because I had been a clown on staging.

Footer still had my name.
I left it.

/en/about is a company.
/en/solutions is a machine with a form.
/en/career is a hiring page with a real opening.
/en/projects is waiting on humans.

I did not put a dashboard screenshot in this article
on purpose.
Volume 1 is not a pentest.
[Redacted Admin login architecture detail for sec reasons]

──[ 0x11 argue ]────────────────────────────────────────────────────────────//──
─

People will argue with me about this build.
I have already argued with me about this build.

They will say the client cannot fix a typo without me.
Correct.
They also cannot unpublish /privacy from a phone on a bus.
I picked that on a day I was feeling kind
and also tired of Sanity.

They will say shared slugs look ugly
when a Turkish slug sits inside /en/.
I thought about localized slugs for a whole evening.
I sketched redirects.
Then I thought about one record becoming three URLs
that rot at different speeds
and I went to bed
and in the morning I kept one slug.

They will say WordPress editors will hate the hardcoded copy.
They will.
They can email a paragraph.
Tuesday tools are for jobs and projects.
That is the whole product.

They will quote Volume 3 at me about TypeScript.
I wrote that rant.
I meant it for brochures.
I sat with the form tables
and I used TypeScript.
[Redacted Validation architecture detail for sec reasons]

They will want SSO.
There is staff who need an inbox.
Not a company directory.
[Redacted Admin authentication architecture detail for sec reasons]
It is launch.

They will want one funnel number.
I will not ingest analytics into the database
to make a chart feel native.

They will want glass on legal
because the rest of the site is glass.
Pretty is not a legal requirement.
Readable is.
I still almost glassed the prose
on a night when I was bored.
I undid it.

I have used AI as a junior on SAGD.
I called those sites a stain and I meant it.
Still going to tell the story though.
Honesty is the best policy.

This one I did not let a model invent the schema
or the auth
or the idea that a database is not a CMS.
I wrote a 280-step hand-build plan
so I could not skip Auth because I was bored.
I am not pasting 280 boxes here.
That would be a list
and lists are how I hide from the story.
The boxes existed so tired me
would still do the next right thing.

I did not port BSOYD's PWA.
I opened that thought,
remembered SAGD deciding a service worker was unnecessary,
and closed it.
A call center marketing page does not need to sit next to Instagram.

I did not put meta in a client island.
I did not configure an oppressive CSP as a flex.
I did not seed fake case studies.
I did not build a 3D cube that janks on mobile.
That corpse is in the SAGD paragraph
with the physics engine
and the romantic website that crashed
(even that failed, damn...).
I walk past it every time I want to make this site "more interesting."
It is already interesting.
It works.

──[ 0x12 request ]──────────────────────────────────────────────────────────//──
─

After all of that, a request feels like this.

I open /en/solutions like I am not the person who built it.
The page is already HTML.
The form is an island.
I type a company that does not exist.
[Redacted Lead form submit architecture detail for sec reasons]
A row lands.
Mail maybe arrives, maybe not.
The lead is in the inbox.

Then I pretend I am staff and I publish a job.
[Redacted On-Demand ISR architecture detail for sec reasons]
I pretend I am an applicant and I open /en/career
and the HTML already has the row
because the Next.js Server Component did the work
and not after hydration.
I open the slug.
JobPosting JSON-LD sits in the source
like a polite note to Google.
If I was lazy on the German tab,
/de/career/that-slug is 404
and I do not claim de exists.
That 404 is not a bug.
That is me not lying.

A project will travel the same way
when they send me a case study I am allowed to publish.
I will not redesign the page.
I will insert a row.
I keep saying that
because the empty grid is tempting
and I know how to fill it with fiction
in twenty minutes.
That was the point of making admin so small.
Small admin means I have to wait for the truth.

Volume 1 said
my work speaks for me already
and this is just a more detailed explanation
of the speech my work does for me.

I also keep thinking about SAGD me
who shipped in five nights and called it survival
and was correct
and still did not want that to be the last chapter
anyone read before they decided what I am.

The work is
https://zeynelsahin.com/en
and /tr and /de.

The speech is
I stopped treating every website like a CMS
and every CMS like a personality.
I used Next.js. Heavily.
The thing I postponed in March.
I put jobs and projects in a database
and everything else in git.
I isolated staff from the public site.
I let the projects grid be empty in production
because lying is easier than architecture
and I had already done easy.

After SAGD I said
things were about to get a lot more interesting.
This is what I meant.
Not a physics engine.
Not a 3-body problem.
Not a cube that janks on a phone.
A call center website that is allowed to be a call center website
and a backend that is allowed to be a backend.

──[ dispatch ]──────────────────────────────────────────────────────────────//──
─

If you want the chaotic learning path, Volume 0.
If you want me yelling at JavaScript arithmetic, Volume 3.
If you want production,
start at BSOYD,
suffer SAGD,
eat dessert at İdil,
then read this.

I am happy to see you around.
The site is up.
The jobs row is real.
The case studies will be real when the work is public.
Until then I am not going to fake a ${project}.

That would have been easy.
It would also have been the opposite
of why I made the admin small
in the first place.

    :..........................................................................:
──[ EOF :: vol.1 / ZS ]──────────────────────────────────────────────────────//─
──