# devilyxrd — English ## The Turkish dotted İ, CSS and uppercase English terms URL: https://devilyxrddev.com/en/journal/turkish-dotted-i Date: 2026-10 · Interface On a Turkish page, text-transform: uppercase turns i into İ. Right for Turkish words, wrong for JavaScript or Firebase. On this site I solved it with the bilingual data itself rather than a hand-kept word list, and the same rule runs on the share cards too. ### The browser is actually right On this site, small mono text, tags, filter buttons and menu links are uppercased by CSS. In base.css, the .mono, .tag, .chip and .lk classes all have text-transform: uppercase. The source text stays in lowercase; the browser does the uppercasing while it draws the text. To do that, the browser looks at the language, because case mapping is not universal. In English, i becomes I. In Turkish, four letters form two separate pairs: dotless ı with I, and dotted i with İ. When the page is lang="tr", the browser applies the Turkish mapping and the label "Mühendislik" becomes MÜHENDİSLİK. That is exactly what we want. Under lang="en" the same word would become MÜHENDISLIK, which is wrong for a Turkish reader. The problem is that the same rule also applies to technology and project names. JavaScript becomes JAVASCRİPT, Firebase becomes FİREBASE, Linux becomes LİNUX. The browser is not making a mistake here; we are the ones telling it the text is Turkish. On this site the value is bound in App.vue through useHead to the language taken from the URL. So on Turkish pages, unless told otherwise, everything uppercases by the Turkish rule. ### First attempt: a word list My first reflex was to collect the words that should stay English in a list and give matching labels lang="en". It worked with the demo content. Then real content went into the projects and the About page: Go, Dart, gRPC, Kubernetes and NVIDIA Jetson Nano joined the tool list, and project names and interests changed. The list went stale overnight and the labels showed the dotted İ again. The real flaw of the list was not that it went stale, but that it did not tell anyone. When I added a new tool nothing broke, the build passed, and a single label just quietly looked wrong. Bugs like that only get noticed when someone looks at the page carefully. > A hand-kept list breaks quietly as the content grows. ### The rule: identical in both languages means a name The site is already bilingual. Every label has a Turkish and an English version under src/i18n/locales, and that data already holds the information the list was trying to keep. If a label is identical in both languages, it was not translated, and something that is not translated is almost always a name or a term: Linux, Firebase, Motice, Blockchain. If the two differ (Sensör haberleşmesi / Sensor communication), it is a translation and should stay in the page language. ``` export const sameInBoth = (a: string | undefined, b: string | undefined) => a !== undefined && a === b ? 'en' : undefined /** Project stacks: always technology names. */ export const STACK_LANG = 'en' export const catLang = (c: ProjectCat | 'all') => sameInBoth(tr.cats[c], en.cats[c]) export const toolTagLang = (group: number, item: number) => sameInBoth(tr.about.tools[group]?.items[item], en.about.tools[group]?.items[item]) ``` The function returns either "en" or undefined. undefined is a deliberate choice: Vue does not write an attribute whose value is undefined, so the label inherits the page language. All a component does is write :lang="catLang(c)". Four helpers go through the same rule: catLang for project categories, fieldTagLang for the field tags on the home page, toolTagLang for the tools on the About page and outsideLang for interests. Technology stacks are a separate case. A project's stack comes from a single, language-neutral array, so there is no second language to compare against. Every item there is already a technology name, so ProjectView uses STACK_LANG, which is "en", directly. ### CatLabel and the edge cases A project can have more than one category, shown on a single line such as "Web / Platform · Mobil". Giving the whole line one lang would be wrong: Blockchain should uppercase the English way, Mobil the Turkish way. That is why CatLabel.vue puts each category in its own span and gives each span its own lang value; the separators stay in the page language. It also helps to know where the rule stops. The comparison looks for exact equality and is case-sensitive. "Web / Platform" and "Web / Platforms" differ, so the label uppercases by the Turkish rule; since it has no i, the result does not change. "Edge sistemler" stays Turkish and becomes EDGE SİSTEMLER, which is right because half of it is Turkish. Field and tool tags are matched by their position in the array, so if the order of the Turkish and English lists drifts, the rule compares the wrong two tags. If a Turkish word happens to be spelled the same as its English counterpart, the rule takes it for a name; I have not seen such a case among the current labels. I accepted these knowingly, because the two language files already share the same structure. ### The same rule on the share cards Every page's 1200×630 share card is drawn with Satori at build time, and the section label at the top of the card is uppercase there too. But Satori has no page where the lang attribute and text-transform work together; JavaScript does the uppercasing. The difference between toLocaleUpperCase('tr') and toLocaleUpperCase('en') is the exact counterpart of the lang difference in CSS. scripts/seo-files.mjs repeats the same comparison for category labels. ``` const up = (s) => s.toLocaleUpperCase(lang === 'en' ? 'en' : 'tr') // same rule as src/utils/lang.ts const upCat = (c) => (tr.cats[c] === en.cats[c] ? d.cats[c].toLocaleUpperCase('en') : up(d.cats[c])) ``` So a category that shows as BLOCKCHAIN on the site does not turn into BLOCKCHAİN on the card. The rule is written in two places, but both look at the same data; when I add a new category I do not have to update either of them separately. ### Reusing it in another project You do not need bilingual content to carry this approach over, but it makes the job much easier. The general idea has three parts: do not assume case mapping is language-neutral, think of the lang attribute at the level of a piece of text rather than the page, and derive the "is this English" decision from data rather than by hand. Even on a single-language site, keeping terms in a separate field and giving them lang="en" holds up better than a word list. Leaving the uppercasing to CSS has a side benefit too: the source text stays in its natural form, and the same data can be used as is wherever no uppercase is needed. Note (Lesson): Tying a rule to data instead of a hand-kept list means nobody has to remember it when new content arrives. On this site, when I add a new tool or category, the right lang value simply follows. --- ## Making a Vue SPA visible to search engines and AI crawlers URL: https://devilyxrddev.com/en/journal/spa-visible-to-ai-crawlers Date: 2026-10 · Search & GEO A single-page app shows an empty div to any crawler that does not run JavaScript. On this site every route becomes real HTML in two languages at build time; hreflang, JSON-LD, the sitemap, robots.txt and llms.txt come from the same content, and the build checks itself at the end. ### The empty div problem The HTML a Vue SPA gets from the server is almost empty: a
and a script tag. The content appears after the browser runs the JavaScript. Google often renders that, but the page fetchers of AI assistants and many preview bots read the page without running JavaScript. For them, this site was nothing but a boot screen and an empty box. One option was to move the site to an SSR-first framework. Instead, I kept the existing Vue app and added a prerender step to the build. The site has no server side; every page's content is known at build time. In that case static HTML files are enough. ### One app, two entries The shared point is src/app/createSiteApp.ts. On the server, or when hydrating, it uses createSSRApp; otherwise plain createApp. The router uses memory history on the server and web history in the browser. The client entry, main.ts, checks whether #app contains an element: on a prerendered page it does, and the app hydrates; on the dev server #app only holds a comment, and the app mounts from scratch. ``` const container = document.getElementById('app')! const hydrate = !!container.firstElementChild const { app, router } = createSiteApp({ server: false, hydrate }) app.use(createHead()) // wait for the first route (lazy views) before mounting router.isReady().then(() => { app.mount(container) restoreSecret() bootDone() }) ``` Waiting for router.isReady matters: most views are lazy-loaded, and mounting before the first route resolves would produce a tree that does not match the HTML the server wrote. On the server, the render function in entry-server.ts does the same job the other way round: it goes to the URL with router.push, waits for isReady, produces HTML with renderToString and puts it in place of in the template. Head tags are merged with unhead's transformHtmlTemplate. The same module also exports the projects, articles and language files, so the build scripts read content from the app itself rather than a second source. ### One file per route The build command runs, in order, the type check, the client build, the SSR build, the prerender and the SEO check. scripts/prerender.mjs walks the whole publicPaths list for both languages. That list is derived in router.ts from the projects and articles; adding an article means two new HTML files. The 404 page is written separately, with noindex. ``` dist/index.html → / dist/work/yee-cms.html → /work/yee-cms dist/en.html → /en dist/en/journal/
.html → /en/journal/
dist/404.html → noindex ``` ### Two languages, two URLs Turkish pages live at the root, English pages under /en. router.ts generates the English routes as copies of the Turkish ones, prefixes their names with en- and sets meta.lang to "en"; the catch-all 404 routes go last. The language is read from the URL, and the setLocale call in beforeEach runs on the server as well as in the browser. That is why the prerendered HTML and the page after hydration speak the same language. useSeo gives every page a self-referencing canonical and hreflang links for tr, en and x-default; x-default points at the Turkish page. og:locale becomes tr_TR or en_US, and the other language is written as og:locale:alternate. The 404 page is noindex, so it carries neither a canonical nor language alternates; it should not point search engines anywhere. ### The layer machines read Every page has a single JSON-LD block with an @graph inside. The Person and WebSite nodes are on every page; Person has a fixed @id, so all pages point at the same person. Page-specific nodes are added on top: BlogPosting for articles, BreadcrumbList for subpages, CollectionPage for the sitemap page. Before the JSON is written into the script tag, < characters are escaped, so no text in the content can close the tag early. sitemap.xml lists every address in both languages; each entry has xhtml:link alternates for tr, en and x-default, and the page's share card as image:image. robots.txt is open to everyone and names the bots in three groups: search engines (Googlebot, Bingbot, Applebot, DuckDuckBot, YandexBot), AI search and assistants that cite sources (OAI-SearchBot, ChatGPT-User, Claude-SearchBot, Claude-User, PerplexityBot, Perplexity-User), and training crawlers and control tokens (GPTBot, ClaudeBot, Google-Extended, Applebot-Extended, CCBot). Each group gets an explicit Allow; if I want to opt out, changing one word to Disallow is enough. The sitemap address sits at the end of the file. llms.txt is a plain markdown map in the llmstxt.org format: a short description, then pages, projects and articles in both languages. llms-full.txt gives the full text of every article and project note in both languages, each with its own URL; article blocks are turned into markdown headings, quotes and code blocks. All of these files are generated in scripts/seo-files.mjs from the app's own content module. > SEO should not be a setting you make once, but a test that runs on every build. ### Let the build check itself The last step is scripts/check-seo.mjs. In every HTML file under dist it looks for exactly one and exactly one <h1>, checks that <html lang> matches the file's path, that og:locale matches the language and that the required meta set is complete. The description must be between 50 and 170 characters. The canonical must be in the sitemap. hreflang links must be self-referencing, x-default must go to the Turkish page, and the page in the other language must link back. og:image and twitter:image must be absolute, the file must exist and it must be 1200×630. JSON-LD must parse, and BlogPosting, BreadcrumbList and Person nodes must carry their required fields. robots.txt must advertise the sitemap. If anything is wrong, the build fails. Note (Note): This article went down the same path: content added, build run, checks passed, and it went live as a separate page in each language, and as plain text in llms-full.txt. --- ## A separate share card for every page and language, drawn with Satori URL: https://devilyxrddev.com/en/journal/og-cards-with-satori Date: 2026-10 · Search & GEO Every page on this site has two 1200×630 share cards, one in Turkish and one in English, all drawn with Satori at build time. The hard part was not the layout but the fonts: ş, ğ and İ first came out as boxes. The fix was to drop font subsets and switch to full TTF files. ### Why one card per page At first the site had a single share image, public/og.png. Whichever page's link you shared, the chat app showed the same picture. For someone sharing an article that says very little. What I wanted was a card that describes the page itself: section label, title, short description and the cover drawing that belongs to that page on the site. Since the site is bilingual, every page needed two cards. og.png is still there; the 404 page and the Person image in the JSON-LD use it. ### Tree, SVG, PNG Satori takes a React-like element tree and font files and produces an SVG. Writing the tree in JSX is not required; plain objects with type and props fields are enough. scripts/og.mjs builds these objects with a small h helper. Layout is done with the flexbox subset Satori supports. @resvg/resvg-js fits the resulting SVG to a width of 1200 pixels and turns it into a PNG. The loadSystemFonts: false setting keeps the build machine's fonts from leaking into the card; the card is drawn with the same fonts on every machine. ``` const h = (type, style, ...children) => ({ type, props: { style, children: children.length === 1 ? children[0] : children } }) const svg = await satori(tree, { width: 1200, height: 630, fonts }) return new Resvg(svg, { fitTo: { mode: 'width', value: 1200 }, font: { loadSystemFonts: false } }).render().asPng() ``` The cover drawing is the same procedural shape list the site draws in the browser. The rectangles and ellipses from artOf are written into an SVG and placed on the card as a data URI through an img element. Shapes that contain text are separated and laid out by Satori on their own, so they are drawn in the site's font too. Very thin lines are kept at least about 1.2 pixels thick so they do not disappear during rasterisation. ogEntries in scripts/seo-files.mjs decides which cards to draw: one entry per public page, per language. Each entry has a section label, title, description and cover name. If a page has no matching entry, the function throws; a page whose card was forgotten stops the build. ogKeyFor produces the file name: the home page is home, /work/yee-cms is work-yee-cms, and English cards get an en- prefix. prerender.mjs writes them under dist/og, and useSeo binds og:image and twitter:image to the same address. ### When Turkish letters became boxes In the first attempt I took the fonts from @fontsource packages. These packages split fonts into subsets by script: basic Latin in one file, extended Latin, where letters such as ş, ğ and İ live, in another. In the browser that split makes sense, because the browser only downloads the file it needs. Satori, however, needs every file handed to it as a separate font. I registered the extended subsets under separate family names and expected the missing letters to fall through to them. They did not: ş, ğ and İ came out on the cards as boxes or in the wrong face. The family names I gave the subsets were not parsed the way I expected either. The comment in og.mjs sums it up: Satori does not fall back between families reliably. The fix was to stop dealing with subsets. The @expo-google-fonts packages on npm ship Google Fonts families as full-charset TTF files. With one complete file per weight, no fallback is needed. These packages are only in devDependencies; they are never sent to the browser. ``` async function ttf(pkg, file) { const dir = path.dirname(require.resolve(`${pkg}/package.json`)) return fs.readFile(path.join(dir, file)) } { name: 'Syne', weight: 800, style: 'normal', data: await ttf('@expo-google-fonts/syne', '800ExtraBold/Syne_800ExtraBold.ttf') } ``` > Splitting a font into pieces saves bytes in a browser and causes bugs in a card generator. ### A small patch for g and ğ On the site, the g and ğ in headings come from Archivo Expanded rather than Syne; I told that story in a separate article. The cards had to show the same letters. At build time, the archivoPatch function asks Google Fonts for a stylesheet containing only these two letters. It sends Mozilla/4.0 as the User-Agent; Google gives such an old client TrueType instead of WOFF2. Satori can read TTF, OTF and WOFF, but not WOFF2. Both requests are bound to an 8 second timeout. Without a network, the function logs a warning and returns null, and the cards are drawn with Syne's g; the build does not break. Satori has no per-glyph font selection like the browser, so displayText splits the title itself. Every word becomes its own flex item, and the runs of g and ğ inside a word go into a separate span with fontFamily: 'Archivo'. Using one flex item per word has a second benefit: a line never breaks in the middle of a word. The space between words is a columnGap of 0.24 times the font size. ### Layout limits and the check Titles take one of four sizes based on length: 88 pixels up to 14 characters, 72 up to 22, 60 up to 36, and 50 beyond that. The description is cut at 150 characters on a word boundary and ends with an ellipsis. The text column is 610 pixels wide, with a 420 pixel square cover on the right. The bottom row holds the devilyxrd wordmark (the x in iris) and the domain taken from SITE_URL. The section label is uppercase and follows the Turkish İ rule. The last build step, check-seo, verifies that og:image and twitter:image on every page are absolute and that the file exists. It then reads the width and height from the PNG header and checks for 1200×630. The cards can be seen during development too: the plugin in vite.config.ts draws /og/<key>.png requests on the fly with the same renderOg function and keeps the fonts in memory after the first load. Note (Lesson): If you generate images with fonts on a server, use full font files. Subsets are an optimisation made for the browser; in an environment without fallback logic they only move the problem somewhere else. --- ## Syne's g: not a bug but the design, and I still replaced two letters URL: https://devilyxrddev.com/en/journal/syne-g-archivo-patch Date: 2026-10 · Design & interaction The lowercase g and ğ of Syne, the site's display font, looked broken to us at large sizes. I checked the font file; it was not a bug but the design itself. The fix was a patch of a few KB that takes only these two letters from Archivo, plus the browser choosing fonts letter by letter. ### A letter that looked broken The site's headings are set in Syne, mostly at weight 800. It is a wide font with a lot of character, and it carries much of the site's identity. But the lowercase g always looked odd to us at large sizes. At first we took it for a rendering bug: the letter looked as if a part were missing or misplaced. Since ğ is common in Turkish, it stood out not only in English words but in Turkish headings too. ### First I looked for the bug on my side The usual suspects were clear: the subset Google Fonts sends, the browser's synthetic bold, or a setting in my own CSS. I ruled them out one by one. I compared the letter with the full-charset Syne TTF that I also use for the share cards; the same shape was there. I looked at the font's stylistic sets as well and did not find the kind of alternative g I was hoping for. The conclusion was clear: this is not a bug, it is Syne's original design. Knowing that mattered, because if it had been a bug, loading the right file or fixing a setting would have been enough. Since it was the design, I had three options: change the display font entirely, accept the letter as it is, or take only these two letters from another font. The first would have changed the look of the site from scratch, the second would have kept bothering us. I chose the third. ### A two-letter font I picked Archivo as the stand-in. Archivo has a width axis, and wdth 125 corresponds to its Expanded cut; to my eye these wide proportions were the option that sat most quietly next to Syne. The Google Fonts css2 endpoint supports a text parameter: it returns a font file containing only the requested characters. text=g%C4%9F means gğ. Since --f-display is used at weights 800 and 600 on the site, I ask for 600, 700 and 800. The stylesheet loads with the media="print" and onload trick, without blocking the first paint. ``` <!-- index.html --> <link rel="stylesheet" media="print" onload="this.media='all'" href="https://fonts.googleapis.com/css2?family=Archivo:wdth,wght@125,600;125,700;125,800&text=g%C4%9F&display=swap" /> /* src/styles/tokens.css */ --f-display: 'Archivo', 'Syne', 'Space Grotesk', sans-serif; ``` The CSS Google returns for this request is instructive. There is one @font-face rule per weight: font-family 'Archivo', font-stretch 125% and unicode-range: U+67, U+11F, U+306. The first two are g and ğ. The third is the combining breve; since ğ can also be written as g followed by that mark, it is in the file too. The result is a font of a few KB for two letters. ### Fallback works letter by letter The browser's font matching rules do the rest. Archivo is first in the --f-display list. When drawing text, the browser looks for the first suitable face in the list for each character; if the character is outside a @font-face rule's unicode-range, or the font has no glyph for it, it moves on to the next family. So in the word "doğru", d, o, r and u come from Syne and ğ comes from Archivo. Uppercase G is not in the range, so it stays in Syne as well. Weights match by the same logic. Since Archivo has a separate @font-face rule per weight, the g in a heading set at 800 comes from Archivo's 800 cut, and in text set at 600, such as .ablead, from the 600 cut. Space Grotesk, third in the list, is a safety net for when Syne has not loaded yet or cannot load at all; the final sans-serif is its own fallback. unicode-range has another benefit: the browser only downloads this font file when a character in that range is about to be drawn with this font stack. For a heading without g or ğ, Archivo is never requested. To be honest, there is a small cost too: since the stylesheet loads without blocking and with display=swap, the g can briefly show in Syne before Archivo arrives and then switch. The boot screen is outside this rule. Its DEVILYXRD word uses Syne directly rather than --f-display; it is uppercase and has no g, so it does not need the patch. ### The same patch on the share cards Satori, which draws the share cards, has no unicode-range and no per-glyph fallback, so the same split has to be made by hand. At build time, scripts/og.mjs downloads the same Archivo subset from Google Fonts as TrueType. displayText splits the title into words, and each word into runs of g and ğ and everything else; the g and ğ runs are drawn with the Archivo family. Without a network the patch is skipped and the cards are drawn with Syne's g. ``` const runs = hasPatch ? w.split(/([gğ]+)/).filter(Boolean) : [w] return h('span', { display: 'flex' }, ...runs.map((r) => (/^[gğ]+$/.test(r) ? h('span', { fontFamily: 'Archivo' }, r) : r))) ``` > You can change two letters of a font without changing the font; the browser already picks letter by letter. Note (Note): Before calling a glyph a bug, look at the original file. If it turns out to be the design, the cleanest path is to admit openly that you are overriding a designer's decision and to keep the change as narrow as possible. Note (Update): After this article I moved the fonts off Google Fonts and serve them from my own server. Archivo's g and ğ file is now a 2 KB woff2 under /fonts; the @font-face rules live in index.html and cover only those two letters through unicode-range. Same method, the file just no longer comes from a third-party server. --- ## Moving the loading screen jank to the compositor URL: https://devilyxrddev.com/en/journal/compositor-only-loading-screen Date: 2026-10 · Interface The DEVILYXRD boot screen stuttered in its first version. The fix was not faster JavaScript, but leaving no work at all for the main thread: four real milestones, parts that move only with transform, and timers that step in when things go wrong. ### Why it stuttered The boot screen runs at the moment the browser is busiest: HTML is being parsed, fonts are downloading, the JavaScript bundle is being evaluated and Vue is hydrating the prerendered page. In the first version, the progress bar and the counter were updated by a JavaScript loop running roughly every 16 milliseconds. While the main thread was setting up the app, the steps of that loop were delayed, and the animation moved in jerks exactly when it was on screen. The screen itself lives in index.html as inline CSS and an inline script. That way it can paint before any JavaScript bundle or font arrives. The container has contain: strict, so nothing that changes inside it affects the layout of the rest of the page. ### Transform only I kept the rule simple: everything that moves on the boot screen uses only transform, plus opacity when fading. These two properties can be animated on the browser's compositor layer, so they stay smooth even while the main thread is busy. JavaScript no longer drives the animation; it only writes the target value. A CSS transition does the motion in between. When the .out class is added, the curtain slides up with translateY(-100%). Each letter of DEVILYXRD is a separate span; they start hidden below with translateY(112%) and rise into place when the .ready class arrives. Each letter's --i variable shifts its transition delay in 55 millisecond steps, so the word opens from left to right. On exit the same letters escape upward with -112%, this time in 22 millisecond steps. The bar changes its scaleX value, not its width. ``` function set(v) { if (v <= shown) return shown = v bar.style.transform = 'scaleX(' + v / 100 + ')' var d = [Math.floor(v / 100), Math.floor(v / 10) % 10, v % 10] for (var i = 0; i < 3; i++) cols[i].style.transform = 'translateY(' + (-1.6 * d[i]) + 'em)' } ``` The counter is an odometer. Each digit is a strip of numbers stacked on top of each other: only 0 and 1 for the hundreds, 0 to 9 for the tens and the ones. Each number is 1.6em tall, and so is the window of each digit. To show the right number it is enough to shift the strip up by -1.6 × the digit; CSS does the transition again. The set function ignores any smaller value, so the counter never goes backwards. ### Four real milestones Instead of a fake timer, progress is tied to real events. One task after the script runs, the value becomes 12; I delay it with a setTimeout so that the first transition actually animates. On DOMContentLoaded it becomes 42. When document.fonts.load has loaded Syne 800 and IBM Plex Mono 500, it becomes 76 and the .ready class that starts the letter reveal is added. The last step is 100: after main.ts mounts the app it calls bootDone in boot.ts, which waits for document.fonts.ready and then triggers window.__bootDone. Every milestone also has a safety net. Waiting for fonts takes at most 1.5 seconds; after that the letters open without the font. If the app never reports back, __bootDone is called anyway after 6 seconds. The screen stays for at least about 1.1 seconds so the letters can finish moving. After reaching 100 it waits another 420 milliseconds, so the bar can land and the hydrated page can paint once under the curtain. ### As the curtain lifts The leave function adds the .out class, sets window.__bootLeft to true and dispatches an event named boot:leave. Once the transition is over, the element is removed from the DOM and the booting class on <html>, which locks scrolling, is removed. The bootReady promise in boot.ts waits for that event; on the server, on a page without a boot screen, or if the curtain has already lifted, it resolves at once. The HeroScene on the home page starts its animation with bootReady.then, so the hero opens as the curtain lifts, not behind it. The script calls performance.mark at two points: boot:fonts when the fonts are ready and boot:leave when the curtain lifts. These marks show up on the timeline in the browser's performance panel, so you can see which step the boot was waiting on instead of guessing. ### For people who prefer less motion If the reduce motion setting is on in the operating system, the screen behaves differently. Under prefers-reduced-motion, the letters, the bar and the counter sit in their final state without transitions. The curtain fades out in 240 milliseconds instead of sliding. The minimum time drops from 1.1 seconds to 200 milliseconds, the wait after 100 is dropped, and the element is removed after 300 milliseconds. The same check is made both in CSS as a media query and in the script with matchMedia, so the timings follow the preference too. > Smoothness usually comes not from making code faster, but from taking work off the main thread. After this change, all the script does is write a few lines of style. However busy the main thread gets, when the next target value arrives the motion continues from where it was. Note (Lesson): If an animation stutters, the first question should be "what am I moving, and who is moving it". If width, height or position is changing, the answer is usually transform; if JavaScript runs on every frame, the answer is usually a CSS transition. --- ## x³: keeping the Mauve hidden mode discoverable and cheap URL: https://devilyxrddev.com/en/journal/hidden-mode-easter-egg Date: 2026-10 · Design & interaction Click the x in the devilyxrd wordmark in the footer three times and the site switches to Mauve mode. In this note I explain how the secret gives itself away, what changes when it turns on, and how I kept all of it cheap, with a class and a few variables, while respecting reduced motion. ### A secret that can be found A hidden feature nobody finds does not really exist. So I layered the hints. The x in the big footer wordmark is actually a button, and a small exponent sits next to it: x³. Until someone touches it, the x wobbles now and then on a seven-second loop (xwob). Anyone who opens the console sees a coloured x³ and a short note telling them to tap the x below three times; consoleHint prints it after the app mounts. There is also a shortcut: an action in the command palette that toggles Mauve mode. The x button has an aria-label (toggle Mauve mode). So for someone using a screen reader it is not even a secret, just a plain button. The surprise stays on the visual side, not on the access side. The command palette action is part of the search index too. searchIndex.ts registers it with its label in both languages and keywords such as mauve, iris, secret and gizli; someone who opens the palette and types secret or mauve lands directly on it. It is a shortcut for someone who knows the secret but does not want to scroll down to the footer. Like the header badge, the palette action calls toggleSecret, so it flips the mode without waiting for three clicks and plays the celebration from the centre of the screen. ### Three clicks The counter is a single number in the ui store: secret. It goes up with every click, and whether the mode is on is derived from it. The exponent in the footer shows ui.secret % 3 || 3; in turn x¹, x², x³. With each click the x turns another 30 degrees and a short toast says x¹ or x². The third click flips the mode. ``` // src/stores/ui.ts export const mauve = computed(() => Math.floor(ui.secret / 3) % 2 === 1) // src/composables/useSecretMode.ts export function clickSecret(x: number, y: number, xEl?: HTMLElement) { ui.secret++ const step = ui.secret % 3 if (xEl && !isReduced()) gsap.to(xEl, { rotate: step * 30, duration: 0.5, ease: 'back.out(3)' }) document.documentElement.classList.add('x-found') if (step === 0) celebrate(x, y) else showToast(step === 1 ? 'x¹' : 'x²', 1200) } ``` On the first click the html element gets the x-found class and the wobble stops. The hint has done its job and no longer needs to draw attention. Deriving the state from a single number also means turning off works with the same arithmetic: the sixth click turns the mode off, the ninth turns it back on. ### What happens when it turns on The celebrate function first saves the state and shows a toast. Then, if reduced motion is off, two animations play. The first is the shockwave: a clip-path circle that grows from the click point to the farthest corner of the screen. The element is appended to body, inverts the page beneath it with mix-blend-mode: difference, and is removed when the GSAP timeline finishes. The second is the scatter: the letters of the footer wordmark spring back into place from random positions and angles with an elastic ease. Everything that changes while the mode is on lives in CSS, under .app.mv. The Iris variable is set to Mauve (#C7A8BE), so everything using the accent colour changes together. The cursor variables switch to x-shaped SVGs. .app.mv::after lays thin horizontal lines over the whole screen without blocking clicks. In the hero the chaos level goes from 1 to 2; the rotation and random offsets of the crosses are multiplied by that number, and the rings turn further. ``` /* tokens.css */ .app.mv { --iris: var(--mauve); } /* shell.css */ .app.mv::after { content: ''; position: fixed; inset: 0; z-index: 300; pointer-events: none; background: repeating-linear-gradient(0deg, rgba(199, 168, 190, .045) 0 1px, transparent 1px 3px); } html:not(.x-found) .ftw .xbtn { animation: xwob 7s var(--e) 3s infinite; } @media (prefers-reduced-motion: reduce) { .ftw .xbtn { animation: none !important; } } ``` > A secret is worth the small surprise of finding it; every visitor should not have to pay for it. ### Persistence and the way out The mode lasts for the session. persist writes 1 or 0 to sessionStorage under the key dvx-secret; main.ts calls restoreSecret after mount, and if the value is 1, ui.secret becomes 3. I put this after mount on purpose: the prerendered HTML is always in normal mode, and the client's first render has to produce the same markup. I chose sessionStorage because this is a game; I wanted the site to return to normal once the tab is closed. If storage is blocked, a try/catch makes the mode simply start off. While the mode is on, a badge with a dashed Mauve border appears in the header: Mauve ×. Clicking it runs toggleSecret, which jumps the counter to the next multiple of three and plays the same celebration from the click point. The badge is there so someone who turned the mode on by accident can leave without scrolling back down to the footer. ### Reduced motion and cost With prefers-reduced-motion on, the x does not turn, the shockwave and scatter do not play, and the wobble animation is disabled in CSS. The mode still turns on and the toast still appears; there is just no motion. The click pulse in the hero is never created in that case either. On the cost side, almost nothing runs while the mode is off: a number, a computed and one CSS animation on the x. The shockwave element exists only at the moment of the click and is removed when it is done. When the mode turns on, the hero watches the mauve value and redraws all of its cells once (repaintAll), then everything returns to its normal loop. There is no separate theme file and no separate component tree. Note (Lesson): What made the easter egg discoverable was the layering of hints: a visual exponent, a slight wobble, a note in the console. What keeps it cheap is that the mode is nothing more than a class and a few variables. --- ## Language lives in the URL: Turkish at the root, English under /en URL: https://devilyxrddev.com/en/journal/bilingual-routing Date: 2026-10 · Interface When I made the site bilingual I did not leave the language to localStorage or the browser setting; I put it in the address. Turkish sits at the root, English under /en; the router reads the language from the URL and LocLink keeps every link in the language you are reading. ### Why the address? In the first version the language lived in a small typed store and the preference was written to localStorage under the key dvx-lang. It worked for visitors but got stuck in three places. First, search engines: when one address serves content in two languages, which one gets indexed is unclear. Second, sharing: a link I sent to someone who reads English could open in Turkish in their browser. Third, prerendering: the build turns every route into static HTML, and there is no localStorage on the server. For the server to know which language to render, the language had to be part of the request itself. I also considered reading the browser language and redirecting, but that brings the same problem back through another door: the same URL shows different content to different people. I wanted every page to have its own address in each language. Turkish is the default, so it stayed at the root (/work), and English got the /en prefix (/en/work). ### Copying the routes I did not write two route lists. The Turkish routes are the single source; the English ones are a copy produced with map. The path gets /en in front, the name gets the en- prefix, and lang: 'en' is written into meta. Home becomes /en, and the catch-all route used for 404 becomes /en/:pathMatch(.*)*. ``` const english = base.map((r) => ({ ...r, path: r.path === '/' ? '/en' : r.path === '/:pathMatch(.*)*' ? '/en/:pathMatch(.*)*' : '/en' + r.path, name: 'en-' + String(r.name), meta: { ...r.meta, lang: 'en' as const } })) as RouteRecordRaw[] export const routes: RouteRecordRaw[] = [...english.slice(0, -1), ...base.slice(0, -1), english[english.length - 1], base[base.length - 1]] // makeRouter(server): runs on the server render and in the browser router.beforeEach((to) => { setLocale(localeFromPath(to.path)) return true }) ``` Order matters here. Both catch-all routes go to the end of the list, and the English one comes first; otherwise an unknown address under /en would fall into the Turkish 404. For project and article slugs that do not exist, a helper called notFound runs inside beforeEnter: it keeps the address the visitor typed and renders the 404 view in the right language. ### The URL decides The actual decision lives in a single beforeEach. On every navigation the router looks at to.path, derives the language with localeFromPath and calls setLocale. This hook is deliberately installed before the server check, so it runs both during the server render at build time and in the browser. The server uses memory history, the browser uses web history; the language decision comes from the same line in both. This matters for hydration too. The language store always starts in Turkish, but before the first render the router steps in and writes the language from the address. In main.ts, mounting waits for router.isReady(). So /en/work renders in English on the server and on the client, and the markup matches exactly. The comment in main.ts says as much: nothing is left to do for language after mount. ### Path helpers All conversions live in three small functions in src/i18n/paths.ts. localeFromPath returns en if the path is exactly /en or starts with /en/, and tr otherwise. basePath strips the prefix and gives the language-neutral path, and localize turns that path into the target language. ``` export const localeFromPath = (path: string): Locale => (path === '/en' || path.startsWith('/en/') ? 'en' : 'tr') /** '/en/work' → '/work', '/en' → '/' */ export const basePath = (path: string) => (localeFromPath(path) === 'en' ? path.slice(3) || '/' : path) /** '/work' + en → '/en/work', '/' + en → '/en' */ export const localize = (path: string, locale: Locale) => { const base = basePath(path) return locale === 'en' ? (base === '/' ? '/en' : '/en' + base) : base } ``` Checking only for the exact match and the /en/ prefix is a small but deliberate detail. Had I written a plain startsWith('/en'), adding a page like /entries later would have quietly counted it as English. ### LocLink and the language switch Writing /work in a RouterLink all over the components would send a reader on an English page back to Turkish on every click. Instead I wrote a small component called LocLink and registered it globally in createSiteApp. It takes the same to value: a string starting with / goes through localize, an object with a path field has only that field translated, and targets that are only a hash or a query pass through untouched. Then it renders an ordinary RouterLink. The header, footer, menu and cards all use it; a crawler following links never switches language at any point. The language switch does the reverse of the same idea. The TR and EN buttons in SiteHeader do not flip a variable; they go to the current page's address in the other language with router.push, carrying the query and hash along. aria-pressed says which language is active. When the language changes, <html lang> updates through useHead in App.vue, and every page writes hreflang links for tr, en and x-default through useSeo. > Once language stops being a preference and becomes part of the address, the server, the browser and the search engine all see the same thing. ### What I gained, what I gave up The prerender script now renders every path twice: Turkish as dist/<route>.html, English as dist/en/<route>.html. The sitemap lists both languages together. When I share a link, the other person sees the language I saw, and nothing needs to be remembered for that. What I gave up is automatic language guessing. Someone arriving with an English browser sees the Turkish root on the first visit and has to press EN. setLocale still writes the preference to localStorage, but the address, not that value, now decides the language. I accepted this on purpose; for a portfolio, predictable addresses felt worth more than saving one click on the first visit. Note (Lesson): Once the language went into the address, the server render, hydration, SEO and sharing all started drawing from the same source. A single beforeEach in the router replaced logic I would otherwise have written in four different places. --- ## From an XSLT-styled sitemap.xml to a real HTML sitemap page URL: https://devilyxrddev.com/en/journal/xslt-sitemap-to-html Date: 2026-10 · Search & GEO I first dressed up sitemap.xml with an XSLT stylesheet so that people could read it too. When browsers set out to remove XSLT support, I dropped it: the XML stayed for machines only, and for people I wrote a bilingual, prerendered /sitemap page. ### Showing XML to people The site's sitemap.xml lists every page in two languages; each entry also carries its counterpart in the other language and the page's share card. That is exactly what search engines need. But a person who opens the file in a browser sees a tree of nested tags. I wanted the sitemap to be useful for people too: which pages exist, in which language, at which address. The first solution was an old and well-known one. I added an xml-stylesheet processing instruction to the top of the XML file and pointed the browser at an XSLT file at /sitemap.xsl. That file was also generated in scripts/seo-files.mjs and served as text/xsl. The browser ran the XML through this transform and showed an HTML table in the site's colours: share cards, language tags and links to the other language. Crawlers kept reading the raw XML. ``` <?xml-stylesheet type="text/xsl" href="/sitemap.xsl"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:s="http://www.sitemaps.org/schemas/sitemap/0.9"> <xsl:output method="html" encoding="UTF-8" indent="yes" doctype-system="about:legacy-compat"/> <xsl:template name="rel"> <xsl:param name="u"/> <xsl:variable name="rest" select="substring-after(substring-after($u, '://'), '/')"/> <xsl:value-of select="concat('/', $rest)"/> </xsl:template> ``` ### Why I gave it up When I opened the page in Chrome, the console showed a warning that XSLT is being deprecated. Browsers are on the way to removing XSLT support. Tying a feature to a technology that is openly on its way out is a dead end; the fact that it works today does not mean it will not quietly stop working one day. Even without the warning, this path had other costs. XSLT 1.0 is a separate and fairly limited language: even turning an absolute address into a relative one meant writing a template with nested substring-after calls, as above. The view lived entirely outside the Vue app; the components, the menu and the design tokens were not there. I had rewritten the colours in a separate style block inside the XSL file. Whenever a colour changed on the site, I would have had to remember that copy too. ### XML for machines, HTML for people In the end I split the job in two. The XSL file and the processing instruction went away; sitemap.xml became plain XML again and is there for machines only. A comment at the top of the file shows anyone who opens it the addresses of the Turkish and English sitemap pages, and the robots.txt, llms.txt and llms-full.txt files. The split did no harm to sitemap.xml. Everything that matters to search engines is in place: for every address, xhtml:link alternates for tr, en and x-default, the share card as image:image, lastmod, changefreq and priority. The home page is listed at 1.0, top-level pages at 0.8, project and article pages at 0.6, and the sitemap page itself at 0.3. For people there is src/views/SitemapView.vue. It is a Vue route like the others: it lives at /sitemap and /en/sitemap, is prerendered in both languages at build time, has its own share card, and since it is in publicPaths, it is listed in sitemap.xml as well. The page is reached from a link in the footer. ### The /sitemap page The page shows three groups: pages, projects and articles. Pages are listed from the home page and the menu links, projects with category, year and status, articles with category and date. Each row has a small image from the page's share card, a title link, the page's address in mono type, a short subline and a link to its counterpart in the other language. That last link carries an hreflang attribute. The card images are deliberately loaded from relative addresses, so the same page works the same on localhost, in previews and on the live site. The counter at the top shows twice as many addresses as the length of publicPaths. At the bottom there is a "For machines" section: sitemap.xml, robots.txt, llms.txt and llms-full.txt, each with a short description. The page's JSON-LD contains a CollectionPage node with an ItemList that counts every public page in order, along with a BreadcrumbList. So machines can recognise this page as a directory too. > Rather than trying to make people read XML, it is more honest to give people a page of their own. ### The same files on the dev server Since the sitemap page links to the machine files and shows the share cards, those files had to exist during development too. Normally they are all generated at the end of the build. A small plugin in vite.config.ts runs only on the dev server and intercepts requests for /robots.txt, /sitemap.xml, /llms.txt, /llms-full.txt and /og/<key>.png. It reads the content through entry-server.ts with Vite's SSR loader and calls the same textFiles and renderOg functions the build uses. ``` const isText = ['/robots.txt', '/sitemap.xml', '/llms.txt', '/llms-full.txt'].includes(url) const og = /^\/og\/([a-z0-9-]+)\.png$/.exec(url) if (!isText && !og) return next() const mod = await load(server) // ssrLoadModule('/src/app/entry-server.ts') const seo = await import('./scripts/seo-files.mjs') ``` This has a nice side effect: the file I see on the dev server and the file that goes live come from the same code. When I change an article's title and reload the page, sitemap.xml, llms.txt and the card image have changed too. Note (Lesson): When a file has two different readers, it is often simpler to give each its own format than to try to please both with one. Machines read XML, people read HTML; as long as both are generated from the same content, they never drift apart. --- ## I stopped chasing the pointer: a native SVG cursor with CSS cursor URL: https://devilyxrddev.com/en/journal/native-svg-cursor Date: 2026-10 · Design & interaction On most sites a custom cursor is an element chasing the mouse, and it always lags a little. On this site the browser draws the cursor itself: an SVG data URI in the cursor property, two CSS variables and an x for hidden mode. The JavaScript disc that remains only carries a label. ### The old approach In the first version a ring followed the system cursor around. A fixed-position element took the target coordinates from pointermove, and a requestAnimationFrame loop moved it a bit closer to that target every frame. It grew over interactive elements and showed a label inside on elements with a data-cur attribute. It looked nice but had two problems. The first was structural lag: the element is drawn after the event arrives, and with easing added the ring always trails the real pointer. The second was cost: the loop asked for the next frame on its first line, so it ran every frame even while the mouse was still. When the main thread was busy, this ring was the first thing to stutter. The operating system cursor, on the other hand, is drawn independently of the page's JavaScript load. CSS's cursor property accepts a custom image through url(). So I could leave the movement to the browser and supply only the look. ### Two variables, two cursors tokens.css has two variables: --c-def and --c-ptr. Their default values are plain keywords: default and pointer. Only when (hover: hover) and (pointer: fine) matches, meaning there is a precise pointer, are these values replaced with SVG data URIs. The default cursor is a cross of four short arms with a small Iris circle in the middle; the cursor on clickable elements is a ring with a Soft dot at its centre. ``` :root { --c-def: default; --c-ptr: pointer; } @media (hover: hover) and (pointer: fine) { :root { --c-def: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'><path d='M16 4v7M16 21v7M4 16h7M21 16h7' stroke='%230B0C11' stroke-width='3.5'/><path d='M16 4v7M16 21v7M4 16h7M21 16h7' stroke='%23EDECF2' stroke-width='1.5'/><circle cx='16' cy='16' r='3.2' fill='%23A8A1F1' stroke='%230B0C11' stroke-width='1.5'/></svg>") 16 16, default; --c-ptr: url("data:image/svg+xml,...") 16 16, pointer; } .app.mv { --c-def: url("data:image/svg+xml,...") 16 16, default; /* the x */ } html, .app { cursor: var(--c-def); } .app a, .app button, .app label, .app summary, .app [role='button'], .app input[type='range'] { cursor: var(--c-ptr); } .app input[type='text'], .app input[type='search'], .app textarea { cursor: text; } } ``` In each SVG the strokes are drawn twice: first a thick Void (#0B0C11) outline, then the actual colour, thinner, on top. That keeps the cursor readable on the dark background as well as on Iris and Soft surfaces. Because the colours sit inside a data URI, the # character is written as %23. I wrote the image into the CSS as a data URI rather than as a separate file. That way the cursor arrives with the stylesheet, does not wait for a separate request, and sits in the same file where the colours are defined, right below the tokens. When I want to change a colour, I know where to look. The cost is readability: editing a one-line SVG is not as comfortable as editing a separate file. I accepted that; cursors are not something that changes often. ### Hotspot and fallback The 16 16 after url() is the cursor's click point. The SVG is 32×32, so the hotspot is dead centre; the middle of the cross and the point that gets clicked are the same. Without it the browser would use the top-left corner, and the cursor would look like it sat half an icon away from where it clicks. The default and pointer after the comma are the fallback. If the browser cannot use the image, it drops to that keyword; the cursor syntax requires a url() list to end with a keyword anyway. Usage is simple: html and .app get --c-def; a, button, label, summary, [role='button'] and range inputs inside .app get --c-ptr. Text fields keep the text cursor, because there the reader really needs to see the caret position. Component styles also use cursor: var(--c-ptr) directly, and some playground stages pick their own cursor, such as ew-resize. > When the browser draws the cursor, there is no loop left to fall behind. ### Touch devices and hidden mode Touch screens have no cursor to begin with. On devices with a coarse pointer the media query does not match and the variables stay as plain keywords. I did not need a separate device check. The real benefit of variables showed up in hidden mode. When Mauve mode is on, the .app element gets the mv class, and inside the same media query .app.mv redefines both variables: the default cursor becomes a Mauve x, and the cursor on clickable elements becomes a ring with an x inside. No component changes, the cursor rules stay the same; only the variable's value changes, and the whole page picks up the new cursor. ### The disc that remains CustomCursor.vue is still there, but its job changed. Some elements carry a data-cur attribute; going by the examples in a comment in shell.css, a label like Open or Copy. The disc appears only while the pointer is over such an element, and it carries the label. The pointermove listener looks for the label; if there is none, it never starts the loop. ``` function loop() { raf = 0 if (!vis || !el.value) return rx += (cx - rx) * 0.22 ry += (cy - ry) * 0.22 el.value.style.transform = `translate3d(${rx.toFixed(1)}px,${ry.toFixed(1)}px,0)` if (Math.abs(cx - rx) > 0.2 || Math.abs(cy - ry) > 0.2) raf = requestAnimationFrame(loop) } ``` When there is a label, the disc closes part of the remaining distance each frame, and once the gap drops below 0.2 pixels the loop stops itself. Touch events are ignored; with reduced motion on or a coarse pointer, the listeners are never added. The lag is acceptable here, because the disc is not the cursor but a label next to it. The real cursor is always in place. The cost is clear too: since the cursor is an image, I cannot animate it, for example growing on hover; state changes are instant. Note (Lesson): For a custom cursor, the first thing to try is changing the cursor's image, not the cursor itself. Lag, an idle loop and the touch-device check all disappeared with one CSS property; JavaScript was left only for the label, which genuinely needs it. --- ## Pause what you cannot see: sleeping loops offscreen and behind overlays URL: https://devilyxrddev.com/en/journal/pausing-what-you-cannot-see Date: 2026-10 · Interface A lot moves on this site: the hero field, the rings, the scrolling word band, the cover art in the menu. In this note I write down the rules they run by: transform and opacity only, rAF loops that bypass Vue, and small mechanisms that stop anything you cannot see. ### The rule: transform and opacity only My first rule for animation: whatever changes per frame should be transform or opacity. These two properties can usually be handled on the compositor without triggering layout or paint. That is why the scroll-linked reveals in base.css (rv1, rv2, rv3) use only translate, scale, rotate and opacity. They run entirely in CSS with animation-timeline: view(); browsers without support simply show the content. The same rule holds in small places. The crosshair lines in the hero move with translateX and translateY rather than left and top. The hero's size is measured once, cached, and reset only on resize or scroll, so no layout is read while things move. Scrolling needs the same care. The scroll listener in usePointer does not do work on every event; it asks for a frame and runs in that frame. Writing scroll progress to a :root variable would have been easy, but as the comment in the code says, that would restyle the whole page. Instead the value goes to just two consumers: the progress bar in the header moves with scaleX, the hero rings with translate. The rings' parallax is not updated past the first 1200 pixels, or at all with reduced motion on. ### Keeping Vue out of the loop The hero on the home page is a 14×8 field of crosses and five rings; they follow the pointer, and a click sends a pulse through the field. Had I written this with reactive state, every pointermove would make Vue re-render 112 cells. Instead the field is rendered once. pointermove only updates two numbers and calls schedule, which asks for a requestAnimationFrame only if no frame is already pending. ``` function onMove(e: PointerEvent) { const p = local(e) px = p.x py = p.y schedule() } function schedule() { if (!raf) raf = requestAnimationFrame(paint) } // inside paint(), for each of the 112 cells const still = k === 0 && w === 0 if (still && c.rest) continue // far from pointer and already drawn at rest: nothing to write c.rest = still // end of paint(): only a running pulse asks for another frame if (pulse) schedule() ``` Inside paint, each cell gets its distance to the pointer and, if there is one, the effect of the pulse. If both are zero and the cell was already drawn at rest, continue skips it and nothing is written to the DOM. The loop also stops itself: it asks for another frame only while a pulse is running. When the pointer stops, no frame is requested, so almost nothing runs while idle. GenerativeField in the playground uses the same pattern: 144 bars, angles rounded to 3 degree steps, and style left untouched when the value matches the previous frame. For pointer work on other pages there is useRafThrottle in src/composables/useRaf.ts; it collapses rapid events into one update per frame. ### Pausing what is offscreen CSS animations keep running even when nobody can see them. For that I wrote a directive called v-pause-offscreen (src/composables/useOffscreen.ts). There is one shared IntersectionObserver for the page; when an observed element leaves the viewport it gets the off class. A single rule in base.css does the rest. ``` let io: IntersectionObserver | null = null function observer() { if (!io) { io = new IntersectionObserver( (entries) => { for (const e of entries) (e.target as HTMLElement).classList.toggle('off', !e.isIntersecting) }, { rootMargin: '120px 0px' } ) } return io } /* base.css */ .off, .off * { animation-play-state: paused !important; } ``` Because rootMargin is 120 pixels, loops restart a little before they enter the viewport; you never see a frozen frame while scrolling. The directive is used on the hero rings, the spinning drawings in ProceduralArt, and the OrbitRings and ReactiveGrid experiments. The JavaScript-driven word band, MarqueeBand, applies the same idea with its own IntersectionObserver: when the band is not visible, the loop does not ask for another frame. ### Pausing what is behind an overlay While the menu or the command palette is open, the page sits behind it, but its loops could keep running. The ui store has a computed called overlay: true when the menu or the palette is open. usePointer watches it and puts the ovl class on the html element; in shell.css the spinning and pulsing classes under .ovl (spin, horb, orr, rpA, rpB) are paused. MarqueeBand reads overlay directly; when it is true the loop lets go, and when it turns false a watch starts it again. > Running an animation nobody can see helps no one; stopping it is usually a single class. ### Not rendering at all For some things, creating them late is cheaper than pausing them. The five cover drawings in the menu (about 200 shapes, according to the comment in the code) do not enter the DOM until the menu is opened for the first time; a ref called seen controls that with v-if. The lower sections of the home page, the footer and the closing sections of long pages use content-visibility: auto, so sections far from the viewport skip layout and paint. contain-intrinsic-size: auto 900px remembers a section's real height once it has rendered, so the scrollbar does not jump. Fonts do not hold up the first paint either. In index.html the Google Fonts stylesheet is preloaded first, then loaded with media="print" and switched to all on load; display=swap shows text right away. If JavaScript is off, the plain link inside noscript takes over. ### Reduced motion With prefers-reduced-motion on, some of these loops never start. useReducedMotion keeps a single matchMedia listener. MarqueeBand checks isReduced before starting, the hero creates no pulse on click and skips its entrance animation, and responsive.css turns off the spinning animations and the reveals. A loop that never runs is cheaper than one that is paused. Note (Lesson): Most of the performance work was not about drawing faster but about choosing not to draw. Nothing that is out of view, behind an overlay or standing still needs to do work on every frame. --- ## From design prototype to production code: porting controlled chaos to Vue URL: https://devilyxrddev.com/en/journal/design-canvas-to-production Date: 2026-10 · Design & interaction This site did not start as code but as an interactive design prototype: devilyxrd, Controlled Chaos. In this note I describe how I ported that prototype page by page into Vue 3 and TypeScript, what I took over as it was, and what I added and changed afterwards. ### The starting point: a design canvas The first form of the project was a design prototype. It was called Controlled Chaos, kontrollü kaos in Turkish. The prototype had pages, colours, typography and motion, and next to it a handoff board. That board described what kind of structure to build on the Vue side and which tool should drive which animation. A prototype is a good tool for showing look and behaviour. But routing, SEO, accessibility and performance are not its job. My job was to add those without losing the look. I did it in order rather than all at once: tokens first, then structure, then the scenes, and finally the improvements. ### Tokens first I started the port with the colours, because everything else depends on them. The first line of src/styles/tokens.css still points to the relevant section of the handoff. The palette has nine colours: Void #0B0C11 for the background, Graphite #171922 and Slate #292C38 for surfaces, Soft #EDECF2 for text, Iris #A8A1F1 as the accent, Mauve #C7A8BE as the hidden mode's colour, Dusty #8999BF as a secondary accent, Steel #8A8E9E for muted text and Border #363945 for lines. I added a --body tone for body text. ``` :root { --void: #0B0C11; --graphite: #171922; --slate: #292C38; --soft: #EDECF2; --iris: #A8A1F1; --dusty: #8999BF; --mauve: #C7A8BE; --steel: #8A8E9E; --border: #363945; --body: #C9C8D3; --f-display: 'Archivo', 'Syne', 'Space Grotesk', sans-serif; --f-body: 'Space Grotesk', system-ui, sans-serif; --f-mono: 'IBM Plex Mono', ui-monospace, monospace; } /* hidden mode: footer x clicked three times */ .app.mv { --iris: var(--mauve); } ``` Typography is three families: Syne for headings, Space Grotesk for body text, IBM Plex Mono for labels and numbers. The variables are defined as --f-display, --f-body and --f-mono. A detail came later: an Archivo cut used only for the g and ğ glyphs sits at the front of the --f-display stack. The benefit of putting tokens first showed up in hidden mode: a single line in .app.mv changes the entire accent colour. Had the colours been written into components one by one, that would have turned into a search-and-replace job. ### From pages to components The prototype was thought of page by page; code should be organised by responsibility. I split the folders accordingly. views holds one file per route. features holds the large scenes that belong to a specific page: home page scenes such as HeroScene, MarqueeBand and ProjectIndex, and the eight playground experiments with the ExperimentShell that wraps them. components holds the pieces used everywhere: header, menu, route shutter, command palette, cursor, footer, wordmark and the procedural cover art. composables holds recurring behaviour: reduced motion, the pointer layer, route transitions, hidden mode. Moving content out of the templates was a separate step. The project and article lists are typed files under src/content; the text itself lives in tr.ts and en.ts under src/i18n/locales. Adding an article does not require touching a component, and every key in both languages goes through type checking. I did not add a library for state management. The state shared by the header, menu, palette, footer and views is a single reactive object in src/stores/ui.ts: whether the menu and palette are open, the toast text, the hidden mode counter, the scroll state. Most of the prototype's interactions could be reduced to these few fields; a bigger tool would not have found enough work to justify what it brings along. > A prototype says what will be seen; code says what can be maintained. ### Who owns the motion Everything moved in the prototype. Moving to code, I wanted every motion to have an owner. The rule in the handoff was simple and I kept it: state changes with CSS and Vue transitions, the route shutter and hero entrance with GSAP, the geometry lab with Anime.js. Anime.js lives only in the lazily loaded /playground bundle; a comment in the router repeats this, and the home bundle never reaches it. GSAP was later also used for the shockwave and letter scatter in hidden mode. This split may look like a list of rules, but its real benefit is in debugging. When something stutters, I know which layer to look at; there is no case where two libraries animate the same element at the same time. ### After the port The first commit brought the prototype's nine routes, TR/EN text, the command palette, the full-screen menu and hidden mode. The real work started after that. On performance, I moved the hero out of Vue's reactivity into a loop that writes directly to the DOM, paused offscreen animations and moved the cursor to a native CSS cursor. On SEO, I started rendering every route to static HTML at build time, then moved English under /en and gave every page its own address in both languages. Finally I replaced the prototype's sample content with real projects, a real about text and real articles. Looking back, the prototype helped most by reducing the load of decisions. Colour, spacing, typography and the language of motion arrived already thought through; in the code I could focus on architecture, boundaries and things that can be measured. Note (Lesson): When porting a design to code, moving the tokens first, then the folder structure, and the scenes last worked well. With solid tokens, every later change (hidden mode, new pages, real content) could be added without breaking the existing language. --- ## Moving from npm to pnpm, and why I kept TypeScript at ~5.9 URL: https://devilyxrddev.com/en/journal/npm-to-pnpm Date: 2026-10 · Engineering The project started with npm, and later I moved the package manager to pnpm. In this note I go through what the switch involved: converting the lockfile, the packageManager and engines fields, the strict node_modules layout, and why I pinned TypeScript to the ~5.9 range. ### Why I switched The reason was not speed or disk numbers. I did not measure those, so I will not give any numbers here. What mattered to me was strictness. npm installs node_modules as a largely flat tree; a dependency's dependency also sits at the root, and code can import a package that is never listed in package.json. That phantom dependency works today and breaks the day the package that brought it in drops it. With pnpm, the node_modules at the root exposes only direct dependencies. If the code relies on a package it never declared, I see it right after install, not in production. In a small project that may look like a minor gain, but the habit itself matters: the list in package.json should say what the code actually depends on. ### The lockfile What I watched most closely was that versions did not drift. Instead of letting pnpm-lock.yaml resolve from scratch, I imported it from the existing package-lock.json, so the resolved versions stayed the same. Then I deleted package-lock.json; two lockfiles side by side leave open the question of which one is right. I noted this in the commit message as well: same resolved versions, old lockfile removed. The new lockfile starts with lockfileVersion: '9.0' and records each package with its resolved version and peer dependencies. The @unhead/vue entry, for example, also carries the vite, vue and typescript versions it was resolved against. That makes it easier to see in a diff what changed and why. The settings section at the top of the lockfile is worth reading too: autoInstallPeers: true. In other words, if a package's peer dependency is missing from the project, pnpm installs it itself. In this project peers such as vue are already listed as direct dependencies, so it has little visible effect, but having the setting written into the lockfile makes it explicit how the install will behave. ### Pinning the version I added two fields to package.json and left the scripts alone. ``` "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build && vite build --ssr src/app/entry-server.ts --outDir dist-ssr && node scripts/prerender.mjs && node scripts/check-seo.mjs", ... }, "devDependencies": { ... "typescript": "~5.9", "vite": "^8.3.4", "vue-tsc": "^3.3.13" }, "packageManager": "pnpm@11.9.0", "engines": { "node": ">=20.19", "pnpm": ">=11" } ``` The packageManager field says which pnpm version the project runs with. On a machine with Corepack enabled (corepack enable), the pnpm command reads this field and fetches the right version itself. I wrote that in the README too; someone cloning the project does not need to think about their global pnpm version. The engines field sets the lower bounds: Node 20.19 and pnpm 11. The scripts stayed as they were; pnpm runs the scripts in package.json the same way. Only the spelling changed: pnpm dev instead of npm run dev, pnpm build instead of npm run build. The README and the launch configuration that starts the dev server were updated to these commands. The build chain is the same too: type checking with vue-tsc, the client build, the SSR build, prerendering and the SEO check. > When you move a lockfile, the goal is not new versions but the same versions installed by a different tool. ### Why TypeScript ~5.9 Most packages in devDependencies use ^, but TypeScript uses ~5.9. That is a deliberate choice. ^ accepts everything up to the next major version, while ~ accepts only 5.9.x patches. The reason is TypeScript 7: at the time I wrote this note it was not compatible with vue-tsc. vue-tsc type-checks .vue files on top of TypeScript, and the build script runs vue-tsc --noEmit as its very first step. An incompatible TypeScript version means the build breaks at step one. In the lockfile, typescript is resolved as 5.9.3, and the vue and @unhead/vue entries are tied to that version as well. When vue-tsc gains TypeScript 7 support, widening the range will be a one-line change. Until then I would rather have type checking stop at an explicit boundary than change silently after an install. ### After the switch This project's dependency list is short to begin with: five packages at runtime (vue, vue-router, @unhead/vue, gsap, animejs), and on the development side Vite, vue-tsc, satori and resvg for the OG images, and a few font packages. That is why the switch fit in a single commit: the new lockfile, the deleted old one, two fields in package.json and the updated commands. There was also one thing I deliberately did not do during the switch: update dependencies. Had I changed both the package manager and the versions in the same commit, telling which one caused a breakage would have been harder. I changed the tool first and left version upgrades as separate work. The real value will show later. If I add a new import and forget to put the package in package.json, the install will not hide it; and when I open the project on another machine, I will be working with the same pnpm version, the same lockfile and the same TypeScript. Note (Note): Importing the lockfile, pinning the tool's version with packageManager and keeping an incompatible major version out with ~ let me change package managers without turning it into an upgrade project. --- ## One platform, many sites: the content model in a multilingual CMS URL: https://devilyxrddev.com/en/journal/multi-site-cms Date: 2026-09 · Engineering YEE CMS is a multisite, multilingual platform built to manage the content of Yunus Emre Institute's different centers on a shared foundation. In this article I explain why the content model sits at the center of a system like this, and how I think about the relationship between its layers. ### Shared foundation, separate publishing spaces On paper the problem looks simple: bring the content production, permissions and publishing flow of different centers into one system. The hard part is that every site has to keep its own context while doing so. An editor at one center should see only their own site, languages and approval process, and a visitor should only meet that site's content. This balance between a shared foundation and separate publishing spaces is the real design question, and it runs through every layer. At first glance the two needs pull against each other. Sharing means maintenance, updates and new capabilities reach every site at once. Separation means each center can publish in its own languages, with its own editors and on its own schedule. In systems like this, the first thing I try to do is separate clearly which decisions are common to everyone and which belong to a single site. If that line is blurry, over time you either write a special exception for every site, or the shared structure forces one site's needs onto another. ### Three layers, one data model The system has three parts: a public Next.js web front end, an API written in NestJS and an administration app built with React. All three meet on PostgreSQL, with Drizzle ORM and Docker also part of the stack. This split lets the editor experience and the visitor experience evolve independently. But because they rest on the same content model, both have to follow the same rules. ``` web Next.js → the page a visitor sees admin React → an editor's daily work api NestJS → site context, language, approval db PostgreSQL (Drizzle ORM) ``` The layers also have different priorities. On the public web side, the priority is that a page opens quickly, arrives in the right language and is understandable to search engines. On the administration side, the priority is that editors can work comfortably with forms, drafts and approval steps. The API sits between these two worlds like a referee: it decides which site, language and permission a request belongs to. For me, the most valuable part of this structure is that the rules live in one place, and the interfaces show their results instead of rewriting them. A tool like Docker makes it easier for these layers to come up the same way in different environments. In a system with many parts, that consistency helps reduce the differences between development and production. ### A content model is more than a schema Multilingual content, site context and content approval flows are core parts of this product. Which site a piece of content belongs to, which languages it exists in and which steps it passes before going live shape the screens in the administration panel and the pages on the web as much as the tables in the database. So when I talk about the content model, I am not only thinking about fields and relations. Is a field required, is it translatable, can it be shared across sites: each of these questions turns into a behavior on the editor's screen and a result on the visitor's page. Anything left vague in the model usually comes back as confusion in the interface. In a multisite structure, I find it safer to treat site context not as a filter added later, but as basic information every request carries from the start. > A content model designs the editor's day and the visitor's page together. ### Language and approval: two separate axes A common trap in multilingual systems is to think of a translation as a simple copy of the content. In reality, the versions of the same content in different languages can be prepared at different times, pass through different hands and sit at different stages. In structures like this, it feels healthier to me to treat language and publishing status as two independent axes. That way, one language version being live while another is still in preparation is not an exception, but a natural state of the model. The approval flow deserves the same thinking. Whether content is a draft, in review or published is not just a piece of status information; it also decides who can see what and who can change what. When permissions and workflow are this closely linked, treating them as parts of the same model rather than designing them separately makes consistency easier to keep. ### Where I fit in YEE CMS is a Unitic project and a team effort. My part is full-stack development and integration: front end, services, content management flows and making the layers work together. Integration work shows me, more than anything, how a decision made in one layer finds its counterpart in the others. In this work I focus on how the content model shapes not only the database, but also an editor's daily work and the page a visitor sees. Working in a team, the answers to these questions take shape in shared decisions rather than in one person. My share is to help those decisions find the same expression in the interface, the services and the data. Note (Note): In a multisite system, the question "whose content is this, in which language and at which stage" has to get the same answer in every layer. Inconsistency usually starts there. --- ## One product, three surfaces: web, service and mobile in one monorepo URL: https://devilyxrddev.com/en/journal/one-product-three-surfaces Date: 2026-08 · Engineering Motice is a CRM/ERP product for pilates, yoga, personal training and wellness studios. The administration app, customer portal and mobile app are different faces of the same business processes. This article is about what it means to keep them in one TypeScript monorepo. ### The cost of scattered tools Daily work in an appointment-based studio moves between bookings, customers, classes and business operations. When these are spread across different tools, both the business and the customer look for the same information in several places. Motice brings these flows into a shared product that businesses manage within their own separate workspaces. The cost of this kind of scattering rarely shows up as one big failure; it builds up as small frictions. A piece of information is updated in one place and stays stale in another, and the same question gets two different answers from two different sources. In appointment-based work, time itself is part of the product, so inconsistencies like these land directly on the business's day. In my view, this is exactly where the value of gathering the flows into one product model lies. ### Three surfaces, one model The product has three surfaces: the administration app the business uses, the portal customers reach from the browser, and the customer app built with Expo. All three look at the same booking, the same customer, the same class. So we treat them not as separate projects but as different views of a shared product model. Each surface has different users and expectations. The business side wants a wide view, quick editing and the ability to see operations at a glance. The customer wants a short path to their own booking and their own information, sometimes from a browser, sometimes from a phone. Screens can differ, but the underlying concepts must stay the same. What a booking is and whom it belongs to should get the same answer whichever surface you look from. ``` TypeScript monorepo ├─ React + Vite web interfaces ├─ Expo customer app ├─ Fastify + tRPC service layer └─ PostgreSQL + Drizzle ORM ``` ### Why a monorepo A single TypeScript monorepo keeps the React interfaces, the Fastify and tRPC service layer, and the PostgreSQL and Drizzle data layer under one roof. Shared components aim for consistency between web and mobile, and with tRPC the interfaces speak the same types as the services. When a field's name or shape changes, the change becomes visible at compile time on every surface involved. This has a price too. A monorepo makes it very easy to loosen boundaries; since everything is in one place, it becomes easy for one package to reach into another's internals. That is why I think structures like this require choosing deliberately what to share: types, contracts and genuinely common components should be shared, while behavior specific to one surface should stay there. The point of shared components is not to make everything identical, but to make the same concept look the same way everywhere. > Software that understands a business's daily rhythm is measured by how its flows fit together, not by a single screen. ### Workspaces and data boundaries In Motice, businesses work within their own separate workspaces. In products like this, one of the most important questions for me is that one business's data never mixes into another business's view under any condition. Trying to guarantee that with a filter in the interface alone is fragile; thinking of the boundary in the service and data layers, as part of every request's context, feels much sturdier to me. A typed service and data layer helps make boundaries like this visible in code. The same thinking applies to the calendar. Classes, bookings and customers may look like independent records, but in reality they constantly refer to each other. When a change is made in one place, the other surfaces reflecting it in the same way is the basic condition for users trusting the product. In products like this, the hard part is usually not individual features but the places where features intersect. A class, the bookings for that class and the business's view of the day are really different readings of the same information. Expressing these intersections in a shared model keeps every new screen from inventing its own rules from scratch. ### What I find most interesting Motice is a shared product of the Unitic team, and I contribute to its development within the team. What draws me most in this project is not finishing a single screen, but making calendar, customer and operations flows complement each other correctly inside the same product. One benefit of working as a team on a shared codebase is seeing early how a change affects someone else's work. Shared types make that conversation concrete: when a contract changes, the affected places show up without needing a debate. Working on a product that is still in development is its own experience. Because decisions are not frozen yet, the model built today needs to be able to carry flows added tomorrow. So when I work on a structure, I try to think not only about today's screen but also about how that structure might grow later. Note (Note): When several apps look at the same data, writing the contract between them as types is far more durable than keeping it in a document. --- ## Blockchain on real devices: 72 hours with three Raspberry Pis URL: https://devilyxrddev.com/en/journal/blockchain-on-real-devices Date: 2026-07 · R&D and hardware Devilyxrd Blockchain is my personal R&D project that combines a blockchain core written in Go with TypeScript and NestJS services. I ran it not as a local process but on three Raspberry Pis for 72 hours. This article covers what I measured, and what I did not. ### Why real devices In a distributed system, keeping several nodes on the same chain looks easy when they are processes on one computer. Once network latency, limited hardware and long runtimes come into play, the picture changes. That is why I wanted to observe the system not only in a local process, but on real devices and in a long run. Several processes on one machine share the same clock, the same memory and the same network stack. That makes testing fast, but it also hides some questions. Nodes running on separate devices each have their own resources and their own timing, and they talk over a real network in between. Resource-limited devices like the Raspberry Pi seemed to me a suitable environment for observing these conditions over a long period. ### Architecture The system runs as a permissioned PoA, with finality reached through signed validator votes. The blockchain core is written in Go, a NestJS layer exposes the services, gRPC connects the services and PostgreSQL holds persistent data. GraphQL and Docker are also part of the stack. Block production, validator coordination, wallet and contract services are treated as separate responsibilities. ``` core Go (PoA, signed validator votes) services TypeScript / NestJS transport gRPC data PostgreSQL nodes 3 × Raspberry Pi 4B ``` Keeping the core and the services in separate layers is a deliberate choice. Separating the parts that determine the system's correctness, such as consensus and block production, from the parts that face the outside world, such as wallet and contract services, makes it easier to evaluate each under its own conditions. The split also shows in the choice of language: Go in the core, TypeScript in the services. It also becomes easier to see how a change in one layer affects the other. Having the services on the TypeScript side makes it quicker to experiment with work outside the core. In the core, the priority is not speed but predictability. Unexpected behavior in a blockchain core does not just cause a bug; it can also push nodes into different states. ### Permissioned PoA and finality In a permissioned PoA, the right to produce blocks belongs to predetermined validators. Finality is reached through signed validator votes. For me, the instructive side of this model is that it separates two questions: are blocks still being produced, and are the produced blocks being finalized? A system can keep producing blocks while finality stalls; conversely, a chain that looks consistent may have stopped producing new blocks. So in a long run you need to look at the two separately. Signed votes also make it traceable which validators' approval a finalization came from. A permissioned structure lets me work on consensus and finality without needing a participation model as complex as the one on open networks. For a personal R&D project, that is a reasonable way to narrow down the questions I want to focus on. ### The 72-hour test In a 72-hour test between 13 and 16 July 2026, three Raspberry Pi 4B nodes reported 86,553 blocks. Across 863 checks between the nodes, no chain split or finality stall was observed. When I look at these numbers, I try to keep two things apart. The block count shows that production continued throughout the run. The cross-node checks show whether the different devices shared the same chain. Each is incomplete without the other: a high block count means nothing if the nodes are advancing on different chains, and a consistent but stalled chain does not prove long-running operation. In a run like this, the most valuable information is often not a single number but numbers confirming each other. If the reported block count and the result of the cross-node checks tell the same story, I can trust the run more; if they diverged, that divergence would be the thing to investigate. > A node that looks alive does not mean the system works. ### What this result does not say This is a finding for that specific run. It does not cover every failure scenario of the system; cases like a network partition, a malicious validator or many more nodes need separate tests. Recording the result together with its limits matters as much as the result itself. Three nodes make a small network. In a larger validator set, vote collection, network latency and resource usage may behave differently. So I see the 72-hour run not as a destination, but as an observation, with clear limits, of how the system behaves on real devices. Verifying long-running operation means looking at consistency between nodes, continued block production and a record of the run's limits, all together. Doing this in a personal R&D project has one advantage: there is no pressure to make the result look bigger. I can write down what I saw and what I did not see with the same clarity. Those gaps are also what shape the next steps. Note (Note): When you share a test result, write down how long it ran, on what hardware and under which conditions. Otherwise the number claims more than it should. --- ## Before writing code: clarifying decisions in Mentorual URL: https://devilyxrddev.com/en/journal/decisions-before-code Date: 2026-06 · Product & analysis In Mentorual, the current centre of the work is not code but the decisions code will rest on. In this note I explain why drawing the boundaries of the first release through requirements, use cases, screen families and a role-permission matrix is part of building the product. ### Why before code Mentorual brings mentoring, availability, appointments and institution management into one product. There are mentor, member, institution and administrator roles, and the experience spans web, mobile and administration. My role in this project is product analysis, requirements and design preparation. The work is currently in the analysis and design preparation stage, so there is no codebase I can talk about. What I can talk about are the decisions that need to be made before code is written, and why they carry so much weight. A product with multiple roles and institutions needs the same action to behave consistently across different screens. If questions like how an appointment looks from the mentor's side, which states it passes through on the member's side and where the institution comes into it stay unanswered, everyone has to answer them on their own during development. In my view this is the most common source of inconsistency in products: not bad code, but decisions that were never discussed. ### From requirement to screen family The parts of the preparation package are requirements, use cases, screen families and role-permission analysis. It is easy to think of them as separate documents, but the real value lies in the links between them. A requirement should appear in at least one use case, each use case should live on specific screens, and each screen should connect to the question of who can see and do what. When a link in this chain breaks, you end up with either work that never gets done or a screen nobody asked for. This is why I find the idea of a screen family useful. Instead of listing screens one by one, thinking about the ones that share similar behavior together lets you make recurring decisions once. When patterns like list, detail, form and confirmation repeat across roles, the behavior is expected to stay the same. Web and mobile may present the same family at different sizes, but the rule should stay the same. ``` requirement -> use case -> screen family (web / mobile / administration) -> role-permission row -> acceptance condition ``` ### A role-permission matrix is a contract, not a table At first glance a role-permission matrix looks like a table with roles as rows and actions as columns. I see it more as a contract: the single answer to “who can do what” shared by analysis, design and development. In a structure with roles such as mentor, member, institution and administrator, that answer needs to be reflected the same way in screens, services and tests. When thinking through the matrix, the most useful question for me is “in which state is this action in whose hands”. Permission often depends not only on the role but also on the state of the record and the institution context. Writing these distinctions down early cuts off, before it starts, the later debate about why one screen shows a button and another does not. > It is hard to build a workflow correctly without knowing where it ends. ### Separating the MVP from later releases In this work the MVP and subsequent-release scopes have been separated. This split looks simple on paper but is actually one of the hardest decisions. Every feature seems important; the real question is whether the first release is a meaningful product on its own. The measure I use for this kind of split is: what is the shortest path a role needs to complete its core task from start to finish, and can everything outside that path move to a later release? The split also has an invisible side. A feature left for a later release is not forgotten; the first release's workflows need to be thought through in a way that does not make that feature harder to add later. Leaving something out of scope does not mean not thinking about it at all. ### Recording decisions The least visible but, in my view, most lasting part of the preparation package is recording product decisions. Why a decision was made matters as much as what it is. When someone asks “why is it like this” months later, having the answer in a written record rather than in someone's memory prevents the same discussion from happening again and again. Acceptance conditions are an extension of that record. Connecting the MVP scope, screen requirements and acceptance conditions means defining in advance what counts as “done”. Once development starts, these criteria give both the person implementing and the person reviewing a shared language. ### Analysis is part of building The idea I keep returning to in this project is that clarifying a product is part of building it. A stage without code is easily dismissed as “preparation”. Yet the answers to who can perform an action, how a workflow ends and exactly what the first release includes determine most of the code that will be written. Development that starts without those answers has to answer the same questions somewhere more expensive. Note (Lesson): Decisions made before code are invisible but cheap; the same decisions made in the middle of development become both visible and expensive. --- ## Shared identity, separate rules: notes from the OYS & TYS monorepo URL: https://devilyxrddev.com/en/journal/shared-identity-separate-rules Date: 2026-05 · Engineering In OYS & TYS, students, instructors, candidates and administrators share the same identity infrastructure, but each domain has its own data and business rules. In this note I gather my thoughts on keeping domain boundaries clear while sharing common packages inside Turborepo. ### Two systems, one repository OYS & TYS are systems that handle student and course management and Turkish Proficiency Exam workflows within a shared platform structure. They focus on student, course, examination and administration processes and are developed in a monorepo. The interfaces are written with Next.js and the services with NestJS; the data side uses PostgreSQL, Prisma and Redis. Turborepo manages the repository structure. I am actively involved in frontend and backend development of these web-based management systems, and work on student, course and examination management is ongoing. In this note I want to talk less about a specific feature and more about a tension I keep running into in this kind of structure: the tension between sharing what is common and keeping separate what is separate. ### What is shared The needs of students, instructors, candidates and administrators are connected. The same person may enter the system in different roles at different times; things like sign-in, sessions and basic user information should not be rewritten in each domain. That is why shared identity sits at the center of the structure. Turborepo is useful exactly here: it lets shared packages and separate application domains be managed in one repository. The benefit of sharing is clear. A rule is written once, a type definition means the same thing everywhere, and a fix reaches every application at once. But sharing has a cost too, and in my view that cost is often noticed late. ``` Turborepo shared packages : identity infrastructure, shared types and tools student and course area : Next.js interface -> NestJS service examination area : Next.js interface -> NestJS service data : PostgreSQL + Prisma, Redis ``` ### What must stay separate Each domain has different data and business rules. The lifecycle of a course is not the same as the lifecycle of an examination process; a student's enrollment status and a candidate's exam status do not answer the same questions. Moving these rules into a “shared” package may look tidy at first, but over time that package becomes a place carrying the exceptions of both domains. My approach here is that only what is genuinely shared goes into a shared package. Identity, common types and general tools can be shared. A domain's business rule does not become shared just because another domain needs something similar. The difference between similar and identical is where people most often go wrong in a monorepo. Making a package shared is easy, separating it later is much harder; so when I am not sure, I prefer to keep things separate. > Shared identity, clear responsibilities. ### User journeys and service boundaries In this work I focus on considering user journeys alongside data and service boundaries. Something that looks smooth to the user on one screen may touch more than one domain behind the scenes. In those cases the easy path is for one service to reach directly into another domain's data. But that means quietly breaking the boundary. Working on both frontend and backend has an advantage here: since I see both ends of a journey, I can choose more deliberately where to draw the boundary. Rather than widening a service's responsibility to meet a need in the interface, tying the conversation between two domains to an explicit contract looks healthier in the long run. ### A monorepo needs discipline A monorepo makes it easy to keep everything in one place; for the same reason, it makes it easy for everything to become connected. Being in the same repository does not mean being the same module. Keeping the direction of dependencies deliberate, making sure one domain does not reach into another's internals and questioning what goes into shared packages are, in my view, the real work of this structure. The rule looks simple, but it is also the easiest thing to forget at the pace of daily work. A shared piece of infrastructure such as Redis raises the same question. Shared infrastructure does not mean that what it holds can be ownerless. Each domain owning its own data is a principle that needs to hold even when resources are shared. ### An ongoing balance Because development is ongoing, I am not describing this balance as something finished. With every new need, the question “is this shared, or only similar?” gets asked again. Keeping each domain's rules intact while building a shared foundation is not a decision made once and forgotten; it is a habit that needs constant maintenance. Asking this question out loud is a useful habit in code reviews as well. Note (Lesson): Before moving something into a shared package, it is worth asking whether two domains use it for the same reason or merely in a similar way. --- ## End-to-end responsibility: how layers affect each other in BGB URL: https://devilyxrddev.com/en/journal/end-to-end-responsibility Date: 2026-04 · Engineering BGB is a digital product aimed at reducing gambling-related harm. Taking end-to-end responsibility across the mobile app, services, management panel and website let me see how a decision in one layer reflects on the others. This note is about that experience. ### The product's context BGB is a digital product aimed at reducing gambling-related harm. I am aware that the subject is sensitive, so in this note I make no claims about the product's impact. What I want to describe is what it is like to take on the software side of such a product end to end. This is a company project. I carried out product analysis, design and development, taking responsibility across all software layers: the mobile application, backend algorithms, management panel and website. When working in a sensitive area, I think the right stance is to approach the person who will use the product with respect and keep the interface language plain. The software's job here is not to impress; it is to behave in a reliable, understandable and consistent way. This affects small decisions too, from word choice to the tone of error messages. ### Four surfaces, one product The mobile experience was built with React Native and Expo, and the web and administration interfaces with Next.js. The services run on NestJS and PostgreSQL, with Prisma for data access. It is possible to think of these four surfaces as separate projects, but in BGB they are complementary parts of the same product. Mobile workflows, data and administration tools need to work together. ``` React Native + Expo (mobile) Next.js (web) Next.js (admin) \ | / +----------- NestJS services ----------------+ | Prisma -> PostgreSQL ``` This layout has an important consequence: no surface can be considered “finished” on its own. The behavior of a mobile screen depends on what the service layer returns; the usefulness of a tool in the management panel depends on how the data is modeled. So work done on one surface should not be considered complete until it has been checked on the others. ### A decision in one layer The thing I saw most clearly with end-to-end responsibility is that a decision made in one layer always has a counterpart in the others. How you define a field in the data model determines how the service presents it. The way the service presents it determines how the mobile app shows that data and which states it has to handle. How the management panel can edit the same data also depends on that same model. When the layers belong to different people, this chain is managed through meetings and documents. When they all belong to the same person, a different risk appears: because you hold the chain in your head, you neglect to write it down. That is why I think the agreements between layers need to be defined explicitly even when working end to end. > A small decision in one layer comes back as the behavior of a screen at the other end. ### Service contracts I tried to consider the mobile experience, service contracts and administration needs together. By service contract I mean the agreement about what an endpoint accepts, what it returns and how it behaves in which situations. With mobile apps, the versions in users' hands generally do not update as quickly as the web. That holds for mobile products in general, and it means contracts need to be changed carefully. Having TypeScript as a common language across all layers helps here; it makes it easier to think of the same concept the same way in mobile, web and services. But a common language alone is not enough; the contract itself also needs to be designed deliberately. ### Administration needs are part of the product In many projects the management panel is the last surface anyone thinks about. Yet a product's sustainability also depends on how comfortably the people managing it can do their work. Considering interface and backend workflows around the product's shared needs makes administration a surface designed from the start rather than an add-on. When the administration tool is weak, daily operation becomes harder no matter how good the rest of the product is. Because I also carried out the analysis and design, from the moment I first heard about a need I could think together about what it would become on mobile, in the service layer and on the administration side. In my view, that is the most valuable part of end-to-end responsibility: being able to see all the consequences of a decision at once. ### The cost and the gain Taking responsibility across every layer also brings the load of context switching. Moving from the detail of a mobile screen to the structure of a query within the same day takes attention. In return, the gain is significant: seeing faster which layer a problem comes from and putting the fix in the right layer. Fixing an error at its source rather than hiding it in the interface is only possible when you can see the whole chain. Being able to estimate a feature's cost across all layers at once while designing it also makes scope discussions more realistic. Note (Lesson): When working end to end, writing down the agreements between layers is necessary even if you carry them alone, because one day someone else will read that chain too. --- ## Same data, four users: notes on the Turkish Art Platform URL: https://devilyxrddev.com/en/journal/one-data-many-users Date: 2026-03 · Engineering In the Turkish Art Platform, the main center and subsidiary platforms live inside the same product. Visitors, artists, applicants and administrators approach the same data with different needs; in this note I write about thinking through those differences together in a multitenant structure. ### A main center and subsidiary platforms The Turkish Art Platform is an enterprise art platform that presents artist profiles, artworks, events, applications and editorial content through a main center and subsidiary platforms. Subsidiary platforms serve different countries or organizations. Public pages, user areas, administration and the API are considered together. This is a company project; I took responsibility in development, integration and delivery for the enterprise web application. The interfaces use Next.js and React, the services NestJS, and the data layer Prisma and PostgreSQL; Redis and Tailwind CSS are also part of the stack. The most noticeable aspect of this structure at first glance is multitenancy. But what made me think more in this project was the different people standing in front of the same data. ### Four perspectives, one set of data Take an artist profile. For a content visitor, it is a page to read and browse. For the artist, it is a space that represents their work and that they want to keep up to date. For an applicant, it is a record still inside a process whose outcome is not yet known. For an administrator, it is content to review, publish or correct. The data stays the same, but each of them wants something different from it. In my view, not seeing this difference usually leads to one of two extremes. Either each user ends up with a separate copy of the data and separate rules, which drift apart over time. Or everyone is shown the same screen and the same fields, which serves nobody's work fully. The approach I prefer is to keep the data single and express the differences in the presentation, permission and workflow layers. This approach asks for more thought about permissions and presentation, but keeps the data consistent. > The data can stay single; the needs do not have to. ### Multitenancy is a question of boundaries When managing content and user workflows across the main center and subsidiary platforms within one shared product, each platform's local context and management responsibilities need to be retained. I see multitenancy primarily as a question of boundaries: which data belongs to which platform, who can see what on which platform, and where the shared part begins and ends. If these boundaries live only in the interface, they are fragile. A platform context needs to be carried the same way through the service layer and the queries. In an architecture like this, multitenancy, content management and application workflows are not topics to solve separately; they are parts of the same architecture. A decision made in one directly affects the behavior of the other two. ### Local context and management responsibility Difference is the very reason subsidiary platforms exist. Each organization may want to manage its own content, its own events and its own applications. At the same time, the main center needs to be able to see the whole. Striking a balance between these two needs is as much a product decision as a technical one. In the administration interface, this balance shows up most in permissions. Whether an administrator sees only their own platform or several; on which platform a piece of content is published; which organization an application is reviewed by: these are questions that need answers before screen design. A panel written without those answers turns into a panel that keeps collecting exceptions. ### Looking from integration and delivery My role in this project covered integration and delivery alongside development. That gave me a slightly different angle on the product. Public pages, user areas, administration and the API may seem to work separately, but at delivery they all rest on the same data model and the same platform context. Seeing during delivery how a small change in one layer shows up in the others is a reminder of how much that coherence matters. User experience, administration workflows and the multitenant service structure have to work together. Not losing sight of the others while improving one is, in my view, the work that needs the most attention on platforms like this. ### What stays with me The clearest thing I took from this project is that when designing a data model, you need to think not only about the entities but also about the people looking at them. Content visitors, artists, applicants and administrators come to the same record with different questions. A good structure should be able to answer all of them, and do so without fragmenting the data. The way to do that, in my view, is not a technical trick; it is asking again, whenever a new screen or workflow is added, what that data means for the other users. Note (Note): In a multitenant product, the question “who is looking at this data, and with what intent” matters as much as “which data lives where”. --- ## Publishing in two languages: content, workflow and interface in NoktaTR URL: https://devilyxrddev.com/en/journal/bilingual-publishing Date: 2026-02 · Product & analysis NoktaTR brings the culture, art and literature content of .tr magazine into a Turkish and English digital publication. In this note I explain why content structure, editorial workflow and interface hierarchy are not independent decisions. ### From magazine to screen NoktaTR is a Turkish and English digital publishing platform for the culture, art and literature magazine .tr. It consists of a website, an administration interface and an API. The reader-facing website and the workflows for preparing and managing content are parts of the same system. The web and administration interfaces, built with Next.js, work with NestJS services and Prisma data access. This is a company project; I contributed to application layers, content management workflows, and development and delivery. Turning magazine content into an enjoyable web reading experience is only half the work. The other half is giving the editorial team a sustainable way to manage that content. When these two halves are considered separately, you end up with either a site that is pleasant to read but hard to manage, or one that is easy to manage but ordinary to read. That is why I try to think about both halves at the same time. ### Three decisions, one decision The most important idea I took from this project is that in digital publishing, content structure, editorial workflow and interface hierarchy are not independent decisions. How you model the content determines how the editor prepares it. How the editor prepares it determines what the reader sees first on the page. And the reader's experience comes back to ask something of the content model. During development I try to consider all three together. > The content model, the editor's workflow and the reader's eye are three faces of one decision. ### Content structure A magazine piece is not just a title and a body. Each part, such as the author, category, images, quotes and related content, corresponds both to a field in the administration interface and to a place on the reader's page. When building the content structure, I think the question to ask is not “which fields do we need” but “what will this field tell the reader, and how will the editor fill it in”. When the structure is too loose, every piece looks different and the interface loses consistency. When it is too rigid, room for editorial expression narrows. In a field like culture and literature, where form is part of meaning, striking this balance matters even more. So when settling the structure, I think it is a good habit to take a few different kinds of content as examples and check whether each fits the same mold. ### Editorial workflow The administration interface is the publishing team's daily workspace. However steps like preparing, reviewing and publishing a piece are expressed in the panel, the team's work becomes that much easier or harder. What I value in workflows like this is that the panel reflects the editor's way of thinking rather than the technical data model. An editor thinks “I am preparing a piece”, not “I am creating a record”. The website, administration interface and API working through a shared publishing model is decisive here. If content entered in the panel and content shown on the website are fed by the same model, the editor can more easily anticipate how a change will reach the reader. When the model diverges, small but tiring differences appear between what is seen in the panel and what the reader sees. ### Two languages, one publishing structure Turkish and English content are considered together within the product's publishing structure. In bilingual publishing, I think the most critical question is how the two languages are connected. Are the two language versions of a piece two faces of the same content, or two independent pieces? The answer affects the data model, the editor's workflow and how the reader moves between languages. The first approach binds the languages tightly, the second gives more freedom; both have a cost. Language here is not an interface setting but a dimension of the content itself. Title lengths, the rhythm of the text and typographic balance can change from one language to the other; the interface hierarchy needs to stay balanced in both. That is why I prefer to think of a bilingual publication not as a translation layer added later but as a structure designed from the start. ### The delivery side Because my role also covered development and delivery, I had to look at the publication from the angle of staying live as well. A change on a publishing platform affects not only the code but also the published content and the editor's habits. That is why I think the question to ask at delivery is not only “does it work” but also “did anything break for the reader or the editor”. In publishing products, trust is built by the reader and the editor meeting the same thing every day. Note (Note): In publishing products, improving the content model, the administration interface and the reader's page separately is not enough; all three need to be treated as parts of the same decision. --- ## The process behind the form in a clinic application system URL: https://devilyxrddev.com/en/journal/clinic-application-flow Date: 2026-01 · Product & analysis In the system I am building for Kadir Yücebaş, the application form is only the visible part of the work. In this note I explain how I think about applications reaching the clinic team, how their status is followed, and how bilingual interfaces share one data model. ### The work that starts where the form ends When designing an application form, the first thing that comes to mind is the fields: what information to ask for, which fields are required, how to order them. On the independent project I am building for Kadir Yücebaş it was tempting to start there too. But the real need of the project was somewhere else. Receiving an application, delivering it to the clinic team and following its progress are connected tasks. However well the form is designed, if it is unclear what happens after submission, both the applicant and the clinic team are left in a gap. So I tried to think of the system as a flow from the very beginning. Clinic applications and preliminary assessment are handled within one structure. Turkish and English application interfaces, a clinic administration panel and notification services work together as different faces of the same system. What matters to me is that each of these faces makes sense to its own user while sharing the same reality underneath. ### Two languages, one data model When building a bilingual application interface, the easiest path can look like writing a separate form for each language. It is fast in the short term, but in the long term it produces two separate realities. When a field is added or a validation rule changes, two places need updating, and at some point the two quietly drift apart. On this project I chose to connect the Turkish and English forms to the same data model through shared schemas. The language changes the text the user sees, not the structure of the application. So when an application is opened in the administration panel, it reads the same way regardless of which language it came from. Shared schemas have another benefit: the checks the interface performs and the data the server accepts are fed by the same definition. That reduces mismatches that otherwise build up unnoticed between the two layers. ``` Turkish application interface ──┐ English application interface ──┼──> shared schemas ──> service layer ──> Drizzle + PostgreSQL Clinic administration panel ──┘ │ └──> email notification and document generation ``` ### The admin side is a user interface too In projects like this, the administration panel often ends up as an afterthought. The screens the applicant sees are prepared with care, while the screens the team uses every day get rushed because they count as internal tools. I think the opposite: the person running the process is the person using the administration panel. How they see an application, how quickly they reach the information they need and how they move to the next step directly shape the quality of the process. In the clinic administration panel, reviewing applications and following their status are parts of the same flow. My aim there is a structure that does not create extra work for the team but always makes visible where the process stands. Working in a health-related context, I also deliberately keep one boundary: the system does not make assessments, the clinic team does. The software's job is to deliver information to the right person in an orderly and complete way. > The form is where the process begins; the real work starts after submission. ### Status tracking and notifications From the applicant's point of view, the most uncomfortable thing is uncertainty. Submitting a form and hearing nothing back makes you wonder whether anything is happening at all. That is why I placed status tracking close to the center of the system. An application has a status, and that status carries meaning both on the administration side and in how the applicant follows the process. Notification and email flows come in here. The email side, which runs on Nodemailer, carries information to the relevant parties at certain points in the process. The document generation component turns the information in an application into an orderly output. In flows like this, the approach I prefer is to think of notifications as tied to the status of the application. When sending an email is a rule that has to be remembered separately somewhere, it becomes easy to forget. When it is handled together with the status, consistency improves, and when the flow changes later it is clear where to make the change. ### The data layer and the runtime environment The Next.js applications share the same data layer through Drizzle and PostgreSQL. What I like about Drizzle is that the schema definition reads clearly on the TypeScript side and queries can be written together with their types. In a structure where fields like those of an application may change over time, seeing the database schema in the same place as the code makes changes safer. Docker helps keep the development and runtime environments close to each other. On this project I work across the web, service and data layers. The advantage is being able to follow, in one mind, how a change spreads from the form to the database, then to the administration panel and the notifications. The downside is that keeping this integrity requires discipline. Shared schemas and a single data model are the most concrete tools of that discipline. Note (Lesson): When I look at an application system now, I no longer look at the form first. I look at whose desk the application lands on after submission, how its status is followed and how the applicant stays informed about the process. The form can be good, but if the process behind it is not defined, the system is only half built. --- ## From software to hardware: a building-washing drone and Raspberry Pi displays URL: https://devilyxrddev.com/en/journal/from-software-to-hardware Date: 2025-12 · R&D and hardware At IOTROCOP I built two projects end to end on my own: a drone designed for washing buildings, and phone-managed advertising displays for cafés and markets. Both taught me the same thing: in hardware there is no single component, only the system. ### The building-washing drone In this project, the drone's physical design had to be thought through together with its software, processing unit and communication infrastructure. ROS and an NVIDIA Jetson Nano formed the software and processing side, and I built the technical structure, including the fiber infrastructure, together with the drone's design. From design to technical infrastructure, the whole responsibility was mine. ``` design → building-washing drone software → ROS processing → NVIDIA Jetson Nano infrastructure → fiber ``` In web projects you can think about one layer independently of the others. Here that was not possible: a mechanical decision affected the processing unit, the processing unit affected the software, and the software affected the communication needs. I had to approach the project not as a single component but as a complete system from the start. What struck me most in this project was the order of decisions. In a web application most decisions can be undone later; swapping a library or rewriting a service takes time, but it is possible. In a physical system, some decisions set the frame for the others, and going back is far more expensive. So treating design and technical infrastructure together was not a preference from the start, but a necessity. ### ROS, Jetson Nano and fiber together ROS provides a framework in which the different software parts of a robotic system can talk to each other. The Jetson Nano is the processing unit this software runs on. The fiber infrastructure is part of the system's technical structure. I needed to think of these three not as separately chosen parts but as connected layers of the same system; a decision about any one of them also set the limits of the others. For me, the part of robotics most similar to web development was the need to separate responsibilities. Splitting software into distinct tasks helps you change one part without breaking the whole. The difference is this: here every software decision also has a physical counterpart. ### Raspberry Pi advertising displays The second project is more modest but just as holistic: digital displays running on a Raspberry Pi 4, used for advertising and promotion in cafés and markets. The displays are managed from a phone application. I built the display software, the mobile control and the device's technical infrastructure together. > The venue's digital shop window, managed from the phone in your pocket. The real work was making the display's behavior in the venue and the management flow on the phone serve the same need. For the business owner, it had to be one system, not two separate products. For a display in a venue, web habits alone are not enough. A web page can be closed and reopened; a display in a venue is a device expected to stay on all day and keep running on its own. For devices like this, it seems more accurate to me to think of the display software not as a separate application, but as part of the device itself. The questions I think about in a system like this are a little different from those in web projects. The display runs in a venue, often in a corner where nobody stands next to it, while management happens from a phone held in someone's hand. In between there is a device, a connection and an application, but the user does not want to see any of them separately. They want to see what they changed on the phone show up on the screen. I treated the device side and the application side together around that simple expectation. ### Two sides of working alone In both projects, the whole responsibility from design to technical infrastructure was mine. That has an advantage: you know every part of the system and can predict where a decision will land. But you are also the one who decides at every layer; the person writing the software, the person thinking about the hardware and the person asking questions on the user's behalf are the same. This taught me to own decisions outside the area I was used to. Another risk of working alone is blind spots. You have to ask yourself the questions someone else on a team would ask. So when I work on a layer, I try to look at it through the eyes of the next layer that will use it: what does the software expect from the processing unit, what does the phone application expect from the display? ### The same line, continued I followed a similar approach in a vehicle tracking system I developed as an individual IoT project: an embedded application running on a Raspberry Pi that shows live GPS, an interactive map, and speed, signal strength, battery and sensor data in one interface. There too, the interface had to stay smooth while keeping the data from the hardware up to date. Looking back at these projects, the common thread is clear: whether it is a display, a drone or a vehicle, the behavior a user sees is the result of many layers working together. Having seen all of those layers still helps me today when I work on the web and service side. Note (Lesson): In a hardware project there is no such thing as "my part". You have to accept from the start that when you change one component, the rest of the system will feel it. --- ## A telemetry interface on Raspberry Pi: staying smooth while data flows URL: https://devilyxrddev.com/en/journal/telemetry-on-embedded-ui Date: 2025-11 · R&D and hardware In a vehicle tracking and navigation system running on Raspberry Pi, I brought live GPS, an interactive map and real-time telemetry into one interface. In this note I describe what changes when data collection and interface performance are considered together on an embedded device. ### The one-screen idea I built this project individually: the embedded system, data collection, interface and technical infrastructure were all mine. The starting point was simple. Instead of following vehicle location and status data from separate sources, I wanted to present them in one clear interface on an embedded device. Live GPS data, an interactive map, navigation and real-time telemetry come together on the same screen. On the telemetry side there are speed, signal strength, battery status and sensor data. On paper, placing these on a screen does not look hard. The real challenge is keeping the interface smooth while all of them update at once, and doing that not on a powerful desktop computer but on a Raspberry Pi. The one-screen idea also has a user side. An interface used in a vehicle is not studied at length; it needs to be readable at a glance. So where each piece of information sits on the screen, and which appears large or secondary, is as important a decision as performance. Visual simplicity and technical lightness often pull in the same direction: fewer elements are both easier to read and lighter on the device. ### An embedded device means a different budget When building web interfaces, I often forget that I am working on a powerful machine. An unnecessary redraw, an extra animation or an update that fires too often can go unnoticed there. On an embedded system the same habits are felt immediately. Processor, memory and graphics capacity are limited, and both data collection and the interface share that limit. So I started designing the interface by accepting that every element has a cost. You need to think about how often each value on screen really has to update and which change actually tells the user something. A number changing many times a second can consume resources while adding nothing to readability. ``` GPS and sensors ──> hardware-level data collection ──> current state ├──> map and navigation view └──> telemetry panel (speed, signal, battery, sensors) ``` ### Separating data collection from the interface While bringing hardware-level data collection together with the user interface, I made sure the two did not keep each other waiting. My approach is for the collecting side to run at its own rhythm and for the interface to read that data only as much as it needs. Every new value from a sensor does not have to mean a redraw of the screen. This separation also makes it easier to isolate problems. When a data source slows down or drops out temporarily, the interface should not freeze. A drop in signal strength, for example, is information to show the user, not a condition that locks up the interface. Making such changes visible in a calm way is the actual job of a telemetry screen anyway. > Not every new value has to rush to the screen: the interface should read data only as much as it needs. ### The map is the heaviest component The interactive map is the most resource-hungry part of the interface. Updating the map as the location changes, drawing navigation information on top of it and letting the user interact with it all happen at the same time. Balancing this component so that it shares the same limited resources with the telemetry panel is, in my view, the most critical part of optimization in a system like this. Here I rely on a general principle: give priority to where the user's attention is. While the vehicle is moving, keeping location and navigation current matters. Keeping telemetry values readable matters too, but each of them does not need to update at the millisecond level. Separating priorities this way lets me use limited resources more deliberately. ### Every layer at the same table Owning every layer let me look at performance problems from a single place. To tell whether a stutter in the interface comes from data collection, the system infrastructure or the interface itself, you need to know every layer. In a team these questions can bounce between layers and people. Working alone, they all sit at the same table. This project made me see interface performance not as a detail to improve later, but as something to consider together with the design of the data flow. Keeping information from the hardware up to date and keeping the screen smooth are two faces of the same problem. Note (Lesson): On an embedded system I cannot think about data collection and interface performance separately. Deciding up front which data reaches the screen and how often is much easier than trying to recover smoothness afterwards. --- ## Data from the field: edge processing and connectivity in smart agriculture URL: https://devilyxrddev.com/en/journal/edge-data-smart-agriculture Date: 2025-10 · R&D and hardware GAIA / AgroBlock is an R&D effort that treats sensors, camera nodes and edge devices in agricultural settings as one system. In this note I collect my thoughts on how the physical environment and connectivity conditions shape software architecture. ### Desk assumptions do not hold in the field When building web applications, I accept some things almost without question: the server is reachable, there is a connection, data that is sent arrives right away. Working on GAIA / AgroBlock, I had to accept that I could rely on none of these assumptions. An agricultural field is a very different place from the environment software is usually imagined in. This R&D effort aims to bring IoT sensors and camera nodes together with edge processing, offline communication, AI and blockchain-based data integrity components. The work is ongoing. What I write here are not finished results but notes on how I approach the scope. ### The physical environment is part of the architecture Collecting field data from different devices, handling changing connectivity and processing that data traceably are not needs that can be considered separately. Where a sensor stands, the conditions a camera node operates in and how often a device can talk to the center all directly affect software decisions. So when thinking about the architecture, I try to treat the physical layer as part of the design rather than abstracting it away as just an input source. How data taken from a device moves into the next stages of the system is the question I focus on most in this work. This approach has a practical consequence too: every decision on the software side carries an assumption about how a device will behave in the field. Writing those assumptions down explicitly, for example discussing early how long a device might stay disconnected or which data takes priority, reduces surprises later. In R&D, I find asking such questions early even more valuable than finding their answers. ``` IoT sensors ──┐ Camera nodes ──┴──> edge processing ──> offline communication ──> AI and blockchain-based data integrity ``` ### Why edge processing In an environment where connectivity is not constant, a design that depends on sending everything to the center stays fragile. Edge processing finds a sensible place here: if part of the data is processed close to where it is produced, the system does not go completely blind when communication with the center drops. This matters even more for sources that produce large volumes of data, such as camera nodes, because moving raw data is not always possible or meaningful. The question I ask myself here is which work should happen on the device and which at the center. I do not think there is a single right answer. The device's capacity, the state of the connection and what the data will be used for are factors that need to be weighed together. Striking that balance is one of the parts of the R&D process that takes the most thinking. ### Offline communication is not an exception I try to think of offline communication not as an error state but as one of the system's normal operating conditions. How data is kept while there is no connection, how it is transferred when the connection returns and how its integrity is preserved along the way are not features to add later. In my view they need to be part of the design from the start. > In the field, a dropped connection is not an exception but a condition the design must account for from the start. Coming from the web world, this perspective changed some of my habits. Showing the user an error when a request fails is not enough, because in the field there is often no user waiting in front of a screen. The system needs to recover on its own, without anyone stepping in. ### Data integrity and traceability The blockchain-based data integrity side of the work is about data being traceable. Data that comes from different devices, at different times and under changing connectivity needs to be reliably followed later on. Blockchain is one of the approaches I am researching within this scope. I believe a technology should be used not because it is interesting but because the problem it solves is clearly defined, and I evaluate this component within that frame. I take a similar stance on the AI component. Making sense of field data is part of this scope, but which work happens where depends on the overall balance between the edge and the center. To me, the value of this R&D work comes from thinking about the components together within one system rather than one by one. Note (Note): GAIA / AgroBlock is ongoing R&D, and these notes are an approach, not results. The clearest idea I have reached so far is this: the physical environment, connectivity conditions and software architecture cannot be designed separately. The path from field data to the whole system runs through handling the three together. --- ## From analysis to infrastructure: owning the whole freelance process URL: https://devilyxrddev.com/en/journal/owning-the-whole-process Date: 2025-09 · Product & analysis On the e-commerce, congress and corporate websites I built as a freelancer, analysis, design, software and technical infrastructure were all mine. In this note I describe what carrying the whole process alone makes easier, and what it makes harder. ### One owner, several hats In my freelance work I rarely had a single role. I built corporate websites for companies and agencies, event websites for congresses and sales-oriented e-commerce websites. In each of them, understanding the need, designing the interface, developing the application and setting up the technical infrastructure were parts of the same responsibility. I carried all of it out individually, from start to finish. At first I saw this only as workload. Over time I noticed it also gives you a perspective. When every stage sits with a different person in a team, some decisions get lost on the way as they change hands between stages. When one person carries the whole process, that loss shrinks, but other risks take its place. In this note I want to look at both honestly. ### Not underestimating analysis For someone owning the whole process, the easiest mistake is to rush through analysis and jump straight into design or code. The moment a solution takes shape in my head, it is tempting to start building it. Yet in freelance work the client's need is often not fully clear in the first conversation. So in every job I first tried to define the scope, the user journey and the content structure. On an e-commerce site, the user experience and the underlying operation need to be designed together. On a congress site, presenting information in a clear structure comes first. On corporate sites, the core is explaining clearly what the company or agency does and what value it offers. Each type starts with a different question, and analysis, to me, is simply asking that question well. ### Design and development at the same table The most concrete benefit of one person doing both design and development is being able to think about whether a design decision is buildable while still drawing it. While designing an interface, I can weigh at the same time how it will be coded, where each piece of content will come from and how the page will behave on different screens. This reduces the "this cannot be built this way" kind of back and forth between design and implementation. I felt this especially on congress websites. On event sites, the organization of content and the navigation experience directly affect each other. Running design and software through the same information structure helped pages stay both readable and easy to maintain. On corporate sites too, developing information architecture, visual interface and implementation together makes the organization's identity on the web more consistent. > When one person carries the whole process, decisions do not get lost changing hands, but there is no second pair of eyes to question them. ### The invisible part: technical infrastructure What the client sees is mostly the interface. But the technical infrastructure that keeps the site running is part of the work too, and the responsibility is mine again. Because it is invisible, it is the part most exposed to neglect. It is not enough for a site to work properly on the day it is delivered; it also needs to stay understandable and sustainable afterwards. My approach here is simple: plan the infrastructure as part of the work from day one, not leave it to the end. When interface, application logic and infrastructure are handled together, later patches become fewer. Especially in projects like e-commerce, where the operation reflects directly on the user, a solid structure underneath matters as much as the experience on the surface. Being understandable to whoever opens the site after delivery, whether the client's team or a developer taking over the work later, is part of that sustainability too. That is why I try to set up an orderly structure and clear decisions before technical debt builds up. ### The limits of working alone Owning the whole process has a weakness that should be stated plainly: there is no second person to question your decisions. In a team, the designer can catch the developer's assumptions and the developer can catch the designer's. Working alone, I have to build that check deliberately myself. For that I tried to build a few habits. When I finish a stage, I come back to it after some time and look at it through the eyes of a different role. When I finish the design I try to think like a developer, and when I finish the code I try to think like a user. I also care about using conversations with the client not just to get approval, but to test my assumptions. It is not a perfect method, but in a one-person process it at least reduces the blind spots. Note (Lesson): What owning the whole process taught me most is this: even if each stage is good on its own, if the connections between them are weak, the resulting product feels scattered. Gathering analysis, design, development and infrastructure around the same product need is worth more than trying to perfect each of them separately. ## Motice URL: https://devilyxrddev.com/en/work/motice Category: Web / Platforms · Mobile · 2026 · Team project · development · stack: TypeScript, React, Vite, Fastify, tRPC, PostgreSQL, Drizzle ORM, Expo Motice is a CRM/ERP product for pilates, yoga, personal training and wellness studios. Its management interface, customer portal and mobile experience address different sides of the same business workflows. Spreading appointments, customers, classes and business operations across separate tools complicates daily work. The project brings these flows into a shared product with separate business workspaces. > Software that understands a business’s daily rhythm. Contribution: Contributing developer in the Unitic team Customer and daily operational needs of appointment-based service businesses. I contribute to Motice’s development as part of the Unitic team. Web, service, data and mobile layers working around a shared product model. A TypeScript monorepo brings together React interfaces, Fastify and tRPC services, and PostgreSQL with Drizzle. Shared components support consistency across web and mobile experiences. What interests me here is how calendar, customer and operations workflows fit together within one product, beyond completing individual screens. We are working as a team on a product with a management application, customer portal and Expo-based customer application. My contribution forms part of that shared development effort. --- ## Mentorual URL: https://devilyxrddev.com/en/work/mentorual Category: Product / Analysis · 2026 · Analysis and design preparation · stack: Product analysis, Requirements, User flows, Wireframes, RBAC, Web / Mobile Mentorual brings mentoring, availability, appointments and institution management into one product concept. Product scope and behavior are being prepared together for web, mobile and administration experiences. A product with multiple roles and institutions needs consistent behavior across different screens. Clarifying scope, workflows, roles and acceptance conditions before implementation is central to this work. > Clarifying a product is part of building it. Contribution: Product analysis, requirements and design preparation Considering mentor, member, institution and administrator journeys across web and mobile together. Preparing requirements, user journeys, product decisions and role-permission analysis. Connecting MVP scope, screen requirements and acceptance conditions. Requirements, use cases, screen families and the role-permission matrix are connected. Product decisions are recorded with separate scopes for the MVP and subsequent releases. I focus on the value of decisions before code: who can perform an action, how a workflow ends and exactly what the first release includes. MVP and subsequent-release scopes have been separated, with a preparation package covering requirements, use cases, screen families and role-permission analysis. Work is progressing through product analysis and design preparation. --- ## YEE CMS URL: https://devilyxrddev.com/en/work/yee-cms Category: Web / Platforms · 2026 · Company project · stack: Next.js, React, NestJS, TypeScript, PostgreSQL, Drizzle ORM, Docker YEE CMS is a multisite platform being developed to manage content for different Yunus Emre Institute centers on a shared foundation. The public website, administration application and service layer work together. A shared platform must handle content creation, permissions and publication workflows across centers while preserving each site’s context. > One foundation, distinct publishing spaces. Contribution: Full-stack development and integration at Unitic Bringing content, languages and publication processes from different centers into a shared platform. Contributing to interfaces, services, content management and integrations. Keeping the content model, site context and administration workflows aligned. A Next.js website, NestJS API and React administration application connect through PostgreSQL. Multilingual content, site context and approval workflows are central parts of the product. I focus on how the content model shapes not only the database, but also an editor’s daily work and the pages visitors see. I worked on development and integration within a multisite CMS comprising a website, administration interface and API, focusing on content management workflows and application layers. --- ## NoktaTR URL: https://devilyxrddev.com/en/work/noktatr Category: Web / Platforms · 2026 · Company project · stack: Next.js, React, NestJS, TypeScript, Prisma, PostgreSQL NoktaTR brings .tr magazine’s content into a bilingual digital publication. The reader-facing website and the workflows for preparing and managing content form parts of the same system. Magazine content needs an enjoyable reading experience on the web and a sustainable way for the editorial team to manage publication. > From content to a reading experience. Contribution: Full-stack development, content workflows and delivery Presenting .tr magazine’s culture, art and literature content in two languages. Taking development responsibility in application layers, content management and delivery. Connecting the website, administration interface and API through a shared publishing model. Next.js web and administration interfaces work with NestJS services and Prisma data access. Turkish and English content are considered together within the publishing structure. Content structure, editorial workflows and interface hierarchy are connected decisions in digital publishing. I consider all three during development. I contributed to application layers, content management workflows, development and delivery for the publication platform. --- ## Türk Sanatı Platformu URL: https://devilyxrddev.com/en/work/turk-sanati-platformu Category: Web / Platforms · 2026 · Company project · stack: Next.js, React, NestJS, Prisma, PostgreSQL, Redis, Tailwind CSS The Turkish Art Platform presents artist profiles, artworks, events, applications and editorial content through a main center and subsidiary platforms. Public pages, user areas, administration and the API are considered together. Platforms serving different countries or organizations need shared content and user workflows while retaining local context and management responsibilities. > Connecting different areas of art within one structure. Contribution: Software development, integration and delivery Considering artists, artworks, events and applications across a main center and subsidiary platforms. Contributing to development, integration and delivery of the enterprise application. Connecting user experience, administration workflows and multitenant services. Next.js and React interfaces connect to a NestJS API with Prisma and PostgreSQL. Multitenancy, content management and application workflows form parts of the same architecture. The different users matter together: visitors, artists, applicants and administrators approach the same data with different needs. I took responsibility in development, integration and delivery for the enterprise web application, working within a platform spanning web, administration and service layers. --- ## OYS & TYS URL: https://devilyxrddev.com/en/work/oys-tys Category: Web / Platforms · 2026 · Active development · stack: TypeScript, Next.js, NestJS, PostgreSQL, Prisma, Redis, Turborepo OYS & TYS focuses on student, course, examination and administration processes. It is developed in a monorepo that brings shared identity together with distinct data and application boundaries. Students, instructors, candidates and administrators have connected needs, but each domain has different data and business rules. Responsibilities need to remain clear while common capabilities are shared. > Shared identity, clear responsibilities. Contribution: Active frontend and backend development Considering different user and administration needs across student, course and examination processes. Taking an active role in frontend and backend development. Retaining domain data and service boundaries alongside shared identity and packages. Next.js interfaces and NestJS services work with PostgreSQL, Prisma and Redis. Turborepo supports shared packages and separate application domains within one repository. I focus on considering user journeys alongside data and service boundaries, retaining domain-specific rules within a shared foundation. I am actively involved in frontend and backend development of these web-based management systems. Work on student, course and examination management is ongoing. --- ## BGB URL: https://devilyxrddev.com/en/work/bgb Category: Web / Platforms · Mobile · 2026 · Company project · stack: React Native, Expo, TypeScript, Next.js, NestJS, PostgreSQL, Prisma BGB is a digital product aimed at reducing gambling-related harm. The mobile experience, service layer, administration interface and website are developed as complementary parts of one product. Mobile workflows, data and administration tools need to work together. The scope extends beyond individual application screens to all software layers of the product. > Building every layer of a product together. Contribution: End-to-end full-stack development · web, mobile and backend Considering mobile, web and administration needs for a product aimed at reducing gambling-related harm. Taking responsibility for analysis, design and development across software layers. Connecting the mobile application, services and administration tools through shared product workflows. React Native and Expo provide the mobile experience alongside Next.js web and administration interfaces, NestJS services and PostgreSQL. Interface and backend workflows are considered around shared product needs. End-to-end responsibility makes the effects of decisions across layers visible. I consider the mobile experience, service contracts and administration needs together. I carried out product analysis, design and development, taking responsibility across the mobile application, backend algorithms, management panel and website. --- ## Devilyxrd Blockchain URL: https://devilyxrddev.com/en/work/devilyxrd-blockchain Category: Blockchain · 2026 · Personal R&D · stack: Go, TypeScript, NestJS, PostgreSQL, gRPC, GraphQL, Docker, Raspberry Pi Devilyxrd Blockchain System is my personal R&D project combining a Go blockchain core with TypeScript and NestJS services. I work on block production, validator coordination, wallets and contract services together. I wanted to keep multiple nodes on the same chain and observe that behavior on physical devices rather than only within a local process. > Several machines sharing the same state. Contribution: Personal R&D · system architecture and development Separating the responsibilities of the blockchain core and surrounding services. Working on architecture, services and distributed systems with Go and TypeScript. Measuring chain consistency and continued production in long-running tests across three Raspberry Pi devices. Permissioned PoA and finality through signed validator votes work with Go services, a NestJS layer, gRPC and PostgreSQL. Raspberry Pi nodes provide the physical test environment. Validating long-running behavior requires more than seeing one healthy node. Cross-node consistency, continued block production and recorded test limitations need to be considered together. A 72-hour test on three Raspberry Pi 4B devices, dated July 13–16, 2026, reported 86,553 blocks. Across 863 cross-node checks, no chain divergence or finality stall was observed. These findings apply to that run and do not cover every failure scenario. --- ## GAIA / AgroBlock URL: https://devilyxrddev.com/en/work/gaia-agroblock Category: IoT / Hardware · 2024 · R&D project · stack: IoT, Edge computing, Sensor communication, Camera nodes, Offline communication, AI, Blockchain GAIA / AgroBlock Smart Agriculture considers physical agricultural devices together with the data layer. Sensors, camera nodes and edge devices are connected within a shared communication and data integrity approach. Collecting data from different field devices, handling changing connectivity and processing data traceably are connected needs. > From field data to the whole system. Contribution: IoT and blockchain R&D Considering data flows from sensors, cameras and edge devices in agricultural settings together. Researching IoT, communication and blockchain-based data integrity. Evaluating offline conditions, edge processing and data traceability within one system approach. The project brings IoT sensors and camera nodes together with edge processing, offline communication, AI and blockchain-based data integrity components. Physical conditions, connectivity and software architecture need to be considered together. I focus on how device data moves into the next stages of the system. Sensors, cameras, edge processing, communication and data integrity are considered within the same smart agriculture R&D scope, focusing on designing physical and digital layers together. --- ## devilyxrd / Portfolio URL: https://devilyxrddev.com/en/work/devilyxrd-portfolio Category: Web / Platforms · Creative coding · 2026 · Continuously developed personal site · stack: Vue 3, TypeScript, Vite, Vue Router, CSS, GSAP, Anime.js, SVG This site is a personal space for my work and the questions I explore while building it. Projects, About, writing and live experiments connect through a shared visual language. I wanted to explain work across web, mobile, services and R&D without reducing it to a single list, allowing visitors to explore both project context and my approach. > Code, motion and a personal archive. Contribution: Personal portfolio · content and development Creating an archive that brings together my work, approach and experiments. Developing content and the application in small, reviewable steps. Connecting bilingual pages, search, static generation and interactive experiments within one structure. Built with Vue 3 and TypeScript, the site uses URL-based language selection, static page generation and page-specific sharing images. CSS, GSAP and Anime.js have separate motion responsibilities. While building an experimental visual language, I care about keeping content, readability and navigation clear. I develop the site in small steps and review each one in the browser. Turkish and English pages, a project archive, a search palette and eight interactive browser experiments come together here. Content and application development are ongoing. --- ## Kadir Yücebaş URL: https://devilyxrddev.com/en/work/kadir-yucebas Category: Web / Platforms · 2023 · Independent project · stack: Next.js, TypeScript, PostgreSQL, Drizzle ORM, Docker, Nodemailer The system developed for Kadir Yücebaş brings clinic applications and preliminary assessment into one structure. Turkish and English application interfaces work with a clinic administration panel and notification services. Receiving an application, delivering it to the clinic team and tracking its progress are connected tasks. The aim is to bring these steps into a clear digital workflow for applicants and clinic staff. > One flow, from application to follow-up. Contribution: Independent project · full-stack development Considering clinic application, preliminary assessment and follow-up needs together. Developing web interfaces, administration workflows and services. Connecting Turkish and English forms with the data model and notification processes. Next.js applications connect through shared schemas, Drizzle and PostgreSQL, with email and document-generation components. Bilingual forms and administration screens provide different interfaces to the same system. I focus not only on completing a form, but also on how the application is managed by the clinic and how the applicant follows its progress. Application forms, administration, status tracking and notification workflows have been brought together within one application structure. I work across its web, service and data layers. --- ## E-commerce Solutions URL: https://devilyxrddev.com/en/work/freelance-e-ticaret Category: Web / Platforms · 2021 · Individual web development For my freelance e-commerce work, I took responsibility for the entire project. Understanding the need, designing interfaces, developing the application and setting up its technical infrastructure were parts of the same responsibility. A sales-oriented website needs its user experience and underlying operation to be designed together. I developed the application and infrastructure around each client’s requirements. > From an idea to a working web product, end to end. Contribution: Individual · complete analysis, design, full-stack development and technical infrastructure Defining the need, scope and user journey for an e-commerce web project. Individually handling all analysis, design, software development and technical infrastructure. Completing the interface and application behavior around the same product need. I considered the interface, application logic and technical infrastructure together. For each freelance job in this group, I individually handled the process from requirements to the final application. Responsibility for every stage allowed me to bring design and development decisions around the same product need, considering what the user sees alongside how the system operates. I developed e-commerce websites as a freelancer. This entry groups work for different clients; I individually handled the complete process for each project. --- ## Congress Websites URL: https://devilyxrddev.com/en/work/freelance-kongre Category: Web / Platforms · 2020 · Individual web development For my freelance congress website work, I developed the event’s digital presence from start to finish. Defining requirements, translating content into interfaces and building the working web application were my responsibility. A congress website needs to present information clearly and operate in line with the event’s needs. I shaped the scope around those requirements. > Building an event’s digital presence from start to finish. Contribution: Individual · complete analysis, web design, development and technical infrastructure Defining the congress website’s requirements and content structure. Individually handling all design, software development and technical infrastructure. Bringing event information together in a clear web experience. I designed the content structure, pages, interfaces and application infrastructure together, individually carrying out every stage from analysis to development for each congress project. Content organization and navigation directly affect one another on event websites. I approached design and development through the same information structure. I developed congress websites as a freelancer, individually handling every stage. This entry groups my congress-focused web work. --- ## Corporate Websites URL: https://devilyxrddev.com/en/work/freelance-tanitim Category: Web / Platforms · 2019 · Individual web development I developed corporate websites for companies and agencies. I individually handled analysis, design, implementation and technical infrastructure for web experiences presenting each organization’s identity, services and work. A corporate website should clearly communicate what a company or agency does and the value it provides. I shaped content and page structure around each organization’s needs. > Translating an organization’s identity into a web experience. Contribution: Individual · complete analysis, design, web development and technical infrastructure Clarifying the company or agency’s needs, identity and content structure. Individually developing all design, implementation and technical infrastructure. Presenting the organization’s services and work through a clear web experience. I developed information architecture, visual interfaces and implementation together, considering both the organization’s identity online and the technical infrastructure supporting the website. When working for companies and agencies, I focused on how each organization presents itself, shaping content and interface decisions around a clear presentation of services and work. I developed corporate websites for companies and agencies as a freelancer, individually carrying out all analysis, design, software and technical infrastructure for each job. --- ## Digital Advertising Displays URL: https://devilyxrddev.com/en/work/iotrocop-kiosk Category: IoT / Hardware · Mobile · 2024 · IOTROCOP · individual development · stack: Raspberry Pi 4, Embedded systems, Mobile control At IOTROCOP, I developed a system for digital advertising and promotional displays in cafés and markets. The Raspberry Pi 4 display and management through a phone application were parts of the same solution. Advertising and promotional displays in cafés and markets needed to be manageable through a phone application. I considered the physical display, device infrastructure and mobile control flow together. > A venue’s digital showcase, managed from a phone. Contribution: Individual · complete kiosk, mobile control and technical infrastructure Addressing the need for digital advertising displays in cafés and markets. Individually developing the Raspberry Pi 4 display system, mobile control and all technical infrastructure. Connecting the display’s behavior in the venue with management from a phone application. I developed the Raspberry Pi 4 advertising display for management through a phone application, building its display software, mobile control and device infrastructure together. I considered the display experience in the venue alongside management on the phone, focusing on how device and application behavior address the same need. I developed the phone-managed Raspberry Pi 4 display system for advertising in cafés and markets, individually carrying out design, software and the complete technical infrastructure. --- ## Building-Washing Drone URL: https://devilyxrddev.com/en/work/iotrocop-ros-drone Category: IoT / Hardware · 2025 · IOTROCOP · individual development · stack: ROS, NVIDIA Jetson Nano, Fiber infrastructure I carried out this building-washing drone project individually at IOTROCOP. I considered the drone design alongside ROS, Jetson Nano and fiber infrastructure, building the complete technical structure myself. The building-washing drone’s physical design needed to be considered together with software, computing and communication infrastructure. I approached it as a complete system rather than a single component. > One system, from design to technical infrastructure. Contribution: Individual · building-washing drone design and complete technical infrastructure Considering a building-washing drone design together with computing, software and communication infrastructure. Individually carrying out the design and complete technical infrastructure. Connecting ROS, Jetson Nano and fiber infrastructure within the same system structure. ROS and NVIDIA Jetson Nano formed part of the software and computing infrastructure. I individually implemented the technical structure, including fiber infrastructure, alongside the drone design. This work required me to consider software and technical infrastructure decisions together with physical design, focusing on how different components fit within the same system. I designed the building-washing drone using ROS, Jetson Nano and fiber infrastructure and implemented all technical infrastructure myself, taking individual responsibility from design through the system’s technical components. --- ## Vehicle Tracking and Navigation URL: https://devilyxrddev.com/en/work/iot-arac-takip Category: IoT / Hardware · 2022 · Individual IoT project · stack: Raspberry Pi, GPS, Telemetry, Interactive maps, Embedded systems I developed this IoT-based real-time vehicle tracking and navigation solution as an embedded system running on Raspberry Pi. Live GPS data, an interactive map and real-time telemetry come together in one user interface. I wanted to bring vehicle location and status data into one clear interface on an embedded device, rather than having to follow separate information sources. > Location, maps and telemetry on one screen. Contribution: Individual · complete embedded system, data collection, interface and technical infrastructure Presenting vehicle location, navigation and status information together on an embedded device. Individually developing hardware-level data collection, system infrastructure and the interface. Bringing GPS, maps and telemetry together in a smooth real-time experience. I connected hardware-level data collection with the user interface. I optimized the interface displaying speed, signal strength, battery status and sensor data in real time for smooth operation on the embedded system. I considered data collection and interface performance together, focusing on keeping map and telemetry views smooth while maintaining up-to-date information from the hardware. I developed the Raspberry Pi system combining live GPS, interactive maps, navigation and real-time telemetry. I individually carried out every stage of the solution displaying speed, signal strength, battery status and sensor data. # devilyxrd — Türkçe ## Türkçe İ, CSS ve büyük harfle yazılan İngilizce terimler URL: https://devilyxrddev.com/journal/turkish-dotted-i Date: 2026-10 · Arayüz Türkçe bir sayfada text-transform: uppercase, i harfini İ yapar. Türkçe kelimeler için doğru, JavaScript ya da Firebase için yanlış. Bu sitede sorunu elle tutulan bir kelime listesiyle değil, iki dilli verinin kendisiyle çözdüm; aynı kural paylaşım kartlarında da çalışıyor. ### Tarayıcı aslında haklı Bu sitede küçük mono yazılar, etiketler, filtre düğmeleri ve menü bağlantıları CSS ile büyük harfe çevriliyor. base.css içinde .mono, .tag, .chip ve .lk sınıflarının hepsinde text-transform: uppercase var. Kaynakta metin küçük harfle duruyor; büyütme işini tarayıcı, metni ekrana çizerken yapıyor. Tarayıcı bunu yaparken dile bakıyor, çünkü büyük-küçük harf dönüşümü evrensel değil. İngilizcede i büyüyünce I olur. Türkçede ise dört harf iki ayrı çift oluşturur: noktasız ı ile I, noktalı i ile İ. Sayfa lang="tr" olduğunda tarayıcı Türkçe eşlemeyi uyguluyor ve "Mühendislik" etiketi MÜHENDİSLİK oluyor. İstediğimiz tam olarak bu. Aynı kelime lang="en" altında MÜHENDISLIK olurdu ki bu da Türkçe okuyan biri için yanlış. Sorun, aynı kuralın teknoloji ve proje adlarına da uygulanması. JavaScript JAVASCRİPT, Firebase FİREBASE, Linux LİNUX oluyor. Tarayıcı burada hata yapmıyor; metnin Türkçe olduğunu ona biz söylüyoruz. Sitede <html lang> değeri App.vue içinde useHead ile URL'den gelen dile bağlı. Yani Türkçe sayfalarda, aksi belirtilmedikçe, her şey Türkçe kuralla büyüyor. ### İlk deneme: kelime listesi İlk refleksim, İngilizce kalması gereken kelimeleri bir listede toplayıp eşleşen etiketlere lang="en" vermek oldu. Demo içerikle çalıştı. Sonra projelere ve Hakkımda sayfasına gerçek içerik girdi: araç listesine Go, Dart, gRPC, Kubernetes ve NVIDIA Jetson Nano geldi, proje adları ve ilgi alanları değişti. Liste bir anda eskidi ve etiketler yine noktalı İ ile görünmeye başladı. Listenin asıl kusuru eskimesi değildi, eskidiğini kimseye söylememesiydi. Yeni bir araç eklediğimde hiçbir şey kırılmıyor, build geçiyor, yalnızca bir etiket sessizce yanlış görünüyordu. Bu tür hatalar ancak biri sayfaya dikkatle baktığında fark ediliyor. > Elle tutulan bir liste, içerik büyüdükçe sessizce bozulur. ### Kural: iki dilde aynıysa isimdir Site zaten iki dilli. Her etiketin src/i18n/locales altında bir Türkçe, bir de İngilizce karşılığı var ve bu veri, listenin tutmaya çalıştığı bilgiyi zaten içeriyor. Bir etiket iki dilde birebir aynıysa çevrilmemiş demektir; çevrilmeyen şey de neredeyse her zaman bir isim ya da terimdir: Linux, Firebase, Motice, Blockchain. Farklıysa (Sensör haberleşmesi / Sensor communication) bir çeviridir ve sayfanın dilinde kalmalıdır. ``` export const sameInBoth = (a: string | undefined, b: string | undefined) => a !== undefined && a === b ? 'en' : undefined /** Project stacks: always technology names. */ export const STACK_LANG = 'en' export const catLang = (c: ProjectCat | 'all') => sameInBoth(tr.cats[c], en.cats[c]) export const toolTagLang = (group: number, item: number) => sameInBoth(tr.about.tools[group]?.items[item], en.about.tools[group]?.items[item]) ``` Fonksiyon ya "en" ya da undefined döndürüyor. undefined bilinçli bir seçim: Vue, değeri undefined olan bir özelliği DOM'a hiç yazmıyor, böylece etiket sayfanın dilini miras alıyor. Bileşenlerde yapılan tek şey :lang="catLang(c)" yazmak. Aynı kuraldan geçen dört yardımcı var: proje kategorileri için catLang, ana sayfadaki alan etiketleri için fieldTagLang, Hakkımda sayfasındaki araçlar için toolTagLang ve ilgi alanları için outsideLang. Teknoloji yığınları ayrı bir durum. Projelerin stack listesi tek ve dilden bağımsız bir diziden geliyor; karşılaştırılacak ikinci bir dil yok. Oradaki her öğe zaten bir teknoloji adı olduğu için ProjectView doğrudan STACK_LANG, yani "en" kullanıyor. ### CatLabel ve sınır durumları Bir projenin birden fazla kategorisi olabiliyor ve bunlar "Web / Platform · Mobil" gibi tek satırda gösteriliyor. Satırın tamamına tek bir lang vermek yanlış olurdu: Blockchain İngilizce kuralla, Mobil Türkçe kuralla büyümeli. CatLabel.vue bu yüzden her kategoriyi kendi span'ine koyuyor ve her span'e kendi lang değerini veriyor; aradaki ayraçlar sayfanın dilinde kalıyor. Kuralın sınırlarını da bilmek gerekiyor. Karşılaştırma tam eşitlik arıyor ve büyük-küçük harfe duyarlı. "Web / Platform" ile "Web / Platforms" farklı olduğu için Türkçe kuralla büyüyor; içinde i olmadığından sonuç değişmiyor. "Edge sistemler" Türkçede kalıyor ve EDGE SİSTEMLER oluyor; kelimenin yarısı Türkçe olduğu için bu doğru. Alan ve araç etiketleri dizideki sırayla eşleşiyor, yani Türkçe ve İngilizce listelerin sırası kayarsa kural yanlış iki etiketi karşılaştırır. Bir Türkçe kelime İngilizce karşılığıyla tesadüfen aynı yazılırsa kural onu isim sanar; şu anki etiketlerde böyle bir örnek görmedim. Bunları bilerek kabul ettim, çünkü iki dil dosyası zaten aynı yapıyı paylaşıyor. ### Aynı kural paylaşım kartlarında Her sayfanın 1200×630 paylaşım kartı build sırasında Satori ile çiziliyor ve kartın üstündeki bölüm etiketi orada da büyük harfle yazılıyor. Ama Satori'de lang özelliği ve text-transform ile çalışan bir sayfa yok; büyütmeyi JavaScript yapıyor. toLocaleUpperCase('tr') ile toLocaleUpperCase('en') arasındaki fark, CSS'teki lang farkının birebir karşılığı. scripts/seo-files.mjs, kategori etiketleri için aynı karşılaştırmayı tekrar ediyor. ``` const up = (s) => s.toLocaleUpperCase(lang === 'en' ? 'en' : 'tr') // same rule as src/utils/lang.ts const upCat = (c) => (tr.cats[c] === en.cats[c] ? d.cats[c].toLocaleUpperCase('en') : up(d.cats[c])) ``` Böylece sitede BLOCKCHAIN olarak görünen bir kategori kartta BLOCKCHAİN olmuyor. Kural iki yerde yazılı ama ikisi de aynı veriye bakıyor; yeni bir kategori eklediğimde ikisini ayrıca güncellemem gerekmiyor. ### Başka bir projede yeniden kullanmak Bu yaklaşımı taşımak için iki dilli içerik şart değil, ama işi çok kolaylaştırıyor. Genel fikir üç parçadan oluşuyor: büyük harf dönüşümünü dilden bağımsız sanmamak, lang özelliğini sayfa düzeyinde değil metin parçası düzeyinde düşünmek ve "bu İngilizce mi" kararını elle değil veriden çıkarmak. Tek dilli bir sitede bile terimleri ayrı bir alanda tutup onlara lang="en" vermek, bir kelime listesinden daha dayanıklı. Büyütmeyi CSS'e bırakmanın da bir yan faydası var: kaynakta metin doğal halinde kalıyor ve aynı veri büyük harf gerekmeyen yerlerde olduğu gibi kullanılabiliyor. Note (Ders): Bir kuralı elle tutulan bir listeye değil veriye bağlamak, yeni içerik geldiğinde kimsenin onu hatırlamak zorunda kalmaması demek. Bu sitede yeni bir araç ya da kategori eklediğimde doğru lang değeri kendiliğinden geliyor. --- ## Bir Vue SPA'yı arama motorlarına ve yapay zekâ tarayıcılarına görünür kılmak URL: https://devilyxrddev.com/journal/spa-visible-to-ai-crawlers Date: 2026-10 · Arama ve GEO Tek sayfalık bir uygulama, JavaScript çalıştırmayan bir tarayıcıya boş bir div gösterir. Bu sitede her rota build sırasında iki dilde gerçek HTML'e dönüşüyor; hreflang, JSON-LD, sitemap, robots.txt ve llms.txt aynı içerikten üretiliyor ve build sonunda kendini denetliyor. ### Boş div sorunu Vue ile yazılmış bir SPA'nın sunucudan gelen HTML'i neredeyse boştur: bir <div id="app"> ve bir script etiketi. İçerik, tarayıcı JavaScript'i çalıştırdıktan sonra oluşur. Google bunu çoğu zaman render ediyor, ama yapay zekâ asistanlarının sayfa çekicileri ve pek çok önizleme botu sayfayı JavaScript çalıştırmadan okuyor. Onlar için bu site bir açılış ekranından ve boş bir kutudan ibaretti. Seçeneklerden biri siteyi SSR odaklı bir framework'e taşımaktı. Bunun yerine mevcut Vue uygulamasını koruyup build'e bir prerender adımı ekledim. Sitenin bir sunucu tarafı yok; her sayfanın içeriği build anında belli. Bu durumda statik HTML dosyaları yeterli. ### Bir uygulama, iki giriş Ortak nokta src/app/createSiteApp.ts. Sunucu tarafında ya da hidrasyon yapılacaksa createSSRApp, aksi halde düz createApp kullanıyor. Router sunucuda bellek geçmişiyle, tarayıcıda web geçmişiyle kuruluyor. İstemci girişi main.ts, #app içinde bir eleman olup olmadığına bakıyor: prerender edilmiş sayfada var, hidrasyon yapılıyor; geliştirme sunucusunda #app yalnızca bir yorum satırı içeriyor, uygulama sıfırdan kuruluyor. ``` const container = document.getElementById('app')! const hydrate = !!container.firstElementChild const { app, router } = createSiteApp({ server: false, hydrate }) app.use(createHead()) // wait for the first route (lazy views) before mounting router.isReady().then(() => { app.mount(container) restoreSecret() bootDone() }) ``` router.isReady beklemesi önemli: görünümlerin çoğu tembel yükleniyor ve ilk rota çözülmeden mount edilirse istemcinin ürettiği ağaç sunucunun yazdığı HTML ile eşleşmez. Sunucu tarafında entry-server.ts içindeki render fonksiyonu aynı işi tersinden yapıyor: router.push ile adrese gidiyor, isReady bekliyor, renderToString ile HTML üretiyor ve bunu şablondaki <!--app-html--> yerine koyuyor. Head etiketleri unhead'in transformHtmlTemplate fonksiyonuyla birleştiriliyor. Aynı modül projeleri, yazıları ve dil dosyalarını da dışarı açıyor; build scriptleri içeriği ikinci bir kaynaktan değil, uygulamanın kendisinden okuyor. ### Her rota için bir dosya Build komutu sırayla tip kontrolü, istemci build'i, SSR build'i, prerender ve SEO denetimi çalıştırıyor. scripts/prerender.mjs iki dil için bütün publicPaths listesini geziyor. Bu liste router.ts içinde projelerden ve yazılardan türetiliyor; yeni bir yazı eklemek, iki yeni HTML dosyası demek. 404 sayfası ayrıca, noindex ile yazılıyor. ``` dist/index.html → / dist/work/yee-cms.html → /work/yee-cms dist/en.html → /en dist/en/journal/<yazı>.html → /en/journal/<yazı> dist/404.html → noindex ``` ### İki dil, iki URL Türkçe sayfalar kökte, İngilizce sayfalar /en altında. router.ts İngilizce rotaları Türkçe rotaların kopyası olarak üretiyor, adlarına en- ekliyor ve meta.lang değerini "en" yapıyor; her şeyi yakalayan 404 rotaları en sona konuyor. Dil URL'den okunuyor ve beforeEach içindeki setLocale çağrısı hem sunucuda hem tarayıcıda çalışıyor. Bu yüzden prerender edilen HTML ile hidrasyondan sonraki sayfa aynı dili konuşuyor. useSeo her sayfaya kendini gösteren bir canonical ve tr, en, x-default için hreflang bağlantıları ekliyor; x-default Türkçe sayfayı gösteriyor. og:locale tr_TR ya da en_US oluyor, diğer dil og:locale:alternate olarak yazılıyor. 404 sayfası noindex aldığı için ne canonical ne de dil alternatifi taşıyor; arama motorlarını bir yere yönlendirmemesi gerekiyor. ### Makinelerin okuduğu katman Her sayfada tek bir JSON-LD bloğu ve içinde bir @graph var. Person ve WebSite düğümleri her sayfada bulunuyor; Person'ın @id değeri sabit olduğu için bütün sayfalar aynı kişiye işaret ediyor. Bunlara sayfa türüne göre düğümler ekleniyor: yazılar için BlogPosting, alt sayfalar için BreadcrumbList, site haritası için CollectionPage. JSON, script etiketine yazılmadan önce < karakterleri kaçırılıyor; içerikteki hiçbir metin etiketi erken kapatamıyor. sitemap.xml her adresi iki dilde listeliyor; her kayıtta tr, en ve x-default için xhtml:link alternatifleri ve sayfanın paylaşım kartı image:image olarak var. robots.txt herkese açık ve botları üç grupta adıyla anıyor: arama motorları (Googlebot, Bingbot, Applebot, DuckDuckBot, YandexBot), kaynak gösteren yapay zekâ arama ve asistanları (OAI-SearchBot, ChatGPT-User, Claude-SearchBot, Claude-User, PerplexityBot, Perplexity-User) ve eğitim tarayıcıları ile kontrol belirteçleri (GPTBot, ClaudeBot, Google-Extended, Applebot-Extended, CCBot). Hepsine açıkça Allow verilmiş; vazgeçmek istersem tek kelimeyi Disallow yapmam yetiyor. Dosyanın sonunda sitemap adresi duruyor. llms.txt, llmstxt.org biçiminde düz markdown bir harita: kısa bir tanım, iki dilde sayfalar, projeler ve yazılar. llms-full.txt ise bütün yazıların ve proje notlarının tam metnini iki dilde, her biri kendi adresiyle veriyor; yazı blokları başlık, alıntı ve kod bloğu olarak markdown'a çevriliyor. Bu dosyaların hepsi scripts/seo-files.mjs içinde, uygulamanın kendi içerik modülünden üretiliyor. > SEO bir kere yapılan bir ayar değil, her build'de çalışan bir test olmalı. ### Build kendini denetlesin Son adım scripts/check-seo.mjs. dist altındaki her HTML dosyasında tam bir <title> ve tam bir <h1> arıyor, <html lang> değerinin dosyanın yoluyla uyuştuğunu, og:locale değerinin dille eşleştiğini ve zorunlu meta setinin eksiksiz olduğunu kontrol ediyor. Açıklama 50 ile 170 karakter arasında olmalı. Canonical sitemap'te bulunmalı. hreflang bağlantıları kendini göstermeli, x-default Türkçe sayfaya gitmeli ve karşı dildeki sayfa da geri bağlantı vermeli. og:image ve twitter:image mutlak adres olmalı, dosya var olmalı ve 1200×630 olmalı. JSON-LD ayrıştırılabilmeli; BlogPosting, BreadcrumbList ve Person düğümlerinde gerekli alanlar bulunmalı. robots.txt sitemap'i duyurmalı. Bir sorun varsa build başarısız oluyor. Note (Not): Bu yazı da aynı yoldan geçti: içerik eklendi, build alındı, denetim geçti ve iki dilde ayrı birer sayfa olarak, llms-full.txt içinde de düz metin olarak yayına çıktı. --- ## Satori ile her sayfaya ve her dile ayrı paylaşım kartı URL: https://devilyxrddev.com/journal/og-cards-with-satori Date: 2026-10 · Arama ve GEO Bu sitede her sayfanın Türkçe ve İngilizce iki ayrı 1200×630 paylaşım kartı var ve hepsi build sırasında Satori ile çiziliyor. İşin zor kısmı düzen değil fontlar oldu: ş, ğ ve İ önce kutu olarak çıktı. Çözüm, font alt kümelerini bırakıp tam TTF dosyalarına geçmekti. ### Neden sayfa başına bir kart Başlangıçta sitenin tek bir paylaşım görseli vardı, public/og.png. Hangi sayfanın bağlantısını paylaşırsanız paylaşın, sohbet uygulamasında aynı görsel çıkıyordu. Bir yazıyı paylaşan biri için bu pek bir şey söylemiyor. İstediğim, kartın sayfanın kendisini anlatmasıydı: bölüm etiketi, başlık, kısa açıklama ve sitede o sayfaya ait olan kapak çizimi. Site iki dilli olduğu için her sayfanın iki kartı olmalıydı. og.png hâlâ duruyor; 404 sayfası ve JSON-LD'deki Person görseli onu kullanıyor. ### Ağaç, SVG, PNG Satori, React benzeri bir eleman ağacını ve font dosyalarını alıp bir SVG üretiyor. Ağacı JSX ile yazmak zorunlu değil; type ve props alanları olan düz nesneler yetiyor. scripts/og.mjs bu nesneleri küçük bir h yardımcısıyla kuruyor. Düzen, Satori'nin desteklediği flexbox alt kümesiyle yapılıyor. Ortaya çıkan SVG'yi @resvg/resvg-js 1200 piksel genişliğe sığdırıp PNG'ye çeviriyor. loadSystemFonts: false ayarı, build alan makinedeki fontların karta sızmasını engelliyor; kart her makinede aynı fontlarla çiziliyor. ``` const h = (type, style, ...children) => ({ type, props: { style, children: children.length === 1 ? children[0] : children } }) const svg = await satori(tree, { width: 1200, height: 630, fonts }) return new Resvg(svg, { fitTo: { mode: 'width', value: 1200 }, font: { loadSystemFonts: false } }).render().asPng() ``` Kapak çizimi, sitenin tarayıcıda çizdiği prosedürel şekil listesinin aynısı. artOf ile gelen dikdörtgenler ve elipsler bir SVG'ye yazılıyor ve karta veri URI'si olarak, bir img elemanıyla yerleşiyor. Şekillerin içinde metin olanlar ayrılıyor ve Satori ile ayrıca diziliyor; böylece onlar da sitenin fontuyla çiziliyor. Çok ince çizgiler en az yaklaşık 1,2 piksel kalınlıkta tutuluyor ki rasterleştirmede kaybolmasınlar. Hangi kartların çizileceğine scripts/seo-files.mjs içindeki ogEntries karar veriyor: her dil için, her herkese açık sayfaya bir kayıt. Kayıtta bölüm etiketi, başlık, açıklama ve kapak adı var. Listede karşılığı olmayan bir sayfa varsa fonksiyon hata fırlatıyor; kartı unutulmuş bir sayfa build'i durduruyor. Dosya adını ogKeyFor üretiyor: ana sayfa home, /work/yee-cms work-yee-cms, İngilizce kartlar en- önekiyle. prerender.mjs bunları dist/og altına yazıyor, useSeo da og:image ve twitter:image değerlerini aynı adrese bağlıyor. ### Türkçe harfler kutu olunca İlk denemede fontları @fontsource paketlerinden aldım. Bu paketler fontları alfabeye göre alt kümelere bölüyor; temel Latin bir dosyada, ş, ğ ve İ gibi harflerin bulunduğu genişletilmiş Latin başka bir dosyada. Tarayıcıda bu bölme mantıklı, çünkü tarayıcı yalnızca gereken dosyayı indiriyor. Satori'ye ise her dosyayı ayrı bir font olarak vermek gerekiyor. Genişletilmiş alt kümeleri ayrı aile adlarıyla kaydettim ve eksik harflerin oraya düşmesini bekledim. Olmadı: ş, ğ ve İ kartlarda kutu olarak çıktı ya da yanlış yüzle çizildi. Alt kümelere verdiğim aile adları da beklediğim gibi ayrıştırılmadı. og.mjs içindeki yorum durumu kısaca özetliyor: Satori aileler arasında güvenilir biçimde geri düşmüyor. Çözüm, alt kümelerle uğraşmayı bırakmak oldu. npm'deki @expo-google-fonts paketleri Google Fonts ailelerini tam karakter setli TTF dosyaları olarak dağıtıyor. Her ağırlık için tek bir tam dosya olunca geri düşmeye gerek kalmıyor. Bu paketler yalnızca devDependencies içinde; tarayıcıya hiçbir zaman gönderilmiyorlar. ``` async function ttf(pkg, file) { const dir = path.dirname(require.resolve(`${pkg}/package.json`)) return fs.readFile(path.join(dir, file)) } { name: 'Syne', weight: 800, style: 'normal', data: await ttf('@expo-google-fonts/syne', '800ExtraBold/Syne_800ExtraBold.ttf') } ``` > Bir fontu parçalara bölmek tarayıcıda tasarruftur, kart üreticisinde ise hata kaynağı. ### g ve ğ için küçük bir yama Sitede başlıklardaki g ve ğ harfleri Syne'den değil Archivo Expanded'dan geliyor; bunun hikâyesini ayrı bir yazıda anlattım. Kartların da aynı harfleri göstermesi gerekiyordu. archivoPatch fonksiyonu build sırasında Google Fonts'tan yalnızca bu iki harfi içeren bir CSS istiyor. İstekte User-Agent olarak Mozilla/4.0 gönderiyor; Google eski bir istemciye WOFF2 yerine TrueType veriyor. Satori TTF, OTF ve WOFF okuyabiliyor ama WOFF2 okuyamıyor. İki istek de 8 saniyelik bir zaman aşımına bağlı. Ağ yoksa fonksiyon bir uyarı yazıp null döndürüyor ve kartlar Syne'nin g harfiyle çiziliyor; build kırılmıyor. Satori'de tarayıcıdaki gibi harf başına font seçimi olmadığı için displayText başlığı kendisi bölüyor. Her kelime ayrı bir flex öğesi oluyor, kelimenin içindeki g ve ğ dizileri fontFamily: 'Archivo' olan ayrı bir span'e konuyor. Kelime başına bir flex öğesi kullanmanın ikinci bir faydası var: satır hiçbir zaman bir kelimenin ortasından kırılmıyor. Kelimeler arasındaki boşluk, yazı boyutunun 0,24 katı olan columnGap ile veriliyor. ### Düzenin sınırları ve denetim Başlıklar uzunluğa göre dört boyuttan birini alıyor: 14 karaktere kadar 88 piksel, 22'ye kadar 72, 36'ya kadar 60, daha uzunsa 50. Açıklama 150 karakterde, bir kelime sınırında kesiliyor ve sonuna üç nokta ekleniyor. Metin sütunu 610 piksel genişliğinde, sağda 420 piksellik kare bir kapak var. Alt satırda devilyxrd kelime işareti (x harfi iris renginde) ve SITE_URL'den gelen alan adı duruyor. Bölüm etiketi büyük harfle yazılıyor ve Türkçe İ kuralına uyuyor. Build'in son adımı check-seo, her sayfadaki og:image ve twitter:image adreslerinin mutlak olduğunu ve dosyanın var olduğunu kontrol ediyor. Sonra PNG başlığındaki genişlik ve yükseklik değerlerini okuyup 1200×630 olup olmadığına bakıyor. Geliştirme sırasında da kartlar görülebiliyor: vite.config.ts içindeki eklenti /og/<anahtar>.png isteklerini aynı renderOg fonksiyonuyla anında çiziyor ve fontları ilk yüklemeden sonra bellekte tutuyor. Note (Ders): Sunucuda fontla görsel üretiyorsanız tam font dosyası kullanın. Alt kümeler tarayıcı için yapılmış bir optimizasyon; geri düşme mantığı olmayan bir ortamda sorunu yalnızca başka bir yere taşıyor. --- ## Syne'nin g harfi: hata değil tasarım, yine de iki harfi değiştirdim URL: https://devilyxrddev.com/journal/syne-g-archivo-patch Date: 2026-10 · Tasarım ve etkileşim Sitenin başlık fontu Syne'nin küçük g ve ğ harfleri büyük boyutta bize kırık gibi göründü. Font dosyasını inceledim; sorun bir hata değil, tasarımın kendisiydi. Çözüm, yalnızca bu iki harfi Archivo'dan alan, birkaç KB'lık bir yama ve tarayıcının harf harf font seçmesi oldu. ### Kırık görünen bir harf Sitenin başlıkları Syne ile, çoğunlukla 800 ağırlıkta yazılıyor. Geniş, karakterli bir font ve sitenin kimliğinin büyük kısmını o taşıyor. Ama küçük g harfi büyük boyutlarda bize hep tuhaf geldi. İlk bakışta bir render hatası sandık: harf, sanki bir parçası eksikmiş ya da yanlış yerleşmiş gibi duruyordu. Türkçede ğ sık geçtiği için sorun yalnızca İngilizce kelimelerde değil, Türkçe başlıklarda da göze batıyordu. ### Önce hatayı kendimde aradım Akla gelen şüpheliler belliydi: Google Fonts'un gönderdiği alt küme, tarayıcının sentetik kalınlaştırması ya da benim CSS'imdeki bir ayar. Bunları tek tek eledim. Harfi, paylaşım kartlarında da kullandığım tam karakter setli Syne TTF dosyasıyla karşılaştırdım; aynı şekil oradaydı. Fontun stilistik setlerine de baktım, beklediğim türde bir alternatif g bulamadım. Sonuç netti: bu bir hata değil, Syne'nin orijinal tasarımı. Bunu bilmek önemliydi, çünkü bir hata olsaydı doğru dosyayı yüklemek ya da bir ayarı düzeltmek yeterdi. Tasarım olduğu için önümde üç yol vardı: başlık fontunu tamamen değiştirmek, harfi olduğu gibi kabul etmek ya da yalnızca bu iki harfi başka bir fonttan almak. İlki sitenin görünüşünü baştan değiştirirdi, ikincisi bizi rahatsız etmeye devam ederdi. Üçüncüsünü seçtim. ### İki harflik bir font Yedek font olarak Archivo'yu seçtim. Archivo'nun bir genişlik ekseni var ve wdth 125 değeri Expanded kesimine karşılık geliyor; bu geniş oranlar, benim gözümde Syne'nin yanında en az sırıtan seçenekti. Google Fonts'un css2 adresi text parametresini destekliyor: yalnızca istenen karakterleri içeren bir font dosyası döndürüyor. text=g%C4%9F, yani gğ. Sitede --f-display 800 ve 600 ağırlıkta kullanıldığı için 600, 700 ve 800 ağırlıklarını istiyorum. Stil dosyası media="print" ve onload hilesiyle, ilk boyamayı bloklamadan yükleniyor. ``` <!-- index.html --> <link rel="stylesheet" media="print" onload="this.media='all'" href="https://fonts.googleapis.com/css2?family=Archivo:wdth,wght@125,600;125,700;125,800&text=g%C4%9F&display=swap" /> /* src/styles/tokens.css */ --f-display: 'Archivo', 'Syne', 'Space Grotesk', sans-serif; ``` Google'ın bu isteğe döndürdüğü CSS'e bakmak öğretici. Her ağırlık için ayrı bir @font-face kuralı var: font-family 'Archivo', font-stretch 125% ve unicode-range: U+67, U+11F, U+306. İlk ikisi g ve ğ. Üçüncüsü birleşen breve işareti; ğ harfi g ile bu işaretin birleşimi olarak da yazılabildiği için o da dosyada. Sonuç, iki harf için birkaç KB'lık bir font. ### Geri düşme harf harf çalışır İşin geri kalanını tarayıcının font eşleme kuralları yapıyor. --f-display listesinde Archivo ilk sırada. Tarayıcı bir metni çizerken her karakter için listedeki ilk uygun yüzü arıyor; karakter bir @font-face kuralının unicode-range aralığında değilse ya da fontta o glif yoksa sıradaki aileye geçiyor. Yani "doğru" kelimesinde d, o, r ve u Syne'den, ğ Archivo'dan geliyor. Büyük G bu listede olmadığı için o da Syne'de kalıyor. Ağırlıklar da aynı mantıkla eşleşiyor. Archivo için her ağırlık ayrı bir @font-face kuralı olduğundan, 800 ağırlıklı bir başlıkta g harfi de Archivo'nun 800 kesiminden geliyor; .ablead gibi 600 ağırlıklı metinlerde ise 600 kesiminden. Listenin üçüncü sırasındaki Space Grotesk, Syne henüz yüklenmemişken ya da hiç yüklenemezse devreye giren bir güvenlik ağı; en sondaki sans-serif de onun yedeği. unicode-range'in bir faydası daha var: tarayıcı bu font dosyasını yalnızca sayfada o aralıktaki bir karakter bu font yığınıyla çizileceği zaman indiriyor. g ya da ğ içermeyen bir başlık için Archivo hiç istenmiyor. Bir de dürüst olmak gerekirse küçük bir bedeli var: stil dosyası bloklamadan ve display=swap ile yüklendiği için, Archivo gelene kadar g harfi bir an Syne ile görünüp sonra değişebiliyor. Açılış ekranı bu kuralın dışında. Oradaki DEVILYXRD yazısı --f-display değil doğrudan Syne kullanıyor; yazı büyük harf olduğu ve içinde g bulunmadığı için yamaya ihtiyacı yok. ### Aynı yama paylaşım kartlarında Paylaşım kartlarını çizen Satori'de unicode-range ve harf başına geri düşme yok, bu yüzden aynı ayrımı elle yapmak gerekiyor. scripts/og.mjs build sırasında aynı Archivo alt kümesini Google Fonts'tan TrueType olarak indiriyor. displayText başlığı kelimelere, kelimeleri de g ve ğ dizileri ile geri kalanı olarak bölüyor; g ve ğ dizileri Archivo ailesiyle çiziliyor. Ağ yoksa yama atlanıyor ve kartlar Syne'nin g harfiyle çiziliyor. ``` const runs = hasPatch ? w.split(/([gğ]+)/).filter(Boolean) : [w] return h('span', { display: 'flex' }, ...runs.map((r) => (/^[gğ]+$/.test(r) ? h('span', { fontFamily: 'Archivo' }, r) : r))) ``` > Bir fontu değiştirmeden onun iki harfini değiştirmek mümkün; tarayıcı zaten harf harf seçiyor. Note (Not): Bir glife hata demeden önce orijinal dosyaya bakmak gerekiyor. Tasarım olduğu ortaya çıkarsa, tasarımcının bir kararını geçersiz kıldığınızı açıkça kabul edip değişikliği olabildiğince dar tutmak en temiz yol. Note (Güncelleme): Bu yazıdan sonra fontları Google Fonts yerine kendi sunucumdan vermeye başladım. Archivo'nun g ve ğ dosyası da artık /fonts altında duran 2 KB'lık bir woff2; @font-face kuralları index.html içinde, unicode-range ile yalnızca bu iki harfi kapsıyor. Yöntem aynı, yalnızca dosya artık üçüncü taraf bir sunucudan gelmiyor. --- ## Yükleme ekranındaki takılmayı compositor'a taşımak URL: https://devilyxrddev.com/journal/compositor-only-loading-screen Date: 2026-10 · Arayüz DEVILYXRD yazan açılış ekranı ilk sürümde takılıyordu. Çözüm daha hızlı JavaScript yazmak değil, ana iş parçacığına hiç iş bırakmamaktı: dört gerçek kilometre taşı, yalnızca transform ile hareket eden parçalar ve işler ters gittiğinde devreye giren zamanlayıcılar. ### Neden takılıyordu Açılış ekranı, tarayıcının en meşgul olduğu anda çalışıyor: HTML ayrıştırılıyor, fontlar iniyor, JavaScript paketi çözülüyor ve Vue prerender edilmiş sayfanın üzerine hidrasyon yapıyor. İlk sürümde ilerleme çubuğu ve sayaç, yaklaşık 16 milisaniyede bir çalışan bir JavaScript döngüsüyle güncelleniyordu. Ana iş parçacığı uygulamayı kurarken bu döngünün adımları gecikiyor, animasyon tam da ekranda göründüğü anda kesik kesik ilerliyordu. Ekranın kendisi index.html içinde, satır içi CSS ve satır içi bir script olarak duruyor. Böylece hiçbir JavaScript paketi ya da font gelmeden boyanabiliyor. Kapsayıcıda contain: strict var; içindeki hiçbir değişiklik sayfanın geri kalanının düzenini etkilemiyor. ### Sadece transform Kuralı basit tuttum: açılış ekranında hareket eden her şey yalnızca transform kullanacak, solarken de opacity. Bu iki özellik tarayıcının compositor katmanında canlandırılabiliyor; ana iş parçacığı meşgulken bile akıcı kalıyorlar. JavaScript artık animasyonu yürütmüyor, yalnızca hedef değeri yazıyor. Aradaki hareketi bir CSS geçişi yapıyor. Perde .out sınıfı eklendiğinde translateY(-100%) ile yukarı kayıyor. DEVILYXRD harflerinin her biri ayrı bir span; başta translateY(112%) ile aşağıda saklı duruyorlar ve .ready sınıfı geldiğinde yerlerine çıkıyorlar. Her harfin --i değişkeni geçiş gecikmesini 55 milisaniyelik adımlarla kaydırıyor, kelime soldan sağa açılıyor. Çıkışta aynı harfler -112% ile yukarı kaçıyor, bu kez 22 milisaniyelik adımlarla. Çubuk genişliğini değil scaleX değerini değiştiriyor. ``` function set(v) { if (v <= shown) return shown = v bar.style.transform = 'scaleX(' + v / 100 + ')' var d = [Math.floor(v / 100), Math.floor(v / 10) % 10, v % 10] for (var i = 0; i < 3; i++) cols[i].style.transform = 'translateY(' + (-1.6 * d[i]) + 'em)' } ``` Sayaç bir odometre. Her basamak, alt alta dizilmiş rakamlardan oluşan bir şerit: yüzler basamağında yalnızca 0 ve 1, onlar ve birler basamağında 0'dan 9'a kadar. Her rakam 1.6em yüksekliğinde ve basamağın penceresi de 1.6em. Doğru rakamı göstermek için şeridi -1.6 × rakam kadar yukarı kaydırmak yetiyor; geçişi yine CSS yapıyor. set fonksiyonu daha küçük bir değeri yok sayıyor, yani sayaç hiçbir zaman geri gitmiyor. ### Dört gerçek kilometre taşı Sahte bir zamanlayıcı yerine ilerleme gerçek olaylara bağlı. Script çalıştıktan bir görev sonra değer 12 oluyor; bunu bir setTimeout ile geciktirmemin sebebi ilk geçişin gerçekten canlanması. DOMContentLoaded geldiğinde 42. document.fonts.load ile Syne 800 ve IBM Plex Mono 500 yüklendiğinde 76 oluyor ve harflerin açılmasını başlatan .ready sınıfı ekleniyor. Son adım 100: main.ts uygulamayı mount ettikten sonra boot.ts içindeki bootDone fonksiyonunu çağırıyor, o da document.fonts.ready'yi bekleyip window.__bootDone'u tetikliyor. Her kilometre taşının bir de güvenlik ağı var. Font bekleme en fazla 1,5 saniye sürüyor; o süre dolunca harfler fontu beklemeden açılıyor. Uygulama hiç haber vermezse 6 saniye sonunda __bootDone yine de çağrılıyor. Ekran en az yaklaşık 1,1 saniye kalıyor ki harfler hareketini bitirsin. 100'e ulaştıktan sonra da 420 milisaniye bekleniyor; çubuk yerine otursun ve hidrasyonu biten sayfa perdenin altında bir kez boyansın. ### Perde kalkarken leave fonksiyonu .out sınıfını ekliyor, window.__bootLeft değerini true yapıyor ve boot:leave adında bir olay yayınlıyor. Eleman, geçiş bittikten sonra DOM'dan siliniyor ve <html> üzerindeki, kaydırmayı kilitleyen booting sınıfı kaldırılıyor. boot.ts içindeki bootReady sözü bu olayı bekliyor; sunucuda, açılış ekranı olmayan bir sayfada ya da perde zaten kalkmışsa hemen çözülüyor. Ana sayfadaki HeroScene animasyonunu bootReady.then ile başlatıyor; böylece hero, perdenin arkasında değil, perde kalkarken açılıyor. Script iki noktada performance.mark çağırıyor: fontlar hazır olduğunda boot:fonts, perde kalkarken boot:leave. Bu işaretler tarayıcının performans panelinde zaman çizelgesinin üzerinde görünüyor ve açılışın hangi adımda beklediğini tahmin etmek yerine görmeyi sağlıyor. ### Hareket istemeyenler için İşletim sisteminde hareketi azaltma tercihi açıksa ekran farklı davranıyor. prefers-reduced-motion altında harfler, çubuk ve sayaç geçişsiz, doğrudan son hallerinde duruyor. Perde kaymak yerine 240 milisaniyede soluyor. En kısa süre 1,1 saniyeden 200 milisaniyeye iniyor, 100'den sonraki bekleme kalkıyor ve eleman 300 milisaniye sonra siliniyor. Aynı kontrol hem CSS'te media query olarak hem de script'te matchMedia ile yapılıyor; zamanlamalar da tercihe uyuyor. > Akıcılık çoğu zaman kodu hızlandırmakla değil, işi ana iş parçacığından çıkarmakla gelir. Bu değişiklikten sonra script'in yaptığı iş birkaç satır stil yazmaktan ibaret. Ana iş parçacığı ne kadar meşgul olursa olsun, bir sonraki hedef değer geldiğinde hareket kaldığı yerden devam ediyor. Note (Ders): Bir animasyon takılıyorsa ilk soru "neyi hareket ettiriyorum ve kim hareket ettiriyor" olmalı. Genişlik, yükseklik ya da konum değişiyorsa çoğu zaman cevap transform; her karede JavaScript çalışıyorsa çoğu zaman cevap CSS geçişi. --- ## x³: Mauve gizli modunu bulunabilir ve ucuz tutmak URL: https://devilyxrddev.com/journal/hidden-mode-easter-egg Date: 2026-10 · Tasarım ve etkileşim Footer'daki devilyxrd yazısının x harfine üç kez tıklayınca site Mauve moduna geçiyor. Bu notta sırrın kendini nasıl ele verdiğini, açılınca neleri değiştirdiğini ve bütün bunları hareket azaltma tercihine saygı duyarak birkaç sınıf ve değişkenle nasıl ucuz tuttuğumu anlatıyorum. ### Bulunabilir bir sır Kimsenin bulamadığı gizli bir özellik aslında yoktur. O yüzden ipuçlarını katmanlı koydum. Footer'daki büyük wordmark'ın x harfi aslında bir düğme ve yanında küçük bir üs duruyor: x³. Kimse dokunmadıysa x, yedi saniyelik bir döngüyle ara ara sallanıyor (xwob). Konsolu açan biri renkli bir x³ ve alttaki x'e üç kez dokunmasını söyleyen kısa bir not görüyor; consoleHint bunu sayfa mount edildikten sonra yazdırıyor. Bir de kestirme var: komut paletinde Mauve modunu açıp kapatan bir eylem. x düğmesinin bir aria-label'ı var (Mauve modunu aç / kapat). Yani ekran okuyucu kullanan biri için bu bir sır bile değil, düz bir düğme. Sürpriz görsel tarafta kalıyor, erişim tarafında değil. Komut paletindeki eylem de arama dizininin bir parçası. searchIndex.ts bu eylemi iki dildeki etiketiyle ve mauve, iris, secret, gizli gibi anahtar kelimelerle kaydediyor; paleti açıp gizli ya da mauve yazan biri doğrudan bu eylemi buluyor. Bu, sırrı bilen ama footer'a inmek istemeyen biri için bir kestirme. Palet eylemi de header rozeti gibi toggleSecret çağırıyor, yani üç tık beklemeden modu çeviriyor ve kutlamayı ekranın ortasından oynatıyor. ### Üç tık Sayaç ui store'unda tek bir sayı: secret. Her tıkta artıyor ve modun açık olup olmadığı ondan türetiliyor. Footer'daki üs ui.secret % 3 || 3 değerini gösteriyor; sırayla x¹, x², x³. Her tıkta x 30 derece daha dönüyor ve kısa bir toast x¹ ya da x² yazıyor. Üçüncü tık modu çeviriyor. ``` // src/stores/ui.ts export const mauve = computed(() => Math.floor(ui.secret / 3) % 2 === 1) // src/composables/useSecretMode.ts export function clickSecret(x: number, y: number, xEl?: HTMLElement) { ui.secret++ const step = ui.secret % 3 if (xEl && !isReduced()) gsap.to(xEl, { rotate: step * 30, duration: 0.5, ease: 'back.out(3)' }) document.documentElement.classList.add('x-found') if (step === 0) celebrate(x, y) else showToast(step === 1 ? 'x¹' : 'x²', 1200) } ``` İlk tıkta html elemanına x-found sınıfı ekleniyor ve sallanma duruyor. İpucu işini yaptı, artık dikkat çekmesine gerek yok. Durumu tek bir sayıdan türetmenin bir faydası da açıp kapatmanın aynı hesapla çalışması: altıncı tık modu kapatıyor, dokuzuncu yeniden açıyor. ### Açılınca ne oluyor celebrate fonksiyonu önce durumu kaydediyor ve bir toast gösteriyor. Ardından hareket azaltma kapalıysa iki animasyon oynuyor. Birincisi shockwave: tıklanan noktadan ekranın en uzak köşesine kadar büyüyen bir clip-path dairesi. Eleman body'ye ekleniyor, mix-blend-mode: difference ile altındaki sayfayı tersine çeviriyor ve GSAP zaman çizelgesi bitince kaldırılıyor. İkincisi scatter: footer wordmark'ının harfleri rastgele konum ve açılardan elastik bir easing ile yerlerine dönüyor. Mod açıkken değişenlerin hepsi CSS'te, .app.mv altında. Iris değişkeni Mauve'a (#C7A8BE) eşitleniyor, yani vurgu rengini kullanan her şey birlikte değişiyor. İmleç değişkenleri x şeklindeki SVG'lere dönüyor. .app.mv::after bütün ekranın üstüne, tıklamaları engellemeyen ince yatay çizgiler seriyor. Hero'da ise kaos seviyesi 1'den 2'ye çıkıyor; artıların dönüşü ve rastgele sapmaları bu sayıyla çarpılıyor, halkalar daha fazla dönüyor. ``` /* tokens.css */ .app.mv { --iris: var(--mauve); } /* shell.css */ .app.mv::after { content: ''; position: fixed; inset: 0; z-index: 300; pointer-events: none; background: repeating-linear-gradient(0deg, rgba(199, 168, 190, .045) 0 1px, transparent 1px 3px); } html:not(.x-found) .ftw .xbtn { animation: xwob 7s var(--e) 3s infinite; } @media (prefers-reduced-motion: reduce) { .ftw .xbtn { animation: none !important; } } ``` > Bir sırrın değeri bulunduğu andaki küçük sürprizde; bedelini her ziyaretçiye ödetmemek gerekiyor. ### Kalıcılık ve çıkış Mod oturum boyunca sürüyor. persist, sessionStorage'a dvx-secret anahtarıyla 1 ya da 0 yazıyor; main.ts mount sonrasında restoreSecret çağırıyor ve değer 1 ise ui.secret 3 oluyor. Bunu bilerek mount sonrasına koydum: prerender edilen HTML her zaman normal modda, istemcinin ilk render'ı da aynı işaretlemeyi üretmeli. sessionStorage seçmemin sebebi de bunun bir oyun olması; sekme kapandığında sitenin normal haline dönmesini istedim. Depolama engellenmişse try/catch modun kapalı başlamasını sağlıyor. Mod açıkken header'da kesikli Mauve kenarlıklı bir rozet beliriyor: Mauve ×. Tıklanınca toggleSecret çalışıyor; sayacı bir sonraki üçün katına atlatıyor ve aynı kutlamayı tıklanan noktadan oynatıyor. Rozet, modu tesadüfen açan birinin footer'a geri inmeden çıkabilmesi için var. ### Hareket azaltma ve maliyet prefers-reduced-motion açıksa x dönmüyor, shockwave ve scatter oynamıyor, sallanma animasyonu CSS'te kapalı. Mod yine açılıyor, toast yine çıkıyor; sadece hareket yok. Hero'daki tıklama dalgası da bu durumda hiç üretilmiyor. Maliyet tarafında mod kapalıyken neredeyse hiçbir şey çalışmıyor: bir sayı, bir computed ve x üzerinde bir CSS animasyonu. Shockwave elemanı yalnızca tıklama anında oluşuyor ve iş bitince siliniyor. Mod açılınca hero, mauve değerini izleyip bütün hücreleri bir kez yeniden çiziyor (repaintAll), sonra her şey normal döngüsüne dönüyor. Ayrı bir tema dosyası ya da ayrı bir bileşen ağacı yok. Note (Ders): Bir easter egg'i keşfedilebilir yapan şey ipuçlarının katmanları oldu: görsel bir üs, hafif bir sallanma, konsolda bir not. Ucuz tutan şey ise modun bir sınıf ve birkaç değişkenden ibaret olması. --- ## Dil adreste yaşar: Türkçe kökte, İngilizce /en altında URL: https://devilyxrddev.com/journal/bilingual-routing Date: 2026-10 · Arayüz Siteyi iki dilli yaparken dili localStorage'a ya da tarayıcı ayarına bırakmadım, adrese yazdım. Türkçe kökte, İngilizce /en altında duruyor; router dili URL'den okuyor, LocLink de bağlantıları okuduğun dilde tutuyor. Bu notta kurgunun nasıl çalıştığını anlatıyorum. ### Neden adres? İlk sürümde dil tipli küçük bir depoda tutuluyordu ve tercih localStorage'a dvx-lang anahtarıyla yazılıyordu. Ziyaretçi için çalışıyordu ama üç yerde tıkandı. Birincisi arama motorları: tek bir adres iki dilde içerik veriyorsa hangisinin indeksleneceği belirsiz kalıyor. İkincisi paylaşım: İngilizce okuyan birine attığım bağlantı, onun tarayıcısında Türkçe açılabiliyordu. Üçüncüsü prerender: build her rotayı statik HTML'e çeviriyor ve sunucuda localStorage diye bir şey yok. Sunucunun hangi dilde render edeceğini bilmesi için dilin isteğin kendisinde olması gerekiyordu. Tarayıcı dilini okuyup yönlendirmeyi de düşündüm, ama bu aynı sorunu başka bir kapıdan geri getiriyor: aynı URL farklı insanlara farklı içerik gösteriyor. Ben her sayfanın her dilde kendi adresi olsun istedim. Türkçe varsayılan olduğu için kökte kaldı (/work), İngilizce /en önekini aldı (/en/work). ### Rotaları kopyalamak Router tarafında iki ayrı rota listesi yazmadım. Türkçe rotalar tek kaynak, İngilizce olanlar bunların map ile üretilmiş kopyası. Yolun başına /en ekleniyor, isim en- önekini alıyor, meta alanına lang: 'en' yazılıyor. Ana sayfa /en oluyor, 404 için kullanılan catch-all rota da /en/:pathMatch(.*)* oluyor. ``` const english = base.map((r) => ({ ...r, path: r.path === '/' ? '/en' : r.path === '/:pathMatch(.*)*' ? '/en/:pathMatch(.*)*' : '/en' + r.path, name: 'en-' + String(r.name), meta: { ...r.meta, lang: 'en' as const } })) as RouteRecordRaw[] export const routes: RouteRecordRaw[] = [...english.slice(0, -1), ...base.slice(0, -1), english[english.length - 1], base[base.length - 1]] // makeRouter(server): runs on the server render and in the browser router.beforeEach((to) => { setLocale(localeFromPath(to.path)) return true }) ``` Sıralama burada önemli. İki catch-all rota listenin sonuna gidiyor ve İngilizce olan önce geliyor; aksi halde /en altındaki bilinmeyen bir adres Türkçe 404'e düşerdi. Olmayan proje ve yazı slug'ları için beforeEnter içinde notFound adında bir yardımcı var: ziyaretçinin yazdığı adresi koruyup doğru dildeki 404 görünümünü render ediyor. ### Dili URL belirler Asıl karar tek bir beforeEach içinde. Router her geçişte to.path değerine bakıyor, localeFromPath ile dili çıkarıyor ve setLocale çağırıyor. Bu kanca bilerek sunucu kontrolünden önce kuruluyor, yani hem build sırasındaki sunucu render'ında hem tarayıcıda çalışıyor. Sunucu memory history, tarayıcı web history kullanıyor; dil kararı ikisinde de aynı satırdan geliyor. Bunun hydration için de anlamı var. Dil deposu her zaman Türkçe başlıyor, ama ilk render'dan önce router devreye girip adresteki dili yazıyor. main.ts içinde mount işlemi router.isReady() sonrasına bağlı. Böylece /en/work hem sunucuda hem istemcide İngilizce render ediliyor ve işaretleme birebir uyuşuyor. main.ts'teki yorum da bunu söylüyor: dil için mount sonrasında yapılacak bir şey kalmadı. ### Yol yardımcıları Bütün dönüşümler src/i18n/paths.ts içindeki üç küçük fonksiyonda. localeFromPath, yol tam olarak /en ise ya da /en/ ile başlıyorsa en, değilse tr döndürüyor. basePath öneki kesip dil-nötr yolu veriyor, localize ise bu yolu hedef dile çeviriyor. ``` export const localeFromPath = (path: string): Locale => (path === '/en' || path.startsWith('/en/') ? 'en' : 'tr') /** '/en/work' → '/work', '/en' → '/' */ export const basePath = (path: string) => (localeFromPath(path) === 'en' ? path.slice(3) || '/' : path) /** '/work' + en → '/en/work', '/' + en → '/en' */ export const localize = (path: string, locale: Locale) => { const base = basePath(path) return locale === 'en' ? (base === '/' ? '/en' : '/en' + base) : base } ``` Kontrolün yalnızca tam eşleşmeye ve /en/ önekine bakması küçük ama bilinçli bir ayrıntı. Düz bir startsWith('/en') yazsaydım, ileride /entries gibi bir sayfa eklediğimde o sayfa sessizce İngilizce sayılırdı. ### LocLink ve dil düğmesi Bileşenlerin her yerinde RouterLink ile /work yazmak, İngilizce sayfadaki okuru her tıklamada Türkçeye geri atardı. Bunun yerine LocLink adında küçük bir bileşen yazdım ve createSiteApp içinde global olarak kaydettim. Aynı to değerini alıyor: / ile başlayan bir string ise localize'dan geçiriyor, path alanı olan bir nesne ise yalnızca o alanı çeviriyor, sadece hash ya da query içeren hedefleri olduğu gibi bırakıyor. Sonra sıradan bir RouterLink render ediyor. Header, footer, menü ve kartlar hep bunu kullanıyor; bağlantıları izleyen bir tarayıcı hiçbir noktada dil değiştirmiyor. Dil düğmesi aynı fikrin tersini yapıyor. SiteHeader'daki TR ve EN düğmeleri bir değişkeni değiştirmiyor; mevcut sayfanın diğer dildeki adresine router.push ile gidiyor, query ve hash'i de yanında taşıyarak. Hangi dilin etkin olduğunu aria-pressed söylüyor. Dil değişince <html lang> değeri App.vue'deki useHead üzerinden güncelleniyor, her sayfa da useSeo ile tr, en ve x-default için hreflang bağlantılarını yazıyor. > Dil bir tercih olmaktan çıkıp adresin parçası olunca sunucu, tarayıcı ve arama motoru aynı şeyi görüyor. ### Ne kazandım, neyi bıraktım Prerender script'i artık her yolu iki kez render ediyor: Türkçesi dist/<rota>.html, İngilizcesi dist/en/<rota>.html olarak. Sitemap iki dili birlikte listeliyor. Bir bağlantıyı paylaştığımda karşı taraf benim gördüğüm dili görüyor ve bunun için hiçbir şeyin hatırlanmasına gerek yok. Bıraktığım şey otomatik dil tahmini. İngilizce bir tarayıcıyla gelen biri ilk ziyarette Türkçe kökü görüyor ve EN düğmesine basması gerekiyor. setLocale tercihi hâlâ localStorage'a yazıyor, ama dili artık o değer değil adres belirliyor. Bunu bilerek kabul ettim; bir portfolyo için öngörülebilir adresler, ilk ziyarette kazanılacak bir tıktan daha değerli geldi. Note (Ders): Dili adrese koyduğumda sunucu render'ı, hydration, SEO ve paylaşım aynı kaynaktan beslenmeye başladı. Router'daki tek bir beforeEach, aksi halde dört ayrı yerde yazacağım mantığın yerini aldı. --- ## XSLT ile süslenen sitemap.xml'den gerçek bir HTML site haritasına URL: https://devilyxrddev.com/journal/xslt-sitemap-to-html Date: 2026-10 · Arama ve GEO sitemap.xml'i insanlar da okuyabilsin diye önce bir XSLT stil dosyasıyla süsledim. Tarayıcılar XSLT desteğini kaldırma yoluna girince bundan vazgeçtim: XML yalnızca makineler için kaldı, insanlar için iki dilli ve prerender edilen bir /sitemap sayfası yazdım. ### XML'i insanlara göstermek Sitenin sitemap.xml dosyası her sayfayı iki dilde listeliyor; her kayıtta diğer dildeki karşılık ve sayfanın paylaşım kartı da var. Arama motorları için tam olarak gereken bu. Ama dosyayı tarayıcıda açan bir insan, iç içe geçmiş etiketlerden oluşan bir ağaç görüyor. Site haritasının insanlar için de bir işe yaramasını istedim: hangi sayfalar var, hangi dilde, hangi adreste. İlk çözüm eski ve bilinen bir yöntemdi. XML dosyasının başına bir xml-stylesheet işleme talimatı ekledim ve tarayıcıyı /sitemap.xsl adresindeki bir XSLT dosyasına yönlendirdim. Bu dosya da scripts/seo-files.mjs içinde üretiliyor ve text/xsl türüyle sunuluyordu. Tarayıcı XML'i bu dönüşümden geçirip sitenin renklerinde bir HTML tablosu gösteriyordu: paylaşım kartları, dil etiketleri ve diğer dile geçiş bağlantıları. Arama motorlarının tarayıcıları ise ham XML'i okumaya devam ediyordu. ``` <?xml-stylesheet type="text/xsl" href="/sitemap.xsl"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:s="http://www.sitemaps.org/schemas/sitemap/0.9"> <xsl:output method="html" encoding="UTF-8" indent="yes" doctype-system="about:legacy-compat"/> <xsl:template name="rel"> <xsl:param name="u"/> <xsl:variable name="rest" select="substring-after(substring-after($u, '://'), '/')"/> <xsl:value-of select="concat('/', $rest)"/> </xsl:template> ``` ### Neden vazgeçtim Sayfayı Chrome'da açtığımda konsolda XSLT'nin kullanımdan kaldırılacağına dair bir uyarı çıktı. Tarayıcılar XSLT desteğini kaldırma yolunda. Bir özelliği, ortadan kalkacağı açıkça söylenen bir teknolojiye bağlamak çıkmaz sokak; bugün çalışıyor olması, bir gün sessizce çalışmayı bırakmayacağı anlamına gelmiyor. Uyarı olmasa da bu yolun başka maliyetleri vardı. XSLT 1.0 ayrı bir dil ve oldukça kısıtlı: mutlak bir adresi göreli hale getirmek için bile yukarıdaki gibi iç içe substring-after çağrılarıyla bir şablon yazmak gerekiyordu. Görünüm, Vue uygulamasının tamamen dışında yaşıyordu; bileşenler, menü ve tasarım değişkenleri orada yoktu. Renkleri XSL dosyasının içindeki ayrı bir stil bloğunda yeniden yazmıştım. Sitedeki bir renk değiştiğinde bu kopyayı da hatırlamam gerekecekti. ### XML makineler için, HTML insanlar için Sonunda işi ikiye ayırdım. XSL dosyası ve işleme talimatı kalktı; sitemap.xml yeniden düz bir XML oldu ve yalnızca makineler için duruyor. Dosyanın başındaki bir yorum, onu açan insanlara Türkçe ve İngilizce site haritası sayfalarının adreslerini ve robots.txt, llms.txt ve llms-full.txt dosyalarını gösteriyor. Bu ayrımın sitemap.xml'e bir zararı olmadı. Arama motorları için önemli olan her şey yerinde: her adres için xhtml:link ile tr, en ve x-default alternatifleri, image:image ile paylaşım kartı, lastmod, changefreq ve priority. Ana sayfa 1.0, üst düzey sayfalar 0.8, proje ve yazı sayfaları 0.6, site haritası sayfasının kendisi ise 0.3 önceliğiyle listede. İnsanlar için src/views/SitemapView.vue var. Bu, diğerleri gibi bir Vue rotası: /sitemap ve /en/sitemap adreslerinde yaşıyor, build sırasında iki dilde prerender ediliyor, kendi paylaşım kartı var ve publicPaths içinde olduğu için sitemap.xml'de de listeleniyor. Sayfaya altbilgideki bağlantıdan ulaşılıyor. ### /sitemap sayfası Sayfa üç grup gösteriyor: sayfalar, projeler ve yazılar. Sayfalar ana sayfa ile menüdeki bağlantılardan, projeler kategori, yıl ve durumla, yazılar kategori ve tarihle listeleniyor. Her satırda sayfanın paylaşım kartından küçük bir görsel, başlık bağlantısı, mono yazıyla sayfanın adresi, kısa bir alt satır ve diğer dildeki karşılığa giden bir bağlantı var. Bu son bağlantı hreflang özelliği taşıyor. Kart görselleri bilerek göreli adresle yükleniyor; aynı sayfa localhost'ta, önizlemelerde ve canlı sitede aynı şekilde çalışıyor. Üstteki sayaç, publicPaths uzunluğunun iki katı kadar adres olduğunu gösteriyor. En altta "Makineler için" bölümü var: sitemap.xml, robots.txt, llms.txt ve llms-full.txt, her biri kısa bir açıklamayla. Sayfanın JSON-LD'si bir CollectionPage düğümü ve bütün herkese açık sayfaları sırayla sayan bir ItemList içeriyor, yanında da bir BreadcrumbList var. Yani makineler de bu sayfayı bir dizin olarak tanıyabiliyor. > XML'i insanlara okutmaya çalışmak yerine insanlara kendi sayfalarını vermek daha dürüst bir çözüm. ### Geliştirme sunucusunda da aynı dosyalar Site haritası sayfası makine dosyalarına bağlantı verip paylaşım kartlarını gösterdiği için, bu dosyaların geliştirme sırasında da var olması gerekiyordu. Normalde hepsi build sonunda üretiliyor. vite.config.ts içindeki küçük bir eklenti, yalnızca geliştirme sunucusunda çalışıyor ve /robots.txt, /sitemap.xml, /llms.txt, /llms-full.txt ile /og/<anahtar>.png isteklerini yakalıyor. İçeriği Vite'ın SSR yükleyicisiyle entry-server.ts üzerinden okuyor ve build'in kullandığı textFiles ile renderOg fonksiyonlarını çağırıyor. ``` const isText = ['/robots.txt', '/sitemap.xml', '/llms.txt', '/llms-full.txt'].includes(url) const og = /^\/og\/([a-z0-9-]+)\.png$/.exec(url) if (!isText && !og) return next() const mod = await load(server) // ssrLoadModule('/src/app/entry-server.ts') const seo = await import('./scripts/seo-files.mjs') ``` Bunun güzel bir yan etkisi var: geliştirme sunucusunda gördüğüm dosya ile canlıya çıkan dosya aynı koddan geliyor. Bir yazının başlığını değiştirip sayfayı yenilediğimde sitemap.xml, llms.txt ve kart görseli de değişmiş oluyor. Note (Ders): Bir dosyanın iki farklı okuyucusu varsa, ikisini aynı biçimde memnun etmeye çalışmak yerine her birine kendi biçimini vermek çoğu zaman daha basit. Makineler XML okuyor, insanlar HTML; ikisi de aynı içerikten üretildiği sürece aralarında fark oluşmuyor. --- ## İmleci kovalamayı bıraktım: CSS cursor ile yerel SVG imleç URL: https://devilyxrddev.com/journal/native-svg-cursor Date: 2026-10 · Tasarım ve etkileşim Özel imleç çoğu sitede fareyi kovalayan bir elemandır ve hep biraz geride kalır. Bu sitede imleci tarayıcının kendisine çizdirdim: cursor özelliğinde SVG data URI, iki CSS değişkeni ve gizli mod için bir x. JavaScript'le çalışan disk artık yalnızca etiket taşıyor. ### Eski yaklaşım İlk sürümde sistem imlecinin etrafında onu takip eden bir halka vardı. Fixed konumlu bir eleman pointermove ile hedef koordinatı alıyor, bir requestAnimationFrame döngüsü onu her karede bu hedefe biraz daha yaklaştırıyordu. Etkileşimli bir elemanın üzerine gelince büyüyor, data-cur özniteliği olan elemanlarda içinde bir etiket gösteriyordu. Göze hoş geliyordu ama iki sorunu vardı. Birincisi yapısal gecikme: eleman olay geldikten sonra çiziliyor, yumuşatma eklenince halka gerçek imlecin hep gerisinde kalıyor. İkincisi maliyet: döngü ilk satırında bir sonraki kareyi istiyordu, yani fare hareketsizken bile her karede çalışıyordu. Ana thread meşgul olduğunda da ilk takılan şey bu halka oluyordu. İşletim sistemi imleci ise sayfanın JavaScript yükünden bağımsız çiziliyor. CSS'in cursor özelliği url() ile özel bir görsel kabul ediyor. Yani imlecin hareketini tarayıcıya bırakıp sadece görünüşünü ben verebilirdim. ### İki değişken, iki imleç tokens.css'te iki değişken var: --c-def ve --c-ptr. Varsayılan değerleri düz anahtar kelimeler: default ve pointer. Yalnızca (hover: hover) and (pointer: fine) sorgusu eşleşirse, yani hassas bir işaretçi varsa, bu değerler SVG data URI'leriyle değiştiriliyor. Varsayılan imleç dört kısa koldan oluşan bir artı ve ortasında Iris renkli küçük bir daire; tıklanabilir elemanlardaki imleç ise bir halka ve ortasında Soft renkli bir nokta. ``` :root { --c-def: default; --c-ptr: pointer; } @media (hover: hover) and (pointer: fine) { :root { --c-def: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'><path d='M16 4v7M16 21v7M4 16h7M21 16h7' stroke='%230B0C11' stroke-width='3.5'/><path d='M16 4v7M16 21v7M4 16h7M21 16h7' stroke='%23EDECF2' stroke-width='1.5'/><circle cx='16' cy='16' r='3.2' fill='%23A8A1F1' stroke='%230B0C11' stroke-width='1.5'/></svg>") 16 16, default; --c-ptr: url("data:image/svg+xml,...") 16 16, pointer; } .app.mv { --c-def: url("data:image/svg+xml,...") 16 16, default; /* the x */ } html, .app { cursor: var(--c-def); } .app a, .app button, .app label, .app summary, .app [role='button'], .app input[type='range'] { cursor: var(--c-ptr); } .app input[type='text'], .app input[type='search'], .app textarea { cursor: text; } } ``` Her SVG'de çizgiler iki kez çiziliyor: önce kalın bir Void (#0B0C11) kontur, üstüne ince asıl renk. Bu sayede imleç hem koyu zeminde hem Iris ve Soft yüzeylerde okunuyor. Renkler data URI içinde olduğu için # karakteri %23 olarak yazılıyor. Görseli ayrı bir dosya yerine data URI olarak CSS'in içine yazdım. Böylece imleç stil dosyasıyla birlikte geliyor, ayrı bir isteği beklemiyor ve renklerin tanımlandığı aynı dosyada, tokenların hemen altında duruyor. Bir rengi değiştirmek istediğimde nereye bakacağım belli. Bedeli okunabilirlik: tek satırlık bir SVG'yi düzenlemek, ayrı bir dosyayı düzenlemek kadar rahat değil. Bunu kabul ettim; imleçler sık değişen şeyler değil. ### Hotspot ve yedek url() değerinden sonraki 16 16, imlecin tıklama noktası. SVG 32×32 olduğu için hotspot tam merkezde; artının ortası ile tıklanan nokta aynı. Bunu yazmasaydım tarayıcı sol üst köşeyi kullanırdı ve imleç, tıkladığı yerin yarım ikon yanında duruyormuş gibi görünürdü. Virgülden sonraki default ve pointer ise yedek. Tarayıcı görseli kullanamazsa bu anahtar kelimeye düşüyor; zaten cursor sözdiziminde url() listesinin bir anahtar kelimeyle bitmesi gerekiyor. Kullanım tarafı sade: html ve .app --c-def alıyor; .app içindeki a, button, label, summary, [role='button'] ve range input'lar --c-ptr. Metin alanları text imlecini koruyor, çünkü orada okurun gerçekten imleç konumuna ihtiyacı var. Bileşen stilleri de doğrudan cursor: var(--c-ptr) kullanıyor; playground'daki bazı sahneler ew-resize gibi kendi imlecini seçiyor. > İmleci tarayıcı çizdiğinde geride kalacak bir döngü de kalmıyor. ### Dokunmatik cihazlar ve gizli mod Dokunmatik ekranlarda zaten imleç yok. Coarse işaretçili cihazlarda medya sorgusu eşleşmiyor ve değişkenler düz anahtar kelimelerde kalıyor. Ayrı bir cihaz kontrolü yazmama gerek olmadı. Değişken kullanmanın asıl faydası gizli modda ortaya çıktı. Mauve modu açıkken .app elemanına mv sınıfı ekleniyor ve aynı medya sorgusu içinde .app.mv iki değişkeni yeniden tanımlıyor: varsayılan imleç Mauve renkli bir x oluyor, tıklanabilir elemanlardaki imleç içinde x olan bir halka. Hiçbir bileşen değişmiyor, cursor kuralları aynı kalıyor; yalnızca değişkenin değeri değişiyor ve bütün sayfa yeni imleci kullanıyor. ### Geriye kalan disk CustomCursor.vue hâlâ duruyor ama görevi değişti. Bazı elemanlarda data-cur özniteliği var; shell.css'teki yorumun örnekleriyle Aç ya da Kopyala gibi bir etiket. Disk yalnızca böyle bir elemanın üzerindeyken görünüyor ve etiketi taşıyor. pointermove dinleyicisi etiketi arıyor; etiket yoksa döngüyü hiç başlatmıyor. ``` function loop() { raf = 0 if (!vis || !el.value) return rx += (cx - rx) * 0.22 ry += (cy - ry) * 0.22 el.value.style.transform = `translate3d(${rx.toFixed(1)}px,${ry.toFixed(1)}px,0)` if (Math.abs(cx - rx) > 0.2 || Math.abs(cy - ry) > 0.2) raf = requestAnimationFrame(loop) } ``` Etiket varsa disk her karede kalan mesafenin bir kısmını kapatıyor, fark 0.2 pikselin altına inince döngü kendini durduruyor. Dokunma olayları yok sayılıyor; hareket azaltma açıksa ya da işaretçi coarse ise dinleyiciler hiç eklenmiyor. Buradaki gecikme kabul edilebilir, çünkü disk imleç değil, imlecin yanında duran bir etiket. Gerçek imleç her zaman yerinde. Bedeli de belli: imleç bir görsel olduğu için hover'da büyüme gibi geçişler yapamıyorum, durum değişimi anlık. Note (Ders): Özel bir imleç için önce imlecin kendisini değil, görselini değiştirmeyi denemek gerekiyormuş. Gecikme, boşta çalışan döngü ve dokunmatik cihaz kontrolü tek bir CSS özelliğiyle ortadan kalktı; JavaScript yalnızca gerçekten JavaScript gerektiren etikete kaldı. --- ## Görmediğini durdur: döngüleri ekran dışında ve perde arkasında uyutmak URL: https://devilyxrddev.com/journal/pausing-what-you-cannot-see Date: 2026-10 · Arayüz Sitede sürekli hareket eden çok şey var: hero alanı, halkalar, kayan kelime bandı, menüdeki kapak çizimleri. Bu notta bunların hangi kurallarla çalıştığını yazıyorum: yalnızca transform ve opacity, Vue'yu atlayan rAF döngüleri ve görünmeyen her şeyi durduran küçük mekanizmalar. ### Kural: yalnızca transform ve opacity Animasyonlarda ilk kuralım şu: kare başına değişen şey transform ya da opacity olsun. Bu iki özellik genellikle layout ve paint tetiklemeden compositor tarafında çözülebiliyor. base.css'teki kaydırmaya bağlı reveal'lar (rv1, rv2, rv3) bu yüzden yalnızca translate, scale, rotate ve opacity kullanıyor. Bu animasyonlar animation-timeline: view() ile tamamen CSS'te çalışıyor; desteklemeyen tarayıcılar içeriği doğrudan gösteriyor. Aynı kural küçük yerlerde de geçerli. Hero'daki crosshair çizgileri left ve top yerine translateX ve translateY ile hareket ediyor. Hero'nun boyutu bir kez ölçülüp önbelleğe alınıyor ve yalnızca resize ya da scroll olduğunda sıfırlanıyor; böylece hareket sırasında layout okunmuyor. Kaydırma da aynı dikkati istiyor. usePointer içindeki scroll dinleyicisi her olayda iş yapmıyor; bir kare isteyip o karede çalışıyor. Kaydırma ilerlemesini bir :root değişkenine yazmak kolay olurdu, ama koddaki yorumun da dediği gibi bu bütün sayfanın stilini yeniden hesaplatırdı. Onun yerine değer yalnızca iki tüketiciye gidiyor: header'daki ilerleme çubuğu scaleX ile, hero halkaları translate ile hareket ediyor. Halkaların parallax'ı ilk 1200 pikselden sonra ve hareket azaltma açıkken hiç güncellenmiyor. ### Vue'yu döngünün dışında tutmak Ana sayfadaki hero 14×8'lik bir artı alanı ve beş halkadan oluşuyor; imleci takip ediyorlar, tıklayınca alandan bir dalga geçiyor. Bunu reaktif state ile yazsaydım her pointermove, Vue'ya 112 hücrelik bir yeniden render yaptırırdı. Onun yerine alan bir kez render ediliyor. pointermove yalnızca iki sayıyı güncelliyor ve schedule çağırıyor; schedule da bekleyen bir kare yoksa requestAnimationFrame istiyor. ``` function onMove(e: PointerEvent) { const p = local(e) px = p.x py = p.y schedule() } function schedule() { if (!raf) raf = requestAnimationFrame(paint) } // inside paint(), for each of the 112 cells const still = k === 0 && w === 0 if (still && c.rest) continue // far from pointer and already drawn at rest: nothing to write c.rest = still // end of paint(): only a running pulse asks for another frame if (pulse) schedule() ``` paint içinde her hücre için imlece uzaklık ve varsa dalganın etkisi hesaplanıyor. İkisi de sıfırsa ve hücre zaten dinlenme halinde çizildiyse continue ile atlanıyor, DOM'a hiçbir şey yazılmıyor. Döngü kendini de durduruyor: yalnızca dalga sürerken bir sonraki kareyi istiyor. İmleç durduğunda kare istenmiyor, yani boşta neredeyse hiçbir şey çalışmıyor. Playground'daki GenerativeField aynı deseni kullanıyor: 144 çubuk, açılar 3 derecelik adımlara yuvarlanıyor ve değer bir önceki kareyle aynıysa style'a dokunulmuyor. Diğer sayfalardaki pointer işleri için src/composables/useRaf.ts içinde useRafThrottle var; hızlı gelen olayları kare başına tek bir güncellemeye indiriyor. ### Ekran dışındakini durdurmak CSS animasyonları görünmeseler bile çalışmaya devam ediyor. Bunun için v-pause-offscreen adında bir direktif yazdım (src/composables/useOffscreen.ts). Sayfa için tek bir paylaşılan IntersectionObserver var; gözlenen eleman görünür alandan çıkınca ona off sınıfını ekliyor. Gerisini base.css'teki tek bir kural hallediyor. ``` let io: IntersectionObserver | null = null function observer() { if (!io) { io = new IntersectionObserver( (entries) => { for (const e of entries) (e.target as HTMLElement).classList.toggle('off', !e.isIntersecting) }, { rootMargin: '120px 0px' } ) } return io } /* base.css */ .off, .off * { animation-play-state: paused !important; } ``` rootMargin 120 piksel olduğu için döngüler görünür alana girmeden biraz önce yeniden başlıyor; kaydırırken donmuş bir kare görmüyorsun. Direktif hero halkalarında, ProceduralArt'ın dönen çizimlerinde, OrbitRings ve ReactiveGrid deneylerinde kullanılıyor. JavaScript ile çalışan kayan kelime bandı MarqueeBand aynı fikri kendi IntersectionObserver'ı ile uyguluyor: bant görünmüyorsa loop bir sonraki kareyi istemiyor. ### Perde arkasındakini durdurmak Menü ya da komut paleti açıkken sayfa arkada kalıyor ama döngüler çalışmaya devam edebiliyordu. ui store'unda overlay adında bir computed var: menü ya da palet açıksa true. usePointer bunu izleyip html elemanına ovl sınıfını ekliyor; shell.css'te .ovl altındaki dönen ve nabız atan sınıflar (spin, horb, orr, rpA, rpB) duraklatılıyor. MarqueeBand overlay'i doğrudan okuyor; true ise döngüsünü bırakıyor, false olunca watch üzerinden yeniden başlıyor. > Görünmeyen bir animasyonu çalıştırmanın kimseye faydası yok; durdurmak çoğu zaman tek bir sınıf. ### Hiç render etmemek Bazı şeyleri duraklatmak yerine geç oluşturmak daha ucuz. Menüdeki beş kapak çizimi (koddaki yoruma göre yaklaşık 200 şekil) menü ilk kez açılana kadar DOM'a girmiyor; seen adında bir ref bunu v-if ile kontrol ediyor. Ana sayfanın alt bölümlerinde, footer'da ve uzun sayfaların sonundaki bölümlerde content-visibility: auto var; görünür alandan uzak bölümler layout ve paint'i atlıyor. contain-intrinsic-size: auto 900px ise bir kez render edilen bölümün gerçek yüksekliğini hatırlıyor, böylece kaydırma çubuğu zıplamıyor. Fontlar da ilk boyamayı bekletmiyor. index.html'de Google Fonts stil dosyası önce preload ediliyor, sonra media="print" ile yüklenip onload anında all'a çevriliyor; display=swap metni hemen gösteriyor. JavaScript kapalıysa noscript içindeki normal link devreye giriyor. ### Hareket azaltma prefers-reduced-motion açıkken bu döngülerin bir kısmı hiç başlamıyor. useReducedMotion tek bir matchMedia dinleyicisi tutuyor. MarqueeBand başlamadan önce isReduced kontrol ediyor, hero tıklamada dalga üretmiyor ve giriş animasyonunu atlıyor, responsive.css dönen animasyonları ve reveal'ları kapatıyor. Hiç çalışmayan bir döngü, durdurulan bir döngüden de ucuz. Note (Ders): Performans işinin büyük kısmı daha hızlı çizmek değil, çizmemeyi seçmek oldu. Görünmeyen, perde arkasında kalan ya da yerinde duran hiçbir şeyin her karede iş yapmasına gerek yok. --- ## Tasarım prototipinden üretim koduna: kontrollü kaosu Vue'ya taşımak URL: https://devilyxrddev.com/journal/design-canvas-to-production Date: 2026-10 · Tasarım ve etkileşim Bu site kod olarak değil, etkileşimli bir tasarım prototipi olarak başladı: devilyxrd, Controlled Chaos. Bu notta o prototipi sayfa sayfa Vue 3 ve TypeScript koduna nasıl taşıdığımı, neyi olduğu gibi aldığımı ve neleri sonradan ekleyip değiştirdiğimi anlatıyorum. ### Başlangıç: bir tasarım tuvali Projenin ilk hali bir tasarım prototipiydi. Adı Controlled Chaos, Türkçesiyle kontrollü kaos. Prototipte sayfalar, renkler, tipografi ve hareket vardı; yanında bir de handoff panosu. Bu pano, Vue tarafında nasıl bir yapı kurulacağını ve hangi animasyonun hangi araçla yapılacağını tarif ediyordu. Bir prototip görünüşü ve davranışı göstermek için iyi bir araç. Ama yönlendirme, SEO, erişilebilirlik ve performans onun işi değil. Benim işim o görüntüyü kaybetmeden bunları eklemekti. Bunu bir kerede değil, sırayla yaptım: önce tokenlar, sonra yapı, sonra sahneler, en son da iyileştirmeler. ### Önce tokenlar Taşımaya renklerden başladım, çünkü geri kalan her şey onlara bağlı. src/styles/tokens.css dosyasının ilk satırı hâlâ handoff'taki ilgili bölümü işaret ediyor. Palet dokuz renkten oluşuyor: Void #0B0C11 zemin, Graphite #171922 ve Slate #292C38 yüzeyler, Soft #EDECF2 metin, Iris #A8A1F1 vurgu, Mauve #C7A8BE gizli modun rengi, Dusty #8999BF ikincil vurgu, Steel #8A8E9E sönük metin, Border #363945 çizgiler. Gövde metni için bunlara bir --body tonu ekledim. ``` :root { --void: #0B0C11; --graphite: #171922; --slate: #292C38; --soft: #EDECF2; --iris: #A8A1F1; --dusty: #8999BF; --mauve: #C7A8BE; --steel: #8A8E9E; --border: #363945; --body: #C9C8D3; --f-display: 'Archivo', 'Syne', 'Space Grotesk', sans-serif; --f-body: 'Space Grotesk', system-ui, sans-serif; --f-mono: 'IBM Plex Mono', ui-monospace, monospace; } /* hidden mode: footer x clicked three times */ .app.mv { --iris: var(--mauve); } ``` Tipografi üç aile: başlıklar için Syne, gövde için Space Grotesk, etiketler ve sayılar için IBM Plex Mono. Değişkenler --f-display, --f-body ve --f-mono adıyla tanımlı. Sonradan bir ayrıntı eklendi: yalnızca g ve ğ glifleri için kullanılan bir Archivo kesimi --f-display yığınının başında duruyor. Tokenları en başa koymanın faydası gizli modda görüldü: .app.mv içindeki tek satır bütün vurgu rengini değiştiriyor. Renkler bileşenlere tek tek yazılmış olsaydı bu bir arama-değiştirme işine dönerdi. ### Sayfadan bileşene Prototip sayfa sayfa düşünülmüştü; kod ise sorumluluğa göre düzenlenmeli. Klasörleri buna göre ayırdım. views her rota için bir dosya. features belirli bir sayfaya ait büyük sahneler: HeroScene, MarqueeBand, ProjectIndex gibi ana sayfa sahneleri ve playground'daki sekiz deneyle onları saran ExperimentShell. components her yerde kullanılan parçalar: header, menü, rota perdesi, komut paleti, imleç, footer, wordmark ve prosedürel kapak çizimleri. composables tekrar eden davranışlar için: hareket azaltma, pointer katmanı, rota geçişi, gizli mod. İçeriği şablonlardan çıkarmak ayrı bir adımdı. Proje ve yazı listeleri src/content altında tipli dosyalarda; metinlerin kendisi src/i18n/locales içindeki tr.ts ve en.ts'te. Böylece bir yazı eklemek bir bileşene dokunmayı gerektirmiyor ve iki dildeki her anahtar tip denetiminden geçiyor. Durum yönetimi için bir kütüphane eklemedim. Header, menü, palet, footer ve sayfaların paylaştığı durum src/stores/ui.ts içinde tek bir reactive nesne: menünün ve paletin açık olup olmadığı, toast metni, gizli mod sayacı, kaydırma durumu. Prototipteki etkileşimlerin çoğu bu birkaç alana indirgenebiliyordu; daha büyük bir araç, getirdiği yükü haklı çıkaracak kadar iş bulamazdı. > Prototip neyin görüneceğini söyler; kod neyin sürdürülebileceğini. ### Hareketin sahipleri Prototipte her şey hareket ediyordu. Koda geçerken her hareketin bir sahibi olsun istedim. Handoff'taki kural basitti ve onu korudum: durum değişimleri CSS ve Vue geçişleriyle, rota perdesi ve hero girişi GSAP ile, geometri laboratuvarı Anime.js ile. Anime.js yalnızca lazy yüklenen /playground paketinde; router'daki bir yorum da bunu hatırlatıyor, ana sayfa paketi ona hiç ulaşmıyor. GSAP sonradan gizli moddaki shockwave ve harf saçılması için de kullanıldı. Bu ayrım bir kural listesi gibi görünse de asıl faydası hata ayıklamada. Bir şey takıldığında hangi katmana bakacağımı biliyorum; aynı elemanı iki ayrı kütüphanenin aynı anda oynatması gibi bir durum oluşmuyor. ### Port bittikten sonra İlk commit prototipin dokuz rotasını, TR/EN metinleri, komut paletini, tam ekran menüyü ve gizli modu getirdi. Asıl iş ondan sonra başladı. Performans tarafında hero'yu Vue'nun reaktivitesinden çıkarıp doğrudan DOM'a yazan bir döngüye çevirdim, ekran dışındaki animasyonları durdurdum, imleci yerel CSS imlecine taşıdım. SEO tarafında her rotayı build sırasında statik HTML'e render etmeye başladım, sonra İngilizceyi /en altına taşıyıp her sayfaya iki dilde ayrı adres verdim. En son prototipteki örnek içerikleri gerçek projeler, gerçek hakkımda metni ve gerçek yazılarla değiştirdim. Geriye dönüp bakınca prototip bana en çok karar yükünü azaltarak yardım etti. Renk, aralık, tipografi ve hareket dili düşünülmüş olarak geldi; ben kodda mimariye, sınırlara ve ölçülebilir şeylere odaklanabildim. Note (Ders): Bir tasarımı koda taşırken önce tokenları, sonra klasör yapısını, en son sahneleri taşımak işe yaradı. Tokenlar sağlam olunca sonraki her değişiklik (gizli mod, yeni sayfalar, gerçek içerik) mevcut dili bozmadan eklenebildi. --- ## npm'den pnpm'e geçiş ve TypeScript'i neden ~5.9'da tuttum URL: https://devilyxrddev.com/journal/npm-to-pnpm Date: 2026-10 · Mühendislik Proje npm ile başladı, sonra paket yöneticisini pnpm'e taşıdım. Bu notta geçişte neler yaptığımı anlatıyorum: kilit dosyasının dönüşümü, packageManager ve engines alanları, katı node_modules yapısı ve TypeScript'i neden ~5.9 aralığına sabitlediğim. ### Neden geçtim Geçişin sebebi hız ya da disk rakamları değildi. Bunları ölçmedim, o yüzden burada rakam da vermeyeceğim. Benim için asıl mesele katılık. npm node_modules'u büyük ölçüde düz bir ağaç halinde kuruyor; bir bağımlılığın bağımlılığı da kökte duruyor ve kod, package.json'da hiç yazmayan bir paketi import edebiliyor. Bu hayalet bağımlılık bugün çalışır, onu getiren paket bir gün bırakınca bozulur. pnpm'de ise kökteki node_modules yalnızca doğrudan bağımlılıkları gösteriyor. Kod bildirmediği bir pakete dayanıyorsa bunu üretimde değil, kurulumdan hemen sonra görüyorum. Küçük bir projede bunun değeri az gibi görünse de alışkanlığın kendisi önemli: package.json'daki liste, kodun gerçekten neye dayandığını söylemeli. ### Kilit dosyası En çok dikkat ettiğim şey sürümlerin kaymamasıydı. pnpm-lock.yaml'ı sıfırdan çözdürmek yerine mevcut package-lock.json'dan içe aktardım, böylece çözülen sürümler aynı kaldı. Sonra package-lock.json'ı sildim; iki kilit dosyasının yan yana durması, hangisinin doğru olduğu sorusunu açık bırakır. Commit mesajına da bunu not ettim: aynı çözülmüş sürümler, eski kilit dosyası kaldırıldı. Yeni kilit dosyası lockfileVersion: '9.0' ile başlıyor ve her paketi çözülmüş sürümü ve eş bağımlılıklarıyla birlikte yazıyor. Örneğin @unhead/vue satırı hangi vite, vue ve typescript sürümleriyle çözüldüğünü de taşıyor. Bir şey değiştiğinde diff'te neyin neden değiştiğini görmek böylece kolaylaşıyor. Kilit dosyasının başındaki settings bölümü de okumaya değer: autoInstallPeers: true. Yani bir paketin eş bağımlılığı projede yoksa pnpm onu kendisi kuruyor. Bu projede vue gibi eş bağımlılıklar zaten doğrudan bağımlılık olarak listelendiği için bunun pek görünür bir etkisi yok, ama ayarın kilit dosyasına yazılmış olması kurulumun nasıl davranacağını açık ediyor. ### Sürümü sabitlemek package.json'a iki alan ekledim, script'lere ise dokunmadım. ``` "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build && vite build --ssr src/app/entry-server.ts --outDir dist-ssr && node scripts/prerender.mjs && node scripts/check-seo.mjs", ... }, "devDependencies": { ... "typescript": "~5.9", "vite": "^8.3.4", "vue-tsc": "^3.3.13" }, "packageManager": "pnpm@11.9.0", "engines": { "node": ">=20.19", "pnpm": ">=11" } ``` packageManager alanı projenin hangi pnpm sürümüyle çalıştığını söylüyor. Corepack açık bir makinede (corepack enable) pnpm komutu bu alana bakıp doğru sürümü kendisi getiriyor. README'ye de bunu yazdım; projeyi klonlayan birinin global pnpm sürümünü düşünmesine gerek yok. engines alanı alt sınırları belirtiyor: Node 20.19 ve pnpm 11. Script'ler olduğu gibi kaldı; pnpm package.json'daki script'leri aynı şekilde çalıştırıyor. Değişen yalnızca yazılış: npm run dev yerine pnpm dev, npm run build yerine pnpm build. README ve geliştirme sunucusunu başlatan launch yapılandırması bu komutlara güncellendi. Build zinciri de aynı: vue-tsc ile tip kontrolü, istemci build'i, SSR build'i, prerender ve SEO kontrolü. > Kilit dosyası taşınırken amaç yeni sürümler değil, aynı sürümlerin başka bir araçla kurulmasıdır. ### TypeScript neden ~5.9 devDependencies içinde paketlerin çoğu ^ ile yazılı, TypeScript ise ~5.9. Bu bilinçli bir tercih. ^ bir sonraki ana sürüme kadar her şeyi kabul ederken ~ yalnızca 5.9.x yamalarını kabul ediyor. Sebep TypeScript 7: bu notu yazdığım dönemde vue-tsc ile uyumlu değildi. vue-tsc, .vue dosyalarının tip kontrolünü TypeScript'in üzerine kurulu olarak yapıyor ve build script'i ilk adım olarak vue-tsc --noEmit çalıştırıyor. Uyumsuz bir TypeScript sürümü, build'in daha ilk adımda kırılması demek. Kilit dosyasında typescript 5.9.3 olarak çözülmüş durumda; vue ve @unhead/vue satırları da bu sürüme bağlı. vue-tsc tarafında TypeScript 7 desteği geldiğinde aralığı genişletmek tek satırlık bir değişiklik olacak. O zamana kadar tip denetiminin bir install sonrasında sessizce değişmesindense açık bir sınırla durmasını tercih ediyorum. ### Geçişten sonra Bu projenin bağımlılık listesi zaten kısa: çalışma zamanında beş paket (vue, vue-router, @unhead/vue, gsap, animejs), geliştirme tarafında Vite, vue-tsc, OG görselleri için satori ile resvg ve fontlar için birkaç paket. Geçiş bu yüzden tek bir commit'e sığdı: yeni kilit dosyası, silinen eski kilit dosyası, package.json'daki iki alan ve güncellenen komutlar. Geçişte bir şeyi de bilerek yapmadım: bağımlılıkları güncellemedim. Aynı commit'te hem paket yöneticisini hem sürümleri değiştirseydim, bir şey bozulduğunda hangisinin sebep olduğunu ayırmak zorlaşırdı. Önce aracı değiştirdim, sürüm yükseltmelerini ayrı işler olarak bıraktım. Asıl değer ileride görünecek. Yeni bir import eklediğimde paketi package.json'a yazmayı unutursam kurulum bunu saklamayacak; bir başka makinede projeyi açtığımda da aynı pnpm sürümü, aynı kilit dosyası ve aynı TypeScript ile çalışacağım. Note (Not): Paket yöneticisi değiştirirken kilit dosyasını içe aktarmak, aracın sürümünü packageManager ile sabitlemek ve uyumsuz bir ana sürümü ~ ile dışarıda tutmak, geçişi bir yükseltme projesine dönüştürmeden yapmamı sağladı. --- ## Tek altyapı, birçok site: çok dilli bir CMS'te içerik modeli URL: https://devilyxrddev.com/journal/multi-site-cms Date: 2026-09 · Mühendislik YEE CMS, Yunus Emre Enstitüsü'nün farklı merkezlerinin içeriklerini ortak bir altyapıda yönetmek için geliştirilen çok siteli, çok dilli bir platform. Bu yazıda bu tür bir sistemde içerik modelinin neden her şeyin merkezinde durduğunu ve katmanlar arasındaki ilişkiyi nasıl düşündüğümü anlatıyorum. ### Ortak altyapı, farklı yayın alanları Problem kâğıt üzerinde basit görünüyor: farklı merkezlerin içerik üretimini, yetkilerini ve yayın akışını tek bir sistemde toplamak. Zorluk, bunu yaparken her sitenin kendi bağlamını koruması gerekmesinde. Bir merkezin editörü yalnızca kendi sitesini, kendi dillerini ve kendi onay sürecini görmeli; ziyaretçi de yalnızca o sitenin içeriğiyle karşılaşmalı. Ortak altyapı ile ayrı yayın alanı arasındaki bu denge, bütün katmanlara yayılan asıl tasarım sorusu. Bu iki ihtiyaç ilk bakışta birbirine ters düşüyor. Ortaklık; bakımın, güncellemelerin ve yeni yeteneklerin tek seferde bütün sitelere ulaşması demek. Ayrışma ise her merkezin kendi dilinde, kendi editörleriyle ve kendi takvimine göre yayın yapabilmesi. Bu tür sistemlerde benim ilk yaptığım şey, hangi kararın herkes için ortak, hangisinin siteye özel olduğunu açıkça ayırmaya çalışmak. Bu ayrım net değilse, zamanla ya her site için ayrı bir istisna yazılıyor ya da ortak yapı bir sitenin ihtiyacını diğerine dayatıyor. ### Üç katman, bir veri Sistem üç parçadan oluşuyor: kamuya açık Next.js web arayüzü, NestJS ile yazılmış API ve React ile geliştirilen yönetim uygulaması. Üçü de PostgreSQL üzerinde buluşuyor; Drizzle ORM ve Docker da yığının parçası. Bu ayrım, editör deneyimini ve ziyaretçi deneyimini birbirinden bağımsız geliştirmeyi mümkün kılıyor. Ama aynı içerik modeline dayandıkları için ikisi de aynı kurallara uymak zorunda. ``` web Next.js → ziyaretçinin gördüğü sayfa admin React → editörün günlük işi api NestJS → site bağlamı, dil, onay db PostgreSQL (Drizzle ORM) ``` Katmanların öncelikleri de farklı. Ziyaretçinin gördüğü web tarafında öncelik, sayfanın hızlı açılması, doğru dilde gelmesi ve arama motorları için anlaşılır olması. Yönetim tarafında öncelik, editörün formlarla, taslaklarla ve onay adımlarıyla rahat çalışabilmesi. API ise bu iki dünyanın arasında bir hakem gibi duruyor: bir isteğin hangi siteye, hangi dile ve hangi yetkiye ait olduğuna o karar veriyor. Benim için bu yapının en değerli yanı, kuralların tek bir yerde yaşaması; arayüzler o kuralları yeniden yazmak yerine sonuçlarını gösteriyor. Docker gibi bir araç, bu katmanların farklı ortamlarda aynı biçimde ayağa kalkmasını kolaylaştırıyor. Çok parçalı bir sistemde bu tutarlılık, geliştirme ortamı ile canlı ortam arasındaki farkları azaltmaya yardımcı oluyor. ### İçerik modeli yalnızca bir şema değil Çok dillilik, site bağlamı ve içerik onay akışları bu ürünün temel parçaları. Bir içeriğin hangi siteye ait olduğu, hangi dillerde bulunduğu ve yayına çıkmadan önce hangi adımlardan geçtiği; veritabanındaki tablolar kadar yönetim panelindeki ekranları ve web arayüzündeki sayfaları da şekillendiriyor. Bu yüzden içerik modelini konuşurken yalnızca alanları ve ilişkileri düşünmüyorum. Bir alan zorunlu mu, çevrilebilir mi, siteler arasında paylaşılabilir mi; bu soruların her biri editörün ekranında bir davranışa, ziyaretçinin sayfasında da bir sonuca dönüşüyor. Modelde belirsiz bırakılan bir nokta, genellikle arayüzde bir kafa karışıklığı olarak geri geliyor. Çok siteli bir yapıda site bağlamını sonradan eklenen bir filtre olarak değil, her isteğin baştan taşıdığı temel bir bilgi olarak görmeyi daha güvenli buluyorum. > İçerik modeli, editörün gününü ve ziyaretçinin sayfasını birlikte tasarlar. ### Dil ve onay: iki ayrı eksen Çok dilli sistemlerde sık düşülen tuzaklardan biri, çeviriyi içeriğin basit bir kopyası gibi düşünmek. Oysa aynı içeriğin farklı dillerdeki sürümleri farklı zamanlarda hazırlanabilir, farklı kişilerin elinden geçebilir ve farklı aşamalarda bulunabilir. Bu tür yapılarda dili ve yayın durumunu birbirinden bağımsız iki eksen olarak düşünmek bana daha sağlıklı geliyor. Böylece bir dildeki sürümün yayında, diğerinin hâlâ hazırlıkta olması bir istisna değil, modelin doğal bir hâli oluyor. Onay akışı da aynı mantıkla ele alınmalı. Bir içeriğin taslak, incelemede ya da yayında olması yalnızca bir durum bilgisi değil; kimin neyi görebileceğini ve kimin neyi değiştirebileceğini de belirliyor. Yetki ile iş akışı birbirine bu kadar bağlıyken, ikisini ayrı ayrı tasarlamak yerine aynı modelin parçaları olarak düşünmek tutarlılığı korumayı kolaylaştırıyor. ### Bu projede benim yerim YEE CMS bir Unitic projesi ve ekip işi. Benim katkım full-stack geliştirme ve entegrasyon tarafında: arayüz, servis, içerik yönetimi akışları ve katmanların birlikte çalışması. Entegrasyon işi bana en çok, bir katmanda verilen kararın diğer katmanlarda nasıl karşılık bulduğunu gösteriyor. Bu çalışmada içerik modelinin yalnızca veritabanını değil, editörün günlük işini ve ziyaretçinin gördüğü sayfayı da belirlediğini görmeye odaklanıyorum. Ekip içinde çalışırken bu soruların cevabı tek bir kişide değil, ortak kararlarda şekilleniyor. Benim payıma düşen, o kararların arayüzde, serviste ve veride aynı biçimde karşılık bulmasına katkı vermek. Note (Not): Çok siteli bir sistemde "bu içerik kimin, hangi dilde ve hangi aşamada" sorusu her katmanda aynı cevabı vermeli. Tutarsızlık genellikle buradan başlar. --- ## Bir ürün, üç yüz: Motice'te web, servis ve mobil aynı monorepoda URL: https://devilyxrddev.com/journal/one-product-three-surfaces Date: 2026-08 · Mühendislik Motice; pilates, yoga, kişisel antrenman ve wellness stüdyoları için geliştirilen bir CRM/ERP ürünü. Yönetim uygulaması, müşteri portalı ve mobil uygulama aynı işletme süreçlerinin farklı yüzleri. Bu yazı, onları tek bir TypeScript monoreposunda tutmanın ne anlama geldiğini anlatıyor. ### Dağınık araçların maliyeti Randevu tabanlı bir stüdyoda günlük iş; randevular, müşteriler, dersler ve işletme işlemleri arasında gidip geliyor. Bunlar farklı araçlara dağıldığında hem işletme hem müşteri aynı bilgiyi birkaç yerde arıyor. Motice bu akışları, işletmelerin kendi ayrı çalışma alanlarında yönetebileceği ortak bir üründe topluyor. Bu tür bir dağınıklığın maliyeti çoğu zaman tek bir büyük hata olarak görünmüyor; küçük sürtünmeler olarak birikiyor. Bir bilgi bir yerde güncellenip başka bir yerde eski kalıyor, aynı soru iki farklı kaynaktan iki farklı cevap alıyor. Randevu tabanlı işlerde zamanın kendisi ürünün parçası olduğu için bu tür tutarsızlıklar doğrudan işletmenin gününe yansıyor. Bence akışları tek bir ürün modelinde toplamanın değeri tam olarak burada. ### Üç yüz, tek model Ürünün üç yüzü var: işletmenin kullandığı yönetim uygulaması, müşterinin tarayıcıdan eriştiği portal ve Expo ile geliştirilen müşteri uygulaması. Üçü de aynı randevuya, aynı müşteriye, aynı derse bakıyor. Bu yüzden onları ayrı projeler olarak değil, ortak bir ürün modelinin farklı görünümleri olarak ele alıyoruz. Her yüzün kullanıcısı ve beklentisi farklı. İşletme tarafı geniş bir görünüm, hızlı düzenleme ve operasyonu bir bakışta görebilmek istiyor. Müşteri ise kendi randevusuna ve kendi bilgisine kısa yoldan ulaşmak istiyor; bunu bazen tarayıcıdan, bazen telefondan yapıyor. Ekranlar farklılaşabilir, ama altta yatan kavramlar aynı kalmalı. Bir randevunun ne olduğu ve kime ait olduğu, hangi yüzden bakılırsa bakılsın aynı cevabı vermeli. ``` TypeScript monorepo ├─ React + Vite web arayüzleri ├─ Expo müşteri uygulaması ├─ Fastify + tRPC servis katmanı └─ PostgreSQL + Drizzle ORM ``` ### Neden monorepo Tek bir TypeScript monoreposu, React arayüzlerini, Fastify ve tRPC servis katmanını ve PostgreSQL ile Drizzle veri katmanını aynı çatı altında tutuyor. Ortak bileşenler web ve mobil arasında tutarlılığı hedefliyor; tRPC sayesinde arayüzler servis tarafındaki tiplerle aynı dili konuşuyor. Bir alanın adı ya da şekli değiştiğinde bu değişiklik derleme sırasında ilgili bütün yüzlerde görünür hâle geliyor. Bunun bir bedeli de var. Monorepo, sınırları gevşetmeye çok müsait bir yapı; her şey aynı yerde olduğu için bir paketin başka bir paketin iç ayrıntısına uzanması kolaylaşıyor. Bu yüzden bu tür yapılarda neyin paylaşılacağını bilinçli seçmek gerektiğini düşünüyorum: tipler, sözleşmeler ve gerçekten ortak olan bileşenler paylaşılmalı; tek bir yüze özgü davranış o yüzde kalmalı. Ortak bileşenlerin amacı her şeyi aynı yapmak değil, aynı kavramın her yerde aynı biçimde görünmesini sağlamak. > Bir işletmenin günlük ritmini anlayan yazılım, tek ekranla değil akışların uyumuyla ölçülür. ### Çalışma alanları ve veri sınırları Motice'te işletmeler kendi ayrı çalışma alanları içinde çalışıyor. Bu tür ürünlerde benim için en önemli sorulardan biri, bir işletmenin verisinin hiçbir koşulda başka bir işletmenin görünümüne karışmaması. Bunu yalnızca arayüzde bir filtreyle sağlamaya çalışmak kırılgan bir yaklaşım; sınırın servis ve veri katmanında, her isteğin bağlamının parçası olarak düşünülmesi bana daha sağlam geliyor. Tipli bir servis ve veri katmanı, bu tür sınırları kodda görünür kılmaya yardımcı oluyor. Aynı düşünce takvim için de geçerli. Ders, randevu ve müşteri birbirinden bağımsız kayıtlar gibi görünse de gerçekte sürekli birbirine referans veriyor. Bir değişiklik bir yerde yapıldığında diğer yüzlerin bunu aynı biçimde yansıtması, kullanıcının ürüne güvenmesinin temel koşulu. Bu tür ürünlerde zor kısım genellikle tek tek özellikler değil, özelliklerin kesiştiği yerler. Bir ders, o derse ait randevular ve işletmenin gün içindeki görünümü aslında aynı bilginin farklı okumaları. Bu kesişimleri ortak bir modelde ifade etmek, her yeni ekranın kendi kurallarını sıfırdan icat etmesini engelliyor. ### Benim için asıl ilginç olan Motice, Unitic ekibinin ortak ürünü ve ben geliştirmesine ekip içinde katkı sağlıyorum. Bu projede beni en çok çeken şey tek bir ekranı tamamlamak değil; takvim, müşteri ve operasyon akışlarının aynı ürün içinde birbirini doğru tamamlaması. Ortak bir kod tabanında ekip olarak çalışmanın bir getirisi de, bir değişikliğin başka birinin işini nasıl etkilediğini erken görmek. Paylaşılan tipler bu konuşmayı somutlaştırıyor: bir sözleşme değiştiğinde etkilenen yerler tartışmaya gerek kalmadan ortaya çıkıyor. Geliştirme aşamasındaki bir ürün üzerinde çalışmak da ayrı bir deneyim. Kararlar henüz donmadığı için, bugün kurulan modelin yarın eklenecek akışları da taşıyabilmesi gerekiyor. Bu yüzden bir yapıyı ele alırken yalnızca bugünkü ekranı değil, o yapının ileride nasıl genişleyebileceğini de düşünmeye çalışıyorum. Note (Not): Aynı veriye bakan birden fazla uygulama varsa, aralarındaki sözleşmeyi tiplerle yazmak, onu bir dokümanda tutmaktan çok daha dayanıklıdır. --- ## Gerçek cihazlarda blockchain: üç Raspberry Pi ile 72 saat URL: https://devilyxrddev.com/journal/blockchain-on-real-devices Date: 2026-07 · Ar-Ge ve donanım Devilyxrd Blockchain, Go ile yazdığım bir blockchain çekirdeğini TypeScript ve NestJS servisleriyle birleştiren kişisel Ar-Ge projem. Onu yerel bir süreçte değil, üç Raspberry Pi üzerinde 72 saat boyunca çalıştırdım. Bu yazı, neyi ölçtüğümü ve neyi ölçmediğimi anlatıyor. ### Neden gerçek cihazlar Dağıtık bir sistemde birden fazla düğümün aynı zincirde kalması, tek bir bilgisayardaki süreçlerde kolay görünür. Ağ gecikmesi, sınırlı donanım ve uzun süreli çalışma işin içine girdiğinde tablo değişir. Bu yüzden sistemi yalnızca yerel bir süreçte değil, gerçek cihazlarda ve uzun bir koşuda gözlemlemek istedim. Tek makinede çalışan birkaç süreç aynı saati, aynı belleği ve aynı ağ yığınını paylaşıyor. Bu, testleri hızlı yapmayı sağlıyor ama bazı soruları görünmez kılıyor. Ayrı cihazlarda çalışan düğümlerin her biri kendi kaynaklarıyla, kendi zamanlamasıyla ve aradaki gerçek ağ üzerinden haberleşiyor. Raspberry Pi gibi sınırlı kaynaklı cihazlar, bu koşulları uzun süre boyunca gözlemlemek için bana uygun bir ortam gibi geldi. ### Mimari Sistem izinli bir PoA yapısında çalışıyor; kesinleşme imzalı validator oylarıyla sağlanıyor. Blockchain çekirdeği Go ile yazıldı, NestJS katmanı servisleri sunuyor, servisler arasında gRPC ve kalıcı veri için PostgreSQL kullanılıyor. GraphQL ve Docker da yığının parçası. Blok üretimi, validator koordinasyonu, cüzdan ve sözleşme servisleri ayrı sorumluluklar olarak ele alınıyor. ``` çekirdek Go (PoA, imzalı validator oyları) servisler TypeScript / NestJS iletişim gRPC veri PostgreSQL düğümler 3 × Raspberry Pi 4B ``` Çekirdeği ve servisleri ayrı katmanlarda tutmak bilinçli bir tercih. Konsensüs ve blok üretimi gibi sistemin doğruluğunu belirleyen kısımları, cüzdan ya da sözleşme servisleri gibi dış dünyaya açılan kısımlardan ayırmak, her birini kendi koşullarında değerlendirmeyi kolaylaştırıyor. Bu ayrım dil seçiminde de görünüyor: Go çekirdekte, TypeScript servislerde. Bir katmanda yapılan değişikliğin diğerini nasıl etkilediğini görmek de böylece daha kolay oluyor. Servislerin TypeScript tarafında olması, çekirdeğin dışındaki işleri daha hızlı denemeyi sağlıyor. Çekirdekte ise öncelik hız değil, öngörülebilirlik. Bir blockchain çekirdeğindeki beklenmedik bir davranış yalnızca bir hata doğurmaz; düğümlerin farklı durumlara sürüklenmesine de yol açabilir. ### İzinli PoA ve kesinleşme İzinli bir PoA yapısında blok üretme yetkisi, önceden belirlenmiş validator'lara ait. Kesinleşme ise imzalı validator oylarıyla sağlanıyor. Benim için bu modelin öğretici yanı, iki ayrı soruyu birbirinden ayırması: bloklar üretilmeye devam ediyor mu ve üretilen bloklar kesinleşiyor mu? Bir sistem blok üretirken kesinleşme durabilir; tersine, tutarlı görünen bir zincir yeni blok üretmeyi bırakmış olabilir. Bu yüzden uzun bir koşuda ikisine ayrı ayrı bakmak gerekiyor. İmzalı oylar, kesinleşmenin hangi validator'ların onayıyla gerçekleştiğini de izlenebilir kılıyor. İzinli bir yapı, açık ağlardaki kadar karmaşık bir katılım modeline ihtiyaç duymadan konsensüs ve kesinleşme üzerine çalışmamı sağlıyor. Kişisel bir Ar-Ge projesi için bu, odaklanmak istediğim soruları daraltmanın makul bir yolu. ### 72 saatlik test 13–16 Temmuz 2026 tarihleri arasındaki 72 saatlik testte üç Raspberry Pi 4B üzerinde 86.553 blok raporlandı. Düğümler arasında yapılan 863 kontrolde zincir ayrışması ya da kesinleşme durması gözlenmedi. Bu sayılara bakarken iki şeyi ayırmaya çalışıyorum. Blok sayısı, üretimin koşu boyunca sürdüğünü gösteriyor. Düğümler arası kontroller ise farklı cihazların aynı zinciri paylaşıp paylaşmadığını gösteriyor. Biri olmadan diğeri eksik kalıyor: yüksek bir blok sayısı, düğümler farklı zincirlerde ilerliyorsa bir şey ifade etmez; tutarlı ama duran bir zincir de uzun süreli çalışmayı kanıtlamaz. Bu tür bir koşuda en değerli bilgi çoğu zaman tek bir sayı değil, sayıların birbirini doğrulaması. Raporlanan blok sayısı ile düğümler arası kontrollerin sonucu aynı hikâyeyi anlatıyorsa koşuya daha çok güvenebiliyorum; ayrışsalardı, asıl incelenmesi gereken o ayrışma olurdu. > Bir düğümün ayakta görünmesi, sistemin çalıştığı anlamına gelmez. ### Bu sonuç neyi söylemiyor Bu, belirtilen koşuya ait bir bulgu. Sistemin bütün hata senaryolarını kapsamıyor; ağ bölünmesi, kötü niyetli bir validator ya da çok daha fazla düğüm gibi durumlar ayrı testler gerektiriyor. Sonucu bu sınırlarıyla birlikte kaydetmek, sonucun kendisi kadar önemli. Üç düğüm küçük bir ağ. Daha kalabalık bir validator kümesinde oyların toplanması, ağ gecikmesi ve kaynak kullanımı farklı davranabilir. Bu yüzden 72 saatlik koşuyu bir varış noktası olarak değil, sistemin gerçek cihazlardaki davranışına dair sınırları belli bir gözlem olarak görüyorum. Uzun süreli çalışmayı doğrulamak; düğümler arası tutarlılığa, blok üretiminin sürmesine ve koşunun sınırlarının kaydedilmesine birlikte bakmak demek. Kişisel bir Ar-Ge projesinde bunu yapmanın bir avantajı var: sonucu daha büyük göstermek için bir baskı yok. Neyi gördüğümü ve neyi görmediğimi aynı açıklıkla yazabiliyorum. Bir sonraki adımları da bu boşluklar belirliyor. Note (Not): Bir test sonucunu paylaşırken, ne kadar süre, hangi donanımda ve hangi koşullarda alındığını da yazmak gerekir. Yoksa sayı, olduğundan daha büyük bir şey iddia eder. --- ## Kod yazmadan önce: Mentorual'da kararları netleştirmek URL: https://devilyxrddev.com/journal/decisions-before-code Date: 2026-06 · Ürün ve analiz Mentorual'da işin bugünkü merkezi kod değil, kodun dayanacağı kararlar. Gereksinimler, kullanım senaryoları, ekran aileleri ve rol-yetki matrisi üzerinden ilk sürümün sınırlarını çizmenin neden üretimin bir parçası olduğunu bu notta anlatıyorum. ### Neden koddan önce Mentorual; mentörlük, uygunluk, randevu ve kurum yönetimi akışlarını aynı ürün içinde ele alan bir proje. Mentör, üye, kurum ve yönetici rolleri var; deneyim web, mobil ve yönetim tarafına dağılıyor. Bu projedeki rolüm ürün analizi, gereksinimler ve tasarım hazırlığı. Çalışma şu an analiz ve tasarım hazırlığı aşamasında ilerliyor, yani anlatabileceğim bir kod tabanı yok. Anlatabileceğim şey, kod yazılmadan önce verilmesi gereken kararlar ve bunların neden bu kadar ağırlık taşıdığı. Birden fazla rolün ve kurumun bulunduğu bir üründe aynı işlemin farklı ekranlarda tutarlı davranması gerekiyor. Bir randevunun mentör tarafında nasıl göründüğü, üye tarafında hangi durumlardan geçtiği, kurum tarafının buna nereden dahil olduğu gibi sorular cevapsız kalırsa, bu cevapları geliştirme sırasında herkes kendi başına vermek zorunda kalır. Bence ürünlerdeki tutarsızlığın en yaygın kaynağı tam olarak bu: kötü kod değil, hiç konuşulmamış kararlar. ### Gereksinimden ekran ailesine Hazırlık paketinin parçaları gereksinimler, kullanım senaryoları, ekran aileleri ve rol-yetki analizi. Bunları ayrı belgeler gibi düşünmek kolay, ama asıl değer aralarındaki bağda. Bir gereksinim en az bir kullanım senaryosunda karşılık bulmalı, her senaryo belirli ekranlarda yaşamalı, her ekran da kimin neyi görebildiği ve yapabildiği sorusuna bağlanmalı. Bu zincirin bir halkası koptuğunda, ya yapılmayan bir iş ya da kimsenin istemediği bir ekran ortaya çıkıyor. Ekran ailesi kavramını bu yüzden faydalı buluyorum. Ekranları tek tek listelemek yerine benzer davranışı paylaşanları bir arada düşünmek, tekrar eden kararları bir kez vermeyi sağlıyor. Liste, detay, form ve onay gibi kalıplar farklı rollerde tekrar ettiğinde davranışın da aynı kalması beklenir. Web ve mobil aynı aileyi farklı ölçülerde sunabilir, ama kural aynı kalmalı. ``` gereksinim -> kullanım senaryosu -> ekran ailesi (web / mobil / yönetim) -> rol-yetki satırı -> kabul koşulu ``` ### Rol-yetki matrisi bir tablo değil, bir sözleşme Rol-yetki matrisi ilk bakışta satırları roller, sütunları işlemler olan bir tablo gibi görünüyor. Ben onu daha çok bir sözleşme olarak görüyorum: analiz, tasarım ve geliştirme arasında “kim neyi yapabilir” sorusunun tek cevabı. Mentör, üye, kurum ve yönetici gibi rollerin olduğu bir yapıda bu cevabın ekranlara, servislere ve testlere aynı şekilde yansıması gerekiyor. Matrisi düşünürken en çok işe yarayan soru bence “bu işlem hangi durumda kimin elinde” sorusu. Yetki çoğu zaman yalnızca role değil, kaydın durumuna ve kurum bağlamına da bağlı. Bu ayrımları erkenden yazıya dökmek, ileride bir ekranın neden bir düğmeyi gösterip diğerinin göstermediği tartışmasını baştan kesiyor. > Bir akışın nerede bittiğini bilmeden o akışı doğru kurmak zor. ### MVP ile sonraki sürümleri ayırmak Bu çalışmada MVP ve sonraki sürüm kapsamları ayrıldı. Kâğıt üzerinde basit görünen bu ayrım aslında en zor kararlardan biri. Her özellik önemli görünür; asıl soru, ilk sürümün tek başına anlamlı bir ürün olup olmadığı. Benim bu tür ayrımlarda kullandığım ölçüt şu: bir rolün temel işini baştan sona tamamlayabilmesi için gereken en kısa yol hangisi, ve bu yolun dışında kalanlar sonraki sürüme taşınabilir mi. Ayırmanın görünmeyen bir yanı da var. Sonraki sürüme bırakılan bir özellik unutulmuş sayılmaz; ilk sürümün akışlarını, ileride o özelliğin gelmesini zorlaştırmayacak biçimde düşünmek gerekir. Kapsam dışı bırakmak, onu hiç düşünmemek anlamına gelmiyor. ### Kararları kayda almak Hazırlık paketinin en az görünen ama bence en kalıcı parçası, ürün kararlarının kayda alınması. Bir kararın ne olduğu kadar neden verildiği de önemli. Aylar sonra biri “bu neden böyle” diye sorduğunda cevabın birinin hafızasında değil yazılı bir kayıtta olması, aynı tartışmanın tekrar tekrar yapılmasını önlüyor. Kabul koşulları da bu kaydın uzantısı. MVP kapsamı, ekran gereksinimleri ve kabul koşulları arasında izlenebilirlik kurmak, bir işin “bitti” sayılma ölçütünü önceden belirlemek demek. Geliştirme başladığında bu ölçütler hem uygulayan hem gözden geçiren için ortak bir dil oluşturur. ### Analiz de üretimin bir parçası Bu projede en çok üzerinde durduğum düşünce, ürünü netleştirmenin de üretimin bir parçası olduğu. Kod yazılmayan bir aşama kolayca “hazırlık” diye küçümsenebiliyor. Oysa kimin hangi işlemi yapabildiği, bir akışın hangi durumda sona erdiği ve ilk sürümün tam olarak neyi kapsadığı sorularının cevabı, yazılacak kodun büyük kısmını belirliyor. Bu cevaplar olmadan başlayan bir geliştirme, aynı soruları daha pahalı bir yerde cevaplamak zorunda kalıyor. Note (Ders): Kod öncesindeki kararlar görünmez ama ucuzdur; aynı kararlar geliştirmenin ortasında verildiğinde hem görünür hem pahalı olur. --- ## Ortak kimlik, ayrı kurallar: OYS & TYS monoreposundan notlar URL: https://devilyxrddev.com/journal/shared-identity-separate-rules Date: 2026-05 · Mühendislik OYS & TYS'de öğrenci, öğretici, aday ve yönetici aynı kimlik altyapısını paylaşıyor ama her alanın verisi ve iş kuralları farklı. Turborepo içinde ortak paketleri paylaşırken alan sınırlarını açık tutmaya dair düşüncelerimi bu notta topluyorum. ### İki sistem, tek depo OYS & TYS; öğrenci ve kurs yönetimi ile Türkçe Yeterlik Sınavı süreçlerini ortak bir platform yapısında ele alan sistemler. Öğrenci, kurs, sınav ve yönetim süreçlerine odaklanıyor ve bir monorepo içinde geliştiriliyor. Arayüzler Next.js, servisler NestJS ile yazılıyor; veri tarafında PostgreSQL, Prisma ve Redis var. Depo yapısını Turborepo yönetiyor. Bu web tabanlı yönetim sistemlerinin frontend ve backend çalışmalarında aktif rol alıyorum ve öğrenci, kurs ve sınav yönetimi kapsamındaki geliştirmeler devam ediyor. Bu notta belirli bir özellikten çok, bu tür bir yapıda sürekli karşıma çıkan bir gerilimden bahsetmek istiyorum: ortak olanı paylaşmak ile ayrı olanı ayrı tutmak arasındaki gerilim. ### Ortak olan ne Öğrenci, öğretici, aday ve yönetici ihtiyaçları birbirine bağlı. Aynı kişi farklı zamanlarda farklı rollerde sisteme girebilir; giriş, oturum ve temel kullanıcı bilgisi gibi konular her alanda tekrar yazılmamalı. Ortak kimlik altyapısı bu yüzden yapının merkezinde duruyor. Turborepo da tam bu noktada işe yarıyor: ortak paketler ile farklı uygulama alanlarının aynı depoda yönetilmesini sağlıyor. Paylaşmanın getirisi açık. Bir kural bir kez yazılıyor, bir tip tanımı her yerde aynı anlama geliyor, bir düzeltme bütün uygulamalara aynı anda ulaşıyor. Ama paylaşımın da bir maliyeti var ve bence bu maliyet çoğu zaman geç fark ediliyor. ``` Turborepo ortak paketler : kimlik altyapısı, paylaşılan tipler ve araçlar öğrenci ve kurs alanı : Next.js arayüz -> NestJS servis sınav alanı : Next.js arayüz -> NestJS servis veri : PostgreSQL + Prisma, Redis ``` ### Ayrı kalması gereken ne Her alanın veri ve iş kuralları farklı. Bir kursun yaşam döngüsü ile bir sınav sürecinin yaşam döngüsü aynı değil; bir öğrencinin kayıt durumu ile bir adayın sınav durumu aynı soruların cevabı değil. Bu kuralları “ortak” bir pakete taşımak ilk anda düzenli görünebilir, ama zamanla o paket iki alanın da istisnalarını taşıyan bir yere dönüşür. Benim bu konudaki yaklaşımım şu: ortak pakete yalnızca gerçekten ortak olan girer. Kimlik, ortak tipler, genel araçlar paylaşılabilir. Bir alanın iş kuralı, başka bir alan da benzer bir şeye ihtiyaç duyuyor diye ortaklaşmaz. Benzerlik ile aynılık arasındaki fark, monorepo'da en sık yanılınan yer. Bir paketi ortaklaştırmak kolay, sonradan ayırmak çok daha zor; bu yüzden emin olmadığım durumlarda ayrı tutmayı tercih ediyorum. > Ortak kimlik, açık sorumluluklar. ### Kullanıcı akışı ve servis sınırı Bu çalışmada kullanıcı akışlarını veri ve servis sınırlarıyla birlikte düşünmeye odaklanıyorum. Bir ekranda kullanıcıya akıcı görünen bir iş, arka planda birden fazla alana dokunuyor olabilir. Böyle durumlarda kolay yol, bir servisin diğer alanın verisine doğrudan ulaşması. Ama bu, sınırın sessizce delinmesi demek. Frontend ve backend'de birlikte çalışmanın burada bir avantajı var: bir akışın iki ucunu da gördüğüm için, sınırı nerede çizeceğimi daha bilinçli seçebiliyorum. Arayüzdeki bir ihtiyacı karşılamak için servisin sorumluluğunu genişletmek yerine, iki alan arasındaki konuşmayı açık bir sözleşmeye bağlamak uzun vadede daha sağlıklı görünüyor. ### Monorepo bir disiplin gerektiriyor Monorepo her şeyi aynı yerde tutmayı kolaylaştırıyor; bu yüzden her şeyin birbirine bağlanmasını da kolaylaştırıyor. Aynı depoda olmak, aynı modül olmak anlamına gelmiyor. Bağımlılıkların yönünü bilinçli tutmak, bir alanın diğerinin iç detayına erişmemesine dikkat etmek ve ortak paketlere ne girdiğini sorgulamak bence bu yapının asıl işi. Kural basit görünüyor, ama günlük işin temposunda en kolay unutulan şey de bu. Redis gibi paylaşılan bir altyapı parçası da aynı soruyu doğuruyor. Altyapının ortak olması, içinde tutulan şeylerin sahipsiz olabileceği anlamına gelmiyor. Her alanın kendi verisinin sahibi olması, ortak kaynak kullansa bile korunması gereken bir ilke. ### Devam eden bir denge Geliştirme sürdüğü için bu dengeyi tamamlanmış bir şey gibi anlatmıyorum. Her yeni ihtiyaçta “bu ortak mı, yoksa sadece benzer mi” sorusu yeniden soruluyor. Ortak altyapı kurarken her alanın kendi kurallarını koruması, bir kez verilip unutulan bir karar değil; sürekli bakım isteyen bir alışkanlık. Bu soruyu açıkça sormak, kod incelemelerinde de faydalı bir alışkanlık. Note (Ders): Bir şeyi ortak pakete taşımadan önce, iki alanın onu aynı nedenle mi yoksa sadece benzer biçimde mi kullandığını sormak gerekiyor. --- ## Uçtan uca sorumluluk: BGB'de katmanların birbirine etkisi URL: https://devilyxrddev.com/journal/end-to-end-responsibility Date: 2026-04 · Mühendislik BGB, kumar zararlarını azaltmaya yönelik bir dijital ürün. Mobil uygulama, servisler, yönetim paneli ve web arayüzünde uçtan uca sorumluluk almak, bir katmandaki kararın diğerlerine nasıl yansıdığını görmemi sağladı. Bu notta o deneyimi anlatıyorum. ### Ürünün bağlamı BGB, kumar zararlarını azaltmaya yönelik bir dijital ürün. Konunun hassas olduğunun farkındayım; bu yüzden bu notta ürünün etkisi hakkında bir iddiada bulunmuyorum. Anlatmak istediğim şey, böyle bir ürünün yazılım tarafını uçtan uca üstlenmenin nasıl bir iş olduğu. Bu bir şirket projesi. Ürünün analiz, tasarım ve geliştirme süreçlerini yürüttüm; mobil uygulama, backend algoritmaları, yönetim paneli ve web arayüzü olmak üzere tüm yazılım katmanlarında sorumluluk aldım. Hassas bir alanda çalışırken bence en doğru tutum, ürünü kullanacak kişiye saygıyla yaklaşmak ve arayüzün dilini sade tutmak. Yazılımın görevi burada gösteriş değil; güvenilir, anlaşılır ve tutarlı davranmak. Bu, dil seçiminden hata mesajlarının tonuna kadar küçük kararları da etkiliyor. ### Dört yüzey, tek ürün Mobil kullanıcı deneyimi React Native ve Expo ile, web ve yönetim yüzeyleri Next.js ile geliştirildi. Servisler NestJS ve PostgreSQL üzerinde çalışıyor, veri erişiminde Prisma var. Bu dört yüzeyi ayrı projeler gibi düşünmek mümkün, ama BGB'de hepsi aynı ürünün birbirini tamamlayan parçaları. Mobil akışların, verinin ve yönetim araçlarının birlikte işlemesi gerekiyor. ``` React Native + Expo (mobil) Next.js (web) Next.js (yönetim) \ | / +---------- NestJS servisleri ---------------+ | Prisma -> PostgreSQL ``` Bu yerleşimin önemli bir sonucu var: hiçbir yüzey kendi başına “bitmiş” sayılamıyor. Mobilde bir ekranın davranışı, servis katmanının ne döndürdüğüne; yönetim panelinde bir aracın kullanışlılığı, verinin nasıl modellendiğine bağlı. Bu yüzden bir yüzeyde yapılan iş, diğerlerinde de kontrol edilmeden tamamlanmış sayılmamalı. ### Bir katmandaki karar Uçtan uca sorumlulukta en net gördüğüm şey, bir katmanda verilen kararın diğerlerinde mutlaka bir karşılığı olduğu. Veri modelinde bir alanı nasıl tanımladığınız, servisin o alanı nasıl sunduğunu belirliyor. Servisin sunuş biçimi, mobil uygulamanın o veriyi nasıl göstereceğini ve hangi durumları ele alması gerektiğini belirliyor. Yönetim panelinin aynı veriyi nasıl düzenleyebileceği de yine aynı modele bağlı. Katmanlar farklı kişilerde olduğunda bu zincir toplantılarla ve belgelerle yönetilir. Hepsi aynı kişide olduğunda ise başka bir risk doğuyor: zinciri kafanızda tuttuğunuz için açıkça yazmayı ihmal etmek. Bu yüzden bence uçtan uca çalışırken bile katmanlar arasındaki anlaşmaları açıkça tanımlamak gerekiyor. > Bir katmandaki küçük bir karar, öbür uçta bir ekranın davranışı olarak geri döner. ### Servis sözleşmeleri Mobil deneyim, servis sözleşmeleri ve yönetim ihtiyaçlarını birlikte ele almaya çalıştım. Servis sözleşmesi derken, bir uç noktanın ne aldığı, ne döndürdüğü ve hangi durumlarda nasıl davrandığı konusundaki anlaşmayı kastediyorum. Mobil uygulamalarda kullanıcıların elindeki sürümler genelde web kadar hızlı güncellenmez. Bu, her mobil üründe geçerli genel bir durum ve sözleşmelerin dikkatle değiştirilmesi gerektiği anlamına geliyor. TypeScript'in bütün katmanlarda ortak dil olması burada yardımcı oluyor; aynı kavramın mobilde, webde ve serviste aynı biçimde düşünülmesini kolaylaştırıyor. Ama ortak dil tek başına yetmiyor, sözleşmenin kendisinin de bilinçli tasarlanması gerekiyor. ### Yönetim ihtiyaçları da ürünün parçası Yönetim paneli çoğu projede en son düşünülen yüzey oluyor. Oysa bir ürünün sürdürülebilirliği, onu yönetenlerin işini ne kadar rahat yapabildiğine de bağlı. Arayüz ve backend akışlarını ürünün ortak ihtiyaçları üzerinden ele almak, yönetim tarafını sonradan eklenen bir ek değil, baştan düşünülen bir yüzey yapıyor. Yönetim aracı zayıf kaldığında, ürünün geri kalanı ne kadar iyi olursa olsun günlük işleyiş zorlaşır. Analiz ve tasarım süreçlerini de yürüttüğüm için, bir ihtiyacı ilk duyduğum andan itibaren onun mobilde, servis katmanında ve yönetim tarafında neye dönüşeceğini birlikte düşünebildim. Bence uçtan uca sorumluluğun en değerli yanı tam olarak bu: kararın bütün sonuçlarını aynı anda görebilmek. ### Bedeli ve kazancı Bütün katmanlarda sorumluluk almak, bağlam değiştirme yükünü de beraberinde getiriyor. Aynı gün içinde bir mobil ekranın detayından bir sorgunun yapısına geçmek dikkat istiyor. Buna karşılık kazanç da büyük: bir sorunun hangi katmandan kaynaklandığını daha hızlı görmek ve çözümü doğru katmana koymak. Bir hatayı arayüzde gizlemek yerine kaynağında düzeltmek, ancak bütün zinciri gördüğünüzde mümkün oluyor. Ayrıca bir özelliği tasarlarken onun maliyetini bütün katmanlarda birden tahmin edebilmek, kapsam konuşmalarını daha gerçekçi kılıyor. Note (Ders): Uçtan uca çalışırken katmanlar arasındaki anlaşmaları yazıya dökmek, onları tek başına taşıdığınızda bile gerekli; çünkü bir gün o zinciri başkası da okuyacak. --- ## Aynı veri, dört kullanıcı: Türk Sanatı Platformu üzerine notlar URL: https://devilyxrddev.com/journal/one-data-many-users Date: 2026-03 · Mühendislik Türk Sanatı Platformu'nda ana merkez ve alt platformlar aynı ürünün içinde yaşıyor. Ziyaretçi, sanatçı, başvuru sahibi ve yönetici aynı veriye farklı ihtiyaçlarla yaklaşıyor; bu notta çok kiracılı bir yapıda bu farkları birlikte düşünmeyi anlatıyorum. ### Ana merkez ve alt platformlar Türk Sanatı Platformu; sanatçı profillerini, eserleri, etkinlikleri, başvuruları ve editoryal içeriği bir ana merkez ve alt platformlar üzerinden sunan kurumsal bir sanat platformu. Alt platformlar farklı ülke veya yapılar için ayrılıyor. Kamuya açık web, kullanıcı alanı, yönetim paneli ve API birlikte ele alınıyor. Bu bir şirket projesi; ben kurumsal web uygulamasının geliştirme, entegrasyon ve teslim süreçlerinde sorumluluk aldım. Arayüz tarafında Next.js ve React, servis tarafında NestJS, veri tarafında Prisma ve PostgreSQL var; yığında Redis ve Tailwind CSS de yer alıyor. Bu yapının ilk bakışta dikkat çeken yanı çok kiracılık. Ama bu projede beni daha çok düşündüren şey, aynı verinin karşısında duran farklı insanlar oldu. ### Dört bakış, tek veri Bir sanatçı profilini düşünelim. İçerik ziyaretçisi için bu profil okunacak, gezinilecek bir sayfa. Sanatçı için kendi emeğini temsil eden ve güncel tutmak istediği bir alan. Başvuru sahibi için henüz bir sürecin içinde olan, sonucu belli olmayan bir kayıt. Yönetici için ise denetlenecek, yayına alınacak ya da düzeltilecek bir içerik. Veri aynı kalıyor, ama her biri o veriden farklı bir şey istiyor. Bu farkı görmemek, genelde iki uçtan birine götürüyor bence. Ya her kullanıcı için ayrı bir veri kopyası ve ayrı kurallar oluşuyor, ki zamanla birbirinden kopuyorlar. Ya da herkese aynı ekran ve aynı alanlar gösteriliyor, ki bu da kimsenin işini tam görmüyor. Benim tercih ettiğim yaklaşım, verinin tek kalması ve farklılığın görünüm, yetki ve akış katmanında ifade edilmesi. Bu yaklaşımda yetkiler ve görünümler daha fazla düşünce istiyor, ama veri tutarlılığı korunuyor. > Veri tek kalabilir; ihtiyaçlar tek olmak zorunda değil. ### Çok kiracılık bir sınır meselesi Ana merkez ve alt platformlar arasında içerik ve kullanıcı akışlarını ortak bir ürün içinde yönetirken, her platformun yerel bağlamını ve yönetim sorumluluklarını korumak gerekiyor. Çok kiracılığı ben öncelikle bir sınır meselesi olarak görüyorum: hangi veri hangi platforma ait, kim hangi platformda ne görebilir, ortak olan nerede başlayıp nerede bitiyor. Bu sınırlar arayüzde kalırsa kırılgan olur. Bir platform bağlamının servis katmanında da, sorgularda da aynı şekilde taşınması gerekir. Böyle bir mimaride çok kiracılık, içerik yönetimi ve başvuru süreçleri ayrı ayrı çözülecek konular değil; aynı mimarinin parçaları. Birinde verilen karar, diğer ikisinin davranışını doğrudan etkiliyor. ### Yerel bağlam ve yönetim sorumluluğu Alt platformların varlık sebebi zaten farklılık. Her yapı kendi içeriğini, kendi etkinliklerini ve kendi başvurularını yönetmek isteyebilir. Öte yandan ana merkezin bütüne bakabilmesi de gerekiyor. Bu iki ihtiyaç arasındaki dengeyi kurmak teknik olduğu kadar ürünle ilgili bir karar. Yönetim panelinde bu denge en çok yetkilerde görünüyor. Bir yöneticinin yalnızca kendi platformunu mu, yoksa birden fazlasını mı gördüğü; bir içeriğin hangi platformda yayında olduğu; bir başvurunun hangi yapının değerlendirmesine düştüğü gibi sorular, ekran tasarımından önce cevaplanması gereken sorular. Bunlar cevaplanmadan yazılan bir panel, sonradan sürekli istisna eklenen bir panele dönüşür. ### Entegrasyon ve teslim tarafından bakmak Bu projede rolüm geliştirmenin yanında entegrasyon ve teslim süreçlerini de kapsıyordu. Bu, ürüne biraz farklı bir açıdan bakmamı sağladı. Kamuya açık web, kullanıcı alanı, yönetim paneli ve API ayrı ayrı çalışıyor gibi görünse de, teslim anında hepsi aynı veri modeline ve aynı platform bağlamına dayanıyor. Bir katmanda yapılan küçük bir değişikliğin diğerlerinde nasıl karşılık bulduğunu teslim sırasında görmek, bu bütünlüğün ne kadar önemli olduğunu hatırlatıyor. Kullanıcı deneyimi, yönetim akışları ve çok kiracılı servis yapısı birlikte çalışmak zorunda. Birini iyileştirirken diğerlerini gözden kaçırmamak, bence bu tür platformlarda en çok dikkat isteyen iş. ### Geriye kalan Bu projeden aklımda kalan en net şey, bir veri modelini tasarlarken yalnızca varlıkları değil, o varlıklara bakan insanları da düşünmek gerektiği. İçerik ziyaretçisi, sanatçı, başvuru sahibi ve yönetici aynı kayda farklı sorularla geliyor. İyi bir yapı bu soruların hepsine cevap verebilmeli, ama bunu veriyi parçalamadan yapmalı. Bunun yolu bence teknik bir hile değil; her yeni ekran ya da akış eklendiğinde, o verinin diğer kullanıcılar için ne anlama geldiğini yeniden sormak. Note (Not): Çok kiracılı bir üründe en önemli soru “hangi veri nerede” sorusu kadar “bu veriye kim, hangi niyetle bakıyor” sorusu. --- ## İki dilde yayın: NoktaTR'de içerik, akış ve arayüz birlikte URL: https://devilyxrddev.com/journal/bilingual-publishing Date: 2026-02 · Ürün ve analiz NoktaTR, .tr dergisinin kültür, sanat ve edebiyat içeriğini Türkçe ve İngilizce olarak dijital ortama taşıyor. Bu notta içerik yapısı, editoryal akış ve arayüz hiyerarşisinin neden birbirinden bağımsız kararlar olmadığını anlatıyorum. ### Dergiden ekrana NoktaTR, kültür, sanat ve edebiyat dergisi .tr için geliştirilen Türkçe ve İngilizce bir dijital yayın platformu. Vitrin, yönetim paneli ve API'den oluşuyor. Okurun gördüğü web deneyimi ile içerik hazırlama ve yönetme akışları aynı sistemin parçaları. Next.js ile yazılan web ve yönetim arayüzleri, NestJS servisleri ve Prisma veri erişimiyle çalışıyor. Bu bir şirket projesi; uygulama katmanlarında, içerik yönetimi akışlarında ve geliştirme-teslim süreçlerinde görev aldım. Dergi içeriğinin okunabilir bir web deneyimine dönüşmesi işin yalnızca bir yarısı. Diğer yarısı, yayın ekibinin bu içeriği sürdürülebilir biçimde yönetebilmesi. Bu iki yarı birbirinden ayrı düşünüldüğünde, ya okur için güzel ama yönetmesi zor bir site, ya da yönetmesi kolay ama okuması sıradan bir site ortaya çıkıyor. Bu yüzden iki yarıyı aynı anda düşünmeye çalışıyorum. ### Üç karar, tek karar Bu projeden çıkardığım en önemli düşünce şu: dijital yayıncılıkta içerik yapısı, editoryal akış ve arayüz hiyerarşisi birbirinden bağımsız kararlar değil. İçeriği nasıl modellediğiniz, editörün onu nasıl hazırlayacağını belirliyor. Editörün hazırlama biçimi, okurun sayfada neyi önce göreceğini belirliyor. Okurun deneyimi de geri dönüp içerik modelinden bir şey talep ediyor. Geliştirme sırasında bu üç alanı birlikte düşünmeye çalışıyorum. > İçerik modeli, editörün iş akışı ve okurun gözü aynı kararın üç yüzü. ### İçerik yapısı Bir dergi yazısı yalnızca başlık ve gövdeden oluşmuyor. Yazar, kategori, görseller, alıntılar, ilişkili içerikler gibi parçaların her biri, hem yönetim panelinde bir alana hem de okur sayfasında bir yere karşılık geliyor. Bence içerik yapısını kurarken sorulması gereken soru “hangi alanlar lazım” değil, “bu alan okura ne anlatacak ve editör onu nasıl dolduracak” sorusu. Yapı çok serbest olduğunda her yazı farklı görünür ve arayüz tutarlılığını kaybeder. Çok katı olduğunda ise editoryal ifade alanı daralır. Kültür ve edebiyat içeriği gibi biçimin de anlamın parçası olduğu bir alanda, bu dengeyi kurmak ayrıca önem taşıyor. Bu yüzden yapıyı belirlerken birkaç farklı içerik türünü örnek alıp, her birinin aynı kalıba sığıp sığmadığına bakmak bence iyi bir alışkanlık. ### Editoryal akış Yönetim paneli, yayın ekibinin günlük çalışma alanı. Bir içeriğin hazırlanması, gözden geçirilmesi ve yayına alınması gibi adımlar panelde nasıl ifade ediliyorsa, ekibin işi de o kadar rahat ya da o kadar zor oluyor. Benim bu tür akışlarda önem verdiğim şey, panelin teknik veri modelini değil, editörün düşünme biçimini yansıtması. Editör “bir yazı hazırlıyorum” diye düşünür, “bir kayıt oluşturuyorum” diye değil. Vitrin, yönetim paneli ve API'nin ortak bir yayın modeliyle çalışması burada belirleyici. Panelde girilen içerik ile vitrinde görünen içerik aynı modelden besleniyorsa, editör yaptığı değişikliğin okura nasıl yansıyacağını daha kolay öngörebilir. Model ayrıştığında ise panelde görülen ile okurun gördüğü arasında küçük ama yorucu farklar oluşur. ### İki dil, bir yayın yapısı Türkçe ve İngilizce içerikler ürünün yayın yapısında birlikte ele alınıyor. İki dilli yayında bence en kritik soru, iki dilin birbirine nasıl bağlandığı. Bir yazının iki dildeki hâli aynı içeriğin iki yüzü mü, yoksa birbirinden bağımsız iki içerik mi? Bu sorunun cevabı hem veri modelini, hem editörün iş akışını, hem de okurun diller arasında nasıl geçiş yapacağını etkiliyor. İlk yaklaşım dilleri sıkı bağlar, ikincisi daha fazla serbestlik verir; ikisinin de bir bedeli var. Dil burada bir arayüz ayarı değil, içeriğin kendisinin bir boyutu. Başlık uzunlukları, metin ritmi ve tipografik denge dilden dile değişebiliyor; arayüz hiyerarşisinin iki dilde de dengeli kalması gerekiyor. Bu yüzden iki dilli bir yayını sonradan eklenen bir çeviri katmanı olarak değil, baştan tasarlanan bir yapı olarak düşünmeyi tercih ediyorum. ### Teslim tarafı Görevim geliştirme ve teslim süreçlerini de kapsadığı için, yayın ürününe bir de yayında kalma açısından bakmam gerekti. Bir yayın platformunda yapılan değişiklik yalnızca kodu değil, yayındaki içeriği ve editörün alışkanlıklarını da etkiliyor. Bu yüzden bence teslimde sorulması gereken soru “çalışıyor mu” kadar “okur ve editör için bir şey bozuldu mu” sorusu. Yayın ürünlerinde güven, okurun ve editörün her gün aynı şeyle karşılaşmasıyla kuruluyor. Note (Not): Yayın ürünlerinde içerik modelini, yönetim panelini ve okur sayfasını ayrı ayrı iyileştirmek yetmiyor; üçünü aynı kararın parçaları olarak ele almak gerekiyor. --- ## Klinik başvuru sisteminde formun arkasındaki süreç URL: https://devilyxrddev.com/journal/clinic-application-flow Date: 2026-01 · Ürün ve analiz Kadir Yücebaş için geliştirdiğim sistemde başvuru formu işin yalnızca görünen kısmı. Bu notta başvurunun klinik ekibine ulaşmasını, durumunun takip edilmesini ve iki dilli arayüzlerin aynı veri modeline bağlanmasını nasıl düşündüğümü anlatıyorum. ### Formun bittiği yerde başlayan iş Bir başvuru formu tasarlarken ilk akla gelen şey alanlar oluyor: hangi bilgiler istenecek, hangileri zorunlu olacak, sıralama nasıl kurulacak. Kadir Yücebaş için geliştirdiğim özel projede de işe buradan başlamak cazipti. Ama projenin asıl ihtiyacı başka bir yerdeydi. Başvurunun alınması, klinik ekibine iletilmesi ve sürecin takip edilmesi birbirine bağlı işler. Form ne kadar iyi tasarlanırsa tasarlansın, gönderildikten sonra ne olduğu belirsizse hem başvuru sahibi hem de klinik ekibi bir boşlukta kalıyor. Bu yüzden sistemi en baştan bir akış olarak düşünmeye çalıştım. Klinik başvurusu ve ön değerlendirme süreci tek bir yapı içinde ele alınıyor. Türkçe ve İngilizce başvuru arayüzleri, klinik yönetim paneli ve bildirim servisleri aynı sistemin farklı yüzleri olarak birlikte çalışıyor. Benim için önemli olan, bu yüzlerin her birinin kendi kullanıcısına anlaşılır gelmesi ve arka planda aynı gerçekliği paylaşması. ### İki dil, tek veri modeli İki dilli bir başvuru arayüzü kurarken en kolay yol her dil için ayrı bir form yazmak gibi görünebilir. Kısa vadede hızlı, ama uzun vadede iki ayrı gerçeklik üretiyor. Bir alan eklendiğinde ya da bir doğrulama kuralı değiştiğinde iki yeri birden güncellemek gerekiyor ve bir noktada ikisi birbirinden sessizce ayrışıyor. Bu projede Türkçe ve İngilizce formları ortak şemalar üzerinden aynı veri modeline bağlamayı tercih ettim. Dil, kullanıcının gördüğü metni değiştiriyor, başvurunun yapısını değiştirmiyor. Böylece yönetim panelinde bir başvuru açıldığında, hangi dilden geldiğinden bağımsız olarak aynı biçimde okunabiliyor. Ortak şemaların bir faydası daha var: arayüzün yaptığı kontrol ile sunucunun kabul ettiği veri aynı tanımdan besleniyor. Bu, iki katman arasında fark edilmeden oluşan uyumsuzlukları azaltıyor. ``` Türkçe başvuru arayüzü ──┐ İngilizce başvuru arayüzü ──┼──> ortak şemalar ──> servis katmanı ──> Drizzle + PostgreSQL Klinik yönetim paneli ──┘ │ └──> e-posta bildirimi ve belge üretimi ``` ### Yönetim tarafı da bir kullanıcı arayüzü Bu tür projelerde yönetim paneli çoğu zaman ikinci planda kalıyor. Başvuru sahibinin gördüğü ekranlar özenle hazırlanıyor, ekibin her gün kullandığı ekranlar ise iç araç sayıldığı için aceleyle geçiştiriliyor. Ben tam tersini düşünüyorum: süreci yürütecek olan kişi yönetim panelini kullanan kişi. Başvuruyu nasıl gördüğü, hangi bilgiye ne kadar hızlı ulaştığı ve bir sonraki adıma nasıl geçtiği, sürecin kalitesini doğrudan belirliyor. Klinik yönetim panelinde başvuruların incelenmesi ve durumlarının takip edilmesi aynı akışın parçaları. Buradaki amacım, ekibe fazladan iş çıkarmayan ama sürecin nerede olduğunu her zaman görünür kılan bir yapı kurmak. Sağlıkla ilgili bir bağlamda çalışırken bir sınırı da bilinçli olarak koruyorum: sistem değerlendirme yapmıyor, değerlendirmeyi klinik ekibi yapıyor. Yazılımın görevi bilgiyi düzenli ve eksiksiz biçimde doğru kişiye ulaştırmak. > Form sürecin başlangıcı, asıl iş gönderildikten sonra başlıyor. ### Durum takibi ve bildirimler Başvuru sahibi açısından en rahatsız edici şey belirsizlik. Bir form gönderip hiçbir geri dönüş alamamak, sürecin işleyip işlemediğini merak ettiriyor. Bu yüzden durum takibini sistemin merkezine yakın bir yerde düşündüm. Başvurunun bir durumu var ve bu durum, hem yönetim tarafında hem de başvuru sahibinin süreci izleme biçiminde anlam taşıyor. Bildirim ve e-posta akışları burada devreye giriyor. Nodemailer üzerinden çalışan e-posta tarafı, sürecin belirli anlarında ilgili taraflara bilgi taşıyor. Belge üretimi bileşeni de başvurudaki bilgilerin düzenli bir çıktıya dönüşmesini sağlıyor. Bu tür akışlarda tercih ettiğim yaklaşım, bildirimleri başvurunun durumuna bağlı düşünmek. Bir e-postanın gönderilmesi, bir yerlerde ayrıca hatırlanması gereken bir kural olduğunda unutulmaya açık hale geliyor. Durumla birlikte ele alındığında hem tutarlılık artıyor hem de akış ileride değiştiğinde nereye dokunulacağı belli oluyor. ### Veri katmanı ve çalışma ortamı Next.js uygulamaları, Drizzle ve PostgreSQL üzerinden aynı veri katmanını paylaşıyor. Drizzle'ı sevmemin sebebi, şema tanımının TypeScript tarafında açıkça okunabilmesi ve sorguların tiplerle birlikte yazılabilmesi. Başvuru gibi alanları zamanla değişebilecek bir yapıda, veritabanı şemasının kodla aynı yerde görünmesi değişiklikleri daha güvenli kılıyor. Docker ise geliştirme ve çalışma ortamlarının birbirine yakın kalmasına yardım ediyor. Bu projede web, servis ve veri katmanlarının hepsinde çalışıyorum. Bunun avantajı, bir değişikliğin formdan veritabanına, oradan yönetim paneline ve bildirime kadar nasıl yayıldığını tek bir zihinde takip edebilmek. Dezavantajı ise bu bütünlüğü korumanın disiplin gerektirmesi. Ortak şemalar ve tek veri modeli, bu disiplinin en somut aracı. Note (Ders): Bir başvuru sistemine bakarken artık ilk olarak forma bakmıyorum. Gönderildikten sonra başvurunun kimin önüne düştüğüne, durumunun nasıl izlendiğine ve başvuru sahibinin süreçten nasıl haberdar olduğuna bakıyorum. Form iyi olabilir, ama arkasındaki süreç tarif edilmemişse sistem yarım kalıyor. --- ## Yazılımdan donanıma: bina yıkama drone'u ve Raspberry Pi ekranları URL: https://devilyxrddev.com/journal/from-software-to-hardware Date: 2025-12 · Ar-Ge ve donanım IOTROCOP'ta iki projeyi baştan sona tek başıma geliştirdim: bina yıkama amaçlı bir drone ve kafe ile marketler için telefondan yönetilen reklam ekranları. İkisi de bana aynı şeyi öğretti: donanımda tek bir bileşen yoktur, sistem vardır. ### Bina yıkama drone'u Bu projede drone'un fiziksel tasarımı ile yazılımı, işlem birimi ve iletişim altyapısı birlikte düşünülmek zorundaydı. Yazılım ve işlem tarafında ROS ve NVIDIA Jetson Nano yer aldı; fiber altyapıyı da kapsayan teknik yapıyı drone'un tasarımıyla birlikte kurdum. Tasarımdan teknik altyapıya kadar bütün sorumluluk bendeydi. ``` tasarım → bina yıkama drone'u yazılım → ROS işlem → NVIDIA Jetson Nano altyapı → fiber ``` Web projelerinde bir katmanı diğerlerinden bağımsız düşünebiliyorsunuz. Burada bu mümkün değildi: mekanik bir karar işlem birimini, işlem birimi yazılımı, yazılım da iletişim ihtiyacını etkiliyordu. Projeyi tek bir bileşen olarak değil, baştan bütün bir sistem olarak ele almam gerekti. Bu projede dikkatimi en çok çeken şey, kararların sırası oldu. Bir web uygulamasında çoğu karar sonradan geri alınabilir; bir kütüphaneyi değiştirmek ya da bir servisi yeniden yazmak zaman alsa da mümkündür. Fiziksel bir sistemde ise bazı kararlar diğerlerinin çerçevesini çiziyor ve geri dönmek çok daha pahalı. Bu yüzden tasarımı ve teknik altyapıyı birlikte ele almak, işin başından itibaren bir tercih değil, bir zorunluluktu. ### ROS, Jetson Nano ve fiber bir arada ROS, robotik bir sistemde farklı yazılım parçalarının birbiriyle konuşabildiği bir çerçeve sunuyor. Jetson Nano, bu yazılımın üzerinde çalıştığı işlem birimi. Fiber altyapı ise sistemin teknik yapısının bir parçası. Bu üçünü ayrı ayrı seçilmiş parçalar gibi değil, aynı sistemin birbirine bağlı katmanları gibi düşünmek gerekiyordu; her birine dair bir karar, diğerlerinin sınırlarını da belirliyordu. Benim için robotik tarafın web geliştirmeye en çok benzeyen yanı, sorumlulukları ayırma ihtiyacı oldu. Yazılımı birbirinden ayrı görevlere bölmek, bir parçayı değiştirirken bütünü bozmamaya yardımcı oluyor. Farkı ise şu: burada her yazılım kararının bir de fiziksel karşılığı var. ### Raspberry Pi reklam ekranları İkinci proje daha mütevazı ama aynı derecede bütünsel: kafe ve marketlerde reklam ve tanıtım için kullanılan, Raspberry Pi 4 üzerinde çalışan dijital ekranlar. Ekranlar bir telefon uygulamasından yönetiliyor. Ekran yazılımını, mobil kontrolü ve cihazın teknik altyapısını birlikte kurdum. > Mekânın dijital vitrini, cebindeki telefondan yönetiliyor. Buradaki asıl iş, mekândaki ekranın davranışı ile telefondaki yönetim akışının aynı ihtiyacı karşılamasıydı. İşletme sahibi için iki ayrı ürün değil, tek bir sistem olmalıydı. Mekândaki bir ekran için web'deki alışkanlıklar tek başına yetmiyor. Bir web sayfası kapanıp yeniden açılabilir; mekândaki ekran ise gün boyu açık kalması ve kendi başına ayakta durması beklenen bir cihaz. Bu tür cihazlarda ekran yazılımını ayrı bir uygulama olarak değil, cihazın kendisinin bir parçası olarak düşünmek bana daha doğru geliyor. Bu tür bir sistemde düşündüğüm sorular web projelerindekinden biraz farklı. Ekran bir mekânda, çoğu zaman başında kimsenin durmadığı bir köşede çalışıyor; yönetim ise elde tutulan bir telefondan yapılıyor. Arada bir cihaz, bir bağlantı ve bir uygulama var, ama kullanıcı bunların hiçbirini ayrı ayrı görmek istemiyor. Telefondan yaptığı değişikliğin ekranda karşılığını görmek istiyor. Cihaz tarafını ve uygulama tarafını bu basit beklentinin etrafında birlikte ele aldım. ### Bireysel çalışmanın iki yüzü İki projede de tasarımdan teknik altyapıya kadar bütün sorumluluk bendeydi. Bunun bir avantajı var: sistemin her parçasını tanıyorsunuz ve bir kararın nereye dokunacağını kestirebiliyorsunuz. Ama aynı zamanda her katmanda karar verecek kişi de sizsiniz; yazılımı yazan, donanımı düşünen ve kullanıcı adına soru soran kişi aynı. Bu durum bana, alışık olduğum alanın dışındaki kararları da sahiplenmeyi öğretti. Tek başına çalışmanın bir riski de kör noktalar. Bir ekipte başka birinin soracağı soruyu kendinize sormanız gerekiyor. Bunun için bir katmanı ele alırken, onu kullanacak bir sonraki katmanın gözünden bakmaya çalışıyorum: yazılım işlem biriminden ne bekliyor, telefon uygulaması ekrandan ne bekliyor? ### Aynı çizginin devamı Bireysel bir IoT projesi olarak geliştirdiğim araç takip sisteminde de benzer bir yaklaşımı izledim: Raspberry Pi üzerinde çalışan, canlı GPS, interaktif harita ve hız, sinyal gücü, batarya ile sensör verilerini tek arayüzde gösteren gömülü bir uygulama. Orada da donanımdan gelen veriyi güncel tutarken arayüzün akıcı kalması gerekiyordu. Bu projelere dönüp baktığımda ortak nokta açık: ekran, drone ya da araç fark etmeksizin, kullanıcının gördüğü davranış birçok katmanın birlikte çalışmasının sonucu. Bu katmanların hepsini görebilmiş olmak, bugün web ve servis tarafında çalışırken de işime yarıyor. Note (Ders): Donanımlı bir projede "benim kısmım" diye bir şey yok. Bir bileşeni değiştirdiğinizde, sistemin geri kalanının da bunu hissedeceğini baştan kabul etmek gerekiyor. --- ## Raspberry Pi üzerinde telemetri arayüzü: veri akarken akıcı kalmak URL: https://devilyxrddev.com/journal/telemetry-on-embedded-ui Date: 2025-11 · Ar-Ge ve donanım Raspberry Pi üzerinde çalışan araç takip ve navigasyon sisteminde canlı GPS, interaktif harita ve anlık telemetriyi tek bir arayüzde topladım. Bu notta gömülü bir cihazda veri toplama ile arayüz performansını birlikte düşünmenin neleri değiştirdiğini anlatıyorum. ### Tek ekran fikri Bu projeyi bireysel olarak geliştirdim: gömülü sistem, veri toplama, arayüz ve teknik altyapının tamamı bendeydi. Çıkış noktası basitti. Araçla ilgili konum ve durum verilerini ayrı kaynaklardan takip etmek yerine, bunları gömülü cihaz üzerinde tek bir anlaşılır arayüzde sunmak istedim. Canlı GPS verisi, interaktif harita, navigasyon ve anlık telemetri aynı ekranda bir araya geliyor. Telemetri tarafında hız, sinyal gücü, batarya durumu ve sensör verileri yer alıyor. Kâğıt üzerinde bunları bir ekrana yerleştirmek zor görünmüyor. Asıl zorluk, hepsi aynı anda güncellenirken arayüzün akıcı kalması ve bunun güçlü bir masaüstü bilgisayarda değil, Raspberry Pi üzerinde gerçekleşmesi. Tek ekran fikrinin bir de kullanıcı tarafı var. Araç içinde kullanılan bir arayüze uzun uzun bakılmıyor, bir bakışta okunabilmesi gerekiyor. Bu yüzden hangi bilginin ekranın neresinde duracağı, hangisinin büyük, hangisinin ikincil görüneceği de performans kadar önemli bir karar. Görsel sadelik ile teknik hafiflik çoğu zaman aynı yöne çekiyor: daha az öğe hem daha kolay okunuyor hem de cihazı daha az yoruyor. ### Gömülü cihaz farklı bir bütçe demek Web arayüzü geliştirirken çoğu zaman güçlü bir makinenin üzerinde çalıştığımı unutuyorum. Gereksiz bir yeniden çizim, fazladan bir animasyon ya da sık tetiklenen bir güncelleme orada fark edilmeyebiliyor. Gömülü bir sistemde aynı alışkanlıklar hemen hissediliyor. İşlemci, bellek ve grafik kapasitesi sınırlı ve bu sınırı hem veri toplama hem de arayüz paylaşıyor. Bu yüzden arayüzü tasarlarken her öğenin bir maliyeti olduğunu kabul ederek başladım. Ekranda görünen her değerin ne sıklıkla güncellenmesi gerektiğini ve hangi değişikliğin kullanıcıya gerçekten bir şey söylediğini düşünmek gerekiyor. Bir sayının saniyede defalarca değişmesi, okunabilirliğe hiçbir katkı sağlamadığı halde kaynak tüketebiliyor. ``` GPS ve sensörler ──> donanım seviyesinde veri toplama ──> güncel durum ├──> harita ve navigasyon görünümü └──> telemetri paneli (hız, sinyal, batarya, sensörler) ``` ### Veri toplama ile arayüzü ayırmak Donanım seviyesinde veri toplama ile kullanıcı arayüzünü bir araya getirirken ikisinin birbirini bekletmemesine özen gösterdim. Benim yaklaşımım, veriyi toplayan tarafın kendi ritminde çalışması ve arayüzün bu veriyi kendi ihtiyacı kadar okuması. Bir sensörden gelen her yeni değer, ekranın yeniden çizilmesi anlamına gelmek zorunda değil. Bu ayrım, sorunları birbirinden yalıtmayı da kolaylaştırıyor. Bir veri kaynağı yavaşladığında ya da geçici olarak kesildiğinde arayüzün donmaması gerekiyor. Örneğin sinyal gücünün düşmesi kullanıcıya gösterilmesi gereken bir bilgi, arayüzü kilitleyen bir durum değil. Telemetri ekranının asıl işi de zaten bu tür değişimleri sakin bir biçimde görünür kılmak. > Her yeni değer ekrana koşmak zorunda değil: arayüz, veriyi ihtiyacı kadar okumalı. ### Harita en ağır bileşen İnteraktif harita, arayüzün en çok kaynak isteyen parçası. Konum değiştikçe haritanın güncellenmesi, navigasyon bilgisinin üzerine işlenmesi ve kullanıcının haritayla etkileşime girebilmesi aynı anda gerçekleşiyor. Bu bileşeni telemetri paneliyle aynı sınırlı kaynakları paylaşacak şekilde dengelemek, bence bu tür bir sistemde optimizasyonun en kritik kısmı. Burada genel bir ilkeye dayanıyorum: kullanıcının dikkatinin olduğu yere öncelik vermek. Araç hareket halindeyken konumun ve navigasyonun güncel kalması önemli. Telemetri değerlerinin okunabilir olması da önemli, ama her birinin milisaniye düzeyinde güncellenmesi gerekmiyor. Öncelikleri böyle ayırmak, sınırlı kaynağı daha bilinçli kullanmamı sağlıyor. ### Bütün katmanların aynı masada olması Bütün katmanların bende olması, performans sorunlarına tek bir yerden bakabilmemi sağladı. Arayüzde hissedilen bir takılmanın kaynağı veri toplamada mı, sistem altyapısında mı, yoksa arayüzün kendisinde mi, bunu ayırt edebilmek için her katmanı tanımak gerekiyor. Bir ekipte bu sorular katmanlar ve kişiler arasında gidip gelebilir. Tek başına çalışırken hepsi aynı masada duruyor. Bu proje, arayüz performansını sonradan iyileştirilecek bir ayrıntı olarak değil, veri akışının tasarımıyla birlikte düşünülmesi gereken bir konu olarak görmemi sağladı. Donanımdan gelen bilgiyi güncel tutmak ile ekranın akıcı kalması, aynı sorunun iki farklı yüzü. Note (Ders): Gömülü bir sistemde veri toplama ile arayüz performansını ayrı ayrı düşünemiyorum. Hangi verinin ne sıklıkla ekrana yansıyacağına baştan karar vermek, akıcılığı sonradan yakalamaya çalışmaktan çok daha kolay. --- ## Sahadan gelen veri: akıllı tarımda edge işleme ve bağlantı koşulları URL: https://devilyxrddev.com/journal/edge-data-smart-agriculture Date: 2025-10 · Ar-Ge ve donanım GAIA / AgroBlock, tarım alanındaki sensörleri, kamera düğümlerini ve edge cihazlarını tek bir sistem yaklaşımında ele alan bir Ar-Ge çalışması. Bu notta fiziksel ortamın ve bağlantı koşullarının yazılım mimarisini nasıl şekillendirdiğine dair düşüncelerimi topluyorum. ### Masa başındaki varsayımlar sahada geçerli değil Web uygulaması geliştirirken bazı şeyleri neredeyse hiç sorgulamadan kabul ediyorum: sunucu erişilebilir, bağlantı var, gönderilen veri karşı tarafa hemen ulaşıyor. GAIA / AgroBlock üzerinde çalışırken bu varsayımların hiçbirine güvenemeyeceğimi kabul etmem gerekti. Tarım alanı, yazılımın genellikle düşünüldüğü ortamdan çok farklı bir yer. Bu Ar-Ge çalışması; IoT sensörlerini ve kamera düğümlerini edge işleme, offline haberleşme, yapay zekâ ve blockchain tabanlı veri bütünlüğü bileşenleriyle bir araya getirmeyi hedefliyor. Çalışma devam ediyor. Burada yazdıklarım bitmiş sonuçlar değil, kapsamı ele alış biçimime dair notlar. ### Fiziksel ortam mimarinin bir parçası Sahadan gelen verinin farklı cihazlardan toplanması, bağlantı koşullarının değişmesi ve bu verinin izlenebilir biçimde işlenmesi birbirinden ayrı düşünülebilecek ihtiyaçlar değil. Bir sensörün nerede durduğu, bir kamera düğümünün hangi koşullarda çalıştığı ve bir cihazın merkezle ne sıklıkla konuşabildiği doğrudan yazılım kararlarını etkiliyor. Bu yüzden mimariyi düşünürken fiziksel katmanı yalnızca bir girdi kaynağı olarak soyutlamak yerine tasarımın bir parçası olarak ele almaya çalışıyorum. Cihazdan alınan verinin sonraki sistem adımlarına nasıl taşındığı, bu çalışmada en çok odaklandığım soru. Bu yaklaşımın pratik bir sonucu da var: yazılım tarafında verilen her karar, bir cihazın sahada nasıl davranacağına dair bir varsayım içeriyor. Bu varsayımları açıkça yazmak, örneğin bir cihazın ne kadar süre bağlantısız kalabileceğini ya da hangi verinin öncelikli olduğunu baştan tartışmak, sonradan çıkacak sürprizleri azaltıyor. Ar-Ge sürecinde bu tür soruları erken sormayı, cevaplarını bulmaktan bile daha değerli buluyorum. ``` IoT sensörleri ──┐ Kamera düğümleri ──┴──> edge işleme ──> offline haberleşme ──> yapay zekâ ve blockchain tabanlı veri bütünlüğü ``` ### Neden edge işleme Bağlantının sürekli olmadığı bir ortamda her şeyi merkeze göndermeye dayanan bir tasarım kırılgan kalıyor. Edge işleme fikri burada mantıklı bir yer buluyor: verinin bir kısmı üretildiği yere yakın işlenirse, merkezle iletişim kesildiğinde sistem tamamen kör kalmıyor. Kamera düğümleri gibi büyük hacimli veri üreten kaynaklarda bu daha da önemli hale geliyor, çünkü ham veriyi her zaman taşımak ne mümkün ne de anlamlı olabiliyor. Burada kendime sorduğum soru, hangi işin cihazda, hangisinin merkezde yapılması gerektiği. Bu sorunun tek bir doğru cevabı olduğunu düşünmüyorum. Cihazın kapasitesi, bağlantının durumu ve verinin ne için kullanılacağı birlikte tartılması gereken etkenler. Bu dengeyi kurmak, Ar-Ge sürecinin en çok düşünme gerektiren kısımlarından biri. ### Offline haberleşme bir istisna değil Offline haberleşmeyi bir hata durumu olarak değil, sistemin olağan çalışma koşullarından biri olarak düşünmeye çalışıyorum. Bağlantı yokken verinin nasıl tutulacağı, bağlantı geri geldiğinde nasıl aktarılacağı ve bu süreçte verinin bütünlüğünün nasıl korunacağı sonradan eklenecek özellikler değil. Bence baştan tasarımın içinde olmaları gerekiyor. > Sahada bağlantının kopması bir istisna değil, tasarımın baştan hesaba katması gereken bir durum. Bu bakış açısı, web dünyasından gelen biri olarak alışkanlıklarımı değiştiren bir şey oldu. Bir istek başarısız olduğunda kullanıcıya hata göstermek yetmiyor, çünkü sahada çoğu zaman ekranın başında bekleyen bir kullanıcı yok. Sistemin, kimse müdahale etmeden kendi kendine toparlanabilmesi gerekiyor. ### Veri bütünlüğü ve izlenebilirlik Çalışmanın blockchain tabanlı veri bütünlüğü tarafı, verinin izlenebilir olmasıyla ilgili. Farklı cihazlardan, farklı zamanlarda ve değişen bağlantı koşullarında gelen bir verinin sonradan güvenilir biçimde takip edilebilmesi gerekiyor. Blockchain bu kapsamda araştırdığım yaklaşımlardan biri. Bir teknolojiyi ilgi çekici olduğu için değil, çözdüğü sorun açıkça tanımlandığı için kullanmak gerektiğini düşünüyorum ve bu bileşeni de o çerçevede değerlendiriyorum. Yapay zekâ bileşeni için de benzer bir tutum içindeyim. Sahadan gelen verinin anlamlandırılması bu kapsamın bir parçası, ama hangi işin nerede yapılacağı edge ile merkez arasındaki genel dengeye bağlı. Bana göre bu Ar-Ge çalışmasının değeri, bileşenleri tek tek değil, aynı sistem içinde birlikte düşünmekten geliyor. Note (Not): GAIA / AgroBlock devam eden bir Ar-Ge çalışması ve buradaki notlar sonuç değil, yaklaşım. Şimdiye kadar edindiğim en net fikir şu: fiziksel ortamı, bağlantı koşullarını ve yazılım mimarisini ayrı ayrı tasarlamak mümkün değil. Sahadaki veriden sistemin bütününe giden yol, bu üçünü birlikte ele almaktan geçiyor. --- ## Analizden altyapıya: freelance işlerde bütün süreci üstlenmek URL: https://devilyxrddev.com/journal/owning-the-whole-process Date: 2025-09 · Ürün ve analiz Freelance olarak geliştirdiğim e-ticaret, kongre ve kurumsal web sitelerinde analiz, tasarım, yazılım ve teknik altyapının tamamı bendeydi. Bu notta tek kişinin bütün süreci taşımasının neyi kolaylaştırdığını ve neyi zorlaştırdığını anlatıyorum. ### Tek sorumlu, birden fazla şapka Freelance işlerimde genellikle tek bir rolüm olmadı. Firmalar ve ajanslar için kurumsal web siteleri, kongreler için etkinlik siteleri ve satış odaklı e-ticaret siteleri geliştirdim. Her birinde ihtiyacın anlaşılması, arayüzün tasarlanması, uygulamanın geliştirilmesi ve teknik altyapının kurulması aynı sorumluluğun parçalarıydı. Bunların hepsini bireysel olarak, baştan sona yürüttüm. Bu durumu başlarda yalnızca bir iş yükü olarak görüyordum. Zamanla bunun bir bakış açısı da kazandırdığını fark ettim. Bir ekipte her aşama farklı kişilerde olduğunda, kararlar aşamalar arasında el değiştirirken bir kısmı yolda kayboluyor. Tek kişi bütün süreci taşıdığında bu kayıp azalıyor, ama yerine başka riskler geliyor. Bu notta ikisini de dürüstçe ele almak istiyorum. ### Analiz aşamasını küçümsememek Bütün süreci üstlenen biri için en kolay hata, analiz aşamasını hızlıca geçip doğrudan tasarıma ya da koda atlamak. Kafamda bir çözüm şekillendiği anda onu üretmeye başlamak cazip geliyor. Oysa freelance işlerde müşterinin ihtiyacı çoğu zaman ilk konuşmada tam olarak netleşmiyor. Bu yüzden her işte önce kapsamı, kullanıcı akışını ve içerik yapısını belirlemeye çalıştım. Bir e-ticaret sitesinde kullanıcı deneyimi ile arka plandaki işleyişin birlikte tasarlanması gerekiyor. Bir kongre sitesinde bilgilerin anlaşılır bir yapıda sunulması öne çıkıyor. Kurumsal sitelerde ise firmanın ya da ajansın ne yaptığını ve nasıl bir değer sunduğunu açıkça anlatmak esas. Her tür farklı bir soruyla başlıyor ve analiz, bence o soruyu doğru sormaktan ibaret. ### Tasarım ve geliştirme aynı masada Tasarımı ve geliştirmeyi aynı kişinin yapmasının en somut faydası, tasarım kararlarının uygulanabilirliğini daha çizerken düşünebilmek. Bir arayüzü tasarlarken onun nasıl kodlanacağını, hangi içeriğin nereden geleceğini ve sayfanın farklı ekranlarda nasıl davranacağını aynı anda tartabiliyorum. Bu, tasarım ile uygulama arasındaki "bu böyle yapılamaz" türünden geri dönüşleri azaltıyor. Kongre sitelerinde bunu özellikle hissettim. Etkinlik sitelerinde içeriğin düzeni ile gezinme deneyimi birbirini doğrudan etkiliyor. Tasarımı ve yazılımı aynı bilgi yapısı üzerinden yürütmek, sayfaların hem okunabilir hem de bakımı kolay kalmasına yardım etti. Kurumsal sitelerde de içerik mimarisini, görsel arayüzü ve uygulamayı birlikte geliştirmek, kurumun kimliğinin web üzerindeki karşılığını daha tutarlı kılıyor. > Tek kişi bütün süreci taşıdığında kararlar el değiştirirken kaybolmuyor, ama onları sorgulayacak ikinci bir göz de olmuyor. ### Görünmeyen kısım: teknik altyapı Müşterinin gördüğü şey çoğunlukla arayüz. Ama sitenin çalışmasını sağlayan teknik altyapı da işin bir parçası ve sorumluluğu yine bende. Bu kısım görünmediği için ihmal edilmeye en açık olan kısım. Bir sitenin teslim edildiği gün düzgün çalışması yetmiyor, sonrasında da anlaşılır ve sürdürülebilir kalması gerekiyor. Bu konuda benimsediğim yaklaşım basit: altyapıyı ilk günden işin bir parçası olarak planlamak, sona bırakmamak. Arayüz, uygulama mantığı ve altyapı birlikte ele alındığında sonradan yapılan yamalar azalıyor. Özellikle e-ticaret gibi işleyişin doğrudan kullanıcıya yansıdığı projelerde, arka plandaki yapının sağlam olması ön yüzdeki deneyim kadar önemli. Teslimden sonra siteyi açan bir başkasının, ister müşterinin ekibi ister ileride işi devralacak bir geliştirici olsun, yapıyı anlayabilmesi de bu sürdürülebilirliğin bir parçası. Bu yüzden düzenli bir yapıyı ve anlaşılır kararları, teknik borç birikmeden kurmaya özen gösteriyorum. ### Tek başına çalışmanın sınırları Bütün süreci üstlenmenin açıkça söylenmesi gereken bir zayıf yanı var: kendi kararlarını sorgulayacak ikinci bir kişi yok. Bir ekipte tasarımcı geliştiricinin varsayımlarını, geliştirici de tasarımcının varsayımlarını yakalayabiliyor. Tek başına çalışırken bu kontrolü bilinçli olarak kendim kurmam gerekiyor. Bunun için birkaç alışkanlık edinmeye çalıştım. Bir aşamayı bitirdiğimde ona biraz zaman geçtikten sonra, farklı bir rolün gözüyle yeniden bakıyorum. Tasarımı bitirdiğimde bir geliştirici gibi, kodu bitirdiğimde bir kullanıcı gibi düşünmeye çalışıyorum. Müşteriyle yapılan konuşmaları da yalnızca onay almak için değil, varsayımlarımı sınamak için kullanmayı önemsiyorum. Kusursuz bir yöntem değil, ama tek kişilik bir süreçte en azından kör noktaları azaltıyor. Note (Ders): Bütün süreci üstlenmek bana en çok şunu öğretti: aşamalar tek tek iyi olsa bile aralarındaki bağlantılar zayıfsa ortaya çıkan ürün dağınık kalıyor. Analizi, tasarımı, geliştirmeyi ve altyapıyı aynı ürün ihtiyacı etrafında toplamak, her birini ayrı ayrı mükemmelleştirmeye çalışmaktan daha değerli. ## Motice URL: https://devilyxrddev.com/work/motice Category: Web / Platform · Mobil · 2026 · Ekip projesi · geliştirme · stack: TypeScript, React, Vite, Fastify, tRPC, PostgreSQL, Drizzle ORM, Expo Motice; pilates, yoga, kişisel antrenman ve wellness stüdyoları için geliştirilen bir CRM/ERP ürünü. Yönetim arayüzü, müşteri portalı ve mobil deneyim aynı işletme süreçlerinin farklı yüzleri olarak ele alınıyor. Randevu, müşteri, ders ve işletme işlemlerinin farklı araçlara dağılması günlük işi zorlaştırıyor. Proje, bu akışları işletmelerin ayrı çalışma alanları içinde yönetebileceği ortak bir üründe topluyor. > Bir işletmenin günlük ritmini anlayan yazılım. Contribution: Unitic ekibinde yazılım geliştirmeye katkı Randevu tabanlı hizmet işletmelerinin müşteri ve günlük operasyon ihtiyaçları. Motice’in geliştirilmesine Unitic ekibi içinde katkı sağlıyorum. Web, servis, veri ve mobil katmanlarının ortak bir ürün modeli etrafında çalışması. TypeScript tabanlı monorepo; React arayüzlerini, Fastify ve tRPC servis katmanını, PostgreSQL ve Drizzle veri katmanını bir araya getiriyor. Ortak bileşenler ile web ve mobil deneyimler arasında tutarlılık hedefleniyor. Bu projede ilgimi çeken konu, tek bir ekranı tamamlamanın ötesinde takvim, müşteri ve operasyon akışlarının aynı ürün içinde birbirini doğru tamamlaması. Yönetim uygulaması, müşteri portalı ve Expo tabanlı müşteri uygulaması içeren bir ürün üzerinde ekip olarak çalışıyoruz. Katkım, bu ortak ürünün geliştirme çalışmalarının bir parçası. --- ## Mentorual URL: https://devilyxrddev.com/work/mentorual Category: Ürün / Analiz · 2026 · Analiz ve tasarım hazırlığı · stack: Product analysis, Requirements, User flows, Wireframes, RBAC, Web / Mobile Mentorual; mentörlük, uygunluk, randevu ve kurum yönetimi akışlarını aynı ürün içinde ele alıyor. Web, mobil ve yönetim deneyimleri için ürün kapsamı ve davranışları birlikte hazırlanıyor. Birden fazla rol ve kurumun bulunduğu bir üründe aynı işlemin farklı ekranlarda tutarlı davranması gerekiyor. Geliştirmeye başlamadan önce kapsam, akışlar, roller ve kabul koşullarını netleştirmek bu çalışmanın merkezinde. > Ürünü netleştirmek de üretimin bir parçası. Contribution: Ürün analizi, gereksinimler ve tasarım hazırlığı Mentör, üye, kurum ve yönetici rollerinin web ve mobil akışlarını birlikte ele almak. Gereksinimleri, kullanıcı akışlarını, ürün kararlarını ve rol-yetki analizini hazırlamak. MVP kapsamı, ekran gereksinimleri ve kabul koşulları arasında izlenebilirlik kurmak. Gereksinimler, kullanım senaryoları, ekran aileleri ve rol-yetki matrisi birbirine bağlanıyor. MVP ve sonraki sürüm kapsamları ayrılarak ürün kararları kayda alınıyor. Bu projede kod öncesindeki kararların değerine odaklanıyorum: kimin hangi işlemi yapabildiği, bir akışın hangi durumda sona erdiği ve ilk sürümün tam olarak neyi kapsadığı. MVP ve sonraki sürüm kapsamları ayrıldı; gereksinimler, kullanım senaryoları, ekran aileleri ve rol-yetki analizi üzerinden bir hazırlık paketi oluşturuldu. Çalışma, ürün analizi ve tasarım hazırlığı aşamasında ilerliyor. --- ## YEE CMS URL: https://devilyxrddev.com/work/yee-cms Category: Web / Platform · 2026 · Şirket projesi · stack: Next.js, React, NestJS, TypeScript, PostgreSQL, Drizzle ORM, Docker YEE CMS, Yunus Emre Enstitüsü’nün farklı merkezlerinin içeriklerini ortak bir altyapı üzerinde yönetmek için geliştirilen çok siteli platform. Kamuya açık web arayüzü, yönetim uygulaması ve servis katmanı bir arada çalışıyor. Farklı merkezlerin içerik üretimini, yetkilerini ve yayın akışını ortak bir altyapıda yönetirken her sitenin kendi bağlamını koruması gerekiyor. > Ortak altyapı, farklı yayın alanları. Contribution: Unitic’te full-stack geliştirme ve entegrasyon Farklı merkezlerin içeriklerini, dillerini ve yayın süreçlerini ortak bir altyapıda toplamak. Arayüz, servis, içerik yönetimi ve entegrasyon çalışmalarına katkı sağlamak. İçerik modeli, site bağlamı ve yönetim akışlarının birlikte çalışması. Next.js tabanlı web arayüzü, NestJS API ve React yönetim uygulaması PostgreSQL üzerinde buluşuyor. Çok dillilik, site bağlamı ve içerik onay akışları ürünün temel parçaları. Bu çalışmada içerik modelinin yalnız veri tabanını değil, editörün günlük işini ve ziyaretçinin gördüğü sayfayı da şekillendirmesine odaklanıyorum. Web, yönetim paneli ve API’den oluşan çok siteli CMS altyapısında geliştirme ve entegrasyon çalışmaları yürüttüm. İçerik yönetimi akışları ve uygulama katmanları bu katkının odağında. --- ## NoktaTR URL: https://devilyxrddev.com/work/noktatr Category: Web / Platform · 2026 · Şirket projesi · stack: Next.js, React, NestJS, TypeScript, Prisma, PostgreSQL NoktaTR, .tr dergisinin içeriğini iki dilde dijital ortama taşıyan bir yayın ürünü. Okurun gördüğü web deneyimi ile içerik hazırlama ve yönetme akışları aynı sistemin parçaları. Dergi içeriğinin okunabilir bir web deneyimine dönüşmesi kadar, yayın ekibinin bu içeriği sürdürülebilir biçimde yönetebilmesi de gerekiyor. > İçerikten okuma deneyimine. Contribution: Full-stack geliştirme, içerik akışları ve teslim süreçleri .tr dergisinin kültür, sanat ve edebiyat içeriğini iki dilde sunmak. Uygulama katmanları, içerik yönetimi ve teslim süreçlerinde geliştirme sorumluluğu almak. Vitrin, yönetim paneli ve API’nin ortak yayın modeliyle çalışması. Next.js web ve yönetim arayüzleri, NestJS servisleri ve Prisma veri erişimiyle çalışıyor. Türkçe ve İngilizce içerikler ürünün yayın yapısında birlikte ele alınıyor. Dijital yayıncılıkta içerik yapısı, editoryal akış ve arayüz hiyerarşisi birbirinden bağımsız kararlar değil. Geliştirme sırasında bu üç alanı birlikte düşünüyorum. Kurumsal yayın ürününün uygulama katmanlarında, içerik yönetimi akışlarında ve geliştirme-teslim süreçlerinde görev aldım. --- ## Türk Sanatı Platformu URL: https://devilyxrddev.com/work/turk-sanati-platformu Category: Web / Platform · 2026 · Şirket projesi · stack: Next.js, React, NestJS, Prisma, PostgreSQL, Redis, Tailwind CSS Türk Sanatı Platformu; ana merkez ve alt platformlar üzerinden sanatçı profilleri, eserler, etkinlikler, başvurular ve editoryal içerik sunuyor. Kamuya açık web, kullanıcı alanı, yönetim paneli ve API birlikte ele alınıyor. Farklı ülke veya yapılar için ayrılan platformlarda içerik ve kullanıcı akışlarını ortak bir ürünün içinde yönetmek; yerel bağlamı ve yönetim sorumluluklarını korumak gerekiyor. > Sanatın farklı alanlarını ortak bir yapıda buluşturmak. Contribution: Yazılım geliştirme, entegrasyon ve teslim süreçleri Sanatçı, eser, etkinlik ve başvuru süreçlerini ana merkez ve alt platformlarla ele almak. Kurumsal uygulamanın geliştirme, entegrasyon ve teslim süreçlerinde görev almak. Kullanıcı deneyimi, yönetim akışları ve çok kiracılı servis yapısını birlikte çalıştırmak. Next.js ve React arayüzleri, NestJS API, Prisma ve PostgreSQL ile geliştiriliyor. Çok kiracılı yapı, içerik yönetimi ve başvuru süreçleri aynı mimarinin parçaları. Bu projede platformun farklı kullanıcılarını birlikte düşünmek önemli: içerik ziyaretçisi, sanatçı, başvuru sahibi ve yönetici aynı veriye farklı ihtiyaçlarla yaklaşıyor. Kurumsal web uygulamasının geliştirme, entegrasyon ve teslim süreçlerinde sorumluluk aldım. Çalışma; web, yönetim ve servis katmanlarını kapsayan bir platform yapısı içinde yürütüldü. --- ## OYS & TYS URL: https://devilyxrddev.com/work/oys-tys Category: Web / Platform · 2026 · Aktif geliştirme · stack: TypeScript, Next.js, NestJS, PostgreSQL, Prisma, Redis, Turborepo OYS & TYS; öğrenci, kurs, sınav ve yönetim süreçlerine odaklanıyor. Ortak kimlik altyapısı ile farklı iş alanlarının verilerini ve uygulama sınırlarını birlikte ele alan bir monorepo içinde geliştiriliyor. Öğrenci, öğretici, aday ve yönetici ihtiyaçları birbirine bağlı olsa da her alanın veri ve iş kuralları farklı. Ortak özellikleri paylaşırken bu sorumlulukların açık kalması gerekiyor. > Ortak kimlik, açık sorumluluklar. Contribution: Aktif frontend ve backend geliştirme Öğrenci, kurs ve sınav süreçleri için farklı kullanıcı ve yönetim ihtiyaçlarını ele almak. Frontend ve backend geliştirme çalışmalarında aktif rol almak. Ortak kimlik ve paylaşılan paketlerle birlikte alanların veri ve servis sınırlarını korumak. Next.js arayüzleri ve NestJS servisleri; PostgreSQL, Prisma ve Redis ile çalışıyor. Turborepo yapısı, ortak paketler ile farklı uygulama alanlarının aynı depoda yönetilmesini sağlıyor. Bu çalışmada kullanıcı akışlarını veri ve servis sınırlarıyla birlikte düşünmeye odaklanıyorum. Ortak altyapı kurarken her alanın kendi kurallarını koruması önemli. Web tabanlı yönetim sistemlerinin frontend ve backend çalışmalarında aktif rol alıyorum. Öğrenci, kurs ve sınav yönetimi kapsamındaki geliştirmeler devam ediyor. --- ## BGB URL: https://devilyxrddev.com/work/bgb Category: Web / Platform · Mobil · 2026 · Şirket projesi · stack: React Native, Expo, TypeScript, Next.js, NestJS, PostgreSQL, Prisma BGB, kumar zararlarını azaltmaya yönelik bir dijital ürün. Mobil kullanıcı deneyimi, servis katmanı, yönetim paneli ve web arayüzü aynı ürünün birbirini tamamlayan parçaları olarak geliştiriliyor. Böyle bir üründe mobil akışların, verinin ve yönetim araçlarının birlikte işlemesi gerekiyor. Çalışmanın kapsamı tek bir uygulama ekranının ötesinde, ürünün bütün yazılım katmanlarını içeriyor. > Bir ürünün tüm katmanlarını birlikte kurmak. Contribution: Uçtan uca full-stack geliştirme · web, mobil ve backend Kumar zararlarını azaltmaya yönelik ürünün mobil, web ve yönetim ihtiyaçlarını ele almak. Analiz, tasarım ve tüm yazılım katmanlarının geliştirilmesinde sorumluluk almak. Mobil uygulama, servisler ve yönetim araçlarını ortak ürün akışlarıyla birleştirmek. React Native ve Expo mobil deneyimi; Next.js web ve yönetim yüzeyleriyle, NestJS ve PostgreSQL tabanlı servislerle buluşuyor. Arayüz ve backend akışları ürünün ortak ihtiyaçları üzerinden ele alınıyor. Uçtan uca sorumlulukta bir katmandaki kararın diğerlerini nasıl etkilediğini görmek önemli. Mobil deneyim, servis sözleşmeleri ve yönetim ihtiyaçlarını birlikte ele alıyorum. Ürünün analiz, tasarım ve geliştirme süreçlerini yürüttüm. Mobil uygulama, backend algoritmaları, yönetim paneli ve web arayüzü olmak üzere tüm yazılım katmanlarında sorumluluk aldım. --- ## Devilyxrd Blockchain URL: https://devilyxrddev.com/work/devilyxrd-blockchain Category: Blockchain · 2026 · Kişisel Ar-Ge · stack: Go, TypeScript, NestJS, PostgreSQL, gRPC, GraphQL, Docker, Raspberry Pi Devilyxrd Blockchain System; Go ile geliştirilen blockchain çekirdeğini TypeScript ve NestJS servisleriyle bir araya getiren kişisel Ar-Ge projem. Blok üretimi, validator koordinasyonu, cüzdan ve sözleşme servislerini birlikte ele alıyorum. Dağıtık bir sistemde birden fazla düğümün aynı zincirde kalmasını sağlamak ve bunu yalnız yerel bir süreçte değil, gerçek cihazlar üzerinde gözlemlemek istedim. > Aynı durumu paylaşan birden fazla makine. Contribution: Kişisel Ar-Ge · sistem mimarisi ve geliştirme Blockchain çekirdeği ve servislerin sorumluluklarını ayrı katmanlarda ele almak. Go ve TypeScript ile mimari, servis geliştirme ve dağıtık sistem çalışmalarını yürütmek. Üç Raspberry Pi üzerinde uzun süreli koşularda zincir tutarlılığını ve üretimin sürekliliğini ölçmek. İzinli PoA yapısı ve imzalı validator oylarıyla kesinleşme; Go servisleri, NestJS katmanı, gRPC ve PostgreSQL ile birlikte çalışıyor. Raspberry Pi düğümleri gerçek test ortamını oluşturuyor. Uzun süreli çalışmayı doğrulamak, yalnız bir düğümün ayakta görünmesine bakmak değil. Düğümler arası tutarlılık, blok üretiminin sürmesi ve koşunun sınırlarının kaydedilmesi birlikte değerlendirilmeli. 13–16 Temmuz 2026 tarihli 72 saatlik testte üç Raspberry Pi 4B üzerinde 86.553 blok raporlandı. 863 düğümler arası kontrolde zincir ayrışması veya kesinleşme durması gözlenmedi. Bu, belirtilen koşuya ait bir bulgudur; sistemin tüm hata senaryolarını kapsamaz. --- ## GAIA / AgroBlock URL: https://devilyxrddev.com/work/gaia-agroblock Category: IoT / Donanım · 2024 · Ar-Ge projesi · stack: IoT, Edge computing, Sensor communication, Camera nodes, Offline communication, AI, Blockchain GAIA / AgroBlock Smart Agriculture, tarım alanındaki fiziksel cihazlarla veri katmanını birlikte ele alan bir Ar-Ge projesi. Sensörler, kamera düğümleri ve edge cihazları; haberleşme ve veri bütünlüğü yaklaşımıyla aynı sistem içinde düşünülüyor. Sahadan gelen verinin farklı cihazlardan toplanması, bağlantı koşullarının değişmesi ve bu verinin izlenebilir biçimde işlenmesi bir arada ele alınması gereken ihtiyaçlar. > Sahadaki veriden sistemin bütününe. Contribution: IoT ve blockchain tabanlı Ar-Ge çalışması Tarım ortamında sensör, kamera ve edge cihazlarının veri akışlarını birlikte ele almak. IoT, haberleşme ve blockchain tabanlı veri bütünlüğü üzerine Ar-Ge yapmak. Offline koşullar, edge işleme ve veri izlenebilirliğini aynı sistem yaklaşımında değerlendirmek. Çalışma; IoT sensörleri ve kamera düğümlerini edge işleme, offline haberleşme, yapay zekâ ve blockchain tabanlı veri bütünlüğü bileşenleriyle bir araya getiriyor. Bu projede fiziksel ortam, bağlantı koşulları ve yazılım mimarisi birlikte düşünülüyor. Cihazdan alınan verinin sonraki sistem adımlarına nasıl taşındığına odaklanıyorum. Akıllı tarım senaryosunda sensör, kamera, edge işleme, haberleşme ve veri bütünlüğü bileşenleri aynı Ar-Ge kapsamı içinde ele alınıyor. Çalışma, farklı fiziksel ve dijital katmanların birlikte tasarlanmasına odaklanıyor. --- ## devilyxrd / Portfolio URL: https://devilyxrddev.com/work/devilyxrd-portfolio Category: Web / Platform · Yaratıcı kodlama · 2026 · Sürekli geliştirilen kişisel site · stack: Vue 3, TypeScript, Vite, Vue Router, CSS, GSAP, Anime.js, SVG Bu site, yaptığım işleri ve onları geliştirirken ilgilendiğim soruları bir arada tutan kişisel alanım. Projeler, Hakkımda, yazılar ve canlı deneyler aynı görsel dil içinde birbirine bağlanıyor. Web, mobil, servis ve Ar-Ge çalışmalarımı tek bir listeye sıkıştırmadan anlatmak; ziyaretçinin hem işlerin bağlamını hem de yaklaşımımı keşfedebilmesini sağlamak istedim. > Kod, hareket ve kişisel bir arşiv. Contribution: Kişisel portfolyo · içerik ve geliştirme Çalışmalarımı, kişisel yaklaşımımı ve deneylerimi birlikte sunan bir arşiv kurmak. İçeriği ve uygulamayı küçük, incelenebilir adımlarla geliştirmek. İki dilde sayfalar, arama, statik üretim ve etkileşimli deneyleri ortak bir yapı altında çalıştırmak. Vue 3 ve TypeScript ile kurulan yapı; adres üzerinden dil seçimi, statik sayfa üretimi ve sayfaya özel paylaşım görselleri kullanıyor. CSS, GSAP ve Anime.js farklı hareket sorumluluklarını üstleniyor. Deneysel bir görsel dil kurarken içerik, okunabilirlik ve gezinme akışının da anlaşılır kalmasına önem veriyorum. Bu siteyi küçük adımlarla geliştirip her adımı tarayıcıda kontrol ediyorum. Türkçe ve İngilizce sayfalar, proje arşivi, arama paleti ve sekiz etkileşimli tarayıcı deneyi bu sitede bir araya geliyor. İçerik ve uygulama üzerinde geliştirme devam ediyor. --- ## Kadir Yücebaş URL: https://devilyxrddev.com/work/kadir-yucebas Category: Web / Platform · 2023 · Özel proje · stack: Next.js, TypeScript, PostgreSQL, Drizzle ORM, Docker, Nodemailer Kadir Yücebaş için geliştirilen sistem, klinik başvuru ve ön değerlendirme sürecini tek bir yapıda ele alıyor. Türkçe ve İngilizce başvuru arayüzleri, klinik yönetim paneli ve bildirim servisleri birlikte çalışıyor. Başvurunun alınması, klinik ekibine iletilmesi ve sürecin takip edilmesi birbiriyle bağlantılı işler. Amaç, bu adımları başvuru sahibi ve klinik ekibi için anlaşılır bir dijital akışta toplamak. > Başvurudan takibe, tek bir akış. Contribution: Özel proje · full-stack geliştirme Klinik başvuru, ön değerlendirme ve takip ihtiyaçlarını birlikte ele almak. Web arayüzleri, yönetim akışları ve servis katmanlarında geliştirme yapmak. Türkçe ve İngilizce formları, veri modelini ve bildirim süreçlerini bir araya getirmek. Next.js uygulamaları; ortak şemalar, Drizzle ve PostgreSQL veri katmanı, e-posta ve belge üretimi bileşenleriyle bir araya geliyor. İki dilde formlar ve yönetim ekranları aynı sistemin farklı yüzlerini oluşturuyor. Bu projede formun doldurulması kadar, başvurunun yönetim tarafında nasıl ele alındığı ve kullanıcının süreci nasıl takip ettiği üzerinde duruyorum. Başvuru formları, yönetim paneli, durum takibi ve bildirim akışları aynı uygulama yapısında bir araya getirildi. Klinik başvuru sürecinin web, servis ve veri katmanları üzerinde çalışıyorum. --- ## E-Ticaret Çözümleri URL: https://devilyxrddev.com/work/freelance-e-ticaret Category: Web / Platform · 2021 · Bireysel web geliştirme Freelance e-ticaret işlerimde projenin tamamını üstlendim. İhtiyacın anlaşılması, arayüzlerin tasarlanması, uygulamanın geliştirilmesi ve teknik altyapının kurulması aynı sorumluluğun parçalarıydı. Satış odaklı bir web sitesinde kullanıcı deneyimi ile arka plandaki işleyişin birlikte tasarlanması gerekiyor. İşin kapsamını müşterinin ihtiyacına göre ele alıp uygulama ve altyapıyı birlikte geliştirdim. > Fikirden çalışan web ürününe, uçtan uca. Contribution: Bireysel · analiz, tasarım, full-stack geliştirme ve teknik altyapının tamamı E-ticaret web projesinin ihtiyacını, kapsamını ve kullanıcı akışını belirlemek. Analiz, tasarım, yazılım geliştirme ve teknik altyapının tümünü bireysel yürütmek. Arayüzü ve uygulamanın işleyişini aynı ürün ihtiyacı etrafında tamamlamak. Arayüz, uygulama mantığı ve teknik altyapıyı birlikte ele aldım. Bu gruptaki her freelance işte, gereksinimlerden son uygulamaya uzanan bütün süreci bireysel yürüttüm. Tüm aşamalardan sorumlu olmak, tasarım ve geliştirme kararlarını aynı ürün ihtiyacı etrafında toplamamı sağladı. Kullanıcının gördüğü yüz ile sistemin işleyişini birlikte düşündüm. Freelance olarak e-ticaret web siteleri geliştirdim. Bu kayıt, farklı müşteri işleri için yürüttüğüm e-ticaret çalışmalarını bir araya getiriyor; her birinde tüm süreci bireysel üstlendim. --- ## Kongre Web Siteleri URL: https://devilyxrddev.com/work/freelance-kongre Category: Web / Platform · 2020 · Bireysel web geliştirme Freelance kongre sitesi çalışmalarında, etkinliğin dijital yüzünü baştan sona geliştirdim. İhtiyacın belirlenmesi, içeriğin arayüze taşınması ve çalışan web uygulamasının kurulması benim sorumluluğumdaydı. Bir kongrenin web sitesinde bilgilerin anlaşılır bir yapı içinde sunulması ve sitenin etkinliğin ihtiyacına uygun çalışması gerekiyor. Kapsamı bu ihtiyaçlara göre ele aldım. > Etkinliğin dijital yüzünü baştan sona kurmak. Contribution: Bireysel · analiz, web tasarımı, geliştirme ve teknik altyapının tamamı Kongrenin web sitesi için ihtiyaçları ve içerik yapısını belirlemek. Tasarım, yazılım geliştirme ve teknik altyapının tamamını bireysel gerçekleştirmek. Etkinliğin bilgilerini anlaşılır bir web deneyiminde bir araya getirmek. İçeriği, sayfa yapısını, arayüzü ve uygulama altyapısını birlikte tasarladım. Her kongre işi için analizden geliştirmeye uzanan bütün aşamaları bireysel gerçekleştirdim. Etkinlik sitelerinde içeriğin düzeni ve gezinme deneyimi birbirini doğrudan etkiliyor. Tasarım ve yazılım geliştirmeyi aynı bilgi yapısı üzerinden yürüttüm. Kongre web sitelerini freelance olarak, tüm aşamaları bireysel üstlenerek geliştirdim. Bu kayıt kongre odaklı web işlerimi bir çalışma grubu altında topluyor. --- ## Kurumsal Web Siteleri URL: https://devilyxrddev.com/work/freelance-tanitim Category: Web / Platform · 2019 · Bireysel web geliştirme Firmalar ve ajanslar için kurumsal web siteleri geliştirdim. Kurumun kimliğini, hizmetlerini ve çalışmalarını web üzerinde anlatan yapının analiz, tasarım, uygulama ve teknik altyapısını bireysel üstlendim. Bir kurumsal web sitesi, firmanın veya ajansın ne yaptığını ve nasıl bir değer sunduğunu anlaşılır biçimde anlatmalı. İçeriği ve sayfa yapısını her kurumun ihtiyacına göre ele aldım. > Kurumun kimliğini web deneyimine taşımak. Contribution: Bireysel · analiz, tasarım, web geliştirme ve teknik altyapının tamamı Firmanın veya ajansın ihtiyaçlarını, kurumsal kimliğini ve içerik yapısını netleştirmek. Tasarım, uygulama ve teknik altyapının tamamını bireysel geliştirmek. Kurumun hizmetlerini ve çalışmalarını anlaşılır bir web deneyiminde sunmak. İçerik mimarisini, görsel arayüzü ve uygulamayı birlikte geliştirdim. Kurumsal kimliğin web üzerindeki karşılığı ile sitenin çalışmasını sağlayan teknik altyapıyı aynı süreçte ele aldım. Firmalar ve ajanslar için çalışırken kurumun kendini nasıl anlattığına odaklandım. İçerik ve arayüz kararlarını, hizmetlerin ve çalışmaların anlaşılır sunulması etrafında şekillendirdim. Firma ve ajanslara yönelik kurumsal web sitelerini freelance olarak geliştirdim. Her işte analiz, tasarım, yazılım ve teknik altyapının tamamını bireysel gerçekleştirdim. --- ## Dijital Reklam Ekranları URL: https://devilyxrddev.com/work/iotrocop-kiosk Category: IoT / Donanım · Mobil · 2024 · IOTROCOP · bireysel geliştirme · stack: Raspberry Pi 4, Embedded systems, Mobile control IOTROCOP’ta kafe ve marketlerde reklam ve tanıtım amacıyla kullanılan dijital ekranlar için bir sistem geliştirdim. Raspberry Pi 4 tabanlı ekran yapısı ve telefon uygulamasından yönetim, aynı çözümün parçalarıydı. Kafe ve marketlerdeki reklam ve tanıtım ekranlarının, telefon uygulaması üzerinden yönetilebilmesi gerekiyordu. Fiziksel ekranı, cihaz altyapısını ve mobil kontrol akışını birlikte ele aldım. > Mekânın dijital vitrini, telefondan yönetim. Contribution: Bireysel · kiosk, mobil kontrol ve teknik altyapının tamamı Kafe ve marketlerde reklam ve tanıtım için dijital ekran ihtiyacını ele almak. Raspberry Pi 4 ekran sistemi, mobil kontrol ve teknik altyapının tümünü bireysel geliştirmek. Mekândaki ekranın davranışı ile telefon uygulamasındaki yönetimi bir araya getirmek. Raspberry Pi 4 üzerinde çalışan reklam ve tanıtım ekranını telefon uygulamasından yönetilebilecek şekilde geliştirdim. Ekran yazılımını, mobil kontrolü ve cihazın teknik altyapısını birlikte kurdum. Bu projede mekândaki ekran deneyimi ile telefon üzerindeki yönetim akışını birlikte düşündüm. Cihaz ve uygulama tarafındaki davranışların aynı ihtiyacı karşılamasına odaklandım. Kafe ve marketlerde reklam ve tanıtım için kullanılabilen, telefondan yönetilen Raspberry Pi 4 ekran sistemini geliştirdim. Tasarımdan yazılıma ve tüm teknik altyapıya kadar çalışmayı bireysel gerçekleştirdim. --- ## Bina Yıkama Drone’u URL: https://devilyxrddev.com/work/iotrocop-ros-drone Category: IoT / Donanım · 2025 · IOTROCOP · bireysel geliştirme · stack: ROS, NVIDIA Jetson Nano, Fiber infrastructure Bu bina yıkama drone çalışmasını IOTROCOP bünyesinde bireysel olarak gerçekleştirdim. Bina yıkama drone’unun tasarımını; ROS, Jetson Nano ve fiber altyapıyla birlikte ele alarak bütün teknik yapıyı kendim oluşturdum. Bina yıkama drone’unun fiziksel tasarımı ile yazılım, işlem birimi ve iletişim altyapısının birlikte düşünülmesi gerekiyordu. Projeyi tek bir bileşen yerine bütün bir sistem olarak ele aldım. > Tasarımından teknik altyapısına, tek bir sistem. Contribution: Bireysel · bina yıkama drone’unun tasarımı ve tüm teknik altyapı Bina yıkama amaçlı drone tasarımını işlem, yazılım ve iletişim altyapısıyla birlikte ele almak. Tasarım ve tüm teknik altyapıyı bireysel gerçekleştirmek. ROS, Jetson Nano ve fiber altyapıyı aynı sistem yapısı içinde birleştirmek. ROS ve NVIDIA Jetson Nano, projenin yazılım ve işlem altyapısında yer aldı. Fiber altyapıyı da kapsayan teknik yapıyı bina yıkama drone’unun tasarımıyla birlikte bireysel gerçekleştirdim. Bu çalışma, mekanik tasarımın yanında yazılım ve teknik altyapı kararlarını da birlikte ele almamı gerektirdi. Farklı bileşenlerin aynı sistem içinde bir araya gelmesine odaklandım. ROS, Jetson Nano ve fiber altyapı kullanan bina yıkama drone’unu tasarladım ve tüm teknik altyapısını kendim gerçekleştirdim. Tasarımdan sistemin teknik bileşenlerine kadar bütün sorumluluğu bireysel üstlendim. --- ## Araç Takip ve Navigasyon URL: https://devilyxrddev.com/work/iot-arac-takip Category: IoT / Donanım · 2022 · Bireysel IoT projesi · stack: Raspberry Pi, GPS, Telemetry, Interactive maps, Embedded systems IoT tabanlı gerçek zamanlı araç takip ve navigasyon çözümünü, Raspberry Pi üzerinde çalışan gömülü bir sistem olarak geliştirdim. Canlı GPS verisi, interaktif harita ve anlık telemetri aynı kullanıcı arayüzünde bir araya geliyor. Araçla ilgili konum ve durum verilerini ayrı kaynaklardan takip etmek yerine, bunları gömülü cihaz üzerinde tek bir anlaşılır arayüzde sunmak istedim. > Konum, harita ve telemetri aynı ekranda. Contribution: Bireysel · gömülü sistem, veri toplama, arayüz ve teknik altyapının tamamı Araç konumu, navigasyon ve durum verilerini gömülü cihaz üzerinde birlikte sunmak. Donanım seviyesinde veri toplama, sistem altyapısı ve arayüzü bireysel geliştirmek. GPS, harita ve telemetriyi akıcı bir gerçek zamanlı kullanıcı deneyiminde bir araya getirmek. Donanım seviyesinde veri toplama ile kullanıcı arayüzünü bir araya getirdim. Hız, sinyal gücü, batarya durumu ve sensör verilerini anlık gösteren arayüzü, gömülü sistemde akıcı çalışacak şekilde optimize ettim. Bu projede veri toplama ile arayüz performansını birlikte düşündüm. Donanımdan gelen bilgiyi güncel tutarken harita ve telemetri görünümünün akıcı kalmasına odaklandım. Canlı GPS, interaktif harita, navigasyon ve anlık telemetriyi tek arayüzde sunan Raspberry Pi tabanlı sistemi geliştirdim. Hız, sinyal gücü, batarya ve sensör verilerini gösteren çözümün tüm aşamalarını bireysel gerçekleştirdim.