<?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: kouana</title>
    <description>The latest articles on DEV Community by kouana (@kouanahesham).</description>
    <link>https://dev.to/kouanahesham</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%2F4062772%2Fdde717e5-8e2a-4975-aa3c-a6ff3cd662f1.jpg</url>
      <title>DEV Community: kouana</title>
      <link>https://dev.to/kouanahesham</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kouanahesham"/>
    <language>en</language>
    <item>
      <title>Google Ignored Our Sitemap for Months. The Cause Was One Database Column</title>
      <dc:creator>kouana</dc:creator>
      <pubDate>Tue, 04 Aug 2026 19:39:08 +0000</pubDate>
      <link>https://dev.to/kouanahesham/google-ignored-our-sitemap-for-months-the-cause-was-one-database-column-2ee0</link>
      <guid>https://dev.to/kouanahesham/google-ignored-our-sitemap-for-months-the-cause-was-one-database-column-2ee0</guid>
      <description>&lt;p&gt;Our site had a problem we kept misdiagnosing: Google was slow to pick up changes. New pages sat in "Discovered – currently not indexed". Updated pages kept stale snippets for weeks.&lt;/p&gt;

&lt;p&gt;Every obvious suspect was clean. The sitemap validated. &lt;code&gt;robots.txt&lt;/code&gt; allowed everything. Every URL returned 200. So we blamed crawl budget and moved on. Twice.&lt;/p&gt;

&lt;p&gt;Then we looked at the data inside the sitemap instead of its structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  293 pages claimed to change at the same second
&lt;/h2&gt;

&lt;p&gt;Our sitemap carried 402 URLs. Grouping them by &lt;code&gt;lastmod&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://example.com/sitemap.xml &lt;span class="se"&gt;\&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;'&amp;lt;lastmod&amp;gt;[^&amp;lt;]*&amp;lt;/lastmod&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;    293 &lt;span class="nt"&gt;&amp;lt;lastmod&amp;gt;&lt;/span&gt;2026-07-21T14:22:07+00:00&lt;span class="nt"&gt;&amp;lt;/lastmod&amp;gt;&lt;/span&gt;
      6 &lt;span class="nt"&gt;&amp;lt;lastmod&amp;gt;&lt;/span&gt;2026-07-19T09:03:11+00:00&lt;span class="nt"&gt;&amp;lt;/lastmod&amp;gt;&lt;/span&gt;
      4 &lt;span class="nt"&gt;&amp;lt;lastmod&amp;gt;&lt;/span&gt;2026-07-18T21:40:55+00:00&lt;span class="nt"&gt;&amp;lt;/lastmod&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;293 of 402 pages shared one identical timestamp, down to the second.&lt;/strong&gt; The whole file contained only &lt;strong&gt;87 distinct dates&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The cause was mundane. Months earlier we ran a bulk update across the site. It rewrote &lt;code&gt;post_modified&lt;/code&gt; on every row it touched. From then on our sitemap told Google that most of the site changed at the same instant and never changed again.&lt;/p&gt;

&lt;p&gt;Put yourself on the crawler's side. A file claims three hundred unrelated pages were modified at 14:22:07 on the same day. That is not a signal, it is noise. The rational response is to stop trusting &lt;code&gt;lastmod&lt;/code&gt; for that site and fall back to the existing crawl schedule. Which is what we were seeing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Regenerating the sitemap fixes nothing
&lt;/h2&gt;

&lt;p&gt;The bad value lives in the database, not in the file. Regeneration copies it faithfully.&lt;/p&gt;

&lt;p&gt;Touching every post is worse. It stamps them all with a new identical timestamp and reproduces the problem with a fresher date.&lt;/p&gt;

&lt;p&gt;What worked was deriving the date from the content itself. We compute a fingerprint of each page's rendered content and store the date that fingerprint last changed. That value, not &lt;code&gt;post_modified&lt;/code&gt;, is what the sitemap publishes.&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="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'save_post'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$post&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="nf"&gt;wp_is_post_revision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;wp_is_post_autosave&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post_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;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$fingerprint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;post_title&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'|'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;post_content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$previous&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_post_meta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'_content_fingerprint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&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="nv"&gt;$fingerprint&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$previous&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="p"&gt;;&lt;/span&gt;                      &lt;span class="c1"&gt;// same content, so the date stands&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;update_post_meta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'_content_fingerprint'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$fingerprint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;update_post_meta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'_content_changed_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;current_time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'mysql'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&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;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then expose it through the REST layer the front end reads:&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="nf"&gt;register_rest_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'page'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sitemap_lastmod'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'get_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$changed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_post_meta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s1"&gt;'_content_changed_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$changed&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'modified_gmt'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;   &lt;span class="c1"&gt;// fallback for legacy content&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 property that matters is that it is &lt;strong&gt;honest by construction&lt;/strong&gt;. A page whose content has not changed keeps its old date no matter how often you re-save it. A page whose content actually changed gets today's date. Nothing in the admin UI can inflate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second half: your sitemap index is lying too
&lt;/h2&gt;

&lt;p&gt;Most generators stamp index files with the generation time:&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;// ❌ every crawl sees a freshly modified index&lt;/span&gt;
&lt;span class="nx"&gt;lastModified&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;Date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That tells Google every index changed on every crawl, which is another way of saying your dates carry no information. Report the newest real date among the children 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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newestOf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;lastmod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;max&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="o"&gt;=&amp;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;lastmod&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;max&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;lastmod&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;items&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;lastmod&lt;/span&gt;&lt;span class="p"&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sitemapIndex&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;SECTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;section&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="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/sitemap/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;section&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.xml`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lastModified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;newestOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;section&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&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;A trap we fell into ourselves while shipping this: we wrote &lt;code&gt;newestOf&lt;/code&gt;, then forgot to use it in four of the entries. They kept emitting &lt;code&gt;new Date()&lt;/code&gt;. Grep for your generator's date call after the change instead of trusting the diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Distinct &lt;code&gt;lastmod&lt;/code&gt; values&lt;/td&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;337&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Largest group sharing one timestamp&lt;/td&gt;
&lt;td&gt;261&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URLs in sitemap&lt;/td&gt;
&lt;td&gt;402&lt;/td&gt;
&lt;td&gt;402&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Related: the pages that were never crawled at all
&lt;/h2&gt;

