home.social

Search

273 results for “pix”

  1. Hello, I'm Manda.

    I've actually been on Mastodon for a while under my cross stitch business account, @infinitybear, but I wanted to be able to post more casually, so that's what this account is for!

    Aside from cross stitch, I enjoy cooking, gardening, reading, making open source indie games, and I'm just starting to get more into poetry.

    I'll mostly be trying to post or boost joyful or cozy things I come across or make.

    #introduction #CrossStitch #PlantBased #GameDev #Gardening #Books #Art #FediArt #MastoArt #PixelArt

  2. Dune Watcher

    This sanderling stands atop a low sand dune with blurred surf in the background. I spotted the bird a few years ago at Holly Beach, Louisiana on the Gulf of Mexico.

    Step into a world where even the smallest moments—like the glint in a bird's eye—tell their own story.

    swede1952-photographs.pixels.c

    #photo #photography #photographer #photographylovers #wildlife #nature #bird #birds #birding #birdwatching #birdphotography #shorebirds #sanderling

  3. since i'm starting to work on fedi related software, i'd like to gauge opinions - are you happy with the social network you're currently using? are you happy with the app you're currently using?

    #fediverse #fedimeta #fedidev #bluesky #sns #poll #socialnetworking #fedilab #fedibird #toot #tusky #ivory #mastodon #misskey #sharkey #firefish #calckey #pixelfed #fyp #trending #news

  4. man, Windows 11 came out 5 years ago and it still surprises me with random small stupid details like this.
    1440p resolution, scale set to 100% - left folder is empty, right has files. What is this janky, pixelated look.

    #tech #technology #windows #windows11 #microslop #computers #design #it

  5. #podcast time! This time very special guest co-host @sunie joins for chat about (among other things) #AI, #retro gaming and preserving your games locally, plus #enshrouded, #warframe and, as always, #puppets!

    Search Pixels and Puppets where you get your podcasts or at this direct link: shows.acast.com/pixels-and-pup

  6. The Need for Speed: Unlocking Your Website's Potential with the Critical Rendering Path


    Ever felt that pang of impatience waiting for a website to load? You know, that agonizing moment where the screen is just a blank canvas, and you start questioning your internet connection, your life choices, anything to explain the delay? We've all been there. In the blink-and-you'll-miss-it world of the internet, speed isn't just a nice-to-have; it's the make-or-break factor for user engagement, conversion rates, and even your search engine ranking. And at the heart of this speedy performance lies a concept called the Critical Rendering Path (CRP).

    Think of your website as a delicious cake. To bake that cake and present it to your eager guests (your users), you need a recipe. The CRP is essentially the most efficient recipe for building your website's visual representation on the user's screen, as quickly as possible. It's about understanding the essential ingredients and the most direct steps to get that cake out of the oven and onto the table.

    This article will dive deep into the fascinating world of the Critical Rendering Path. We'll break down what it is, why it's so darn important, and how you can optimize it to make your website lightning-fast and your users incredibly happy. So, buckle up, grab a virtual coffee, and let's get cracking!

    What Exactly is This "Critical Rendering Path" Thing Anyway?


    Imagine you're building a LEGO castle. You can't just throw all the bricks together randomly. You need a plan, a sequence. First, you lay the foundation. Then, you build the walls, followed by the towers, and finally, you add the decorative flags. The Critical Rendering Path is the web browser's equivalent of this LEGO plan.

    In technical terms, the Critical Rendering Path refers to the sequence of steps a web browser takes from the moment it receives HTML and CSS to the point where it can visually render pixels on the screen. It's the chain of events that must happen for your user to see your beautiful website.

    Let's break down these steps:

    1. Parsing HTML: The browser receives the HTML document and starts parsing it, creating a Document Object Model (DOM). The DOM is like a tree-like structure representing the content and hierarchy of your HTML.
    2. Processing CSS: As the browser encounters CSS in your HTML (either inline, in <style> tags, or linked external stylesheets), it parses this CSS and creates a CSS Object Model (CSSOM). The CSSOM represents the styles applied to your DOM elements.
    3. Combining DOM and CSSOM into a Render Tree: This is where the magic starts to happen. The browser combines the DOM and CSSOM to create a Render Tree. The Render Tree contains only the visible elements of your page and their associated styles. Elements like <head> or elements with display: none; are excluded.
    4. Layout (or Reflow): With the Render Tree in hand, the browser needs to figure out the exact position and size of each element on the screen. This is the layout or reflow process. It's like measuring and marking where each LEGO brick goes.
    5. Painting (or Rasterization): Finally, the browser takes the layout information and "paints" the pixels onto the screen. This is the painting or rasterization step, where your website finally comes to life visually.

    Crucially, the CRP is "critical" because any delay in these steps will directly impact the perceived loading speed of your page. If one step is slow, the entire process is held up.

    Why Should You Even Care About the CRP? (The "Advantages" Section, aka "Why You Need This Speed!)


    Optimizing the Critical Rendering Path isn't just an exercise for web developers to show off their technical prowess. It has tangible, real-world benefits for your website and your business.

    • Blazing-Fast User Experience: This is the most obvious and impactful advantage. Users get to see content sooner. This leads to less frustration, more engagement, and a higher likelihood of them sticking around. Imagine landing on a site and seeing content appear within seconds rather than staring at a blank screen. That's the power of CRP optimization.
    • Improved Conversion Rates: When users can quickly see and interact with your product or service, they are more likely to convert – whether that's making a purchase, signing up for a newsletter, or filling out a form. Speed directly translates to revenue.
    • SEO Boost: Search engines like Google absolutely love fast websites. A good Core Web Vitals score, heavily influenced by CRP performance, is a significant ranking factor. A faster site can mean higher search engine rankings, leading to more organic traffic.
    • Reduced Bounce Rates: If your site takes too long to load, users will likely hit the "back" button and try a competitor. Optimizing the CRP helps keep users on your site longer.
    • Mobile-First Performance: With the majority of internet traffic coming from mobile devices, optimizing for speed on these networks is paramount. CRP optimization is especially critical for mobile users who often have slower connections.


    The Flip Side of the Coin: Potential "Disadvantages" (Or, "Why It's Not Always a Walk in the Park")


    While the benefits are undeniable, optimizing the CRP isn't always a straightforward process. There are challenges and considerations to keep in mind.

    • Complexity and Learning Curve: Understanding the intricacies of the CRP and implementing effective optimizations can be challenging. It requires a good grasp of web technologies and how browsers work.
    • Balancing Performance with Functionality: Sometimes, aggressive optimization might involve trade-offs. For instance, deferring non-critical JavaScript might mean certain interactive elements appear slightly later. Finding the right balance is key.
    • Requires Ongoing Maintenance: The web is constantly evolving, and new content or features can impact your CRP. It's not a "set it and forget it" kind of optimization. Regular monitoring and adjustments are necessary.
    • Potential for Over-Optimization: While rare, it's possible to get so caught up in optimizing that you might accidentally break something or make your code less readable and maintainable.
    • Tooling and Debugging: Identifying and fixing CRP bottlenecks can sometimes involve complex debugging and the use of specialized performance tools.


    Key "Features" of a Well-Optimized Critical Rendering Path (The "How-To" Section)


    Now that we understand what the CRP is and why it matters, let's get to the good stuff: how to actually optimize it! This is where we get hands-on with some actionable strategies.

    1. Minimize the Critical HTML & CSS


    The core idea here is to reduce the amount of data the browser has to process for the initial render.

    • Inline Critical CSS: This is a game-changer. Instead of waiting for an external CSS file to download, you embed the CSS required to render the above-the-fold content directly within your HTML. This allows the browser to start rendering immediately.

      Example:

      <!DOCTYPE html>
      <html>
      <head>
      <title>My Speedy Page</title>
      <style>
      /* Inline critical CSS for above-the-fold content */
      body {
      font-family: sans-serif;
      margin: 0;
      }
      .hero-section {
      background-color: #f0f0f0;
      padding: 50px;
      text-align: center;
      }
      h1 {
      color: #333;
      }
      </style>
      <!-- Link to non-critical CSS -->
      <link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
      <noscript><link rel="stylesheet" href="styles.css"></noscript>
      </head>
      <body>
      <section class="hero-section">
      <h1>Welcome to My Fast Website!</h1>
      <p>Content that loads instantly.</p>
      </section>
      <!-- Rest of your content -->
      </body>
      </html>

      Explanation:

      • The <style> block contains the CSS needed for the .hero-section to be visible immediately.
      • The link rel="preload" with as="style" tells the browser to fetch the styles.css file but not to block rendering. onload then applies it once downloaded.
      • The noscript tag ensures that users with JavaScript disabled still get the stylesheet.


    • Minify HTML and CSS: Remove unnecessary characters (whitespace, comments) from your HTML and CSS files. This reduces file sizes, leading to faster downloads. Many build tools (like Webpack or Gulp) and online minifiers can do this automatically.


    2. Defer and Asynchronously Load JavaScript


    JavaScript can be a major bottleneck in the CRP. When the browser encounters a <script> tag, it typically stops parsing HTML, downloads the script, executes it, and then resumes parsing. This is often unnecessary for scripts that aren't immediately needed for rendering.

    • defer Attribute: This attribute tells the browser to download the script in the background while continuing to parse HTML. The script will then execute only after the HTML parsing is complete.

      Example:

      <script defer src="my-non-critical-script.js"></script>

    • async Attribute: This attribute also downloads the script in the background, but it executes the script as soon as it's downloaded, potentially before HTML parsing is complete. This is useful for independent scripts that don't rely on the DOM.

      Example:

      <script async src="my-analytics-script.js"></script>

    • Move Scripts to the End of <body>: While defer and async are preferred, a common older technique is to place all your non-critical JavaScript just before the closing </body> tag.
    • Code Splitting: Break down your JavaScript into smaller chunks. Load only the JavaScript that's necessary for the current view or interaction. Tools like Webpack excel at this.


    3. Optimize Images


    Images are often the largest assets on a webpage and can significantly impact loading times.

    • Optimize Image Files: Compress your images without sacrificing visual quality. Use tools like TinyPNG, ImageOptim, or even online compressors.
    • Use Modern Image Formats: Consider using formats like WebP, which offer better compression and quality compared to JPEG or PNG.
    • Responsive Images: Use the <picture> element or srcset attribute to serve appropriately sized images for different screen sizes and resolutions.

      Example:

      <picture>
      <source srcset="large-image.webp" media="(min-width: 1024px)">
      <source srcset="medium-image.webp" media="(min-width: 600px)">
      <img src="small-image.webp" alt="A descriptive alt text">
      </picture>

    • Lazy Loading: Defer the loading of images that are not immediately visible on the screen (i.e., below the fold). They will only load when the user scrolls down.

      Example:

      <img src="your-image.jpg" loading="lazy" alt="Descriptive alt text">


    4. Leverage Browser Caching


    Caching allows the browser to store copies of your website's resources (HTML, CSS, JavaScript, images) locally. This way, on subsequent visits, the browser can load these resources from the local cache instead of re-downloading them from the server, dramatically speeding up loading times.

    • Set Appropriate Cache Headers: Configure your web server (e.g., Apache, Nginx) to send Cache-Control headers for static assets.

      Example (Nginx configuration):

      location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|webp)$ {
      expires 1y;
      add_header Cache-Control "public, no-transform";
      }


    5. Prioritize Above-the-Fold Content


    The "above-the-fold" content is what users see immediately when they land on your page without scrolling. This is the most critical part to get right for a good initial impression.

    • Identify Critical Resources: Determine which HTML, CSS, and JavaScript are absolutely essential to render this initial viewport.
    • Inline Critical CSS: As mentioned earlier, this is key for visual content.
    • Defer Non-Critical JavaScript: Move or defer scripts that aren't needed for the immediate display of above-the-fold content.


    6. Reduce Server Response Time


    Even with optimized front-end assets, a slow server can still cripple your CRP.

    • Optimize Server-Side Code: Ensure your backend code is efficient and doesn't take too long to generate the HTML response.
    • Use a Content Delivery Network (CDN): CDNs distribute your website's assets across multiple servers globally. This means users can download assets from a server geographically closer to them, reducing latency.
    • Enable HTTP/2 or HTTP/3: These newer protocols offer significant performance improvements over HTTP/1.1, especially for handling multiple requests concurrently.


    7. Eliminate Render-Blocking Resources


    Any resource that prevents the browser from rendering the page until it's downloaded and processed is considered "render-blocking."

    • Identify Render-Blocking CSS and JavaScript: Use browser developer tools (Performance tab) to identify these bottlenecks.
    • Inline Critical CSS: The primary solution for render-blocking CSS.
    • Defer/Async JavaScript: The primary solution for render-blocking JavaScript.


    Tools for the Job (Your Performance Toolkit)


    To effectively optimize your CRP, you'll need the right tools:

    • Browser Developer Tools: Your best friend! Chrome DevTools, Firefox Developer Edition, and Edge DevTools offer invaluable features:
      • Performance Tab: Record page loads to analyze the CRP, identify bottlenecks, and see how long each step takes.
      • Network Tab: Inspect all network requests, their sizes, and timings.
      • Lighthouse: An automated tool built into Chrome DevTools that audits performance, accessibility, SEO, and more. It provides actionable recommendations for CRP optimization.


    • WebPageTest: An online tool that allows you to test your website's speed from various locations and browsers.
    • GTmetrix: Another popular online tool for analyzing website speed and performance.
    • Google PageSpeed Insights: Provides a score for your page's performance on both mobile and desktop, with specific recommendations.


    Conclusion: Embrace the Speed!


    The Critical Rendering Path is a fundamental concept for anyone serious about website performance. By understanding how browsers construct your webpages and by strategically optimizing the resources that contribute to the initial render, you can unlock a world of benefits: happier users, higher conversions, and better search engine rankings.

    It's not about magic tricks; it's about a systematic approach to delivering your content efficiently. Start by identifying your current CRP bottlenecks, then implement the strategies discussed – inline critical CSS, defer JavaScript, optimize images, leverage caching, and prioritize above-the-fold content.

    Remember, website performance is an ongoing journey, not a destination. The web is dynamic, and your optimizations will need to adapt. But by making the Critical Rendering Path a core part of your development process, you'll be well on your way to building faster, more engaging, and ultimately, more successful websites. So, go forth and optimize – your users (and your bottom line) will thank you for it!#frontend #performance #webdev #software #coding #development #engineering #inclusive #community
    Optimization: Critical Rendering Path

  7. TL;DR


    Synapse Shield is an open-source, self-hosted behavioral biometrics engine for bot mitigation. It scores mouse, touch and keystroke dynamics instead of showing CAPTCHAs, and it collects no PII (only relative millisecond timings).

    v0.10.0 adds the mobile leg: a Mobile SDK v2.0 across the vanilla JS, React and Vue packages, plus matching changes in the Python engine and the native Rust core.

    The problem: desktop heuristics punish real phones


    The first versions were tuned on mouse trajectories. Mobile traffic exposed five false-positive cases, all of which looked like bots to desktop-oriented rules:

    ScenarioWhy it was flaggedSingle tap (no drag)A single point gives the 1D-CNN a zero-padded 60-step tensor, which it scored as a bot. A hard rule also flagged 1-2 mouse points as "teleportation".Flick-scroll, then tap elsewhereFitts's Law deceleration and click-to-cursor distance checks assume a persistent cursor.iOS / WebView radiusX == 0Touch radius is quantized or zero, which looked like a synthetic constant-radius touch.Android Gboard / IMESoft keyboards emit key="Unidentified" with 0-5 ms synthetic dwell times, tripping the mechanical-key rules.Touch drawing on the demo canvasThe cockpit canvas only listened for mousemove.

    What changed in v0.10.0

    1. Client SDK (JS, React, Vue)


    • Tap hold time (touchend - touchstart) is recorded as tap_duration_ms.
    • Touch points carry source: "touch" and a 3-decimal average radius, so WebViews that report r = 0 can still be recognized as touch input.
    • The synthetic click that browsers fire after touchend is de-duplicated.
    • A passive devicemotion listener computes motion_variance (handheld micro-tremor, no permission prompt needed where the browser allows it).
    • device_pixel_ratio, orientation and the WebGL renderer string are included for cross-validation.


    2. Engine rules


    • GPU mismatch: a mobile-sized touch viewport reporting SwiftShader, llvmpipe or a desktop NVIDIA/AMD renderer scores +85 risk.
    • Tap timing: an average tap under 8 ms scores +55. Three or more taps with near-zero duration variance score +50. A tap of 15-800 ms is a small bonus.
    • IME-aware keystrokes: Unidentified / Process keys are exempt from the mechanical dwell-time penalties. Flight-time and entropy checks still apply.
    • Short touch sessions: the desktop 1D-CNN is skipped below 12 points on touch devices, Fitts's Law is not applied to touch, and click-to-cursor compares against the nearest recorded touch point.


    3. Rust parity


    The six new features (tap_count, avg_tap_duration, tap_duration_var, device_pixel_ratio, motion_variance, is_mobile_gpu_mismatch) exist in both the Rust SIMD core (synapse_core_rs) and the Python fallback. A new parity test runs both implementations on the same telemetry and compares the results.

    Verifying it


    The full suite passes with 141 tests, including new cases for:

    • a genuine single tap
    • iOS-style zero-radius touches
    • Android IME typing
    • swipe followed by a distant tap
    • an emulator spoof (NVIDIA renderer on a 390 px touch viewport with a 2 ms tap), which is blocked

    There is also a new mobile_emulator_spoof_plugin in the built-in red-team orchestrator.

    pip install -U synapse-shield
    synapse-shield run --port 8000
    synapse-shield redteam

    Honest limitations


    • The models and thresholds were validated on synthetic and hand-built telemetry. Real-world device diversity (old Android builds, cracked screens, unusual WebViews) still needs field data.
    • devicemotion availability varies by browser and permission policy, so it is a bonus signal, never a requirement.
    • This is a web SDK for mobile browsers, PWAs and WebViews. There is no native Swift or Kotlin SDK yet.
    • It does not do TLS / JA3 fingerprinting, so it works at the application layer only.


    What's next


    • Field data collection to recalibrate the mobile thresholds
    • A native mobile bridge
    • Network-layer fingerprinting through a reverse-proxy plugin

    Feedback, issues and PRs are welcome. If you try it on a real device fleet, I would like to hear which false positives you hit.#opensource #security #python #rust #software #coding #development #engineering #inclusive #community
    Synapse Shield v0.10.0: Bringing Behavioral Bot Detection to Mobile (Without Building an App)

  8. Screenshots are everywhere in software docs, and most of them have alt text like screenshot, image, or the original file name. Sometimes there's none at all. When I edit a guide, the images are often the last thing anyone has looked at closely.

    Alt text matters more in docs than people think. Screen reader users hear it instead of seeing the image. It shows up when an image fails to load, for example on a slow connection or after someone renames a folder in the repo. And it's text, so it can be searched and translated, which a picture of text can't.

    Here's how I approach it.

    Ask what the screenshot is for


    Before writing anything, I ask one question: why is this image here? In docs, the answer is almost always one of these:

    • It shows where something is (a button, a menu, a field).
    • It shows what you should see after a step, so the reader can confirm it worked.
    • It shows a specific value or setting the reader has to match.
    • It's decoration, and the surrounding text already says everything.

    The alt text should carry that purpose, not a full inventory of the screen. A settings page might have thirty controls on it, but if the step is about one checkbox, that checkbox is what the alt text is about.

    Describe the state, not the pixels


    Compare these two:

    ![Screenshot of the settings page](images/settings.png)

    ![The Notifications tab with "Email me when a build fails" turned on](images/settings.png)

    The first tells me nothing I couldn't guess. The second tells me exactly what the screen should look like after the step, which is the whole reason the screenshot exists.

    A few habits that help:

    • Skip "image of" and "screenshot of". Screen readers usually announce that it's an image already. Start with the content.
    • Use the exact UI labels, with the same capitalisation as the interface. If the button says "Save changes", the alt text says "Save changes", not "the save button".
    • Include values that matter. If the reader needs to pick "UTF-8" or set a timeout to 30, put that in the alt text.
    • Keep it to a sentence or two. If you need more, the information probably belongs in the body text.


    Don't hide instructions in the image


    This is the problem I find most often. A step says "Configure the options as shown below", and the actual settings only exist in the screenshot. Anyone who can't see the image, or whose image didn't load, is stuck.

    The fix isn't longer alt text. It's moving the information into the step itself:

    3. In **Format**, select **CSV**, and in **Encoding**, select **UTF-8**.

    ![The Export dialog with CSV and UTF-8 selected](images/export.png)

    Now the step works without the image, and the screenshot becomes a confirmation. The alt text can stay short because the text above it does the heavy lifting.

    When empty alt text is right


    Not every image needs a description. If a screenshot only repeats what the text already says, or it's a decorative banner, an empty alt is the honest choice. In Markdown that's ![](images/banner.png), which renders as alt="". Screen readers generally skip images with empty alt text instead of reading out the file name.

    The difference between "empty on purpose" and "forgot to write it" is invisible in the source, though. In repos I work on, I like a short HTML comment next to deliberate ones, so the next editor doesn't "fix" it:

    <!-- decorative, text above covers it -->
    ![](images/banner.png)

    A quick check before review


    To find the obvious problems in a docs folder, I use a single grep. It looks for three patterns: empty or generic alt text, alt text that is a file name, and alt text that starts with "image of" or "screenshot of".

    grep -rniE \
    -e '!\[(image|screenshot|picture|img)?\]\(' \
    -e '!\[[^]]*\.(png|jpe?g|gif|webp)\]' \
    -e '!\[(an? )?(image|picture|screenshot|photo) (of|showing)' \
    docs/

    -r searches the folder recursively, -n adds line numbers, -i ignores case, and -E enables extended regular expressions. Each -e adds one pattern. On a test file it prints something like:

    docs/setup.md:3:![](images/settings.png)
    docs/setup.md:5:![screenshot](images/login.png)
    docs/setup.md:7:![Screenshot 2026-03-02 at 10.14.png](images/export.png)
    docs/setup.md:11:![image of the dashboard](img/dash.png)

    It's not a verdict. The empty alt on line 3 might be deliberate, and the check can't tell whether a real description is accurate. It only catches images written in Markdown syntax, not HTML <img> tags. But it gives me a list to work through, which beats scrolling every page looking for images.

    Keep it in sync


    The last thing: alt text goes stale with the UI. When a button gets renamed and someone updates the screenshot, the alt text often keeps the old label. When I review a docs change that touches an image, I read its alt text against the new screenshot. It takes a few seconds, and it's usually the only place the old wording survives.#documentation #a11y #writing #markdown #software #coding #development #engineering #inclusive #community
    Writing alt text for screenshots in documentation

  9. ☁️ Co je OSCloud a proč by tě mohl zajímat?

    OSCloud je komunitní cloud postavený na open source a soukromí. Provozuje ho spolek OSCloud, z. s., neziskově: všechno jde zpátky do provozu.

    Místo desítek účtů u velkých firem můžeš mít všechno na jednom místě:

    📦 Nextcloud: soubory, kalendáře, kontakty. Zdarma 2 GB, větší úložiště od 15 Kč/měs
    📷 Ente Photos: šifrované fotky místo Google Photos. Šifruje se přímo v tvém zařízení, takže do nich nevidíme ani my. 10 GB zdarma
    oscloud.photos
    🔑 Vaultwarden: správce hesel, který ovládáš ty, ne korporace
    💬 Matrix na mxchat.cz: decentralizovaná komunikace, přihlášení přes SSO
    🧑‍💻 Forgejo na git.oscloud.cz: vlastní git hosting, otevřená registrace i SSO
    🏃 FitPub na makni.cz: fitness tracking pro Fediverse. Nahraješ aktivity (FIT, GPX, TCX) nebo je zapíšeš ručně, uvidíš trasy a statistiky a sdílíš je přes ActivityPub
    📝 HedgeDoc: společné psaní poznámek v Markdownu
    📚 Wallabag: ukládání článků na později
    🐘 Mastodon, Pixelfed, PeerTube, SearXNG, CryptPad, FreshRSS a další v tarifu Komunita zdarma

    🔐 SSO: jedno přihlášení do většiny aplikací, včetně Matrixu a Forgejo
    🛡️ Server v Hetzneru v Německu, pravidelné zálohy, správa a aktualizace na nás

    Žádné sledování, žádná reklama, žádné prodávání dat. Podpora jde přes Matrix, takže se ptáš lidí, ne anonymního ticketu.

    🧡 Neziskové a dobročinné spolky, dětské oddíly a skauty podporujeme, podmínky domluvíme individuálně.

    Chceš vyzkoušet? Tarif Komunita je zdarma:
    oscloud.cz

    #OSCloud #selfhosting #soukromí #opensource #Matrix #Forgejo #Fediverse #Degooglizace

  10. Just consuming this bit of tasty #BBCmicro history, in which two uni students create the first "true" #3D game (defined in the video as "xyz tracking for player and all objects," differentiating from contemporaries like Battlezone and the arcade Star Wars, which were 3D vector graphics, but gameplay was "on rails").

    Just as fascinating to me, an 8-bit kid but not from the UK, is the short history of the BBC Micro itself. True public service. Good job, 1970s BBC.

    But having spent considerable time in the 90s playing Xwing, Tie Fighter, etc, this story of what was done on a system with the same CPU as the C64, at the time, is really quite mindblowing.

    I will probably have more thoughts about this but gotta get chorin'

    youtube.com/watch?v=lC4YLMLar5I

    edit: ok two quick thoughts

    First off, this video includes the line "that resistor simulates an engineer's finger on the board - it wouldn't boot without it, and to this day nobody knows exactly what it does."

    Also, I find the graphics, particularly the edges, fascinating. This machine didn't have "fuzzy logic" about whether to light a pixel or not, and I think the edges of objects, the weird details, reveal some kind of extremely lean rendering routines.

  11. Artcraft, Mindscaper, et al: Can we really just vibecode creative software?

    Over the past couple days, massive hype has built up around Artcraft, a collection of literal clones of Adobe’s CC, Microsoft Office and other apps, entirely vibecoded and open source. Artists and creative folks battered by subscriptions costing hundreds and thousands of dollars a year finally smell relief that’s not the poorly designed open source jank of yesteryear, and the project receives dozens of similarly vibecoded pull requests each hour. At the same time, I’ve been building Mindscaper, practically with the same goal, but without the hype. As such, I have some thoughts and bias on this subject. And probably typos, this is 100% human-written.

    Background

    Artcraft is developed by Learning Machines LLC, which appears to be a one-man company by Brandon Thomas. Thomas previously developed the deepfake services “vo.codes” and “fakeyou.com” under this company, and developed VTubing tech. Judging by the blurb on “the.storyteller.company”, he appears to believe in “controllable AI” trivializing (“democratizing”) art creation, but with Artcraft opted to primarily clone existing professional software, putting AI on the backburner (although an AI subscription service is available).

    By a lucky coincidence, I founded my company Koytek Wattenberg Media at a similar point in time, with similarly limited success. One of my biggest clients became Audacity, at which I worked on “democratizing” audio creation, and wouldn’t you know I’m also into VTubers. I started my foray into vibecoding about half a year after Artcraft, focusing on Audacity-shaped audio and video editors, Soundscaper and Framescaper.

    On technical matters

    I have nothing new to say here that hasn’t been already said a billion times. Modern coding robots (I used Opus 5 and GPT 5.6 Sol mostly) are perfectly capable of making something that Just Works. They just happen to be lying, lazy bastards that will happily claim something exists when it doesn’t. Test-driven development principles keep them reasonably honest, though they occasionally do manage to find a way to write tests so unspecific that a pixel-by-pixel spectrogram in the Roseus color scheme and a spectrogram with much coarser buckets using a blue-to-yellow color scheme pass the same test.

    I am under no illusion that the code they’ve produced isn’t ugly and suboptimal. But then, the Audacity codebase is 25 years old and also extremely ugly and suboptimal. The Audacity team – all extremely talented software engineers and beautiful people – have been unable to make Audacity 4 feature-equivalent to Audacity 3 in over a year, despite that “only” being a UI migration with some UX changes attached. With Soundscaper, I managed to get an app going featuring all of Audacity 3’s and 4’s features within 3 months, plus a plethora of additional DAW features and video editor on top of it. Similarly, Artcraft managed to get a Photoshop (and general Adobe CC) clone going in half a year.

    On practical matters

    While the robots are perhaps good enough to get functionality implemented, they suck at figuring out the finer details – and pretty much all of the design.

    Robots suck at progressive disclosure

    Progressive disclosure is the part of UX where, instead of showing everything at once, you show things as they’re needed. A trivial example for this would be a menu; the “Format” menu is reduced to that single word on your screen until you, the user, decide that now is the time to format something and click on it, revealing a dozen or so options.

    Robots suck at this. If you tell them to add a feature, they’ll generally attempt to place it on screen with a dedicated button. This is fine for an app with 6 controls in total, but less so for a complex creation app with 500. Forcing them to chuck everything into menus and dialogs helps, but then we’re straight back into design paradigms from the 80s.

    This is where Artcraft and Mindscaper both have an advantage over a pure vibecoded approach: Artcraft already knows where everything goes because they’re busy copying existing design. I know where my stuff goes because I am the designer, and thus have a very strong idea on what these features are, how they should work, where they should go and when they should show up.

    Robots suck at information density

    In a similar vein to the thing above, robots love to implement stuff as “Control title – control – explanation sentence”. Again, this is fine for a small dialog with 2 controls in it, but for any sufficiently complex effect it just either overwhelms people with too many words and/or requires things to be spaced so far apart to fit all this extra text that the dialog becomes massive.

    And again, cloning existing dialogs works here, as does being the original designer to make relevant decisions. Purely vibecoding this from scratch is going to be much more difficult though.

    Robots suck at asking the right questions

    The biggest difference between working with humans and working with robots is what questions I’m being asked. With the Audacity team, I’d get asked about user edge cases as they popped up, I’d get asked about clarifications on my designs, I’d get asked about things by users. With the robots, there are a lot fewer of these kinds of questions; the only time the robot asks questions at all seems to be in plan mode. However, in plan mode, the questions that are being asked almost always are implementation-centric, ie the stuff I don’t really care about. So what ends up happening is that the robot will go ahead and do something, proudly proclaim it’s finished and then it’s up to me to QA whether it actually did the right stuff. And more often than not, it misses some design edge case.

    On philosophical matters

    In this section I’d want to talk about the should and shouldn’ts of vibecoding a creative software suite.

    Copyright

    Artcraft is copying various proprietary programs wholesale using AI. On Artcraft’s side, this has all the usual copyright questions attached (who is the author? is the code copyrightable?), though on Adobe’s side this becomes more interesting: From Tetris v. Xio, we know that the look and feel of a program (in that case, Tetris) can be copyrightable. Note that this is different from copyright on the code itself (which I don’t think Artcraft can even infringe given that their thing is in Rust, which Adobe’s stuff probably isn’t), and also is different from attempting to claim copyright on the app’s structure or underlying ideas (that’s patent stuff, not copyright).

    Artcraft thus may be in for a rude awakening should Adobe feel threatened by them. Adobe may or may not win this case, but they also probably don’t need to in order to hobble Artcraft’s development for years to come. Funnily enough, the actual code being open source may be a saving grace here, as it likely can trivially be forked, copied and modified to avoid Adobe’s gaze, to a better degree than pirates ever could. Another point in favor of Artcraft is AI itself; any rulings in favor of copyright holders over AI developers is also a ruling against Adobe’s own products. It thus may be that Adobe opts to do… nothing, in order to avoid the backblast. I don’t think they’ll necessarily care about the possibility of a Streisand; lawsuits like this generally are boring enough for the wider press to not care beyond perhaps a single story in the margins.

    Mindscaper meanwhile doesn’t have that problem, as Soundscaper is building upon an open source app and crediting it, and Framescaper is original.

    Patents

    Software patents unfortunately arguably exist, some of them on UI (eg the Ribbon pattern found in MS Office). Adobe, unsurprisingly, has a tens of thousands of patents.

    Software patents are one level more devious than mere copyright: It doesn’t matter that you had the idea all on your own, or that the idea may be obvious now, all that matters is that someone else also had the idea before you. And if they have registered it, you now are in trouble. It’s because of software patents that loading screens don’t have minigames, it’s because of software patents that Audacity makes you jump through hoops to download FFmpeg, as there are still over 3000 patents in the AAC patent pool alone. And if just a singular patent survives litigation, which given the sheer number of patents involved is gonna take forever anyway, you’re on the hook for yet more damages.

    Software patents are the ideal tool for corporations to shut down people they don’t like, hence why some corporations (including Adobe and Microsoft, the companies most likely to be upset by what Artcraft is doing here) engage in non-aggression pacts with each other, like the Allied Security Trust or Open Innovation Network – this prevents these companies being at each other’s throats for patent reasons all the time.

    However, these defensive alliances generally don’t apply to people outside the alliance, and neither Artcraft nor Mindscaper are in one. Thus, whenever one of the incumbents ends up being upset or feel threatened by what’s happening, they can pull the litigation lever.

    I don’t know which patents could be relevant here, and frankly, I also don’t wanna know – in the end it doesn’t really matter. Patents are a risk factor for any software, and typically best left ignored as patent holders tend to be more busy with actual business than going after one-man companies. However, when you sufficiently piss off a company – and I’d argue that wholesale cloning of a software might constitute that – you have a lot stacked against you.

    Freeing users

    Subscription services for software create incentives for the builder of the software to put as little work as possible into the software while charging as much as possible. This allows the Autodesks and Adobes of the world to charge ridiculous amounts, while providing software so bad that it regularly attracts user petitions, begging them to fix longstanding bugs degrading the experience.

    Simply providing free software alternatives here may work (eg in the case of blender and musescore), or may not work, as free software does (or at least: used to) suffer from small development teams and quite frequently UX so weird that it wasn’t possible for some folks to figure out how to use the program in question. A famous example for this is the “make a circle in GIMP”. The solution is to make an elliptical selection, intuit that you can convert that to a path, and then use a panel to draw the path – as opposed to Photoshop, where you just use the circle tool.

    Providing free software versions of a program would indeed free people of subscription software to use something else, depending on the copyright situation more or less easy compared to pirating, and this is what Artcraft does provide.

    Freeing new users

    However, I’ve found that the entire creative software field suffers from underdeveloped design. A lot of professional-grade software is modeled after pre-digital hardware and uses design metaphors that simply don’t make sense if you haven’t experienced said hardware. I remember when Lightworks, an Oscar-winning video editor, became a free download in 2010, I couldn’t for the life of me figure it out, not even typically simple things like rendering the video. This is despite me having had some experience in Sony Movie Maker (the cheap version of Vegas) and Powerdirector.

    This is understandable somewhat: To attract professional users in the 80s and 90s, you’d need to replicate their workflows from their pre-digital tools. And once you have these users and they’ve learned how to use the program, they’d hate to have to adapt to a new design. However, this also leads to new users having to jump through enormous quantities of hoops trying to learn the program. If you want to “democratize” art creation, making things free and open source is one thing, but the barrier to entry of learning the tool remains the same.

    This is where my work on Audacity (and now Soundscaper) becomes relevant: It takes quite a bit of design work, but it in principle should be possible to design a complex creation app in such a way to make it easy to use. Notably, not everything has to be equally easy – setting up a simple recording should be a one-button operation, while setting up a more complex multi-track recording may be a bit more involved, as we can expect the user to have a more clear task in mind on which input goes to which track.

    Our vision for Audacity (and Musescore for that matter) was to completely starve the current incumbents of new users by just having a much more modern design and having an app that’s easier to get into. I still think this is the way things will shake out eventually; we’ve already seen Finale starve to death from this pressure.

    Artists

    Of course, AI bros would go further here and envision a future where skill is completely replaced by AI. And while for things that already were sloppy and primarily utilitarian to begin with (shitposts, local business signage, anything using Wordart/clipart in the past), AI certainly is going to replace more dedicated tools, I don’t see it doing so for artists. To draw a chess analogy, computers have been better than humans in chess for a long time, yet humans still play chess, for one simple reason: It’s fun. Similarly, artists don’t really care if a robot can do a hyperrealistic godzilla in mere seconds, as the fun part of making art is actually creating it, not just admiring the result (that would be mere consumption).

    As an artist, you of course get to pick your battles. You can decide that shading or coloring is too annoying, and stick to strict black and white, or that mixing and mastering is too annoying and stick to making melodies and chords. From there you can hire someone to do the shading and coloring and additional work for you, or indeed use AI instead. In either case, your initial creative vision gets diluted by the other work; the more people are involved and the heavier the AI use, the more diluted your vision gets. This can be acceptable to you, especially in a world where art is traded as a commercial good, but if we assume that art as a good is going to lose worth, this tradeoff may become less worthwhile, perhaps making it so that you only wants tools strictly designed for you to express yourself.

    For-profit open source

    Having both worked at and seen for-profit open source, I’m perhaps a bit jaded:

    • We see companies close access to their source the moment they feel like they can extract revenue out of it (various server software comes to mind)
    • We see companies increase the amount of closed components in open source software, to guarantee only they can have a revenue stream (VSCode and Android come to mind)
    • We see companies add user-hostile features to benefit only themselves (Google Chrome/Android is the poster-child here, to the point where regulators need to step in to it)

    There might be a way to make a for-profit company be a good steward of an open source project, but the incentives always point in a different direction – and even if the current leadership can resist these incentives, the next one can flip towards enshittification so fast. I haven’t seen Thomas’ Artcraft project enough yet to know whether he’ll stay true to open source, though given he already has a subscription service (for cloud AI compute, of course) and a launcher ready, I gotta say that I’m worried.

    It’s for this reason that I spun out Soundscaper from my company kw.media into Mindscaper, a non-profit association. It is now immune to me, or anyone else for that matter, deciding I would like it to make me tons of money, as long as its members (so, ideally, the users) don’t want to be milked. As an association (as opposed to a foundation), it also is way more democratic, giving users a real voice in what should happen to Soundscaper.

    Resources & Sustainability

    It’s impossible to discuss AI without touching upon the massive resource use it causes. Throughout my time developing Soundscaper, I have burnt through Codex’ and Claude’s 20x subscription quota almost every single reset cycle, including OpenAI’s ridiculous practice of vibing when a reset occurs and having a dude announce it on twitter. This has cost me hundreds of dollars, would have cost me tens of thousands of dollars in API cost, and has cost OpenAI and Anthropic likely thousands of dollars as well.

    It is clear to me that the current pricing isn’t sustainable, and that slowly but surely, costs will go up. It is for this reason that Soundscaper uses local inference whenever useful and won’t be integrating with any cloud AI services. As far as I’m concerned, the entire point of open source is to fuck around with stuff on the machines you have access to and have it not tied to some service which decides what is and isn’t allowed.

    It also is clear to me that there are real environmental impacts vibecoding this hard has; especially when comparing how long and hard my local setup has to work to achieve much simpler tasks (I let gemma and aya-expanse handle translation work locally, for example). Thus, I try to limit my vibecoding to the areas I really care about. I have a media background, so I shall vibecode media tools I’ll personally use – as opposed to token-maxxing just because I can.

    Conclusion

    So, can we vibecode creative software? On a technical level, yes. On a design level, yes, if we are a designer or follow someone else’s design. But should we? Depending on how adventurous we are with copyright and patent laws, especially if we are just re-implementing someone else’s design – maybe not.

    We absolutely should open source and reverse engineer the things we care about, but we shouldn’t take a robot’s “one-shot” in an area we know nothing about as proof that it’s good now. It’s enormously easy to slop a whole software suite into existence but it actually takes some real work to get the software suite to actually be usable by real people – and surely, the benefit to real people has to be the goal.

    If you’re interested in contributing to a non-profit making creative software, consider joining Mindscaper.

    #ai #artcraft #artificialIntelligence #audacity #mindscaper #openSource #software #soundscaper #technology #vibecoding #writing
  12. I’m building EatNeat, a mobile app that helps people turn a nutrition plan into a manageable routine—before “What should I eat?” takes over the evening.

    A lot of meal-planning tools focus on creating a plan. I’m focused on what comes after: remembering it, finding something practical when hunger clocks in, and tracking what you ate without turning dinner into a spreadsheet assignment.

    EatNeat is still a prototype. Here’s what’s working, what I’m figuring out, and how the mascot Bitey provides moral support.

    The core loop


    The app is organized around a simple cycle:

    1. Create a meal plan or bring in an existing one.
    2. See the day’s meals and get reminders at their scheduled times.
    3. Browse meal ideas that match selected constraints.
    4. Capture or mark a meal as eaten.
    5. Review progress over the day and across recent days.

    The Today screen brings schedule and progress together. Meals can be marked eaten when their scheduled time arrives, and progress includes estimated nutrition metrics. The user stays in control; the app helps without handing out gold stars for lunch.

    AI as a helper, not the whole app

    EatNeat has AI-assisted flows for personalized plans, meal recommendations, recipes, and optional photos. Requests include selected filters and nutrition targets. Practical constraints are requirements: an “under 15 minutes” meal can’t hide a 40-minute oven step, and equipment filters should match the actual cooking method.

    Food photos are optional and handled separately from meal text. Without a photo, the meal still appears with a bundled animation or illustration. The recipe shouldn’t vanish because its glamour shot is late.

    The app can call configured service endpoints. During local development, some AI flows can call Gemini directly with a development environment key. A production app should route requests through an authenticated server and keep provider keys off the client.

    AI nutrition values are estimates. EatNeat is not a medical tool, and the prototype does not treat a missed meal as a failure or make clinical recommendations.

    Local-first state and reminders


    Plans and meal logs are stored on-device with AsyncStorage. Writes are debounced and serialized, and local notifications remind users at planned meal times—because remembering breakfast shouldn’t require remembering to remember breakfast.

    Private meal-log photos stay on-device. If a user uploads a plan photo for parsing, it goes to the configured plan-reading service, with that made clear before upload.

    The stack


    The current app uses:

    • Expo and React Native for the mobile app
    • Expo Router for file-based navigation
    • TypeScript for app screens, state, services, and UI components
    • React Native Paper for a small set of interface components
    • Reanimated for navigation and interface motion
    • AsyncStorage for local plan and log persistence
    • Expo Notifications for scheduled meal reminders


    What is still a prototype


    The accountability feed displays sample posts and does not publish to a social service. Some preview screens use illustrative nutrition data. The app doesn’t ship a backend, so server-based AI features need a configured endpoint. For now, the social circle is supportive and fictional.

    I’m also breaking larger screens into focused components. The Today screen’s progress visualization and schedule calculations now live outside its route file, making the daily flow easier to evolve.

    What I’m learning


    The most useful decision so far: keep the basic habit loop independent of AI. Meal ideas work without generated photos; logging works without image analysis; plans can be reviewed when a service is down. A flaky model shouldn’t get veto power over lunch.

    AI can add convenience, but a slow request or missing key shouldn’t block the everyday action. The meal still exists even if the pixels don’t.

    EatNeat is under active development. I’m currently refining the meal flow, reminders, and progress UI while building toward a more complete, privacy-conscious backend.

    If you’re building a habit-focused app, I’d love to hear how you decide which parts should depend on AI and which should keep working on their own.#buildinpublic #mobile #showdev #software #coding #development #engineering #inclusive #community
    # Building EatNeat: Meal Plans, Gentle Reminders, and a Supportive Mascot

  13. The Problem With "Free" Online PDF Converters
    If you have ever used a standard online PDF compressor, converter, or merger, you have almost certainly uploaded your documents to a third-party server. In many cases, that is an unacceptable risk. Typical PDF payloads contain sensitive data:

    Executed contracts and legal agreements
    Patient medical records and diagnoses
    Banking, tax, and financial statements
    Proprietary internal company documentation
    When uploaded, that data travels across external networks, lands on third-party file systems (often indefinitely), and is processed by infrastructure you cannot audit.

    We set out to build a different model: a document processing suite where files never leave the user device. Here is the technical stack and architecture that makes client-side PDF execution possible.

    Architecture Overview

    ┌─────────────────────────────────────────────┐
    │ Browser │
    │ │
    │ ┌──────────┐ ┌──────────────────────┐ │
    │ │ File API │───▶│ WebAssembly Module │ │
    │ └──────────┘ │ (pdf-lib / PDF.js / │ │
    │ │ custom WASM binary) │ │
    │ ┌──────────┐ └──────────┬───────────┘ │
    │ │ Canvas │◀──────────────┘ │
    │ │ Blob │ │
    │ │ URL │ │
    │ └──────────┘ │
    └─────────────────────────────────────────────┘
    ↕ Zero network requests for file data
    All heavy tasks execute inside dedicated Web Workers to ensure the main UI thread stays at a fluid 60 FPS. Once processing finishes, the output is converted into an in-memory Blob URL and handed directly to the user for immediate download. Not a single byte of file content is sent over the wire.

    Core Stack
    Layer Technology
    PDF parsing & manipulation pdf-lib + PDF.js
    Computation offloading Dedicated Web Workers
    Image compression (WASM) @jsquash/mozjpeg + @jsquash/webp
    Application framework Next.js (App Router)
    State management React useReducer + useRef
    Step 1: Reading PDFs with the HTML5 File API
    The first milestone is reading document bytes into local browser memory without triggering network activity:

    javascript

    // Local memory read — zero network interaction
    async function readPDFFile(file) {
    return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = (e) => resolve(e.target.result); // ArrayBuffer
    reader.onerror = reject;
    reader.readAsArrayBuffer(file);
    });
    }
    // Event handler
    const input = document.getElementById('file-input');
    input.addEventListener('change', async (e) => {
    const file = e.target.files[0];
    const buffer = await readPDFFile(file);

    // buffer is an ArrayBuffer allocated strictly in local client memory
    processPDF(buffer);
    });
    Using FileReader.readAsArrayBuffer() provides raw byte access inside JavaScript memory. From this point forward, every operation operates strictly as local computation.

    Step 2: Document Manipulation via pdf-lib
    pdf-lib is a standalone JavaScript library that runs natively in the browser without any Node.js runtime or server dependencies. Here is how document merging is implemented client-side:

    javascript

    import { PDFDocument } from 'pdf-lib';
    async function mergePDFs(arrayBuffers) {
    // Initialize a new empty PDF document
    const mergedPdf = await PDFDocument.create();
    for (const buffer of arrayBuffers) {
    // Load each PDF from ArrayBuffer in memory
    const sourcePdf = await PDFDocument.load(buffer);

    // Copy all pages into the merged target document
    const pages = await mergedPdf.copyPages(
    sourcePdf,
    sourcePdf.getPageIndices()
    );

    pages.forEach((page) => mergedPdf.addPage(page));

    }
    // Serialize to Uint8Array within client memory
    const mergedBytes = await mergedPdf.save();

    // Create an instant download URL without remote servers
    return URL.createObjectURL(
    new Blob([mergedBytes], { type: 'application/pdf' })
    );
    }
    This entire merge cycle runs synchronously in memory without a single outbound network packet.

    Step 3: Offloading Computation to Web Workers
    PDF serialization and parsing can easily freeze the main JavaScript thread on documents with hundreds of pages. We isolate all computation inside a background Web Worker:

    javascript

    // worker.js
    import { PDFDocument } from 'pdf-lib';
    self.onmessage = async ({ data }) => {
    const { type, payload } = data;

    try {
    switch (type) {
    case 'COMPRESS': {
    const result = await compressPDF(payload.buffer, payload.quality);
    // Transfer the ArrayBuffer back (zero-copy memory transfer)
    self.postMessage({ success: true, buffer: result }, [result]);
    break;
    }
    case 'MERGE': {
    const result = await mergePDFs(payload.buffers);
    self.postMessage({ success: true, buffer: result }, [result]);
    break;
    }
    }
    } catch (err) {
    self.postMessage({ success: false, error: err.message });
    }
    };
    javascript

    // main thread
    const worker = new Worker(new URL('./worker.js', import.meta.url));
    function processPDFInWorker(buffer) {
    return new Promise((resolve, reject) => {
    worker.postMessage(
    { type: 'COMPRESS', payload: { buffer, quality: 0.8 } },
    [buffer] // Transferable object: passes pointer without memory duplication
    );

    worker.onmessage = ({ data }) => {
    if (data.success) resolve(data.buffer);
    else reject(new Error(data.error));
    };

    });
    }
    By specifying [buffer] in the transfer list, ownership of the memory block is transferred instantly to the worker thread without invoking expensive structured clone copying.

    Step 4: PDF Compression with WebAssembly Codecs
    File sizes in scanned or multi-page documents are predominantly caused by embedded raster images. Effective compression requires:

    Rendering each PDF page to an offscreen canvas
    Re-encoding the canvas pixels using WebAssembly-compiled image encoders
    Re-assembling the PDF with re-quantized image streams
    javascript

    import { getDocument } from 'pdfjs-dist';
    import * as mozjpeg from '@jsquash/mozjpeg';
    async function compressPDFImages(buffer, quality = 0.75) {
    const pdfDoc = await getDocument({ data: buffer }).promise;
    const newPdf = await PDFDocument.create();
    for (let i = 1; i <= pdfDoc.numPages; i++) {
    const page = await pdfDoc.getPage(i);
    const viewport = page.getViewport({ scale: 1.5 }); // Target 144 DPI

    // Render to OffscreenCanvas without needing DOM bindings
    const canvas = new OffscreenCanvas(
    viewport.width,
    viewport.height
    );
    const ctx = canvas.getContext('2d');

    await page.render({ canvasContext: ctx, viewport }).promise;

    // Extract raw image data
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    // Encode via MozJPEG WebAssembly
    const compressedBytes = await mozjpeg.encode(imageData, { quality: quality * 100 });

    // Embed back into destination document
    const jpgImage = await newPdf.embedJpg(compressedBytes);
    const newPage = newPdf.addPage([viewport.width, viewport.height]);
    newPage.drawImage(jpgImage, {
    x: 0, y: 0,
    width: viewport.width,
    height: viewport.height,
    });

    }
    return await newPdf.save();
    }
    Why MozJPEG over Native Canvas Encoders?
    Browser native canvas toBlob() implementations prioritize encoding speed over file size compression. MozJPEG compiled to WebAssembly consistently produces 30% to 50% smaller image files at equivalent perceptual quality metrics (SSIM and PSNR). The gzipped WASM module weighs only around 200 KB and runs near native execution speeds.

    Performance Benchmarks
    Benchmark performed on a 10-page scanned color document (original payload: 8.2 MB):

    Processing Method Output Size Visual Fidelity Duration
    Native canvas.toBlob() (quality 0.8) 4.1 MB Good 1.2s
    MozJPEG WASM (quality 75) 2.3 MB Sharp 1.8s
    MozJPEG WASM (quality 60) 1.4 MB Acceptable 1.8s
    Server-side Ghostscript engine 1.1 MB Good ~3s + upload transit
    The client-side WASM pipeline approaches within 30% of server-side Ghostscript performance while completely eradicating upload latency and security risks.

    Step 5: Memory Management and Streaming
    When loading a 50 MB document, naïve implementations can easily inflate memory usage to over 200 MB due to multiple concurrent representations (ArrayBuffer, parsed AST, canvas pixel arrays).

    To prevent tab crashes on memory-constrained devices:

    javascript

    // Stream processing: parse and clean page-by-page
    async function* streamPages(buffer) {
    const pdfDoc = await getDocument({ data: buffer }).promise;

    for (let i = 1; i <= pdfDoc.numPages; i++) {
    const page = await pdfDoc.getPage(i);
    yield page;

    // Explicitly evict page cache from PDF.js internal store
    page.cleanup();

    }
    }
    // Bounded heap iteration
    for await (const page of streamPages(buffer)) {
    await processPage(page);
    }
    For files exceeding 100 MB, the application advises splitting the document before applying compression or conversions — a pattern supported directly in KAOpdf Split PDF.

    Handling Edge Cases
    Password-Protected Documents
    javascript

    async function loadProtectedPDF(buffer, password) {
    try {
    return await PDFDocument.load(buffer, {
    password,
    ignoreEncryption: false
    });
    } catch (err) {
    if (err.message.includes('password')) {
    throw new Error('Incorrect password entered.');
    }
    if (err.message.includes('encrypted')) {
    throw new Error('Unsupported encryption standard (256-bit AES).');
    }
    throw err;
    }
    }
    Corrupted or Non-Standard PDF Streams
    PDF.js is significantly more tolerant of damaged cross-reference tables than strict parsers. When structural modification fails in pdf-lib, we fall back to PDF.js rendering:

    javascript

    async function robustLoad(buffer) {
    try {
    return await PDFDocument.load(buffer);
    } catch {
    // Fallback: render damaged pages via PDF.js and reconstruct
    const pdfJsDoc = await getDocument({
    data: buffer,
    stopAtErrors: false
    }).promise;
    return rebuildFromRenderedPages(pdfJsDoc);
    }
    }
    Security Model and HTTP Headers
    Moving processing to the client eliminates data breach vectors on the backend, but requires strict client-side browser controls.

    Security Guarantees
    Zero server breach exposure for uploaded files
    Immunity to man-in-the-middle transmission interception
    Zero regulatory storage liability (GDPR, HIPAA compliance by design)
    Client Configuration Requirements
    To enable high-performance multithreaded WASM and SharedArrayBuffer, modern browsers mandate Cross-Origin Isolation headers:

    javascript

    // next.config.js — Cross-Origin Isolation headers
    const nextConfig = {
    async headers() {
    return [
    {
    source: '/(.*)',
    headers: [
    { key: 'Cross-Origin-Opener-Policy', value: 'same-origin' },
    { key: 'Cross-Origin-Embedder-Policy', value: 'require-corp' },
    ],
    },
    ];
    },
    };
    Production Implementation at KAOpdf
    This architecture is currently running live in production across more than 35 web-based tools on KAOpdf:

    Compress PDF — MozJPEG WebAssembly quantization
    Merge PDF — In-memory page aggregation
    Split PDF — Client-side page extraction
    OCR PDF — Offline Tesseract.js WASM character recognition
    AI PDF Summarizer — On-device WebGPU model execution
    Across all tools, the fundamental contract remains unchanged: zero file uploads, zero remote storage, and complete user privacy.

    Engineering Takeaways

    1.pdf-lib for structure, PDF.js for rendering: pdf-lib handles fast document generation, while PDF.js is the most resilient parser for non-standard PDFs.

    2.Transferables are mandatory: Always pass ArrayBuffers in worker transfer lists to prevent costly memory duplication.

    1. WebAssembly image codecs deliver real gains: WASM-compiled MozJPEG consistently beats native browser canvas encoders in compression efficiency.
    2. Enforce bounded memory loops: Always call .cleanup() after rendering pages to prevent tab crashes on mobile browsers.

    Resources and Documentation
    pdf-lib Documentation
    pdfjs-dist on npm
    @jsquash/mozjpeg on npm
    KAOpdf Live Tools

    If you are experimenting with client-side document processing or running into WebAssembly memory bottlenecks, feel free to share your thoughts or questions in the discussion below.#webassembly #ai #pdf #webdev #software #coding #development #engineering #inclusive #community
    How I Built a Privacy-First PDF Processor That Never Uploads Your Files

  14. Understanding cosine similarity, Euclidean distance, and how machines find relevant text and images.

    1. Why do we need semantic search?


    Imagine you're building a search feature for your application.

    A user searches:

    How do I make my application faster?


    Your database contains these articles:

    1. How to optimize database queries
    2. Improving API performance with caching
    3. A beginner's guide to JavaScript arrays
    4. How to bake a chocolate cake

    Which articles should appear first?

    Traditional keyword search looks for matching words. Since the first two articles don't contain the exact phrase "make my application faster," a basic keyword search might rank them poorly.

    Semantic search takes a different approach: it searches for meaning, not just matching words.

    It can recognize that "make my application faster," "optimize database queries," and "improve API performance" may describe related ideas.

    To understand how semantic search works, we need three concepts:

    • Embeddings: Representing text or images as numbers.
    • Vectors: Working with those numbers mathematically.
    • Similarity and distance: Comparing vectors to rank relevant results.

    Let's build these concepts from scratch using simple mathematics and JavaScript.

    2. What is a vector?


    A vector is an ordered list of numbers.

    For example:

    const vector = [3, 4];

    We can imagine this vector as a point on a two-dimensional graph:

    • 3 represents the horizontal coordinate.
    • 4 represents the vertical coordinate.

    The point is therefore located at (3, 4).

    A vector can have two dimensions, three dimensions, or thousands of dimensions.

    For learning purposes, we'll use two-dimensional vectors because they're easy to visualize and calculate by hand.

    Important: These examples use illustrative numbers. Real embedding dimensions generally don't have simple, individually interpretable meanings.

    3. What is an embedding?


    An embedding is a numerical representation of content generated by a machine-learning model.

    Imagine an embedding model converts three sentences into vectors:

    SentenceIllustrative embedding"Make my app faster"[0.9, 0.1]"Improve application performance"[0.8, 0.2]"Chocolate cake recipe"[0.1, 0.9]


    The first two vectors point in similar directions. The third points in a different direction.

    This gives us a way to compare content mathematically.

    The embedding model learns useful representations during training. Our search system uses those representations to find and rank related content.

    A simplified semantic-search pipeline looks like this:

    User query
    |
    v
    Embedding model
    |
    v
    Query vector
    |
    v
    Compare with document vectors
    |
    v
    Rank relevant documents

    Now let's explore the mathematics behind those comparisons.

    4. Vector magnitude: how long is a vector?


    Before comparing vectors, let's understand their length, also called their magnitude or L2 norm.

    Suppose we have:

    a = [3, 4]

    The magnitude is calculated using the Pythagorean theorem.

    Step 1: Square both coordinates.

    ‖a‖₂ = √(3² + 4²)

    Step 2: Simplify the squares.

    ‖a‖₂ = √(9 + 16)

    Step 3: Add the values and calculate the square root.

    ‖a‖₂ = √25 = 5

    Therefore, the magnitude of vector [3, 4] is 5.

    In simple terms, the magnitude tells us how far the point is from the origin, (0, 0).

    Implementing magnitude in JavaScript


    function magnitude(vector) {
    const sumOfSquares = vector.reduce(
    (sum, value) => sum + value * value,
    0
    );

    return Math.sqrt(sumOfSquares);
    }

    console.log(magnitude([3, 4])); // 5

    Here's what the code does:

    1. Squares every value.
    2. Adds the squared values together.
    3. Takes the square root of the sum.

    We'll reuse this function when calculating cosine similarity.

    5. Euclidean distance: how far apart are two vectors?


    Magnitude measures the distance from the origin. Euclidean distance measures the distance between two points.

    Suppose we have two points.

    Point A

    a = [1, 2]

    Point B

    b = [4, 6]

    How far apart are they?

    First, calculate the difference between their coordinates:

    • Horizontal difference: 4 − 1 = 3
    • Vertical difference: 6 − 2 = 4

    Using the Pythagorean theorem:

    d = √(3² + 4²)

    d = √(9 + 16)

    d = √25 = 5

    The Euclidean distance is 5 units.

    The general L2 distance formula


    For two vectors with the same number of dimensions:

    dL2(a, b) = √[Σᵢ₌₁ⁿ (aᵢ − bᵢ)²]

    You don't need to memorize the notation immediately. Remember the process:

    1. Subtract corresponding coordinates.
    2. Square each difference.
    3. Add the squared differences.
    4. Take the square root.


    Implementing L2 distance in JavaScript


    function l2Distance(a, b) {
    if (a.length !== b.length) {
    throw new Error("Vectors must have equal dimensions");
    }

    const sumOfSquares = a.reduce((sum, value, i) => {
    const difference = value - b[i];
    return sum + difference * difference;
    }, 0);

    return Math.sqrt(sumOfSquares);
    }

    const a = [1, 2];
    const b = [4, 6];

    console.log(l2Distance(a, b)); // 5

    How does this help with semantic search?


    Imagine our query has already been converted into a vector:

    const query = [2, 3];

    We also have two document embeddings:

    const documentA = [4, 4];
    const documentB = [2, 5];

    Let's calculate the distance from the query to each document.

    Document A

    Distance = √((2 − 4)² + (3 − 4)²)

    Distance = √(4 + 1)

    Distance ≈ 2.236

    Document B

    Distance = √((2 − 2)² + (3 − 5)²)

    Distance = √(0 + 4)

    Distance = 2

    Document B is closer to the query because 2 is smaller than 2.236.

    When ranking by L2 distance, a smaller distance means the vectors are closer together.

    However, Euclidean distance isn't the only way to compare vectors. Another popular metric is cosine similarity.

    6. Dot product: multiplying corresponding values


    The dot product is an operation used in vector mathematics. It also plays an important role in calculating cosine similarity.

    Suppose we have two vectors:

    a = [2, 3]

    b = [4, 5]

    Multiply their corresponding values and add the results:

    a · b = (2 × 4) + (3 × 5)

    a · b = 8 + 15 = 23

    The dot product is 23.

    Implementing dot product in JavaScript


    function dotProduct(a, b) {
    if (a.length !== b.length) {
    throw new Error("Vectors must have equal dimensions");
    }

    return a.reduce(
    (sum, value, i) => sum + value * b[i],
    0
    );
    }

    console.log(dotProduct([2, 3], [4, 5])); // 23

    The dot product combines information from corresponding dimensions. When divided by the magnitudes of both vectors, it gives us the cosine of the angle between them.

    That is the foundation of cosine similarity.

    7. Cosine similarity: are the vectors pointing in the same direction?


    Euclidean distance asks:

    How far apart are two points?

    Cosine similarity asks:

    How closely do two vectors point in the same direction?

    Its formula is:

    cosineSimilarity(a, b) = (a · b) / (‖a‖₂ × ‖b‖₂)

    Let's work through an example.

    Suppose:

    a = [1, 2]

    b = [2, 4]

    Notice that vector B is twice vector A. Both point in exactly the same direction.

    Step 1: Calculate the dot product


    a · b = (1 × 2) + (2 × 4) = 10

    Step 2: Calculate both magnitudes


    For vector A:

    ‖a‖₂ = √(1² + 2²) = √5

    For vector B:

    ‖b‖₂ = √(2² + 4²) = √20

    Step 3: Calculate cosine similarity


    cosineSimilarity = 10 / (√5 × √20)

    Since √5 × √20 = 10:

    cosineSimilarity = 10 / 10 = 1

    The cosine similarity is 1, meaning the vectors point in exactly the same direction.

    Interpreting cosine similarity

    ScoreGeometric interpretation1Same direction0.8Strong positive directional alignment0Perpendicular directions-1Opposite directions


    These are geometric interpretations, not universal thresholds for semantic relevance. Whether a score indicates similar meaning depends on the embedding model and the data.

    Implementing cosine similarity in JavaScript


    function magnitude(vector) {
    return Math.sqrt(
    vector.reduce((sum, value) => sum + value * value, 0)
    );
    }

    function dotProduct(a, b) {
    if (a.length !== b.length) {
    throw new Error("Vectors must have equal dimensions");
    }

    return a.reduce(
    (sum, value, i) => sum + value * b[i],
    0
    );
    }

    function cosineSimilarity(a, b) {
    const magnitudeA = magnitude(a);
    const magnitudeB = magnitude(b);

    if (magnitudeA === 0 || magnitudeB === 0) {
    throw new Error(
    "Cosine similarity is undefined for zero vectors"
    );
    }

    return dotProduct(a, b) / (magnitudeA * magnitudeB);
    }

    console.log(cosineSimilarity([1, 2], [2, 4])); // 1
    console.log(cosineSimilarity([1, 0], [0, 1])); // 0
    console.log(cosineSimilarity([1, 0], [-1, 0])); // -1

    Cosine similarity is useful because multiplying a nonzero vector by a positive number doesn't change its direction. Consequently, cosine similarity is unaffected by positive scaling of either vector.

    This can be helpful when comparing embeddings whose lengths differ.

    8. Cosine distance: turning similarity into distance


    Sometimes we want a distance metric where lower values indicate a closer match.

    A common definition is:

    cosineDistance = 1 − cosineSimilarity

    Suppose Document A has a cosine similarity score of 0.9.

    Distance A = 1 − 0.9 = 0.1

    Document B has a cosine similarity score of 0.6.

    Distance B = 1 − 0.6 = 0.4

    Document A is the closer match under this metric because its cosine distance is smaller.

    Implementing cosine distance


    function cosineDistance(a, b) {
    return 1 - cosineSimilarity(a, b);
    }

    console.log(cosineDistance([1, 2], [2, 4])); // 0
    console.log(cosineDistance([1, 0], [0, 1])); // 1

    Remember the distinction:

    • Cosine similarity: Higher is better for directional alignment.
    • Cosine distance: Lower is better.
    • L2 distance: Lower means the points are closer.

    Implementations and vector databases may define or report metrics differently, so check the documentation for the system you're using.

    9. L2 distance vs. cosine similarity


    Let's compare two vectors:

    a = [1, 1]

    b = [10, 10]

    These vectors point in exactly the same direction, but their magnitudes differ.

    Their cosine similarity is:

    cosineSimilarity(a, b) = 1

    Their cosine distance is:

    cosineDistance = 1 − 1 = 0

    Now calculate their Euclidean distance:

    dL2 = √((1 − 10)² + (1 − 10)²)

    dL2 = √(81 + 81)

    dL2 = √162 ≈ 12.728

    The vectors have perfect directional alignment, yet their Euclidean distance is greater than 12.

    This illustrates the key difference:

    PropertyL2 distanceCosine similarityMeasuresDistance between coordinatesAlignment of directionsAffected by vector magnitude?YesNot by positive scalingBetter scoreSmallerLargerUseful whenCoordinate distance mattersDirectional alignment matters


    Neither metric is always better. The right choice depends on how the embedding model was trained and how its vectors should be compared.

    10. Build a simple semantic-ranking example


    Now let's combine our functions to build a small vector-ranking demonstration in JavaScript.

    Imagine an embedding model has already converted a search query and three documents into two-dimensional vectors.

    const query = [0.8, 0.6];

    const documents = [
    {
    title: "API performance optimization",
    embedding: [0.9, 0.4]
    },
    {
    title: "Database query optimization",
    embedding: [0.7, 0.7]
    },
    {
    title: "Chocolate cake recipe",
    embedding: [-0.5, 0.8]
    }
    ];

    These vectors are invented for demonstration. In a real application, an embedding model would generate them.

    Let's rank the documents by cosine similarity.

    function magnitude(vector) {
    return Math.sqrt(
    vector.reduce((sum, value) => sum + value * value, 0)
    );
    }

    function cosineSimilarity(a, b) {
    if (a.length !== b.length) {
    throw new Error("Vectors must have equal dimensions");
    }

    const dot = a.reduce(
    (sum, value, i) => sum + value * b[i],
    0
    );

    const normA = magnitude(a);
    const normB = magnitude(b);

    if (normA === 0 || normB === 0) {
    throw new Error("Zero vectors are not supported");
    }

    return dot / (normA * normB);
    }

    const rankedDocuments = documents
    .map(document => ({
    title: document.title,
    score: cosineSimilarity(query, document.embedding)
    }))
    .sort((a, b) => b.score - a.score);

    console.log(rankedDocuments);

    The results are approximately:

    [
    {
    title: "Database query optimization",
    score: 0.990
    },
    {
    title: "API performance optimization",
    score: 0.975
    },
    {
    title: "Chocolate cake recipe",
    score: 0.085
    }
    ]

    We sort the results in descending order because higher cosine similarity means stronger directional alignment.

    Notice something important: our JavaScript code doesn't understand language. It only compares vectors.

    The embedding model is responsible for learning useful representations of language. The ranking code uses those representations to order the results.

    Our example demonstrates the mechanics of vector search, not a complete semantic search system.

    11. Where do real embeddings come from?


    So far, we've manually supplied vectors such as [0.8, 0.6].

    Real applications use an embedding model to generate vectors from text or images.

    A typical workflow looks like this:

    1. Choose an embedding model.
    2. Convert each document into a vector.
    3. Store the vectors alongside document IDs and content.
    4. Convert the user's query into a vector using a compatible model.
    5. Find the nearest document vectors.
    6. Return the corresponding documents in ranked order.

    A JavaScript application might call an embedding API like this:

    // Illustrative pseudocode.
    // Replace this with your embedding provider's SDK.

    const queryEmbedding = await embeddingModel.embed(
    "How do I make my application faster?"
    );

    const documentEmbedding = await embeddingModel.embed(
    "Improve API performance with caching."
    );

    const score = cosineSimilarity(
    queryEmbedding,
    documentEmbedding
    );

    console.log(score);

    The exact API depends on your embedding provider. The key requirement is that both vectors must come from compatible embedding representations.

    For thousands or millions of documents, calculating every distance in application code can become expensive. A vector database or search engine can index embeddings and retrieve nearby candidates more efficiently.

    12. Does the same mathematics work for image search?


    Yes. The comparison mathematics is the same, but the process that generates the vectors differs.

    Imagine you're building an image-search application. A user enters:

    A dog playing on a beach.


    A compatible text-image model converts the query into an embedding. It also converts the available images into embeddings in a shared vector space.

    The application compares the query vector against the image vectors and ranks the images by similarity.

    The process looks like this:

    Text query
    |
    v
    Text encoder
    |
    v
    Query embedding
    |
    v
    Compare against image embeddings
    |
    v
    Rank matching images

    This works directly only when the model is designed to produce comparable text and image embeddings.

    Why not simply compare raw pixels?

    Two photographs of the same dog might have different lighting, camera angles, cropping, or backgrounds. Raw pixel differences may be large even when the images depict similar content.

    Learned image embeddings can capture useful visual patterns that raw pixel distance misses.

    However, the model can still make mistakes. A high similarity score is a ranking signal, not proof that an image contains every detail in the query.

    13. Why normalized vectors matter


    Some embedding models produce vectors with a magnitude of 1. These are called L2-normalized vectors.

    For unit-normalized vectors, Euclidean distance and cosine similarity are mathematically related:

    L2 distance² = 2 × (1 − cosine similarity)

    Equivalently:

    L2 distance = √[2 × (1 − cosine similarity)]

    This means that ranking the same unit-normalized vectors by L2 distance or cosine similarity produces the same ordering:

    • Higher cosine similarity means lower L2 distance.
    • Lower cosine similarity means higher L2 distance.

    This relationship can be useful when choosing a metric for a vector index.

    Don't assume every embedding model automatically normalizes its output. Check the model documentation before relying on this property.

    14. What would I use in a real JavaScript application?


    For a practical semantic-search feature, I'd separate the system into four responsibilities.

    1. Embedding model

    Converts text or images into vectors that represent useful features of the content.

    2. Vector database

    Stores the vectors and retrieves nearby candidates efficiently.

    3. Similarity metric

    Determines how the system compares vectors, using cosine similarity, Euclidean distance, or another supported metric.

    4. Application code

    Handles queries, metadata, filtering, ranking, and displaying results to users.

    Start with the metric recommended by your embedding model, then test it against real queries and expected results.

    For example, if you're building documentation search, create a test set of queries such as "make API faster" and identify which articles should appear first. Measure whether those articles show up in the top five results.

    That gives you a practical way to evaluate your search quality rather than choosing a metric based only on intuition.

    You may also want hybrid search, which combines vector similarity with keyword search. This can be especially useful when users search for exact identifiers, error messages, version numbers, or technical terms that must match precisely.

    Conclusion


    Semantic search becomes much easier to understand when we break it into smaller mathematical concepts.

    A vector is an ordered list of numbers. An embedding model converts text or images into vectors. Euclidean distance measures how far apart two vectors are, while cosine similarity measures how closely their directions align. Cosine distance converts similarity into a distance-style score.

    With a few JavaScript functions, we can implement these calculations and build a simple vector-ranking demonstration.

    The most important distinction is this:

    Vector mathematics gives us a way to compare representations. The embedding model learns representations that can capture useful relationships in meaning.

    Both parts matter when building a useful semantic-search system.#ai #machinelearning #search #software #coding #development #engineering #inclusive #community
    How Machines Find Meaning: The Math Behind Semantic Search

  15. Reklam panelinde "lead" sayısı artarken satış ekibinin "bu lead'lerin hiçbiri alıcı değil" demesi, dijital pazarlamada en sık duyduğum cümlelerden biri. Ben Koray Yalçın; lead yönetimi, CRM mimarisi, dijital pazarlama ve yapay zekâ destekli otomasyonların kesiştiği projelerde çalışıyorum, özellikle gayrimenkul ve B2B tarafında. Bu cümlenin arkasında çoğu zaman teknik bir kopukluk yatar: Reklam platformu formun doldurulduğunu bilir, ama o kişinin üç hafta sonra showroom'a gelip sözleşme imzaladığını bilmez. Algoritma da doğal olarak form doldurmaya en yatkın kitleyi bulmaya devam eder, satın almaya en yatkın kitleyi değil.

    Bu boşluğu kapatmanın yolu, CRM'de oluşan "nitelikli lead", "randevu gerçekleşti" ve "satış kapandı" gibi olayları sunucu tarafından Meta Conversions API'ye (CAPI) geri göndermek. Bu yazı bir pazarlama rehberi değil; bu köprüyü kodla kurarken karşılaştığım tasarım kararlarını anlatıyor: kullanıcı verisinin normalizasyonu ve hash'lenmesi, olay kimliği ile tekilleştirme, zaman penceresi, yeniden deneme stratejisi ve izin kontrolü. Örnekler Python ama mantık her dile taşınabilir.

    Önce olay sözlüğü: Hangi CRM durumu hangi olay?


    Kod yazmadan önce yapılması gereken en önemli iş, CRM'deki aşamaları sınırlı ve anlamlı bir olay listesine eşlemek. Her aşama değişikliğini Meta'ya göndermek gürültü üretir. Benim kullandığım tipik eşleme şöyle:

    CRM olayıGönderilen olay adıNotForm / lead oluştuLeadGenellikle tarayıcı pikseli zaten gönderirTemsilci "nitelikli" işaretlediQualifiedLead (özel olay)Ölçüm kurgusuna göre seçilirRandevu gerçekleştiSchedule veya özel olay"Randevu alındı" değil, "gerçekleşti"Sözleşme imzalandıPurchasevalue ve currency zorunlu düşünülmeli


    Burada kritik nokta şu: Olay, CRM'de geri alınabilir bir alan değişikliğinden değil, kalıcı bir olaydan tetiklenmeli. Temsilci bir fırsatı yanlışlıkla "kazanıldı" yapıp beş dakika sonra geri alırsa, gönderilmiş bir Purchase olayını geri çekmenin pratik bir yolu yoktur. Bu yüzden ben aşama değişikliklerini doğrudan API'ye bağlamak yerine bir conversion_events tablosuna yazıyorum ve gönderimi kısa bir bekleme süresinden sonra yapan ayrı bir işçiye bırakıyorum.

    CREATE TABLE conversion_events (
    id BIGSERIAL PRIMARY KEY,
    event_id TEXT NOT NULL UNIQUE, -- tekilleştirme anahtarı
    event_name TEXT NOT NULL,
    lead_id BIGINT NOT NULL REFERENCES leads(id),
    occurred_at TIMESTAMPTZ NOT NULL, -- olayın gerçek zamanı
    value_minor BIGINT, -- kuruş/cent cinsinden
    currency CHAR(3),
    status TEXT NOT NULL DEFAULT 'pending', -- pending, sent, failed, skipped
    attempts INT NOT NULL DEFAULT 0,
    last_error TEXT,
    send_after TIMESTAMPTZ NOT NULL DEFAULT now() + interval '15 minutes'
    );

    send_after alanı, "yanlışlıkla kazanıldı" senaryosuna karşı küçük bir tampon. İşçi göndermeden önce fırsatın hâlâ aynı aşamada olup olmadığını kontrol eder; değilse kaydı skipped yapar.

    Kullanıcı verisini normalize etmek: Hash'ten önce gelen asıl iş


    CAPI, e-posta ve telefon gibi kişisel verilerin SHA-256 ile hash'lenmiş olarak gönderilmesini bekler. Çoğu ekip hashlib.sha256(email) yazıp işi bitmiş sayar. Sorun şu ki hash, girdideki tek bir boşluğu ya da büyük harfi tamamen farklı bir değere dönüştürür. Meta tarafındaki eşleşme de birebir karşılaştırmayla çalıştığı için normalizasyon hatası, sessizce düşük eşleşme oranı olarak geri döner.

    Benim sahada gördüğüm en yaygın hatalar:

    • E-postada baştaki/sondaki boşluklar ve büyük harfler.
    • Telefonun 0533 ..., +90 533 ..., 90533... gibi farklı biçimlerde saklanması.
    • İsimlerde Türkçe karakterlerin farklı yazımları ve "Dr." gibi önekler.
    • Boş alanların boş string olarak hash'lenmesi (boş string'in de bir SHA-256 değeri vardır ve anlamsız bir eşleşme anahtarı üretir).

    import hashlib
    import re
    import phonenumbers

    def _sha256(value: str) -> str:
    return hashlib.sha256(value.encode("utf-8")).hexdigest()

    def norm_email(raw: str | None) -> str | None:
    if not raw:
    return None
    v = raw.strip().lower()
    return v if "@" in v else None

    def norm_phone(raw: str | None, default_region: str = "TR") -> str | None:
    if not raw:
    return None
    try:
    num = phonenumbers.parse(raw, default_region)
    except phonenumbers.NumberParseException:
    return None
    if not phonenumbers.is_valid_number(num):
    return None
    # Ülke kodu dahil, yalnızca rakamlar, başta + yok
    return phonenumbers.format_number(num, phonenumbers.PhoneNumberFormat.E164).lstrip("+")

    def build_user_data(lead) -> dict:
    ud = {}
    if (e := norm_email(lead.email)):
    ud["em"] = [_sha256(e)]
    if (p := norm_phone(lead.phone, lead.country_code or "TR")):
    ud["ph"] = [_sha256(p)]
    if lead.external_id:
    ud["external_id"] = [_sha256(str(lead.external_id))]
    # Tarayıcıdan yakalandıysa hash'lenmeden gönderilen alanlar
    if lead.fbc:
    ud["fbc"] = lead.fbc
    if lead.fbp:
    ud["fbp"] = lead.fbp
    return ud

    Bir detay daha: Kıbrıs gibi çok uluslu alıcı kitlesi olan pazarlarda default_region sabit "TR" olamaz. İngiltere'den gelen bir lead'in 07... ile başlayan numarası Türkiye varsayımıyla ayrıştırılırsa geçersiz sayılır ya da daha kötüsü yanlış bir numara üretir. Bu yüzden formda ülke seçimini ayrı bir alan olarak tutmak, normalizasyonun doğruluğunu doğrudan etkiliyor.

    fbc, fbp ve external_id: Eşleşmeyi güçlendiren köprüler


    E-posta ve telefon tek başına çoğu zaman yeterli olur, ama iki alan eşleşme kalitesini belirgin biçimde artırır:

    • fbc: Reklam tıklamasıyla gelen fbclid parametresinden türetilen tıklama kimliği. Kullanıcı sitenize reklamla geldiyse _fbc çerezinde bulunur.
    • fbp: Pikselin tarayıcıya yazdığı _fbp çerezi.

    Bu değerler satış anında değil, lead oluştuğu anda yakalanmalı. Üç hafta sonra sözleşme imzalanırken kullanıcı tarayıcısında değil, satış ofisinde oturuyor. Bu nedenle form gönderiminde bu iki çerezi gizli alanlarla alıp lead kaydına yazmak, sonraki tüm offline olayların kalitesini belirliyor. Meta Lead Ads formlarından gelen lead'lerde ise platformun verdiği lead kimliğini saklamak aynı işi görür; CRM entegrasyonu senaryolarında bu kimliğin olayla birlikte gönderilmesi eşleşmeyi doğrudan kurar.

    external_id ise sizin CRM'deki kişi kimliğiniz. Hem piksel hem sunucu tarafında aynı değer (hash'lenmiş olarak) gönderilirse, aynı kişinin farklı olaylarını birbirine bağlamak kolaylaşır.

    event_id ile tekilleştirme: Aynı satışı iki kez saymamak


    CAPI'de iki ayrı tekrar riski vardır:

    1. Piksel ile sunucu çakışması: Lead olayını hem tarayıcı pikseli hem sunucu gönderiyorsa, iki olay aynı event_name ve aynı event_id ile gelmelidir. Meta bu ikiliyi tek olay olarak sayar.
    2. Yeniden deneme çakışması: Ağ zaman aşımı yüzünden isteğin ulaşıp ulaşmadığını bilmiyorsanız ve tekrar gönderirseniz, aynı event_id korunmalıdır.

    event_id rastgele üretilmemeli; olaydan deterministik olarak türetilmeli. Benim tercihim şu:

    def make_event_id(event_name: str, lead_id: int, stage_version: int) -> str:
    return f"{event_name.lower()}:{lead_id}:{stage_version}"

    stage_version, fırsatın o aşamaya kaçıncı kez girdiğini tutan bir sayaç. Böylece bir müşteri gerçekten ikinci bir daire satın alırsa yeni bir Purchase üretilir, ama aynı satış için yapılan iki gönderim aynı kimliği taşır. Veritabanındaki UNIQUE kısıtı da aynı olayın kuyruğa iki kez girmesini engeller.

    Zaman penceresi: event_time "şimdi" değil, olayın gerçek zamanıdır


    Offline satış entegrasyonunda en sık yapılan hata, event_time alanına gönderim anını yazmak. Olayın değeri, ne zaman gerçekleştiğiyle anlam kazanır; sözleşme salı günü imzalandıysa ve işçi çarşamba gönderdiyse, event_time salı olmalıdır.

    İkinci önemli nokta, Meta'nın kabul ettiği geriye dönük pencere. Bu yazıyı yazarken belgeler, olayların gerçekleştikten sonra sınırlı bir süre içinde gönderilmesini istiyor ve bu süre action_source değerine göre farklılaşabiliyor. Sınır zamanla değişebildiği için kodda sabit bir sayı gömmek yerine yapılandırmadan okuyorum ve pencereyi aşmış olayları göndermeyi denemek yerine skipped olarak işaretleyip raporluyorum:

    from datetime import datetime, timezone, timedelta

    MAX_AGE = { # yapılandırmadan gelir; güncel değeri resmi belgeden doğrulayın
    "system_generated": timedelta(days=7),
    "physical_store": timedelta(days=7),
    }

    def is_sendable(evt, action_source: str) -> bool:
    age = datetime.now(timezone.utc) - evt.occurred_at
    return age <= MAX_AGE.get(action_source, timedelta(days=7))

    Bu kontrolün pratik sonucu, satış ekibinin CRM'i zamanında güncellemesinin bir veri kalitesi konusu haline gelmesi. Sözleşmeyi on gün sonra sisteme giren bir ekip, reklam optimizasyonuna o satışı hiç kazandıramaz. Bu yüzden ben bu entegrasyonu kurarken satış ekibi için "sözleşme imzalandığı gün CRM'de kapanır" kuralını da süreç tarafında yazılı hale getiriyorum.

    action_source ve olay gövdesi


    Sunucudan giden olay gövdesinin iskeleti şöyle:

    def build_payload(evt, lead, action_source="system_generated") -> dict:
    data = {
    "event_name": evt.event_name,
    "event_time": int(evt.occurred_at.timestamp()),
    "event_id": evt.event_id,
    "action_source": action_source,
    "user_data": build_user_data(lead),
    }
    if evt.value_minor is not None:
    data["custom_data"] = {
    "value": evt.value_minor / 100,
    "currency": evt.currency,
    }
    return {"data": [data]}

    action_source alanı, olayın nerede gerçekleştiğini anlatır. Web formu için website, CRM'in kendi ürettiği aşama değişiklikleri için system_generated, satış ofisinde imzalanan sözleşme için physical_store gibi değerler kullanılabilir. Doğru değeri seçmek hem raporlamayı hem de uygulanan kuralları etkiler; bu yüzden olay sözlüğünde her olayın action_source değerini de sabitliyorum.

    value tarafında bir uyarı: Konut gibi yüksek tutarlı satışlarda gerçek sözleşme bedelini göndermek bazı ekiplerin istemediği bir şey olabilir. Bu durumda tutarlı bir "değer skalası" (örneğin ürün tipine göre sabit bir puan) kullanmak, optimizasyon sinyalini korurken hassas ticari bilgiyi dışarı çıkarmamanın bir yolu. Hangisi seçilirse seçilsin, tutarlılık önemlidir; ay ortasında yöntemi değiştirmek raporları kırar.

    Gönderim işçisi: Toplu istek, yeniden deneme ve hata sınıflandırma


    İşçinin görevi basit görünür ama hata sınıflandırması önemli:

    import requests, time

    def send_batch(events, leads, pixel_id, token):
    payload = {"data": []}
    for evt in events:
    payload["data"].extend(build_payload(evt, leads[evt.lead_id])["data"])
    url = f"https://graph.facebook.com/{API_VERSION}/{pixel_id}/events"
    resp = requests.post(url, params={"access_token": token}, json=payload, timeout=15)
    if resp.status_code >= 500 or resp.status_code == 429:
    raise RetryableError(resp.text) # üstel bekleme ile tekrar
    if resp.status_code >= 400:
    raise PermanentError(resp.text) # kaydı failed yap, alarm üret
    return resp.json()

    Kurallarım:

    • 5xx ve hız sınırı hataları yeniden denenir; bekleme süresi her denemede artar ve event_id aynı kalır.
    • 4xx hatalar (geçersiz parametre, süresi dolmuş token) yeniden denenmez. Tekrar göndermek sonucu değiştirmez, yalnızca log kirletir. Bunlar bir alarm kanalına düşer.
    • Token kod içinde değil, gizli değişken yönetiminde tutulur ve süresi izlenir. Sistem kullanıcısı token'ı kullanmak, bir çalışanın hesabı kapandığında entegrasyonun sessizce durmasını engeller.
    • Test olay kodu ile canlıya geçmeden önce olayların Events Manager'daki test ekranında doğru göründüğü doğrulanır.


    İzin ve veri minimizasyonu


    Kişisel veriyi üçüncü bir platforma, hash'lenmiş olsa bile, göndermek bir hukuki dayanak gerektirir. Türkiye'de KVKK, Avrupa'dan gelen alıcılar için GDPR bu çerçeveyi çizer. Benim teknik tarafta uyguladığım kurallar:

    • Lead kaydında reklam ölçümüne ilişkin izin durumu ayrı bir alan olarak tutulur ve işçi, izni olmayan kişi için olay göndermez.
    • Yalnızca eşleşme için gerekli alanlar gönderilir; adres, doğum tarihi gibi alanlar "olsa iyi olur" diye eklenmez.
    • İzin geri çekildiğinde, kuyruktaki bekleyen olaylar da iptal edilir.

    Bu bir hukuki görüş değil; ama izin kontrolünü kodun içine bir if olarak gömmek, sonradan "kimin verisi nereye gitti" sorusuna cevap verebilmeyi sağlıyor.

    Ölçüm: Entegrasyon gerçekten çalışıyor mu?


    Kurulumdan sonra baktığım üç gösterge var:

    1. Eşleşme kalitesi: Events Manager'daki olay eşleşme göstergesi. Düşükse ilk şüpheli normalizasyondur.
    2. Gecikme dağılımı: occurred_at ile gönderim zamanı arasındaki fark. Satış ekibinin CRM'i geç güncellemesi burada hemen görünür.
    3. CRM ile panel karşılaştırması: Aylık kapanan satış sayısı ile Meta'nın aldığı Purchase sayısı arasındaki fark. Birebir tutması beklenmez, ama fark aniden açılırsa token, izin veya şema tarafında bir şey bozulmuştur.


    Sıkça Sorulan Sorular


    Piksel zaten kurulu, neden ayrıca CAPI gerekiyor?
    Piksel yalnızca tarayıcıda olan olayları görür. Telefonla kapanan, satış ofisinde imzalanan ya da haftalar sonra gerçekleşen satışlar tarayıcıda olmaz. CAPI, bu olayları CRM'den doğrudan göndermeyi sağlar.

    E-postayı hash'lemeden göndersem Meta kendisi hash'lemez mi?
    Bazı resmi kütüphaneler hash işlemini kendisi yapar, ama kendi kodunuzla gönderiyorsanız normalizasyon ve hash sizin sorumluluğunuzdadır. Kontrolü elde tutmak, hatayı ayıklamayı da kolaylaştırır.

    Aynı satışı yanlışlıkla iki kez gönderirsem ne olur?
    Aynı event_name ve event_id ile gönderildiyse tekilleştirilir. Farklı kimlikle gönderildiyse iki satış sayılır; bu yüzden kimliği deterministik üretmek önemlidir.

    Satış on gün sonra CRM'e girildi, yine de gönderebilir miyim?
    Meta'nın kabul ettiği geriye dönük pencereyi aşmışsa olay reddedilir. Güncel sınırı resmi belgeden kontrol edin ve asıl çözümü süreçte arayın: satış, gerçekleştiği gün CRM'e girilmeli.

    Gerçek sözleşme bedelini göndermek zorunda mıyım?
    Hayır. Tutarlı bir değer skalası da optimizasyon için yeterli sinyal üretebilir. Önemli olan, seçilen yöntemin zaman içinde değişmemesi.

    Bu entegrasyon küçük ekipler için fazla karmaşık değil mi?
    Kuyruk tablosu, normalizasyon fonksiyonu ve bir gönderim işçisi birkaç yüz satır kod demek. Karşılığında reklam algoritması form dolduranları değil, satın alanları öğrenmeye başlar. Lead hacmi düşük ama satış değeri yüksek sektörlerde bu fark özellikle belirgin.

    Koray Yalçın — lead yönetimi, CRM mimarisi, dijital pazarlama ve gayrimenkul teknolojileri (PropTech) üzerine çalışıyorum. CRM'den reklam platformlarına geri bildirim kurarken sizi en çok hangi adım zorladı? Yorumlarda konuşalım.#backend #crm #api #marketing #software #coding #development #engineering #inclusive #community
    CRM'de kapanan satışı Meta Conversions API'ye geri bildirmek: Hash, event_id ve zaman penceresi üzerine geliştirici notları

  16. Give it to me scratches, dents and all! Show me a PS2 with blood in her hair and grit in her teeth any day.

    If you are making retro content, invest in the actual hardware. One of the best parts of retro gaming is that it doesn't rely on poorly-created overly polished AI fever dreams.

    #RetroGaming #VideoGames #Gaming #AI

  17. Yaaaaay! Everyone got to be in a challenge for a change. Thanks Woodz! 🤘

    Oh, Just come into #DragonPony #드래곤포니 's world! 🌍 #WOODZ #우즈#Come_into_my_world

    youtube.com/shorts/UN0qZmwfkMM

  18. Aww Fuma came to cheer me up! :blobhaj_heart:

    #Fuma #AndTeam

  19. Well ok, Eric Nam! 😍

    [4K] ERIC NAM “Psycho” Band LIVE [it's Live]
    youtube.com/watch?v=5E04EbbgFZM

    #EricNam #ItsLive

  20. Thanks everyone who joined me play some super creative game jam games and thanks for the raid, twitch.tv/coleslawski 💖!

    Here’s the VOD with chapter marks:
    ► youtu.be/uMzPz0VnkU0

    #ldjam60 #indiegames #ludumdare

  21. It's #SundayAlbumQuilt time ... huzzah.

    With 10/25 of my top albums SVT related, I can safely say they owned the week. It was mainly Jeonghan and Joshua's fault, but also, 8 away all at once is going to be too sad!

    What did you listen to this week?

    #KPop #JPop #Seventeen

  22. "Maldita borregada. Nos chingaron 30 millones de cabrones"

    Lista en el blog para leer en línea, o descargar. Gratis. Obvio.
    #books #literatura #noir #realidadsucia

    historiasnoirdelabismo.blogspo

Share on Mastodon

Enter the server where you have an account.