<?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: kadirhan</title>
    <description>The latest articles on DEV Community by kadirhan (@heykadirhan).</description>
    <link>https://dev.to/heykadirhan</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%2F3843749%2Fa21fcd98-0c53-4de1-a5c6-011865e0723d.png</url>
      <title>DEV Community: kadirhan</title>
      <link>https://dev.to/heykadirhan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/heykadirhan"/>
    <language>en</language>
    <item>
      <title>The half of California's AB 723 that nobody implements</title>
      <dc:creator>kadirhan</dc:creator>
      <pubDate>Mon, 10 Aug 2026 13:00:32 +0000</pubDate>
      <link>https://dev.to/heykadirhan/the-half-of-californias-ab-723-that-nobody-implements-40</link>
      <guid>https://dev.to/heykadirhan/the-half-of-californias-ab-723-that-nobody-implements-40</guid>
      <description>&lt;p&gt;`California's AB 723 has been in force since January 1, 2026. It amends&lt;br&gt;
Business &amp;amp; Professions Code § 10140.8 and it applies to any real estate&lt;br&gt;
listing image that has been digitally altered. Virtual staging is the obvious&lt;br&gt;
case, but the definition is wider than that.&lt;/p&gt;

&lt;p&gt;The rule has two parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A statement that the image has been altered, "reasonably conspicuous" and
placed on or adjacent to the image.&lt;/li&gt;
&lt;li&gt;A link to a publicly accessible URL, or a QR code, that includes and
clearly identifies the original, unaltered image.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everyone builds the first part. It is a text label on a photo, an afternoon of&lt;br&gt;
work. The second part is a small piece of infrastructure: a permanent public&lt;br&gt;
URL, per image, that outlives the tab the agent had open when they exported.&lt;/p&gt;

&lt;p&gt;I build a virtual staging product, so I had to ship both. This is how the&lt;br&gt;
second part is put together, and the one thing I got wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What counts as altered
&lt;/h2&gt;

&lt;p&gt;Worth getting right before writing any code, because it decides which of your&lt;br&gt;
features need the label and which do not. Subsection (b)(2) carves out ordinary&lt;br&gt;
photo editing.&lt;/p&gt;

&lt;p&gt;Covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adding furniture, rugs, art or decor&lt;/li&gt;
&lt;li&gt;Removing furniture, clutter or personal items&lt;/li&gt;
&lt;li&gt;Changing paint, flooring or wall finishes&lt;/li&gt;
&lt;li&gt;Sky replacement and day to dusk&lt;/li&gt;
&lt;li&gt;Greening or reshaping lawns and landscaping&lt;/li&gt;
&lt;li&gt;Anything that changes the facade or the property itself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exposure, lighting, white balance, color correction&lt;/li&gt;
&lt;li&gt;Sharpening&lt;/li&gt;
&lt;li&gt;Straightening, cropping, angle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the codebase that line is a set, and the two omissions are deliberate:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;ts&lt;br&gt;
export const TOOLS_ALTERING_LISTING_IMAGES = new Set([&lt;br&gt;
  "virtual-staging",&lt;br&gt;
  "sky-replacement",&lt;br&gt;
  "day-to-dusk",&lt;br&gt;
  "grass-greener",&lt;br&gt;
  "declutter",&lt;br&gt;
  "object-remover",&lt;br&gt;
]);&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;image-enhancer&lt;/code&gt; is out because exposure and white balance are precisely what&lt;br&gt;
the statute excludes. A floor plan generator is out because a diagram is not an&lt;br&gt;
altered photograph. Attaching a legal claim to a feature the law does not cover&lt;br&gt;
is not a harmless extra: it is the fastest way to make the rest of your&lt;br&gt;
compliance copy untrustworthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part one: burn the label into the pixels
&lt;/h2&gt;

&lt;p&gt;The tempting implementation is a DOM overlay. It is also wrong. The photo does&lt;br&gt;
not stay in your app. It gets downloaded, uploaded to an MLS, dropped into a&lt;br&gt;
PDF flyer, pasted into Instagram. Anything you render in HTML is gone by then.&lt;/p&gt;

&lt;p&gt;So the label is composited server side with sharp, as an SVG layer over the&lt;br&gt;
final buffer:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`ts&lt;br&gt;
const padding  = Math.round(width * 0.03);&lt;br&gt;
const fontSize = Math.round(Math.max(16, width * 0.02));&lt;/p&gt;

&lt;p&gt;const labelSvg = buildLabelSvg(text, style, labelWidth, labelHeight, fontSize);&lt;/p&gt;