&lt;p&gt;While investigating we checked the 41 URLs stuck in "Discovered – currently not indexed" and counted internal links pointing at each one. Eight had &lt;strong&gt;zero&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The correlation was exact. Every city page linked from its category hub was indexed. Every one omitted from that hub was stuck. Google found them in the sitemap, recorded them, then assigned them near-zero crawl priority because nothing on the site pointed at them.&lt;/p&gt;

&lt;p&gt;We added the missing links to the hubs. &lt;strong&gt;Seven of nine pages got indexed with no manual indexing request at all.&lt;/strong&gt; For comparison, manual requests in Search Console are capped at roughly ten per day. Internal links have no cap and fix the cause rather than the symptom.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check on your own site
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Export your sitemap and count distinct &lt;code&gt;lastmod&lt;/code&gt; values. If that number is far below your URL count, your dates are noise and Google has probably stopped reading them.&lt;/li&gt;
&lt;li&gt;Check whether your sitemap index carries the generation timestamp rather than a real content date.&lt;/li&gt;
&lt;li&gt;Never run a bulk update that touches modification dates. The cost stays invisible for months, then it is your entire crawl behaviour.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://roeia.com/blog/%D9%84%D9%85%D8%A7%D8%B0%D8%A7-%D9%8A%D8%AA%D8%AC%D8%A7%D9%87%D9%84-%D8%AC%D9%88%D8%AC%D9%84-%D8%AE%D8%B1%D9%8A%D8%B7%D8%A9-%D8%A7%D9%84%D9%85%D9%88%D9%82%D8%B9" rel="noopener noreferrer"&gt;full write-up&lt;/a&gt; with the complete implementation is on our blog. It is in Arabic, but the code and the tables read the same in any language.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>seo</category>
      <category>webdev</category>
      <category>wordpress</category>
    </item>
    <item>
      <title>سلة واحدة وطلبات متعددة: الخطأ الذي يقتل أي متجر متعدد التجار</title>
      <dc:creator>kouana</dc:creator>
      <pubDate>Tue, 04 Aug 2026 16:15:07 +0000</pubDate>
      <link>https://dev.to/kouanahesham/sl-whd-wtlbt-mtdd-lkht-ldhy-yqtl-y-mtjr-mtdd-ltjr-54an</link>
      <guid>https://dev.to/kouanahesham/sl-whd-wtlbt-mtdd-lkht-ldhy-yqtl-y-mtjr-mtdd-ltjr-54an</guid>
      <description>&lt;p&gt;بنينا سوقًا متعدد التجار: تاجر يفتح متجره ويوثَّق، ودليل متاجر، ومشترٍ يضع في سلة واحدة منتجات من بائعين مختلفين. المنصة تشرف على الجودة والشحن والاسترجاع.&lt;/p&gt;

&lt;p&gt;الخطأ الذي يقع فيه أغلب من يبني هذا النوع يظهر في أول أسبوع تشغيل: طلب واحد يحمل منتجات من ثلاثة تجار. يبدو منطقيًا، لأن المشتري اشترى مرة واحدة ودفع مرة واحدة. ثم يصل أول سؤال من التشغيل: أحد التجار شحن والثاني تأخر والثالث نفد مخزونه. ما حالة هذا الطلب؟&lt;/p&gt;

&lt;p&gt;لا جواب. الحالة الواحدة لا تصف ثلاثة مسارات مستقلة.&lt;/p&gt;

&lt;h2&gt;
  
  
  المشتري يشتري مرة، والتنفيذ يحدث مرات
&lt;/h2&gt;

&lt;p&gt;النموذج الذهني للمشتري: عملية شراء واحدة. النموذج التشغيلي: عدد من الطلبات بعدد التجار، لكل واحد شحنته وحالته ومهلته وسياسة إرجاعه.&lt;/p&gt;

&lt;p&gt;الحل ألا تختار بينهما، بل تمثّل الاثنين:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;checkout (عملية الشراء)
├── order  #1  → التاجر A   (شحن، حالة، تتبع مستقل)
├── order  #2  → التاجر B
└── order  #3  → التاجر C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'checkouts'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'customer_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'payment_reference'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// عملية دفع واحدة&lt;/span&gt;
    &lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unsignedInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'grand_total'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// بالهللات، عدد صحيح&lt;/span&gt;
    &lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'checkout_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'vendor_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;               &lt;span class="c1"&gt;// حالة مستقلة لكل تاجر&lt;/span&gt;
    &lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unsignedInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'items_total'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unsignedInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'shipping_total'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tracking_number'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;nullable&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;code&gt;checkout&lt;/code&gt;، والتنفيذ يرتبط بـ&lt;code&gt;order&lt;/code&gt;. من هنا يصير كل سؤال لاحق قابلًا للإجابة.&lt;/p&gt;

&lt;p&gt;ولاحظ &lt;code&gt;unsignedInteger&lt;/code&gt; للمبالغ: خزّن المال بأصغر وحدة كعدد صحيح. الأعداد العشرية في قواعد البيانات تُنتج فروق كسور تظهر عند جمع عمولات آلاف الطلبات.&lt;/p&gt;

&lt;h2&gt;
  
  
  الحالة المعروضة للعميل مشتقة لا مخزَّنة
&lt;/h2&gt;

&lt;p&gt;بعد الفصل يبقى سؤال الواجهة: ماذا نعرض للمشتري في صفحة "طلباتي"؟&lt;/p&gt;

