<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Camille Guerineau</title>
    <description>The latest articles on DEV Community by Camille Guerineau (@spiritrackingarch).</description>
    <link>https://dev.to/spiritrackingarch</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3986513%2Fab84c618-91c6-4fa5-8c9b-f603e243e1ff.png</url>
      <title>DEV Community: Camille Guerineau</title>
      <link>https://dev.to/spiritrackingarch</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/spiritrackingarch"/>
    <language>en</language>
    <item>
      <title>Building a white-label crystal bracelet builder with vanilla JS, PHP and zero database</title>
      <dc:creator>Camille Guerineau</dc:creator>
      <pubDate>Thu, 15 Oct 2026 13:21:00 +0000</pubDate>
      <link>https://dev.to/spiritrackingarch/building-a-white-label-crystal-bracelet-builder-with-vanilla-js-php-and-zero-database-172k</link>
      <guid>https://dev.to/spiritrackingarch/building-a-white-label-crystal-bracelet-builder-with-vanilla-js-php-and-zero-database-172k</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note from the workshop-lab.&lt;/strong&gt; The algorithmic tools, configurators and articles presented here explore lithotherapy exclusively through the lens of semiotics, cultural anthropology and generative design. This work belongs to an artistic and symbolic practice rooted in fine craftsmanship. It does not, in any way, constitute medical advice, a diagnosis, or an alternative to conventional, evidence-based therapies.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://zenodo.org/records/23428094" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fzenodo.org%2Fstatic%2Fimages%2Feu.png" height="60" class="m-0" width="88"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://zenodo.org/records/23428094" rel="noopener noreferrer" class="c-link"&gt;
            Loch Ness: Algorithmic System for Archetypal Determination and Variable-Geometry Mineral Synergies | Zenodo
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Concept Summary (Abstract) The Loch Ness project redefines the interaction between individual psychology and lithotherapy through a proprietary digital interface. Unlike standardized, fixed-output approaches, this concept rests on two innovative pillars: an original archetypal taxonomy and a variable-geometry mineral sequencing algorithm. 1. The Ontology of the "Noble Monster" The system uses a taxonomy of original archetypes created to reflect the complexity of the human psyche, including its shadow aspects and raw power. The user is not classified according to conventional sociological or demographic criteria, but according to their "wild nature" and their capacity to channel a singular inner force. This classification produces, for each profile, a multidimensional intensity vector rather than a single label. 2. The Variable-Geometry Calculation Algorithm The application processes user data to generate a mineral response whose structure is unique to each profile: Dynamic Sequencing — the number of stones (from 3 to 8 units) is not fixed; it is determined by the algorithm based on the calculated "density" of the archetypal profile. Compensation Synergy — each stone is selected to fulfill a specific functional role (Anchor, Heart, Mind, Shield, Momentum), creating a bespoke vibrational armor rather than an arbitrary assembly. This role-to-stone indirection allows the role nomenclature and the mineral catalog to evolve independently, without recalibrating the entire system with each addition. 3. Algorithmic Pipeline Processing relies on a strictly decoupled three-stage functional pipeline, each stage independently testable: Identity Normalization — identity data (first name(s), last name, date of birth) is transformed into a multidimensional profile vector (intensity, shadow aspect, momentum, stability), with no dependency on rendering or the interface. Density Calculation and Slot Count Resolution — a density function aggregates the profile vector into a scalar score, which is then projected onto the integer interval [3, 8] to dynamically determine the length of the mineral sequence. Role Resolution, then Mineral Selection — for each slot, a functional role is resolved first (Anchor, Heart, Mind, Shield, Momentum), and a stone is then selected from the catalog to fulfill that role in light of the profile vector. 4. Software Architecture The system is implemented as a static single-page web application (SPA), with no dependency on a JavaScript framework, and features the following technical characteristics: Stack — PHP, HTML5, CSS3 (custom properties), vanilla native JavaScript; no dependency on any rendering framework. Generative Rendering Engine — each mineral in the catalog is rendered as a dynamically relying on a static image bank. Dynamic Geometric Layout — the position of each stone on the support (bracelet) is calculated trigonometrically based on its index and the total number of slots resolved by the algorithm (a variable N, not a constant), with automatic bead-size scaling according to the sequence's density. State Management — the composition state is represented as an ordered array of beads, with undo/redo history stacks and targeted re-rendering (rather than a full re-render) on each mutation, to preserve smoothness on mobile. Progressive Web App — an installation manifest (icons, theme color, standalone mode) and a Service Worker providing static resource caching and partial offline resilience. Structured Data — Schema.org markup (SoftwareApplication, FAQPage) for enriched indexing, in addition to visible HTML content. Full architecture documentation (flow diagrams, justified technical choices) along with simplified code examples for educational purposes are published separately. The actual normalization tables, the density function, the role-resolution thresholds, and the complete mineral catalog are not published and constitute the proprietary core of the system. Purpose of This Filing This document records the intellectual property of the algorithmic logic, the archetype nomenclature, and the mineral selection methodology associated with the Loch Ness brand. It protects the invention of a personalization system in which technology becomes the bridge between an individual's deep identity and the talismanic object. Live application: healing crystal bracelets configurator with lithotherapy score &amp;nbsp;
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fzenodo.org%2Fstatic%2Ffavicon.ico" width="50" height="50"&gt;
          zenodo.org
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;I have been making jewellery in Paris since 2013, and I write code. For a long time those were two separate jobs. This post is about the project where they finally met: a &lt;strong&gt;bracelet builder&lt;/strong&gt; where people compose a bracelet bead by bead from photos of real stones, get a "composition score", and send the design to a shop that strings it by hand.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsjq5havs7kr7ziiypx0i.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsjq5havs7kr7ziiypx0i.jpg" alt="Crystal bracelet builder with real photographed stones" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It started as a single page for my own workshop. It is now a white-label tool that other shops embed on their sites, plus a small ecosystem of static pages around it. Here is how it works under the hood, and the constraints that shaped it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraints
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shared PHP hosting, deployed over FTP.&lt;/strong&gt; No Node server, no containers, no database server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One person&lt;/strong&gt; maintains everything, so every moving part has to justify itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three languages&lt;/strong&gt; (English, French, Spanish) from day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real stones.&lt;/strong&gt; Every bead is a photo of an actual bead from my workshop, cut out by hand. 58 references today.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those constraints pushed me towards a design I would recommend for small projects: a static front end, a thin PHP layer, and JSON files as the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture at a glance
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/pwa/{en,fr,es}/index.html      ← the builder: one self-contained page per language (vanilla JS, PWA)
/pwa/{en,fr,es}/configs/*.json  ← white-label settings, one file per shop
/pwa/api/catalog.json           ← stones, properties, scoring constants (exported from the front end)
/pwa/api/publish.php            ← receives a finished bracelet, recomputes everything, writes static pages
/pwa/api/lib/Litho.php          ← the scoring engine, ported from JS to PHP
/pwa/api/data/*.json            ← index of published creations, rate limits, settings
/creations/  /bracelets/  /pulseras/   ← generated static pages (EN / FR / ES)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No framework, no build step for the builder itself. The page loads, fetches its config, and runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The builder: one page, vanilla JS
&lt;/h2&gt;

&lt;p&gt;The bracelet is a ring of slots. Its size comes from two choices: the wrist circumference (14 to 23 cm) and the bead diameter (6, 8 or 10 mm, depending on the wrist size). The number of beads is simply what fits around the wrist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WRIST_OPTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="c1"&gt;// cm → bead sizes (mm)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;beadCountFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wristCm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;diamMm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;wristCm&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;diamMm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each slot is absolutely positioned on a circle, and the whole ring sits in a rotating container. A few details that made a big difference in feel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fly-in animation&lt;/strong&gt;: when you tap a stone card, a clone of its image flies to the next free slot (Web Animations API), then the real bead "lands" with a short scale-and-brightness keyframe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Undo&lt;/strong&gt;: removing a bead or emptying the bracelet shows a toast with an Undo button for 5 seconds (and Ctrl+Z on desktop). The state is just an array of bead objects, so undo is a copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two layouts, one DOM.&lt;/strong&gt; Below 700 px wide the original mobile layout is untouched; above, a &lt;code&gt;html.wide&lt;/code&gt; class switches to a two-column layout. I verify the mobile layout with pixel diffs (Playwright screenshots before/after at 375, 390 and 430 px) every time I touch the desktop CSS.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// the layout switch, run before first paint&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wide&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;innerWidth&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;700&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;innerHeight&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;560&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wide&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wide&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. The composition score
&lt;/h2&gt;

&lt;p&gt;When the bracelet is complete, the builder computes a score out of 100 and names the result, from "Emerging Amulet" to "Alchemical Bracelet". Each stone carries symbolic attributes taken from lithotherapy traditions: a chakra position (1 to 7), an element, a "vibration level", a polarity (yin, yang, neutral) and a dominant property (grounding, soothing, protective…).&lt;/p&gt;

&lt;p&gt;The interesting part is that &lt;strong&gt;position matters&lt;/strong&gt;. In the &lt;em&gt;energetic&lt;/em&gt; mode, the score looks at every pair of beads and asks how well their attributes go together, but a bead's neighbours count more than a bead on the opposite side of the ring. The distance is circular:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// N beads on a ring; rho = 0.5 in the current catalog&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rho&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// neighbours: 1, then 0.5, 0.25…&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;pairScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                 &lt;span class="c1"&gt;// phi(a, b) → compatibility, or null if unknown&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;num&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;den&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;phi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;num&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;den&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;den&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;num&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;den&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four &lt;code&gt;phi&lt;/code&gt; functions (chakra distance, element pairs, vibration gap, property pairs) produce four weighted averages. Polarity balance and a few small bonuses (an odd number of beads, a chakra sequence that rises around the ring) complete the total, clamped to 0–100. The panel shows the five sub-scores as bars, the chakras and elements represented, and practical care notes such as stones that should not go in water.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;aesthetic&lt;/em&gt; mode is a different model: it rewards coverage and variety (how many chakras and elements are represented, how diverse and evenly spread the properties are, how many different stones are used) plus bonuses for known combinations.&lt;/p&gt;

&lt;p&gt;To be clear about what this is: a &lt;strong&gt;generative design system built on symbolic data&lt;/strong&gt;, in the same spirit as a colour-harmony tool. It encodes traditions; it does not measure anything about the person wearing the bracelet.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Never trust the browser: recompute on the server
&lt;/h2&gt;

&lt;p&gt;Finished bracelets can be published as pages in a public gallery. The tempting shortcut would be to let the browser send the score and the texts. Instead, &lt;code&gt;publish.php&lt;/code&gt; accepts exactly three things: the list of bead IDs, the display mode, and a PNG of the share card.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// publish.php (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ids&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;is_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;is_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;ctype_digit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bad_ids'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$L&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasBead&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'unknown_bead'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;$contents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$L&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$clean&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// full score + texts, recomputed in PHP&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PNG is decoded and &lt;strong&gt;re-encoded with GD&lt;/strong&gt;, which strips anything hidden in the file. Every word on the generated page comes from &lt;code&gt;catalog.json&lt;/code&gt;, never from the request.&lt;/p&gt;

&lt;p&gt;That meant porting the scoring engine from JavaScript to PHP, and keeping both in sync. The subtle part was the deterministic text variation: each page picks phrasing variants with a seeded hash, and the seed must be identical in both languages. JavaScript does 32-bit multiplication with &lt;code&gt;Math.imul&lt;/code&gt; on UTF-16 code units; PHP has neither, so the port re-implements both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 32-bit unsigned multiply, matching Math.imul(a, b) &amp;gt;&amp;gt;&amp;gt; 0&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;imulU&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$a&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;=&lt;/span&gt; &lt;span class="mh"&gt;0xFFFFFFFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;=&lt;/span&gt; &lt;span class="mh"&gt;0xFFFFFFFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFFFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFFFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$hi&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFFFFFFFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// FNV-1a over UTF-16 code units, like s.charCodeAt(i) in JS&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;utf16Units&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$h&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;imulU&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16777619&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same bracelet in, same page out, in both runtimes: that is what makes the port checkable.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Static pages that regenerate themselves
&lt;/h2&gt;

&lt;p&gt;Each published bracelet becomes a static HTML page in three languages, plus pages for each stone and a sitemap. There is no database query at read time: the web server just serves files.&lt;/p&gt;

&lt;p&gt;A few rules keep this from turning into a spam factory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deduplication&lt;/strong&gt;: the same set of stones (in any proportions) maps to the same page, so a visitor who tweaks one bead does not create a near-duplicate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indexing rules&lt;/strong&gt;: only bracelets above a score threshold and with enough different stones are proposed to search engines; the rest stay &lt;code&gt;noindex&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Moderation&lt;/strong&gt;: new creations are published in a &lt;em&gt;pending&lt;/em&gt; state. The visitor's share link works immediately, but the page stays out of the gallery and the sitemap until it is reviewed in a small admin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits&lt;/strong&gt; per visitor and per day, stored in a JSON file behind an exclusive &lt;code&gt;flock&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a setting or a stone description changes, one call regenerates every page from the index. Writes are atomic (write to a temp file, then &lt;code&gt;rename&lt;/code&gt;), so a visitor never sees half a page.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. White label without a multi-tenant backend
&lt;/h2&gt;

&lt;p&gt;Shops do not get a copy of the code. They get a JSON file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"nom"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Maison Opale"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"logo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/logo.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"couleurs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"accent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#6d3f7d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"btn-color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#6d3f7d"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"masquer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"4"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.9&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cadre"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"epaisseur"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arrondi"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The builder reads &lt;code&gt;?client=…&lt;/code&gt;, fetches &lt;code&gt;configs/&amp;lt;client&amp;gt;.json&lt;/code&gt;, and applies it: colours become CSS custom properties (shades are derived from the main colour in JS), hidden stones are removed from the catalogue, prices are overridden. A client area lets each shop edit its file through a form, validated server-side, with a backup of the previous version.&lt;/p&gt;

&lt;p&gt;Embedding works three ways: a direct link, an &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt;, or a one-line script that injects a &lt;strong&gt;floating button&lt;/strong&gt;. The button lives in a Shadow DOM so the host site's CSS cannot break it, and the iframe is only created on first click, so the shop's page stays fast. When the builder detects it runs inside an iframe (&lt;code&gt;window.self !== window.top&lt;/code&gt;), it draws a discreet, configurable frame so it does not look like a pasted screenshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The holistic layer: Stone of the Day
&lt;/h2&gt;

&lt;p&gt;The same catalogue feeds a sister app, &lt;strong&gt;Stone of the Day&lt;/strong&gt;, which suggests a stone by intention, by numerology "life path" number, by moon phase or by zodiac season. The moon phases are computed in PHP with Jean Meeus's algorithm from &lt;em&gt;Astronomical Algorithms&lt;/em&gt; (chapter 49), accurate to a few minutes, so the pages stay correct without any external API.&lt;/p&gt;

&lt;p&gt;Because everything reads from one catalogue, improving a stone's description updates the builder, its score texts, the stone pages and the Stone of the Day at once. For a solo maintainer, that single source of truth is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do the same way again
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JSON files instead of a database&lt;/strong&gt; for a few thousand records with rare writes. &lt;code&gt;flock&lt;/code&gt; and atomic renames cover concurrency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-side recomputation&lt;/strong&gt; of anything that gets published. The browser only sends identifiers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pixel-diff tests&lt;/strong&gt; to protect a layout that already works, before redesigning another one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prices are hard-coded in euros; a currency setting per shop should have existed from the start.&lt;/li&gt;
&lt;li&gt;One HTML file per language means fixes have to be applied three times. A small build step generating the three files from one template would pay for itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The builder: &lt;a href="https://lochness-paris.com/pwa/en/" rel="noopener noreferrer"&gt;lochness-paris.com/pwa/en/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Stone of the Day: &lt;a href="https://lochness-paris.com/pwa/pierre-du-jour/en/" rel="noopener noreferrer"&gt;lochness-paris.com/pwa/pierre-du-jour/en/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm happy to answer questions about the scoring model, the JS-to-PHP port or running a "static site that writes itself" on shared hosting.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Note from the workshop-lab: the algorithmic tools, configurators and articles presented here explore lithotherapy exclusively through the lens of semiotics, cultural anthropology and generative design. This work belongs to an artistic and symbolic practice rooted in fine craftsmanship. It does not, in any way, constitute medical advice, a diagnosis, or an alternative to conventional, evidence-based therapies.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>javascript</category>
      <category>php</category>
      <category>webdev</category>
    </item>
    <item>
      <title>It's a philosophical question! For my part, I contribute to implementing this distinction for artists solely through my label VERUMANO. It's a technically sophisticated open-source project (GitHub and npm repository) that allows for reliable human.</title>
      <dc:creator>Camille Guerineau</dc:creator>
      <pubDate>Wed, 14 Oct 2026 17:26:59 +0000</pubDate>
      <link>https://dev.to/spiritrackingarch/its-a-philosophical-question-for-my-part-i-contribute-to-implementing-this-distinction-for-162e</link>
      <guid>https://dev.to/spiritrackingarch/its-a-philosophical-question-for-my-part-i-contribute-to-implementing-this-distinction-for-162e</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/spiritrackingarch/i-built-a-verifiable-human-made-badge-without-collecting-a-single-file-4l6n" class="crayons-story__hidden-navigation-link"&gt;I built a verifiable "human-made" badge without collecting a single file&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/spiritrackingarch" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3986513%2Fab84c618-91c6-4fa5-8c9b-f603e243e1ff.png" alt="spiritrackingarch profile" class="crayons-avatar__image" width="460" height="460"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/spiritrackingarch" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Camille Guerineau
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Camille Guerineau
                
                
              
              &lt;div id="story-author-preview-content-4850713" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/spiritrackingarch" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3986513%2Fab84c618-91c6-4fa5-8c9b-f603e243e1ff.png" class="crayons-avatar__image" alt="" width="460" height="460"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Camille Guerineau&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/spiritrackingarch/i-built-a-verifiable-human-made-badge-without-collecting-a-single-file-4l6n" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 14&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/spiritrackingarch/i-built-a-verifiable-human-made-badge-without-collecting-a-single-file-4l6n" id="article-link-4850713"&gt;
          I built a verifiable "human-made" badge without collecting a single file
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/javascript"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;javascript&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/spiritrackingarch/i-built-a-verifiable-human-made-badge-without-collecting-a-single-file-4l6n#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            6 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>github</category>
      <category>opensource</category>
      <category>privacy</category>
    </item>
    <item>
      <title>I built a verifiable "human-made" badge without collecting a single file</title>
      <dc:creator>Camille Guerineau</dc:creator>
      <pubDate>Wed, 14 Oct 2026 14:31:02 +0000</pubDate>
      <link>https://dev.to/spiritrackingarch/i-built-a-verifiable-human-made-badge-without-collecting-a-single-file-4l6n</link>
      <guid>https://dev.to/spiritrackingarch/i-built-a-verifiable-human-made-badge-without-collecting-a-single-file-4l6n</guid>
      <description>&lt;p&gt;I spent ten years as a jewelry maker before going back to full-stack development, so I care about this one personally. I wanted a badge that &lt;strong&gt;means something&lt;/strong&gt;: someone actually looked at how the work was made, the badge can be checked by anyone in one click, and it can be &lt;strong&gt;revoked&lt;/strong&gt; if it turns out to be false.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://zenodo.org/records/23304015" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fzenodo.org%2Fstatic%2Fimages%2Feu.png" height="60" class="m-0" width="88"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://zenodo.org/records/23304015" rel="noopener noreferrer" class="c-link"&gt;
            Verumano Human Origin (NO AI) Specification v0.3 | Zenodo
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            The Verumano Specification defines how the Verumano label (formerly Human Origin) certifies human-made creative work through a review of the creative process, not through AI detection. It specifies: Creator identifiers (VM-YYYY-NNNN; legacy HO- IDs remain valid) A closed set of statuses: reviewed, self-declared, revoked, demo Review requirements: live walkthrough or screen recording with a one-time code, evidence by discipline (writing, illustration, photography, music, video) Public verification pages and their required fields Dynamic SVG badges, seals and watermarks, rendered by the server so that revocation shows everywhere within one hour Work fingerprints: SHA-256 computed on the user's device; files are never uploaded A read-only public verification API (CORS, no key, rate-limited without storing IP addresses) Revocation, domain blocking, privacy and retention rules Security limits and conformance rules for third-party platforms The label does not claim to prove that no AI was used. It certifies that a human creative process was shown and examined. What's new in v0.3: the label is renamed Verumano. New creator IDs use the VM prefix; existing HO IDs stay valid and are never reassigned. The reference implementation moves to lochness-paris.com/verumano and the former subdomain redirects permanently. Statuses, badges and API are unchanged. Links: Reference implementation: https://lochness-paris.com/verumano/ Specification (online): https://lochness-paris.com/verumano/spec/ Verification API: https://lochness-paris.com/verumano/api/ Comparison with other labels: https://lochness-paris.com/verumano/compare/ Schemas, examples and client library: github.com/spiritracking-arch/human-origin-label-nope-ai Companion concept paper: doi:10.5281/zenodo.23072755 Text licensed under CC BY-NC 4.0. Code samples under MIT. Creative Commons Attribution-NonCommercial 4.0 International Public License (CC BY-NC 4.0) Copyright (c) 2026 Camille Guerineau Applies to the documentation, specifications (spec/), and the Creator Charter (charter/). You are free to: - Share: Copy and redistribute the material in any medium or format. - Adapt: Remix, transform, and build upon the material. Under the following terms: 1. Attribution: You must give appropriate credit, provide a link to the license, and indicate if changes were made. 2. Non-Commercial: You may not use the material for commercial purposes. Any commercial exploitation, corporate internal use, or monetization requires a separate written commercial agreement from the copyright holder. Full legal code is available at: https://creativecommons.org Trademarks &amp;amp; Certification Notice: The name "VERUMANO", the official badges, and the logo are strictly excluded from this public license. Implementing or referencing this specification does not grant any right to issue Human Origin badges, use the logo, or present a commercial service under the "Human Origin" brand identity.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fzenodo.org%2Fstatic%2Ffavicon.ico" width="50" height="50"&gt;
          zenodo.org
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;That project is &lt;strong&gt;Verumano&lt;/strong&gt;. Here is how it works under the hood, and the trade-offs I made.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint that shaped everything: no files
&lt;/h2&gt;

&lt;p&gt;The obvious design is "upload your work, we analyse it". I refused that for three reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creators don't want to hand their unpublished work to yet another platform.&lt;/li&gt;
&lt;li&gt;Storing files means storage, security and GDPR headaches.&lt;/li&gt;
&lt;li&gt;AI detectors on the final file are unreliable. They produce false positives on real human work.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So Verumano reviews the &lt;strong&gt;process&lt;/strong&gt; (drafts, sketches, timelapses, version history, raw files) shown by the creator through links, and it never keeps the work itself. What it stores is a public record: who, which discipline, which status, since when.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: boring on purpose
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PHP 8 + Apache, shared hosting&lt;/li&gt;
&lt;li&gt;The registry is a &lt;strong&gt;JSON file&lt;/strong&gt;, written with &lt;code&gt;flock()&lt;/code&gt; to avoid concurrent write corruption&lt;/li&gt;
&lt;li&gt;The data folder is outside the web root logic and protected with &lt;code&gt;Require all denied&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;No framework, no database, no build step&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a registry with hundreds (not millions) of entries, this is fast, cheap and easy to back up. A database can come later if it is ever needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Badges are rendered, not downloaded
&lt;/h2&gt;

&lt;p&gt;The badge a creator embeds is not a static image. It is an SVG generated on every request from the registry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://lochness-paris.com/verumano/verify/VM-2026-0001"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://lochness-paris.com/verumano/badges/badge-illustration-reviewed-light-0001.svg"&lt;/span&gt;
       &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Verumano: human-made illustration, process reviewed"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"380"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"112"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Because the server draws it, the status is always current:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Colour&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reviewed&lt;/td&gt;
&lt;td&gt;green &lt;code&gt;#1E6B45&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the creation process was examined&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Declared&lt;/td&gt;
&lt;td&gt;orange &lt;code&gt;#FF8C1A&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the creator declares it, not reviewed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revoked&lt;/td&gt;
&lt;td&gt;red &lt;code&gt;#A3261B&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;no longer valid, shown crossed out&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few details that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Status is never colour-only.&lt;/strong&gt; Each state has its own text and icon, for colour-blind readers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache of 1 hour&lt;/strong&gt; (&lt;code&gt;Cache-Control: public, max-age=3600&lt;/code&gt;). A revocation shows everywhere within the hour, without hammering the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An unlisted discipline returns 404.&lt;/strong&gt; You can't request &lt;code&gt;badge-music-reviewed-light-0001.svg&lt;/code&gt; if you were reviewed for illustration only.&lt;/li&gt;
&lt;li&gt;The badge links to the public verification page, which is the real proof. The image is just the door.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Detecting copied badges (and why it's only partial)
&lt;/h2&gt;

&lt;p&gt;When a badge is loaded, the server can read the &lt;code&gt;Referer&lt;/code&gt; header and compare it with the domains declared by the creator. If a badge shows up on a site that isn't on the list, it gets logged for review.&lt;/p&gt;

&lt;p&gt;This is deliberately &lt;strong&gt;not&lt;/strong&gt; used to block automatically. Modern browsers send &lt;code&gt;strict-origin-when-cross-origin&lt;/code&gt; by default, many sites send &lt;code&gt;no-referrer&lt;/code&gt;, and privacy extensions strip it. So the rule is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No information = no sanction.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The referer is a signal for a human to look at, never a verdict.&lt;/p&gt;
&lt;h2&gt;
  
  
  Certifying the exact file, without uploading it
&lt;/h2&gt;

&lt;p&gt;Creators can attach a fingerprint of a specific file to their record: a SHA-256 hash, computed &lt;strong&gt;in the browser&lt;/strong&gt;. The file never leaves the computer, only 64 hex characters do.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;crypto.subtle.digest()&lt;/code&gt; needs the whole file in memory, which is a problem for a 2 GB video. So the file is read in 4 MB chunks and fed to an incremental SHA-256. Simplified:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hashFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CHUNK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// 4 MB&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSha256&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;           &lt;span class="c1"&gt;// incremental SHA-256&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;CHUNK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;CHUNK&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;hasher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;CHUNK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;hasher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                     &lt;span class="c1"&gt;// "9f2c…"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Anyone can later drop the same file on the verify page and get "this exact file is registered" or "not found".&lt;/p&gt;

&lt;p&gt;The honest limit: a hash identifies &lt;strong&gt;bytes&lt;/strong&gt;, not an image. Re-export, crop or recompress the file, and the hash changes. That's fine for originals, masters and deliverables. It's not a tool to track copies across social networks.&lt;/p&gt;
&lt;h2&gt;
  
  
  A read-only public API
&lt;/h2&gt;

&lt;p&gt;Platforms, contests and juries need to check entries in bulk, so there is a small read-only API:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check a creator record&lt;/span&gt;
curl https://lochness-paris.com/verumano/api/v1/creators/VM-2026-0001

&lt;span class="c"&gt;# Check whether an exact file is registered&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://lochness-paris.com/verumano/api/v1/works?sha256=9f2c...e41a"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"api_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"creator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VM-2026-0001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reviewed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"demo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Nora Lindqvist (demo)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"disciplines"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"illustration"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example: a series of ink and watercolor illustrations"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reviewed_on"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Live video walkthrough"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://lochness-paris.com/verumano/verify/VM-2026-0001"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;(Trimmed: the real response also lists the evidence examined, the certified works and the charter version.)&lt;/p&gt;

&lt;p&gt;Design choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CORS open&lt;/strong&gt;, GET only (&lt;code&gt;405&lt;/code&gt; otherwise), clear &lt;code&gt;400 / 404 / 429&lt;/code&gt; errors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limit of 60 requests per minute&lt;/strong&gt;, keyed on a salted IP hash whose salt rotates daily. No raw IP is stored&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5-minute cache&lt;/strong&gt; on responses&lt;/li&gt;
&lt;li&gt;No personal data beyond what is already public on the verification page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On top of it there is a small &lt;strong&gt;jury tool&lt;/strong&gt;: paste a CSV of entries (name, record number, social link), and it checks each one against the registry, including whether the social profile actually matches the one declared. A first version accepted any Instagram URL. Comparing the full profile path fixed that.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it in 30 seconds: the npm package
&lt;/h2&gt;

&lt;p&gt;Calling the API by hand is fine, but nobody wants to re-implement chunked hashing. So there is a small client on npm, with zero dependencies, for browsers and Node.js 18+:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i verumano
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getCreator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;checkFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;verumano&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Is this creator registered, and what is their status?&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getCreator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;VM-2026-0001&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;creator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "reviewed"&lt;/span&gt;

&lt;span class="c1"&gt;// Is this exact file certified? Hashed locally, never uploaded.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;checkFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// a File in the browser, a path in Node&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not registered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And if you just want to show a live badge on a page, there is a web component, no build step needed:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/verumano@0.1/badge.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;verumano-badge&lt;/span&gt; &lt;span class="na"&gt;creator=&lt;/span&gt;&lt;span class="s"&gt;"VM-2026-0001"&lt;/span&gt; &lt;span class="na"&gt;discipline=&lt;/span&gt;&lt;span class="s"&gt;"illustration"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/verumano-badge&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The badge image is still rendered by the server at display time, so a revoked badge turns red everywhere within the hour. The package is published from GitHub Actions with npm trusted publishing and provenance, so you can check exactly which commit it was built from.&lt;/p&gt;

&lt;p&gt;Want to try it without installing anything? Here is a live demo, running the package against the real API:&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://stackblitz.com/github/spiritracking-arch/human-origin-label-nope-ai/tree/main/examples/demo?file=main.js" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;stackblitz.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;(&lt;a href="https://stackblitz.com/github/spiritracking-arch/human-origin-label-nope-ai/tree/main/examples/demo?file=main.js" rel="noopener noreferrer"&gt;Open the demo in StackBlitz&lt;/a&gt; if the embed does not load.)&lt;/p&gt;

&lt;h2&gt;
  
  
  An open specification
&lt;/h2&gt;

&lt;p&gt;A label only becomes a standard if others can read the rules and implement them. So the rules are public:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Specification (RFC 2119 style, v0.3): &lt;a href="https://lochness-paris.com/verumano/spec/" rel="noopener noreferrer"&gt;https://lochness-paris.com/verumano/spec/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Schemas, examples and the npm client on GitHub: &lt;a href="https://github.com/spiritracking-arch/human-origin-label-nope-ai" rel="noopener noreferrer"&gt;https://github.com/spiritracking-arch/human-origin-label-nope-ai&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;npm package: &lt;a href="https://www.npmjs.com/package/verumano" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/verumano&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Specification on Zenodo: &lt;a href="https://doi.org/10.5281/zenodo.23072758" rel="noopener noreferrer"&gt;https://doi.org/10.5281/zenodo.23072758&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Concept paper: &lt;a href="https://doi.org/10.5281/zenodo.23072755" rel="noopener noreferrer"&gt;https://doi.org/10.5281/zenodo.23072755&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Docs are CC BY 4.0, code is MIT. The server code itself isn't open yet. The review process is still in a pilot phase, and I'd rather stabilise it before inviting forks of the registry.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not claim
&lt;/h2&gt;

&lt;p&gt;I think this part matters as much as the code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is &lt;strong&gt;not proof that no AI was used&lt;/strong&gt;. Nothing can prove that. It certifies that a human creation process was shown and examined.&lt;/li&gt;
&lt;li&gt;A process can be staged. The review raises the cost of lying, it doesn't make lying impossible. That's also why revocation exists.&lt;/li&gt;
&lt;li&gt;During the pilot, reviews are done by one person (me). More reviewers will come with the specification.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://lochness-paris.com/verumano/" rel="noopener noreferrer"&gt;https://lochness-paris.com/verumano/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install the client: &lt;code&gt;npm i verumano&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Verify a demo record: &lt;a href="https://lochness-paris.com/verumano/verify/VM-2026-0001" rel="noopener noreferrer"&gt;https://lochness-paris.com/verumano/verify/VM-2026-0001&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Fingerprint a file in your browser: &lt;a href="https://lochness-paris.com/verumano/fingerprint/" rel="noopener noreferrer"&gt;https://lochness-paris.com/verumano/fingerprint/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;API docs: &lt;a href="https://lochness-paris.com/verumano/api/" rel="noopener noreferrer"&gt;https://lochness-paris.com/verumano/api/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;How it compares to other labels (sourced): &lt;a href="https://lochness-paris.com/verumano/compare/" rel="noopener noreferrer"&gt;https://lochness-paris.com/verumano/compare/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apply as a creator: &lt;a href="https://lochness-paris.com/verumano/apply/" rel="noopener noreferrer"&gt;https://lochness-paris.com/verumano/apply/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you find it useful, a ⭐ on the &lt;a href="https://github.com/spiritracking-arch/human-origin-label-nope-ai" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; helps a lot.&lt;/p&gt;

&lt;p&gt;I'd really like feedback from developers here, especially on two points: &lt;strong&gt;how would you make badge copying harder without tracking people&lt;/strong&gt;, and &lt;strong&gt;what would make you trust (or ignore) a label like this on a portfolio?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>opensource</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Building a Generative Jewelry Configurator with Vanilla JS, SVG, and Numerology</title>
      <dc:creator>Camille Guerineau</dc:creator>
      <pubDate>Mon, 22 Jun 2026 01:41:38 +0000</pubDate>
      <link>https://dev.to/spiritrackingarch/building-a-generative-jewelry-configurator-with-vanilla-js-svg-and-numerology-7n0</link>
      <guid>https://dev.to/spiritrackingarch/building-a-generative-jewelry-configurator-with-vanilla-js-svg-and-numerology-7n0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note from the workshop-lab.&lt;/strong&gt; The algorithmic tools, configurators and articles presented here explore lithotherapy exclusively through the lens of semiotics, cultural anthropology and generative design. This work belongs to an artistic and symbolic practice rooted in fine craftsmanship. It does not, in any way, constitute medical advice, a diagnosis, or an alternative to conventional, evidence-based therapies.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A few months ago we set out to build something that doesn't fit neatly into the usual "product configurator" template: a tool that turns your name and birth date into a personalized gemstone bracelet, lets you fine-tune every single bead by hand, and ends with a real artisan crafting the physical piece.&lt;br&gt;
&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://zenodo.org/records/23428094" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fzenodo.org%2Fstatic%2Fimages%2Feu.png" height="60" class="m-0" width="88"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://zenodo.org/records/23428094" rel="noopener noreferrer" class="c-link"&gt;
            Loch Ness: Algorithmic System for Archetypal Determination and Variable-Geometry Mineral Synergies | Zenodo
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Concept Summary (Abstract) The Loch Ness project redefines the interaction between individual psychology and lithotherapy through a proprietary digital interface. Unlike standardized, fixed-output approaches, this concept rests on two innovative pillars: an original archetypal taxonomy and a variable-geometry mineral sequencing algorithm. 1. The Ontology of the "Noble Monster" The system uses a taxonomy of original archetypes created to reflect the complexity of the human psyche, including its shadow aspects and raw power. The user is not classified according to conventional sociological or demographic criteria, but according to their "wild nature" and their capacity to channel a singular inner force. This classification produces, for each profile, a multidimensional intensity vector rather than a single label. 2. The Variable-Geometry Calculation Algorithm The application processes user data to generate a mineral response whose structure is unique to each profile: Dynamic Sequencing — the number of stones (from 3 to 8 units) is not fixed; it is determined by the algorithm based on the calculated "density" of the archetypal profile. Compensation Synergy — each stone is selected to fulfill a specific functional role (Anchor, Heart, Mind, Shield, Momentum), creating a bespoke vibrational armor rather than an arbitrary assembly. This role-to-stone indirection allows the role nomenclature and the mineral catalog to evolve independently, without recalibrating the entire system with each addition. 3. Algorithmic Pipeline Processing relies on a strictly decoupled three-stage functional pipeline, each stage independently testable: Identity Normalization — identity data (first name(s), last name, date of birth) is transformed into a multidimensional profile vector (intensity, shadow aspect, momentum, stability), with no dependency on rendering or the interface. Density Calculation and Slot Count Resolution — a density function aggregates the profile vector into a scalar score, which is then projected onto the integer interval [3, 8] to dynamically determine the length of the mineral sequence. Role Resolution, then Mineral Selection — for each slot, a functional role is resolved first (Anchor, Heart, Mind, Shield, Momentum), and a stone is then selected from the catalog to fulfill that role in light of the profile vector. 4. Software Architecture The system is implemented as a static single-page web application (SPA), with no dependency on a JavaScript framework, and features the following technical characteristics: Stack — PHP, HTML5, CSS3 (custom properties), vanilla native JavaScript; no dependency on any rendering framework. Generative Rendering Engine — each mineral in the catalog is rendered as a dynamically relying on a static image bank. Dynamic Geometric Layout — the position of each stone on the support (bracelet) is calculated trigonometrically based on its index and the total number of slots resolved by the algorithm (a variable N, not a constant), with automatic bead-size scaling according to the sequence's density. State Management — the composition state is represented as an ordered array of beads, with undo/redo history stacks and targeted re-rendering (rather than a full re-render) on each mutation, to preserve smoothness on mobile. Progressive Web App — an installation manifest (icons, theme color, standalone mode) and a Service Worker providing static resource caching and partial offline resilience. Structured Data — Schema.org markup (SoftwareApplication, FAQPage) for enriched indexing, in addition to visible HTML content. Full architecture documentation (flow diagrams, justified technical choices) along with simplified code examples for educational purposes are published separately. The actual normalization tables, the density function, the role-resolution thresholds, and the complete mineral catalog are not published and constitute the proprietary core of the system. Purpose of This Filing This document records the intellectual property of the algorithmic logic, the archetype nomenclature, and the mineral selection methodology associated with the Loch Ness brand. It protects the invention of a personalization system in which technology becomes the bridge between an individual's deep identity and the talismanic object. Live application: healing crystal bracelets configurator with lithotherapy score &amp;nbsp;
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fzenodo.org%2Fstatic%2Ffavicon.ico" width="50" height="50"&gt;
          zenodo.org
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
The result is Loch Ness™, the engine behind Loch Ness Paris' crystal bracelet configurator. This post walks through the architecture, the technical trade-offs, and a few of the more interesting problems we had to solve — without giving away the proprietary parts (the numerology algorithm and the full gem-rendering engine stay closed source, but the shape of the system is worth sharing).&lt;br&gt;
If you want to dig deeper, the architecture documentation and a few simplified code examples are public on GitHub: application-configurateur-de-bracelets-en-pierres-fines.&lt;br&gt;
The brief, in one sentence

&lt;blockquote&gt;
&lt;p&gt;Turn an identity (first name, last name, birth date) into 8 symbolic gemstones, let the user build a real bracelet around them bead by bead, and hand the final design off to a craftsperson.&lt;br&gt;
No accounts, no backend database of users, no checkout — just a fast, installable web app that ends in a personalized request sent to a human artisan.&lt;br&gt;
Why no framework?&lt;br&gt;
This was a deliberate choice. The whole app — numerology engine, SVG bead renderer, drag-to-build canvas, smart suggestions, cart — is built in php, vanilla JavaScript, plain CSS, and a single HTML file.&lt;br&gt;
Three reasons drove that decision:&lt;br&gt;
Load time matters more than developer convenience here. A big chunk of traffic comes from organic/mobile search for terms like "bracelet chemin de vie." Every extra hundred kilobytes of framework runtime is a few more bounces.&lt;br&gt;
The DOM tree is genuinely simple. Four tabs, one SVG canvas, a handful of lists. State management doesn't need a library when the state is "an ordered array of beads plus some UI flags."&lt;br&gt;
PWA installability and offline behavior are easier to reason about when you fully control the bootstrapping sequence instead of going through a framework's hydration lifecycle.&lt;br&gt;
That said — vanilla JS at this scale means you have to be disciplined about state, which brings us to the next point.&lt;br&gt;
State management without a framework&lt;br&gt;
The entire bracelet is represented as an ordered array of bead objects. Every mutation (add, remove, reorder, change size) goes through a small set of functions that:&lt;br&gt;
update the array,&lt;br&gt;
push the previous state onto an undo stack,&lt;br&gt;
re-render only the parts of the SVG that changed.&lt;br&gt;
That last point matters a lot for performance: with up to ~19 beads rendered as gradient-filled SVG circles, a naive full re-render on every drag event is noticeable on mid-range phones. Re-rendering only the affected bead (and recalculating positions trigonometrically around the guide circle) keeps things smooth.&lt;br&gt;
&lt;/p&gt;


&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified illustration of bead positioning on the guide circle&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getBeadPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;center&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;angle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;center&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;radius&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;center&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;radius&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Undo/redo is just two stacks of array snapshots — simple, but it's the kind of "boring technology" that makes a configurator feel trustworthy. Users mis-click constantly; cheap undo is a UX feature, not a nice-to-have.&lt;br&gt;
Generating gemstones instead of photographing them&lt;br&gt;
This is the part I find most interesting. With 70+ stone types in the catalog, maintaining a photo library (multiple angles, lighting consistency, file size, retina variants…) would have been a maintenance nightmare.&lt;br&gt;
Instead, every bead is a procedurally generated SVG. Each stone type has a "preset" describing its visual family (translucent crystal, opaque mineral, banded stone, metallic finish…), and each individual bead instance gets a random seed that introduces small variations — hue jitter, gradient stop shifts — so that no two beads of the same stone type look perfectly identical, the same way no two real gemstones do.&lt;br&gt;
A heavily simplified version of the idea:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renderBead&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseHue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;seed&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;seededRandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;baseHue&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// small natural variation&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;radialGradient id="grad-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" cx="40%" cy="35%" r="65%"&amp;gt;
      &amp;lt;stop offset="0%"  stop-color="hsla(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hue&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, 45%, 75%, 1)" /&amp;gt;
      &amp;lt;stop offset="60%" stop-color="hsla(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hue&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, 45%, 55%, 1)" /&amp;gt;
      &amp;lt;stop offset="100%" stop-color="hsla(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hue&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, 45%, 35%, 1)" /&amp;gt;
    &amp;lt;/radialGradient&amp;gt;
    &amp;lt;circle cx="50" cy="50" r="48" fill="url(#grad-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)" /&amp;gt;
  `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The production renderer is considerably more involved — it layers multiple gradients, clip paths, and noise patterns per mineral family to approximate veining (think lapis lazuli or labradorite) and translucency — but the core trick is the same: vector + seed beats a static image library when you need infinite, lightweight variation.&lt;br&gt;
The numerology layer&lt;br&gt;
The app's signature feature is the "Life Path Bracelet": you type in a first name and a birth date, and the engine computes 8 symbolic stones — Foundation, Summit, Life Path, Calling, Personality, Expression, Final Touch, and Wish — each tied to a different facet of numerology.&lt;br&gt;
I won't detail the letter-to-number mapping or reduction rules here since that's the proprietary core of the product, but structurally it's a pure function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;identity (name + date) → numeric reduction → archetype (1–9) → stone(s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping it as a pure, side-effect-free transformation made it trivial to unit test and to keep completely decoupled from the rendering and cart logic. The numerology module has no idea an SVG even exists.&lt;br&gt;
Smart suggestions and the "litho score"&lt;br&gt;
Beyond manual bead-picking, there's a "Smart" tab with:&lt;br&gt;
Harmonious palettes — pre-grouped stones by color coherence,&lt;br&gt;
Predefined themes — ready-made compositions for intents like "protection" or "calm,"&lt;br&gt;
A coherence score that evaluates how energetically consistent a user's manual selection is.&lt;br&gt;
These are all derived/precomputed layers sitting on top of the same stone catalog — no real-time AI inference, just well-structured data and some scoring heuristics. It's a good reminder that "smart suggestions" don't always need a model; sometimes a well-curated lookup table does the job at a fraction of the latency and cost.&lt;br&gt;
PWA bits that actually mattered&lt;br&gt;
A few small, unglamorous details ended up making a real difference on mobile:&lt;br&gt;
&lt;code&gt;touch-action: manipulation&lt;/code&gt; on all interactive elements to kill the old 300ms tap delay.&lt;br&gt;
Loading the webfont with &lt;code&gt;media="print"&lt;/code&gt; then switching to &lt;code&gt;all&lt;/code&gt; on load, so it never blocks first paint.&lt;br&gt;
A Service Worker caching static assets for offline resilience and faster repeat visits.&lt;br&gt;
A &lt;code&gt;manifest.json&lt;/code&gt; so the app can be installed straight from the browser, no app store round-trip.&lt;br&gt;
None of this is novel, but it's the kind of checklist that's easy to skip under deadline pressure — and it's exactly the stuff that determines whether a configurator feels like an app or like a website pretending to be one.&lt;br&gt;
From pixels to a physical object&lt;br&gt;
The configurator deliberately doesn't end in a checkout button. Once a composition is validated, the user lands on a recap + contact form, and the actual fulfillment is handled by a human artisan partner who crafts the bracelet by hand using the validated design as a spec sheet.&lt;br&gt;
That constraint shaped a lot of UI decisions — for instance, the cart view always shows an estimated price range rather than a fixed price, since the final quote depends on material choices (precious metal findings, natural pearls, etc.) confirmed with the artisan.&lt;br&gt;
What's public, what isn't&lt;br&gt;
We open-sourced the documentation and architecture of this project, along with a few deliberately simplified code examples, in a public GitHub repo. What you'll find there:&lt;br&gt;
a full architecture breakdown (diagrams included),&lt;br&gt;
a feature-by-feature functional spec,&lt;br&gt;
simplified illustrative examples of the numerology pattern and the SVG bead rendering pattern (not the production algorithms),&lt;br&gt;
a roadmap and contribution guidelines.&lt;br&gt;
What stays closed: the actual numerology letter/reduction tables, the full multi-preset rendering engine, the complete stone catalog, and the litho-score algorithm — those remain Loch Ness®'s IP.&lt;br&gt;
Try it&lt;br&gt;
The live configurator is here: lochness-paris.com/configurateur-de-bracelet.html&lt;br&gt;
The project is also archived and citable via &lt;code&gt;Zenodo: zenodo.org/records/19836838&lt;/code&gt;&lt;br&gt;
If you're into vanilla-JS architecture, generative SVG, or PWA details, I'd genuinely love feedback — open an issue on the repo or drop a comment below.&lt;/p&gt;

</description>
      <category>php</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How I Built the Two Missing Payload CMS v3 Plugins — Reviews, JSON-LD &amp; Real Production Bugs</title>
      <dc:creator>Camille Guerineau</dc:creator>
      <pubDate>Tue, 16 Jun 2026 03:35:50 +0000</pubDate>
      <link>https://dev.to/spiritrackingarch/how-i-built-the-two-missing-payload-cms-v3-plugins-reviews-json-ld-real-production-bugs-18a1</link>
      <guid>https://dev.to/spiritrackingarch/how-i-built-the-two-missing-payload-cms-v3-plugins-reviews-json-ld-real-production-bugs-18a1</guid>
      <description>&lt;p&gt;Running 23 European e-commerce shops on Payload CMS v3 taught me that some things simply don't exist yet. So I built them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update, October 2026:&lt;/strong&gt; both plugins are now at &lt;strong&gt;v1.0.2&lt;/strong&gt;. They are published from GitHub Actions with npm trusted publishing (no tokens) and provenance, and their npm pages now link back to the source. If one of them saves you time, a ⭐ on GitHub is the best way to help other Payload devs find it: &lt;a href="https://github.com/spiritracking-arch/payload-plugin-reviews" rel="noopener noreferrer"&gt;reviews&lt;/a&gt; · &lt;a href="https://github.com/spiritracking-arch/payload-plugin-seo-jsonld" rel="noopener noreferrer"&gt;seo-jsonld&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;I maintain a multi-clone e-commerce infrastructure: 23 Next.js + Payload CMS v3 shops deployed across Europe, each on its own subdomain and language. Think &lt;code&gt;fr.myshop.com&lt;/code&gt;, &lt;code&gt;de.myshop.com&lt;/code&gt;, &lt;code&gt;sk.myshop.com&lt;/code&gt;... all running on the same codebase with country-specific patches.&lt;/p&gt;

&lt;p&gt;While building this, I kept running into two missing pieces that no one had published for Payload v3:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;customer reviews system&lt;/strong&gt; with admin moderation and Google star ratings&lt;/li&gt;
&lt;li&gt;Complete &lt;strong&gt;Schema.org JSON-LD&lt;/strong&gt; for Google rich snippets (Product, BreadcrumbList, ItemList, AggregateRating)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both are now published on npm. Here's what I built, the bugs I hit, and how I solved them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: The Reviews Plugin
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What didn't exist
&lt;/h3&gt;

&lt;p&gt;Search npm for &lt;code&gt;payload reviews&lt;/code&gt; or &lt;code&gt;payload ratings&lt;/code&gt; and you'll find nothing for v3. The official plugin ecosystem covers SEO, forms, redirects, Stripe... but not customer reviews.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building the collection
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;reviews&lt;/code&gt; collection itself is straightforward: a relationship to &lt;code&gt;products&lt;/code&gt;, a &lt;code&gt;rating&lt;/code&gt; (1-5), a &lt;code&gt;status&lt;/code&gt; select (pending/approved/rejected) and author fields. The tricky parts came later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Access control gotcha:&lt;/strong&gt; Payload v3 uses a &lt;code&gt;roles&lt;/code&gt; array, not a &lt;code&gt;role&lt;/code&gt; string. This breaks if you copy v2 patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Wrong: always returns false&lt;/span&gt;
&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Correct for v3&lt;/span&gt;
&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Prevent self-verification:&lt;/strong&gt; users can POST any field on &lt;code&gt;create: () =&amp;gt; true&lt;/code&gt; collections. Lock &lt;code&gt;verified&lt;/code&gt; in a &lt;code&gt;beforeChange&lt;/code&gt; hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;beforeChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verified&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="c1"&gt;// admin-only, always reset on create&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Email protection:&lt;/strong&gt; &lt;code&gt;read: () =&amp;gt; true&lt;/code&gt; on the collection exposes &lt;code&gt;authorEmail&lt;/code&gt; in the public API. Add field-level access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authorEmail&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;access&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The relationship bug: &lt;code&gt;"108 0"&lt;/code&gt; instead of &lt;code&gt;108&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;When the form submitted &lt;code&gt;product: productId&lt;/code&gt;, Payload rejected it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"This relationship field has the following invalid relationships: 108 0"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The string &lt;code&gt;"108 0"&lt;/code&gt; was being sent instead of the number &lt;code&gt;108&lt;/code&gt;. The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Sends "108 0" (unknown why the space appears)&lt;/span&gt;
&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Force integer&lt;/span&gt;
&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Custom endpoints don't work for parameterized routes in v3
&lt;/h3&gt;

&lt;p&gt;I tried registering the reviews GET endpoint in &lt;code&gt;payload.config.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;endpoints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/reviews/product/:productId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: &lt;code&gt;Route not found "/api/reviews/product/108"&lt;/code&gt;. Payload v3 doesn't mount parameterized endpoints correctly under &lt;code&gt;/api&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; use a Next.js App Router route instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/app/(app)/api/reviews/product/[productId]/route.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;productId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPayload&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;configPromise&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reviews&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reviews&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;and&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;approved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The database table doesn't auto-create
&lt;/h3&gt;

&lt;p&gt;Payload running with migration &lt;code&gt;batch: -1&lt;/code&gt; (dev mode) won't auto-migrate on build. You need to create the &lt;code&gt;reviews&lt;/code&gt; table manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="nv"&gt;"public"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"enum_reviews_status"&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;ENUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'approved'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'rejected'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="nv"&gt;"reviews"&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nv"&gt;"id"&lt;/span&gt; &lt;span class="nb"&gt;serial&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"product_id"&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="nv"&gt;"products"&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"author_name"&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"author_email"&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"rating"&lt;/span&gt; &lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"title"&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"comment"&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"verified"&lt;/span&gt; &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"status"&lt;/span&gt; &lt;span class="nv"&gt;"enum_reviews_status"&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nv"&gt;"updated_at"&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nv"&gt;"created_at"&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;"payload_locked_documents_rels"&lt;/span&gt;
  &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="nv"&gt;"reviews_id"&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="nv"&gt;"reviews"&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;payload-plugin-reviews
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// payload.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;reviewsPlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payload-plugin-reviews&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;buildConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;reviewsPlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;productsCollection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;→ &lt;a href="https://www.npmjs.com/package/payload-plugin-reviews" rel="noopener noreferrer"&gt;payload-plugin-reviews on npm&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://github.com/spiritracking-arch/payload-plugin-reviews" rel="noopener noreferrer"&gt;Source on GitHub&lt;/a&gt; (⭐ welcome)&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 2: The JSON-LD Plugin
&lt;/h2&gt;
&lt;h3&gt;
  
  
  What didn't exist
&lt;/h3&gt;

&lt;p&gt;Payload's official SEO plugin handles meta tags. It does not handle Schema.org structured data. No one had published a complete JSON-LD solution for Payload v3 e-commerce either.&lt;/p&gt;

&lt;p&gt;After running 23 shops in production and passing Google Rich Results validation on all of them, I extracted the patterns into &lt;code&gt;payload-plugin-seo-jsonld&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Bug 1: &lt;code&gt;price&lt;/code&gt; vs &lt;code&gt;lowPrice&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Google's Rich Results validator rejects &lt;code&gt;AggregateOffer&lt;/code&gt; with &lt;code&gt;price&lt;/code&gt;. It requires &lt;code&gt;lowPrice&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Google rejects this&lt;/span&gt;
&lt;span class="nx"&gt;offers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AggregateOffer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Google accepts this&lt;/span&gt;
&lt;span class="nl"&gt;offers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AggregateOffer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lowPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bug 2: prices stored in cents
&lt;/h3&gt;

&lt;p&gt;Payload e-commerce stores prices in cents (&lt;code&gt;3990&lt;/code&gt; = €39.90). Pass the raw value to Google and you'll have a €3,990 bracelet in your rich snippets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Sends 3990 to Google&lt;/span&gt;
&lt;span class="nx"&gt;lowPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;priceInUSD&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Correct&lt;/span&gt;
&lt;span class="nx"&gt;lowPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;priceInUSD&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bug 3: ItemList duplicates break Google validation
&lt;/h3&gt;

&lt;p&gt;If a page has two &lt;code&gt;ItemList&lt;/code&gt; JSON-LD scripts, Google invalidates both. This happened on shops that inherited a carousel from a base template and then had a new one patched in.&lt;/p&gt;

&lt;p&gt;The plugin returns &lt;code&gt;null&lt;/code&gt; when products are empty, and the pattern makes the single-instance contract explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Returns null if empty: safe conditional render&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;itemListJsonLd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildItemListJsonLd&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;siteUrl&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;itemListJsonLd&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/ld+json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;__html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;itemListJsonLd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  BreadcrumbList with nested categories
&lt;/h3&gt;

&lt;p&gt;The BreadcrumbList recursively walks the category parent chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shop → Jewelry → Bracelets → Product Name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;collectParents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;collectParents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;parents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;collectParents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This requires fetching products at &lt;code&gt;depth: 3&lt;/code&gt; to populate categories and their parents.&lt;/p&gt;

&lt;h3&gt;
  
  
  AggregateRating: connecting reviews to JSON-LD
&lt;/h3&gt;

&lt;p&gt;Plug in your review data to get Google star ratings in search results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;productJsonLd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildProductJsonLd&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;siteUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EUR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;averageRating&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;4.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// from your reviews query&lt;/span&gt;
  &lt;span class="na"&gt;reviewCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plugin only adds &lt;code&gt;aggregateRating&lt;/code&gt; when &lt;code&gt;reviewCount &amp;gt; 0&lt;/code&gt;, so there are no empty rating blocks. The two plugins are designed to work together: reviews collects and moderates the ratings, seo-jsonld turns them into stars in Google.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;payload-plugin-seo-jsonld
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;buildProductJsonLd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;buildBreadcrumbJsonLd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;buildItemListJsonLd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;buildWebSiteJsonLd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;buildOrganizationJsonLd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payload-plugin-seo-jsonld&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;→ &lt;a href="https://www.npmjs.com/package/payload-plugin-seo-jsonld" rel="noopener noreferrer"&gt;payload-plugin-seo-jsonld on npm&lt;/a&gt;&lt;br&gt;
→ &lt;a href="https://github.com/spiritracking-arch/payload-plugin-seo-jsonld" rel="noopener noreferrer"&gt;Source on GitHub&lt;/a&gt; (⭐ welcome)&lt;/p&gt;


&lt;h2&gt;
  
  
  Part 3: Enabling Google indexing on new shops
&lt;/h2&gt;

&lt;p&gt;Two things block Google on fresh Payload v3 deployments that aren't obvious.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Default &lt;code&gt;noindex&lt;/code&gt; in layout.tsx
&lt;/h3&gt;

&lt;p&gt;New Payload projects ship with indexing disabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/app/(app)/layout.tsx&lt;/span&gt;
&lt;span class="nx"&gt;robots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// ← blocks Google&lt;/span&gt;
  &lt;span class="nx"&gt;follow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/index: false,/index: true,/'&lt;/span&gt; src/app/&lt;span class="o"&gt;(&lt;/span&gt;app&lt;span class="o"&gt;)&lt;/span&gt;/layout.tsx
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/follow: false,/follow: true,/'&lt;/span&gt; src/app/&lt;span class="o"&gt;(&lt;/span&gt;app&lt;span class="o"&gt;)&lt;/span&gt;/layout.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. robots.txt
&lt;/h3&gt;

&lt;p&gt;Verify your &lt;code&gt;public/robots.txt&lt;/code&gt; allows crawling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-agent: *
Allow: /
Sitemap: https://myshop.com/sitemap.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check no noindex header&lt;/span&gt;
curl &lt;span class="nt"&gt;-sI&lt;/span&gt; &lt;span class="s2"&gt;"https://myshop.com/shop"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"x-robots&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;noindex"&lt;/span&gt;

&lt;span class="c"&gt;# Check single ItemList&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://myshop.com/shop"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'"@type":"ItemList"'&lt;/span&gt; | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="c"&gt;# → must return 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test structured data with the &lt;a href="https://search.google.com/test/rich-results" rel="noopener noreferrer"&gt;Google Rich Results Test&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Payload v3 routes are Next.js routes.&lt;/strong&gt; Don't fight the framework: use the App Router for parameterized endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always check migration mode.&lt;/strong&gt; &lt;code&gt;batch: -1&lt;/code&gt; means Payload manages the schema automatically in dev, but won't auto-migrate in prod.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google is strict on JSON-LD.&lt;/strong&gt; &lt;code&gt;lowPrice&lt;/code&gt; not &lt;code&gt;price&lt;/code&gt;, one &lt;code&gt;ItemList&lt;/code&gt; per page, &lt;code&gt;reviewCount &amp;gt; 0&lt;/code&gt; before adding &lt;code&gt;aggregateRating&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Field-level access control is underused.&lt;/strong&gt; Protect sensitive fields at the field level, not just the collection level.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/payload-plugin-reviews" rel="noopener noreferrer"&gt;payload-plugin-reviews&lt;/a&gt; on npm · &lt;a href="https://github.com/spiritracking-arch/payload-plugin-reviews" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.payloaddirectory.dev/plugins/1270699948-root" rel="noopener noreferrer"&gt;Payload Plugin Directory&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/payload-plugin-seo-jsonld" rel="noopener noreferrer"&gt;payload-plugin-seo-jsonld&lt;/a&gt; on npm · &lt;a href="https://github.com/spiritracking-arch/payload-plugin-seo-jsonld" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://payloadcms.com" rel="noopener noreferrer"&gt;Payload CMS&lt;/a&gt;, the framework&lt;/li&gt;
&lt;li&gt;&lt;a href="https://search.google.com/test/rich-results" rel="noopener noreferrer"&gt;Google Rich Results Test&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;I'm &lt;strong&gt;Camille Guerineau&lt;/strong&gt;, a freelance developer specializing in European e-commerce infrastructure. Before that, I spent ten years as a jewelry maker and founder of &lt;a href="https://lochness-paris.com/" rel="noopener noreferrer"&gt;Loch Ness Paris&lt;/a&gt;, so I came to e-commerce from the shop side first. Today I build and maintain 23 Payload CMS v3 shops deployed across Europe, each country-specific, SEO-optimized and running in production.&lt;/p&gt;

&lt;p&gt;The plugins in this article came directly from real production needs. If something didn't exist, I built it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔧 &lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/spiritracking-arch" rel="noopener noreferrer"&gt;github.com/spiritracking-arch&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;payload-plugin-reviews&lt;/strong&gt;: &lt;a href="https://www.npmjs.com/package/payload-plugin-reviews" rel="noopener noreferrer"&gt;npm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;payload-plugin-seo-jsonld&lt;/strong&gt;: &lt;a href="https://www.npmjs.com/package/payload-plugin-seo-jsonld" rel="noopener noreferrer"&gt;npm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌐 &lt;strong&gt;ScaleYourShop&lt;/strong&gt;: &lt;a href="https://scaleyourshop.app" rel="noopener noreferrer"&gt;scaleyourshop.app&lt;/a&gt;, the infrastructure behind these 23 shops&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Verumano&lt;/strong&gt;: &lt;a href="https://lochness-paris.com/verumano/" rel="noopener noreferrer"&gt;a verifiable "human-made" label&lt;/a&gt;, my other open project, with its own npm client (&lt;a href="https://www.npmjs.com/package/verumano" rel="noopener noreferrer"&gt;&lt;code&gt;verumano&lt;/code&gt;&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions, bugs or a feature you need? Open an issue on GitHub or leave a comment below. 👇&lt;/p&gt;

</description>
      <category>npm</category>
      <category>nextjs</category>
      <category>opensource</category>
      <category>payload</category>
    </item>
  </channel>
</rss>