&lt;p&gt;sharpInstance = sharp(&lt;br&gt;
  await sharpInstance&lt;br&gt;
    .clone()&lt;br&gt;
    .composite([{ input: Buffer.from(labelSvg), top, left, blend: "over" }])&lt;br&gt;
    .toBuffer(),&lt;br&gt;
);&lt;br&gt;
`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Three details that cost me time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scale everything to image width.&lt;/strong&gt; A 16px label is conspicuous on a 1024px&lt;br&gt;
export and invisible on a 4096px panorama. Every dimension here is a fraction&lt;br&gt;
of &lt;code&gt;width&lt;/code&gt;, never a constant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escape the text before it reaches the SVG.&lt;/strong&gt; User supplied label text goes&lt;br&gt;
into an XML document. &lt;code&gt;&amp;amp;&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt; and &lt;code&gt;&amp;gt;&lt;/code&gt; get replaced or sharp throws on a&lt;br&gt;
malformed SVG, which in an export pipeline surfaces as a corrupt download with&lt;br&gt;
no obvious cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clamp the position.&lt;/strong&gt; A long label at a large font on a small image will&lt;br&gt;
happily render outside the canvas, and sharp errors rather than cropping. Fit&lt;br&gt;
the box to the image first, then clamp the offset.&lt;/p&gt;

&lt;p&gt;The composite is wrapped in its own try/catch that logs and continues. That is&lt;br&gt;
a judgment call about which failure is worse: an export that dies, or an export&lt;br&gt;
that quietly ships without the label. Given the label is the legally required&lt;br&gt;
part, silently continuing is arguably the wrong default and it is on my list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part two: a permanent link to the original
&lt;/h2&gt;

&lt;p&gt;This is the part with actual state behind it. Each image in a project can mint&lt;br&gt;
one share token:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;ts&lt;br&gt;
if (!projectImage.publicShareToken) {&lt;br&gt;
  projectImage.publicShareToken = randomUUID();&lt;br&gt;
  await project.save();&lt;br&gt;
}&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The token lives on the image subdocument, not on the project. The statute talks&lt;br&gt;
about "the original, unaltered image" for a specific altered image, so a project&lt;br&gt;
level link that lands on a gallery of twelve rooms does not clearly identify&lt;br&gt;
anything. One token per image, minted lazily, and idempotent: calling it twice&lt;br&gt;
returns the same URL, because the agent will paste that URL into an MLS field&lt;br&gt;
and it has to survive being re-copied a month later.&lt;/p&gt;

&lt;p&gt;The token resolves to &lt;code&gt;/compare/[token]&lt;/code&gt;, a page with no auth that shows the&lt;br&gt;
original next to the staged version and states, in plain text, that the image&lt;br&gt;
was generated.&lt;/p&gt;

&lt;h3&gt;
  
  
  The robots decision
&lt;/h3&gt;

&lt;p&gt;That page carries other people's listing photos, so it must not be indexed.&lt;br&gt;
But the naive &lt;code&gt;noindex, nofollow&lt;/code&gt; turns out to be a real mistake:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;ts&lt;br&gt;
robots: {&lt;br&gt;
  index: false,&lt;br&gt;
  follow: true,&lt;br&gt;
}&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Every disclosure link on the internet points at this page. That is what the&lt;br&gt;
statute asks agents to publish. With &lt;code&gt;follow: false&lt;/code&gt; all of it dead ends. With&lt;br&gt;
&lt;code&gt;follow: true&lt;/code&gt; the photos still stay out of search, and the links that&lt;br&gt;
compliance produces still reach the rest of the site. Two flags, opposite jobs,&lt;br&gt;
and it is easy to set them together out of habit.&lt;/p&gt;

&lt;h3&gt;
  
  
  QR codes, because print exists
&lt;/h3&gt;

&lt;p&gt;Subsection two allows a QR code, and the reason is obvious once you have held a&lt;br&gt;
flyer. Print cannot carry a hyperlink.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;&lt;code&gt;ts&lt;br&gt;
const compareUrl = new URL(&lt;/code&gt;/compare/${token}&lt;code&gt;, req.url).toString();&lt;br&gt;
const png = await QRCode.toBuffer(compareUrl, { type: "png", width: 512, margin: 2 });&lt;br&gt;
&lt;/code&gt;&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;One guard worth copying: the route resolves the token against the database&lt;br&gt;
before generating anything. Otherwise you have shipped a public, unauthenticated&lt;br&gt;
QR generator that will encode any string a stranger sends it, hosted on your&lt;br&gt;
domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I got wrong
&lt;/h2&gt;

&lt;p&gt;All of the above works. Almost nobody uses it.&lt;/p&gt;

&lt;p&gt;I measured it on production before writing the next feature on top: across every&lt;br&gt;
image eligible for a share link, fewer than one percent had ever minted one. The&lt;br&gt;
number of distinct users who had minted one was one, and it was probably me,&lt;br&gt;
testing.&lt;/p&gt;

&lt;p&gt;The reason is not that the feature is broken. It is that the compliance link&lt;br&gt;
sits behind a button, inside a share panel, next to a download button that works&lt;br&gt;
fine without it. Two clicks that the workflow does not require. An agent&lt;br&gt;
exporting eleven photos before a Monday listing does not go looking.&lt;/p&gt;

&lt;p&gt;The lesson generalizes past real estate: &lt;strong&gt;a compliance feature that depends on&lt;br&gt;
someone finding it is not a compliance feature.&lt;/strong&gt; If the on-image label is&lt;br&gt;
switched on, the link to the original is not optional under the statute, so the&lt;br&gt;
token should be minted at export time and handed over with the file. Making the&lt;br&gt;
user assemble the two halves themselves is a design that produces a&lt;br&gt;
demonstrably compliant product and non-compliant listings.&lt;/p&gt;

&lt;p&gt;That change touches the export flow, so it is not shipped yet. But the&lt;br&gt;
measurement is the useful part, and it took one script to get. Before building&lt;br&gt;
the layer on top of a feature, check whether anyone reaches the feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference
&lt;/h2&gt;

&lt;p&gt;I keep the rule text, what it covers, the NAR Code of Ethics articles that apply&lt;br&gt;
regardless of state, and copy-paste disclosure wording on a single page:&lt;br&gt;
&lt;a href="https://getquickstaging.com/virtual-staging-disclosure" rel="noopener noreferrer"&gt;getquickstaging.com/virtual-staging-disclosure&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Four other states come up constantly in trade coverage. Every one of them is&lt;br&gt;
listed there as unverified, with no summary of what it supposedly requires,&lt;br&gt;
because I could not find the primary source. If you have one, I will add it.&lt;/p&gt;

&lt;p&gt;Not legal advice. Ask your broker or your attorney.`&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>typescript</category>
      <category>imageprocessing</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