&lt;p&gt;لا تخزّن حالة موحّدة في &lt;code&gt;checkouts&lt;/code&gt;، لأنها ستتعارض مع الحالات الفرعية عند أول تحديث. اشتقها:&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;displayStatus&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$statuses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;pluck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&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="nv"&gt;$statuses&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$s&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'delivered'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&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="nv"&gt;$statuses&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$s&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'cancelled'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&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="nv"&gt;$statuses&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'shipped'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;                   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'جزء منه في الطريق'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&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;/p&gt;

&lt;h2&gt;
  
  
  الشحن لا يُجمع ولا يُقسم بالتساوي
&lt;/h2&gt;

&lt;p&gt;كل تاجر يشحن من موقعه بسياسته. جمع تكلفة الشحن في رقم واحد على مستوى &lt;code&gt;checkout&lt;/code&gt; يجعل الاسترجاع الجزئي مستحيلًا: لو أُلغي طلب تاجر واحد، كم تُعيد من الشحن؟&lt;/p&gt;

&lt;p&gt;احسب الشحن على مستوى &lt;code&gt;order&lt;/code&gt; وخزّنه هناك، والمجموع مشتق:&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="nv"&gt;$shipping&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$ordersByVendor&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$o&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;vendor&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;shippingRuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$customerAddress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$o&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$o&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;items_total&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;القاعدة العامة: أي مبلغ قد يُسترجَع وحده يجب أن يُخزَّن في المستوى الذي يُسترجَع عنده.&lt;/p&gt;

&lt;h2&gt;
  
  
  العمولة تُجمَّد لحظة البيع
&lt;/h2&gt;

&lt;p&gt;المنصة تأخذ نسبة. الإغراء أن تحسبها عند العرض من &lt;code&gt;vendors.commission_rate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;أول مرة تعدّل نسبة تاجر، تتغير معها أرباح كل طلب سابق في التقارير، ويصير سجلك المحاسبي غير قابل للتدقيق.&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="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unsignedInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'commission_rate_bps'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// نقاط أساس: 250 = 2.5%&lt;/span&gt;
&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unsignedInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'commission_amount'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// المبلغ كما احتُسب يومها&lt;/span&gt;
&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unsignedInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'vendor_payout'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;خزّن النسبة المطبَّقة والمبلغ الناتج داخل الطلب نفسه. الجدول المرجعي يخدم الطلبات القادمة فقط. المبدأ نفسه ينطبق على الضريبة وعلى أي تعريفة متغيرة.&lt;/p&gt;

&lt;h2&gt;
  
  
  المنتج الواحد عند تجار مختلفين
&lt;/h2&gt;

&lt;p&gt;في السوق المتعدد يبيع أكثر من تاجر المنتج ذاته بسعر ومخزون مختلفين. إن جعلت السعر والمخزون حقلين في جدول &lt;code&gt;products&lt;/code&gt;، فأنت تفترض بائعًا واحدًا، وستعيد بناء الجدول لاحقًا.&lt;/p&gt;

&lt;p&gt;افصل التعريف عن العرض:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;products            # التعريف: اسم، وصف، صور، تصنيف، باركود
└── vendor_offers   # العرض: تاجر + سعر + مخزون + مدة تجهيز
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;صفحة المنتج تعرض التعريف مرة واحدة، وتحته قائمة العروض. السلة تشير إلى &lt;code&gt;vendor_offer_id&lt;/code&gt; لا إلى &lt;code&gt;product_id&lt;/code&gt;، وإلا لم تعرف من الذي يشحن.&lt;/p&gt;

&lt;h2&gt;
  
  
  توثيق التاجر قرار مسجَّل لا علم منطقي
&lt;/h2&gt;

&lt;p&gt;المنصة تضمن الجودة، ومعنى ذلك أن انضمام تاجر يمر بمراجعة. خانة &lt;code&gt;is_verified&lt;/code&gt; تجيب عن "هل هو موثق" ولا تجيب عن "من وثّقه ومتى ولماذا سُحب التوثيق".&lt;/p&gt;

&lt;p&gt;اجعلها سجل قرارات، والحالة الحالية مشتقة من آخر قرار. التكلفة جدول إضافي، والعائد أنك تستطيع مراجعة أي نزاع بعد أشهر بدل الاعتماد على الذاكرة.&lt;/p&gt;

&lt;h2&gt;
  
  
  الاسترجاع الجزئي: الاختبار الذي يكشف النموذج كله
&lt;/h2&gt;

&lt;p&gt;هنا تُقاس صحة كل ما سبق. المشتري دفع مبلغًا واحدًا بعملية واحدة عند البوابة، ثم أراد إرجاع صنف واحد من تاجر واحد.&lt;/p&gt;

&lt;p&gt;بوابة الدفع تعرف عملية واحدة برقم واحد. أنت تحتاج أن تسترجع جزءًا منها، وأن تعرف من أي تاجر يُخصم، وكم من الشحن يُعاد، وكم عمولة تتنازل عنها المنصة.&lt;/p&gt;

&lt;p&gt;الاسترجاع يصبح ممكنًا لأن كل رقم مخزَّن في مستواه الصحيح:&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="nv"&gt;$refund&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'items'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;unit_price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'shipping'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;refundableShipping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$qty&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// من مستوى الطلب&lt;/span&gt;
    &lt;span class="s1"&gt;'commission'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$line&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;commission_amount_share&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$qty&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;span class="nv"&gt;$refund&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'customer_total'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$refund&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'items'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$refund&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'shipping'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$refund&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'vendor_debit'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$refund&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'customer_total'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$refund&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'commission'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nv"&gt;$gateway&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$checkout&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;payment_reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$refund&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'customer_total'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;المبلغ المعاد إلى بطاقة المشتري يخرج من عملية الدفع الأصلية. أما توزيع الخسارة بين التاجر والمنصة فيقع في التسوية، وهي عملية منفصلة تمامًا عن حركة البطاقة.&lt;/p&gt;

&lt;p&gt;لو كان الشحن مجموعًا على مستوى عملية الشراء، والعمولة محسوبة من الجدول المرجعي، لما أمكن اشتقاق أي من هذه الأرقام. لذلك يُقاس نموذج البيانات في المتاجر بقدرته على تنفيذ استرجاع جزئي، لا بقدرته على إتمام عملية شراء.&lt;/p&gt;

&lt;h2&gt;
  
  
  مصيدة الحجز عند الدفع
&lt;/h2&gt;

&lt;p&gt;بين لحظة الضغط على "ادفع" ولحظة تأكيد بوابة الدفع تمر ثوانٍ. إن خصمت المخزون بعد التأكيد فقط، فمشترٍ آخر قد يأخذ آخر قطعة أثناءها.&lt;/p&gt;

&lt;p&gt;احجز المخزون قبل استدعاء البوابة، بعملية شرطية ذرية تفشل بوضوح:&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="nv"&gt;$reserved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VendorOffer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$offerId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'stock'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;gt;='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$qty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;decrement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'stock'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$qty&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="nv"&gt;$reserved&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OutOfStock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$offerId&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;/p&gt;

&lt;h2&gt;
  
  
  الخلاصة
&lt;/h2&gt;

&lt;p&gt;كل قرار هنا يتعلق بمستوى التخزين: أين يعيش الشحن، وأين تعيش الحالة، وأين تُجمَّد العمولة. المتجر يعمل ظاهريًا مع النموذج الخاطئ، ويظهر الثمن عند أول استرجاع جزئي وعند أول تقرير مالي.&lt;/p&gt;

&lt;p&gt;نبني في رؤية أنظمة &lt;a href="https://roeia.com/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%85%D8%AA%D8%AC%D8%B1-%D8%A7%D9%84%D9%83%D8%AA%D8%B1%D9%88%D9%86%D9%8A" rel="noopener noreferrer"&gt;تصميم متجر الكتروني&lt;/a&gt; ومتاجر متعددة التجار، والدرس الذي يتكرر في كل مشروع: ارسم دورة حياة الطلب كاملة على ورقة، من الإضافة إلى السلة حتى الاسترجاع بعد شهر، قبل أن تكتب أول ترحيل.&lt;/p&gt;

</description>
      <category>ecommerce</category>
      <category>database</category>
      <category>laravel</category>
      <category>architecture</category>
    </item>
    <item>
      <title>تطبيق طلب سيارات: خمسة قرارات معمارية تُحسم قبل أول سطر كود</title>
      <dc:creator>kouana</dc:creator>
      <pubDate>Tue, 04 Aug 2026 16:05:53 +0000</pubDate>
      <link>https://dev.to/kouanahesham/ttbyq-tlb-syrt-khms-qrrt-mmry-tuhsm-qbl-wl-str-kwd-3mhg</link>
      <guid>https://dev.to/kouanahesham/ttbyq-tlb-syrt-khms-qrrt-mmry-tuhsm-qbl-wl-str-kwd-3mhg</guid>
      <description>&lt;p&gt;بنينا منصة نقل ركاب كاملة: تطبيق للراكب، وتطبيق للسائق، ولوحة تحكم، وواجهة برمجية من أربعين مسارًا. الخلفية على Laravel مع لوحة Filament، والتطبيقان بـFlutter، والاتصال اللحظي عبر Firebase. ستة عشر نموذج بيانات وعشرون ترحيلًا وتسع شاشات إدارة.&lt;/p&gt;

&lt;p&gt;الجزء الصعب لم يكن أي تقنية بعينها. الصعوبة أن ثلاثة أنظمة تعمل في اللحظة نفسها: راكب ينتظر، وسائق يقرر خلال ثوانٍ، ولوحة تراقب وتسعّر وتوثّق. تأخير ثانيتين في وصول عرض الرحلة يعني رحلة ضائعة، لا تجربة أبطأ.&lt;/p&gt;

&lt;p&gt;هذه القرارات الخمسة حسمناها قبل الكتابة، وكل واحد منها كان سيكلّف إعادة بناء لو أجّلناه.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. مصدران للحقيقة، لكل منهما وظيفة مختلفة
&lt;/h2&gt;

&lt;p&gt;موقع السائق يتغير كل ثانية أو ثانيتين. لو كتبنا كل تحديث في جدول علاقي، لصار جدول المواقع أثقل من قاعدة البيانات كلها خلال أسابيع، بلا فائدة تُذكر من التاريخ الكامل.&lt;/p&gt;

&lt;p&gt;في المقابل، الأجرة وحالة الرحلة لا تحتمل قاعدة بلا معاملات. مبلغ يُحتسب مرتين أو حالة تنتقل من اتجاهين في وقت واحد يعني نزاعًا ماليًا.&lt;/p&gt;

&lt;p&gt;فصلنا الاثنين حسب طبيعتهما:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;البيانات&lt;/th&gt;
&lt;th&gt;المكان&lt;/th&gt;
&lt;th&gt;السبب&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;موقع السائق، حالة الاتصال، إشعار العرض&lt;/td&gt;
&lt;td&gt;Firebase&lt;/td&gt;
&lt;td&gt;تحديث كثيف قصير العمر، يصل فورًا&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;الرحلة، الأجرة وبنودها، السائق، الوثائق&lt;/td&gt;
&lt;td&gt;Laravel + قاعدة علاقية&lt;/td&gt;
&lt;td&gt;معاملات، قيود، تدقيق&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;نقطة الوصل: Laravel هو من يقرر، وFirebase هو من يوصّل. التطبيق يقرأ الموقع من Firebase ليرسم السيارة على الخريطة، لكنه لا يبني عليه أي قرار مالي. عند قبول العرض، يذهب الطلب إلى مسار في الواجهة البرمجية، والخلفية وحدها تكتب الحالة الجديدة.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. الأجرة تُخزَّن بنودًا لا رقمًا واحدًا
&lt;/h2&gt;

&lt;p&gt;أكثر شكوى في هذا السوق: "لماذا صار السعر كذا؟". إن خزّنت الإجمالي فقط، فلا جواب عندك ولا عند خدمة العملاء.&lt;/p&gt;

&lt;p&gt;خزّنّا كل بند مستقلًا: فتح العداد، والمسافة، والزمن، والضريبة، محسوبة من جدول تسعير لكل مدينة وفئة مركبة. الراكب يرى التفصيل قبل الطلب وبعد انتهاء الرحلة.&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;// الشكل العام: بنود منفصلة، والإجمالي مشتق منها لا مخزَّن بدلًا عنها&lt;/span&gt;
&lt;span class="nv"&gt;$fare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'base'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$tariff&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;base_fare&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'distance'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$km&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$tariff&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;per_km&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;span class="s1"&gt;'time'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$minutes&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$tariff&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;per_minute&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;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$fare&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'subtotal'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$fare&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$fare&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'tax'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$fare&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'subtotal'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$tariff&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tax_rate&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;span class="nv"&gt;$fare&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'total'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$fare&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'subtotal'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$fare&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'tax'&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;قيم التعريفة التي استُخدمت فعلًا&lt;/strong&gt; مع الرحلة، لا معرّف جدول التسعير وحده. إن ربطت الرحلة بالجدول بالمعرّف، فأول مرة تعدّل التعريفة تتغير معها أجرة كل رحلة سابقة عند العرض، ويصير سجلك المالي غير قابل للتدقيق.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. الرحلة آلة حالات، وكل شيء آخر يتفرع منها
&lt;/h2&gt;

&lt;p&gt;أربعون مسارًا في الواجهة البرمجية تبدو كثيرة حتى ترسم دورة حياة الرحلة، فتجد أغلبها انتقالات مشروعة بين حالتين.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requested → offered → accepted → arrived → started → completed
     ↘ cancelled_by_rider          ↘ cancelled_by_driver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;اجعل الانتقال المسموح بيانات لا شروطًا متناثرة في المتحكمات:&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="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;ALLOWED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'requested'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'offered'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'cancelled_by_rider'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'offered'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'accepted'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'cancelled_by_rider'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'expired'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'accepted'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'arrived'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'cancelled_by_rider'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'cancelled_by_driver'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'arrived'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'started'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'cancelled_by_driver'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'started'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;moveTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Trip&lt;/span&gt; &lt;span class="nv"&gt;$trip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&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;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$next&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="no"&gt;ALLOWED&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$trip&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InvalidTransition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$trip&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$next&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;p&gt;الفائدة تظهر عند الحالات الغريبة التي ستحدث حتمًا: سائق يضغط "وصلت" بعد أن ألغى الراكب، وتطبيق يعيد إرسال الطلب نفسه لأن الشبكة انقطعت. مع جدول انتقالات صريح، هذه محاولات مرفوضة برسالة واضحة لا حالات فاسدة في قاعدة البيانات.&lt;/p&gt;

&lt;h3&gt;
  
  
  سباق القبول: سائقان يضغطان في اللحظة نفسها
&lt;/h3&gt;

&lt;p&gt;العرض يصل إلى أكثر من سائق. اثنان يقبلان بفارق أجزاء من الثانية. الكود الساذج يقرأ ثم يكتب:&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="nv"&gt;$trip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Trip&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;find&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$trip&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'offered'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;      &lt;span class="c1"&gt;// كلاهما يقرأ 'offered'&lt;/span&gt;
    &lt;span class="nv"&gt;$trip&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'accepted'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'driver_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$driverId&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;/p&gt;

&lt;p&gt;اجعل الشرط جزءًا من الكتابة نفسها، واقرأ عدد الصفوف المتأثرة:&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="nv"&gt;$claimed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Trip&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&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;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'offered'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;// الشرط داخل UPDATE&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'accepted'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'driver_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$driverId&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="nv"&gt;$claimed&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'trip_already_taken'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;409&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;/p&gt;

&lt;h2&gt;
  
  
  4. تطبيقان منفصلان، لا تطبيق واحد بوضعين
&lt;/h2&gt;

&lt;p&gt;الإغراء واضح: تطبيق Flutter واحد يفتح واجهة الراكب أو السائق حسب نوع الحساب. المشترك بينهما كبير فعلًا، من الخرائط إلى التحقق إلى الإشعارات.&lt;/p&gt;

&lt;p&gt;بنيناهما تطبيقين مستقلين. الأسباب عملية لا نظرية:&lt;/p&gt;

&lt;p&gt;السائق يحتاج تتبع موقع في الخلفية وتنبيهات تخترق الوضع الصامت، وهذه أذونات ثقيلة يرفضها متجرا التطبيقات إن طلبها تطبيق ركاب عادي. دورة إصدار السائق أسرع، لأن تعديلات لوحة السائق لا يجوز أن تُوقف تحديث تطبيق الركاب في المراجعة. وحجم تطبيق الراكب يبقى صغيرًا، وهو من يُنزَّل ملايين المرات لا الآخر.&lt;/p&gt;

&lt;p&gt;المنطق المشترك يعيش في حزمة داخلية يستوردها الاثنان:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;packages/core/       # النماذج، عميل الواجهة البرمجية، التحقق، الخرائط
apps/rider/
apps/driver/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. المراجعة البشرية جزء من التصميم لا من سياسة الدعم
&lt;/h2&gt;

&lt;p&gt;شكوى متكررة أخرى في هذا المجال: حظر آلي بلا تفسير ولا مسار مراجعة. عالجناها في البنية: وثائق السائق ورخصته وتأمينه تُرفع، وتُراجَع من لوحة التحكم، والقرار يُسجَّل بصاحبه وسببه ووقته.&lt;/p&gt;

&lt;p&gt;الفرق أن حالة السائق ليست علمًا منطقيًا &lt;code&gt;is_active&lt;/code&gt;، بل سجل قرارات:&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="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'driver_reviews'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'driver_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;foreignId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'reviewer_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;constrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'decision'&lt;/span&gt;&lt;span class="p"&gt;,&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="s1"&gt;'suspended'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'reason'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamps&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;/p&gt;

&lt;h2&gt;
  
  
  مصيدتان صغيرتان كلّفتانا وقتًا
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;رمز التحقق يحتاج حدًّا زمنيًا وحدًّا عدديًا معًا.&lt;/strong&gt; حدّ المحاولات وحده يترك الباب مفتوحًا لإرسال رسائل بلا نهاية على حساب مالك التطبيق. اربط الحد بالرقم لا بالجلسة، لأن الجلسة يسهل تجديدها.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;المسافة المدفوعة ليست خطًا مستقيمًا بين نقطتين.&lt;/strong&gt; خزّن مسار الرحلة نقاطًا واحسب الطول التراكمي. الفارق بين الخط المستقيم والمسار الفعلي في مدينة مزدحمة كبير بما يكفي لتحويل كل رحلة إلى شكوى.&lt;/p&gt;

&lt;h2&gt;
  
  
  الخلاصة
&lt;/h2&gt;

&lt;p&gt;القرارات الخمسة تشترك في أنها &lt;strong&gt;قرارات تخزين وحدود مسؤولية&lt;/strong&gt;، لا قرارات مكتبات. أي منها يمكن تأجيله ويعمل التطبيق، ثم يظهر ثمنه بعد أشهر: سجل مالي لا يُدقَّق، أو حالات فاسدة، أو تطبيق ركاب عالق في مراجعة المتجر بسبب أذونات لا يحتاجها.&lt;/p&gt;

&lt;p&gt;نعمل في رؤية على &lt;a href="https://roeia.com/%D8%B4%D8%B1%D9%83%D8%A9-%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA-%D8%A7%D9%84%D8%AC%D9%88%D8%A7%D9%84" rel="noopener noreferrer"&gt;تصميم تطبيقات الجوال&lt;/a&gt; وأنظمتها الخلفية، وهذا المشروع كان أوضح مثال على أن الوقت المدفوع في نمذجة البيانات يُوفَّر مضاعفًا في الشهر الثالث.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>laravel</category>
      <category>firebase</category>
      <category>mobile</category>
    </item>
    <item>
      <title>لعبة ثلاثية الأبعاد داخل صفحة ويب عادية: سبع مصائد لم يذكرها أي درس</title>
      <dc:creator>kouana</dc:creator>
      <pubDate>Tue, 04 Aug 2026 15:54:05 +0000</pubDate>
      <link>https://dev.to/kouanahesham/lb-thlthy-lbd-dkhl-sfh-wyb-dy-sb-msyd-lm-ydhkrh-y-drs-3p3l</link>
      <guid>https://dev.to/kouanahesham/lb-thlthy-lbd-dkhl-sfh-wyb-dy-sb-msyd-lm-ydhkrh-y-drs-3p3l</guid>
      <description>&lt;p&gt;قررنا أن تكون صفحة خدمة الألعاب عندنا دليلًا لا وعدًا. بدل صور ثابتة لألعاب صنعناها، وضعنا في الصفحة لعبة تُلعب فعلًا: مشهد صحراوي بمحرك three.js، سلاح مُجهَّز بحركات إطلاق وإعادة تلقيم، أعداء يخرجون من الرمل ويطاردون اللاعب، وحلقة مهمة تنتهي عند خمسين هدفًا.&lt;/p&gt;

&lt;p&gt;تشغيل مشهد WebGL &lt;strong&gt;داخل صفحة موقع قائمة&lt;/strong&gt; يختلف عن تشغيله في صفحة فارغة. أغلب الدروس تفترض صفحة نظيفة فيها &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; وحده. أما نحن فأدخلناه وسط تخطيط فيه حركات وتحويلات ورأس ثابت ومحتوى تسويقي.&lt;/p&gt;

&lt;p&gt;في كل مصيدة من السبع التالية، كانت رسالة الخطأ تشير إلى موضع غير الموضع الذي يجب إصلاحه.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;position: fixed&lt;/code&gt; يتوقف عن كونه fixed
&lt;/h2&gt;

&lt;p&gt;أول ما بنينا زر ملء الشاشة، ظهر المشهد داخل حدود القسم لا فوق الصفحة. راجعنا الـCSS مرارًا وكل شيء سليم ظاهريًا.&lt;/p&gt;

&lt;p&gt;السبب ليس في العنصر نفسه بل في أحد آبائه. أي عنصر عليه &lt;code&gt;transform&lt;/code&gt; أو &lt;code&gt;filter&lt;/code&gt; أو &lt;code&gt;perspective&lt;/code&gt; يصنع &lt;strong&gt;حاوية احتواء جديدة&lt;/strong&gt;، فيصير &lt;code&gt;position: fixed&lt;/code&gt; نسبيًا إليه لا إلى نافذة المتصفح. وتخطيط موقعنا مليء بعناصر عليها &lt;code&gt;transform&lt;/code&gt; بسبب الحركات.&lt;/p&gt;

&lt;p&gt;أخرِج المشهد من الشجرة كلها. مطاردة الأب المذنب حل مؤقت، لأن أي تعديل تصميم لاحق يعيد المشكلة:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&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;createPortal&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="s2"&gt;react-dom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;FullscreenGame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&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;mounted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMounted&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setMounted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&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="nx"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;return&lt;/span&gt; &lt;span class="nf"&gt;createPortal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fixed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inset&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="na"&gt;zIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9999&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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;body&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;code&gt;document.body&lt;/code&gt; مباشرة، ولا تعتمد على &lt;code&gt;fixed&lt;/code&gt; وحده داخل موقع حقيقي.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. حاوية بمقاس صفر تُغرق الكونسول
&lt;/h2&gt;

&lt;p&gt;بعد نقل المشهد إلى البورتال، امتلأ الكونسول بسطر واحد يتكرر بالآلاف:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GL_INVALID_FRAMEBUFFER_OPERATION: Framebuffer is incomplete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;الرسالة توحي بخلل في إعداد المشهد أو في بطاقة الرسوم. الحقيقة أبسط: الحاوية كانت لا تزال بعرض صفر وارتفاع صفر في اللحظة التي قرأ فيها three.js مقاسها، فأنشأ إطارًا بمساحة صفر. وكل إطار رسم بعدها يفشل ويطبع السطر نفسه.&lt;/p&gt;

&lt;p&gt;الحاوية تأخذ مقاسها بعد الرسم الأول لا قبله، ونحن نقرأه قبله. العلاج أن يكون للقراءة بديل مضمون:&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;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientWidth&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerWidth&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;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientHeight&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aspect&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;h&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateProjectionMatrix&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ومعها &lt;code&gt;ResizeObserver&lt;/code&gt; يعيد الضبط عند أول مقاس حقيقي. أي كود يقرأ مقاس عنصر لحظة التركيب يحتاج قيمة احتياطية، فالمقاس صفر حالة شرعية لا خطأ، وهذا يتجاوز WebGL إلى أي مكتبة رسم أو مخطط بياني.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. طلب قفل المؤشر قد يُرفض، وأنت لا تتوقع ذلك
&lt;/h2&gt;

&lt;p&gt;التحكم بالفأرة في لعبة منظور أول يحتاج &lt;code&gt;requestPointerLock()&lt;/code&gt;. كتبناها كما تُكتب في كل الأمثلة:&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="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestPointerLock&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;عملت على أجهزتنا. ثم بدأت تظهر شاشة خطأ حمراء من Next.js عند بعض المستخدمين وفي بيئات الاختبار الآلي. السبب أن هذه الدالة ترجع &lt;code&gt;Promise&lt;/code&gt; في المتصفحات الحديثة، وهذا الوعد &lt;strong&gt;يُرفض&lt;/strong&gt; إذا لم يعتبر المتصفح النقرة إيماءة مستخدم موثوقة، أو إذا كانت الصفحة داخل إطار مقيّد. الرفض غير الملتقَط يصير خطأ غير معالَج، وNext يعرضه فوق الصفحة.&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="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&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="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;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestPointerLock&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;req&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;showHint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;اضغط داخل الشاشة للتحكم&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;p&gt;عامِل كل واجهة برمجية تتعلق بأذونات المتصفح بالطريقة نفسها. افترض أنها سترفض، وحوّل الرفض إلى رسالة مفهومة بدل شاشة خطأ.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. المؤشر يختفي فوق قائمة تحتاج فأرة
&lt;/h2&gt;

&lt;p&gt;قفل المؤشر يخفيه عن الشاشة، وهذا مطلوب أثناء اللعب. المشكلة تبدأ عند فتح أي طبقة فوق اللعبة: قائمة إيقاف مؤقت، أو شاشة نهاية، أو تحذير خروج. الأزرار ظاهرة والمؤشر غائب، فيظن اللاعب أن الصفحة تجمّدت.&lt;/p&gt;

&lt;p&gt;المتصفح لا يفك القفل تلقائيًا عند ظهور عنصر جديد، لأنه لا يعرف شيئًا عن طبقاتك. اربط فك القفل بحالة الواجهة نفسها:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&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;overlayOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;paused&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;dead&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;won&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;overlayOpen&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;pointerLockElement&lt;/span&gt;&lt;span class="p"&gt;)&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="nf"&gt;exitPointerLock&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="nx"&gt;paused&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;won&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;وأضفنا &lt;code&gt;cursor: auto&lt;/code&gt; صريحًا على الطبقة، وشريحة تلميح تظهر حين يكون المؤشر غير مقفول. كل حالة تفتح واجهة يجب أن تحرر الفأرة، وإلا صار اللاعب أمام شاشة تبدو معطلة.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. قبة السماء تظهر سوداء بالكامل
&lt;/h2&gt;

&lt;p&gt;استخدمنا نماذج مشاهد جاهزة لبعض العوالم. أغلبها ليس تضاريس بل &lt;strong&gt;قبة مرسومة من الداخل&lt;/strong&gt;: كرة واحدة عليها صورة، والكاميرا تقف بداخلها.&lt;/p&gt;

&lt;p&gt;عند تحميلها ظهرت سوداء تمامًا. المشكلة أن الخامة الافتراضية &lt;code&gt;MeshStandardMaterial&lt;/code&gt; خامة مضاءة، والوجه الداخلي العلوي للقبة لا يصله ضوء، فيُرسم أسود مهما رفعت شدة الإضاءة.&lt;/p&gt;

&lt;p&gt;القبة ليست جسمًا في المشهد، هي لوحة. والحل أن تُعامَل كلوحة:&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="nx"&gt;dome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;traverse&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;o&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;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isMesh&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="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;material&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MeshBasicMaterial&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;material&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BackSide&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// نرى الوجه الداخلي&lt;/span&gt;
    &lt;span class="na"&gt;fog&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;// الضباب لا يمس السماء&lt;/span&gt;
    &lt;span class="na"&gt;toneMapped&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;// بألوانها الأصلية&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Box3&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;setFromObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dome&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;dome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;وأضفنا خلفية متدرجة على &lt;code&gt;scene.background&lt;/code&gt; كضمانة أخيرة، حتى لو فشل تحميل النموذج لا يرى الزائر سوادًا.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. نموذج بـ87 ميجابايت لا يُرسل إلى متصفح أحد
&lt;/h2&gt;

&lt;p&gt;النموذج الرئيسي للشخصية جاء بحجم 87 ميجابايت. رقم مقبول في محرك ألعاب مثبَّت على الجهاز، ومستحيل في صفحة ويب.&lt;/p&gt;

&lt;p&gt;خفّضناه إلى 3.1 ميجابايت بأمر واحد، مع الاحتفاظ بـ92 ألف مثلث وبحركة السكون كاملة و25 خامة:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @gltf-transform/cli optimize &lt;span class="k"&gt;in&lt;/span&gt;.glb out.glb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--texture-compress&lt;/span&gt; webp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--texture-size&lt;/span&gt; 1024 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--compress&lt;/span&gt; draco
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;أغلب الوزن في الخامات لا في الأشكال. تحويل الصور إلى webp وتحديد حدها الأقصى عند 1024 بكسل يقتطع النصيب الأكبر، وضغط draco يتولى الهندسة. بعد التحويل يفتح المشهد على شبكة جوال عادية.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. &lt;code&gt;next/dynamic&lt;/code&gt; داخل مكوّن سيرفر لا يقسّم شيئًا
&lt;/h2&gt;

&lt;p&gt;هذه لم تظهر في الألعاب وحدها، بل في الموقع كله، ولم ننتبه لها إلا عند قياس الحزمة.&lt;/p&gt;

&lt;p&gt;كنا نستورد المحركات الثقيلة عبر &lt;code&gt;next/dynamic&lt;/code&gt; ونظن أن كل صفحة تحمّل ما يخصها فقط. الأرقام قالت غير ذلك: أول تحميل لمسار الصفحات الداخلية كان &lt;strong&gt;2.04 ميجابايت&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;السبب أن &lt;code&gt;next/dynamic&lt;/code&gt; &lt;strong&gt;داخل مكوّن سيرفر لا يقسّم إطلاقًا&lt;/strong&gt;. التقسيم الفعلي يقوم على دلالات &lt;code&gt;React.lazy&lt;/code&gt;، وهي لا تعمل إلا على حدود العميل. المكوّن السيرفري يجمع كل شيء في حزمة صفحة واحدة.&lt;/p&gt;

&lt;p&gt;الحل أن يكون الموزّع نفسه مكوّن عميل:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;next/dynamic&lt;/span&gt;&lt;span class="dl"&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;PAGES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="mi"&gt;106&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./p106&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="mi"&gt;9443&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./p9443&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;BespokePage&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;content&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;C&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PAGES&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="k"&gt;return&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;C&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&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;ويبقى في السيرفر ملف أرقام فقط، لا يستورد أي مكوّن، ليقرر أي فرع يُسلك. النتيجة: أول تحميل نزل من &lt;strong&gt;2.04 ميجابايت إلى 195 كيلوبايت&lt;/strong&gt;، وعلى صفحة حية قِيس الجافاسكربت المرسل 2,012 كيلوبايت قبل، و209 كيلوبايت بعد.&lt;/p&gt;

&lt;h2&gt;
  
  
  كيف تتحقق من مشهد لا تستطيع تصويره
&lt;/h2&gt;

&lt;p&gt;أدوات لقطة الشاشة الآلية كانت تفشل على صفحة اللعبة برسالة انتهاء مهلة. حلقة &lt;code&gt;requestAnimationFrame&lt;/code&gt; لا تتوقف، فلا تجد الأداة لحظة سكون تلتقط فيها.&lt;/p&gt;

&lt;p&gt;المخرج أن نقرأ الإطار من بطاقة الرسوم مباشرة:&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;gl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webgl2&lt;/span&gt;&lt;span class="dl"&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;px&lt;/span&gt; &lt;span class="o"&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;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;drawingBufferWidth&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;drawingBufferHeight&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readPixels&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;drawingBufferWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;drawingBufferHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RGBA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UNSIGNED_BYTE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ثم نصنّف الصفوف بنسبة البكسلات السوداء ومتوسط اللون. بعد إصلاح القبة صار المشهد صفر بالمئة سوادًا، ومتوسط اللون في الصفوف العليا &lt;code&gt;[116, 174, 210]&lt;/code&gt;، أي سماء زرقاء. اختبار رقمي كهذا يعمل داخل خط النشر الآلي.&lt;/p&gt;

&lt;p&gt;انتبه أن أصل الإحداثيات في &lt;code&gt;readPixels&lt;/code&gt; أسفل اليسار لا أعلاه، فأي منطق يفترض العكس سيقرأ الأرض على أنها سماء.&lt;/p&gt;

&lt;h2&gt;
  
  
  الخلاصة
&lt;/h2&gt;

&lt;p&gt;المصائد السبع تنبع كلها من موضع المشهد: &lt;strong&gt;داخل موقع حقيقي&lt;/strong&gt;، لا في صفحة تجريبية معزولة. الأب صاحب الـtransform، والحاوية التي لم تأخذ مقاسها بعد، والحزمة التي جمعت كل الصفحات، لا وجود لأي منها في الدرس التعليمي.&lt;/p&gt;

&lt;p&gt;بنينا هذا الديمو في &lt;a href="https://roeia.com/%D8%B4%D8%B1%D9%83%D8%A9-%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D8%A7%D9%84%D8%B9%D8%A7%D8%A8" rel="noopener noreferrer"&gt;شركة تصميم العاب&lt;/a&gt; رؤية ليكون جزءًا من صفحة الخدمة نفسها، فوقعنا في المصائد بالترتيب. إن كنت تخطط لشيء مشابه، قِس حزمة الجافاسكربت قبل أن تصدّق أن التقسيم يعمل، وابدأ باختبار المشهد داخل تخطيط موقعك لا في صفحة فارغة.&lt;/p&gt;

</description>
      <category>webgl</category>
      <category>threejs</category>
      <category>javascript</category>
      <category>gamedev</category>
    </item>
  </channel>
</rss>
