<?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: hoyeon</title>
    <description>The latest articles on DEV Community by hoyeon (@hoyeon).</description>
    <link>https://dev.to/hoyeon</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%2F4131319%2Fd1ab8601-5876-4209-b02d-56abcc10b1ef.png</url>
      <title>DEV Community: hoyeon</title>
      <link>https://dev.to/hoyeon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hoyeon"/>
    <language>en</language>
    <item>
      <title>Keep shipping quotes honest with versioned inputs</title>
      <dc:creator>hoyeon</dc:creator>
      <pubDate>Wed, 23 Sep 2026 13:18:10 +0000</pubDate>
      <link>https://dev.to/hoyeon/keep-shipping-quotes-honest-with-versioned-inputs-2d9</link>
      <guid>https://dev.to/hoyeon/keep-shipping-quotes-honest-with-versioned-inputs-2d9</guid>
      <description>&lt;p&gt;Disclosure: I am involved with Kimfast, a package-forwarding service. I used AI to help structure and edit this article. The code below is a standalone design example, not a claim about Kimfast's production implementation or conversion results.&lt;/p&gt;

&lt;p&gt;A quote is more than one displayed number. A customer may enter a product link before purchase, while the warehouse later measures a packed parcel. If the UI shows both values as “shipping cost” without their inputs and timestamps, a reasonable user may treat an estimate as a final charge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model the evidence, not only the amount
&lt;/h2&gt;

&lt;p&gt;A small quote record can preserve its source and state:&lt;/p&gt;

&lt;p&gt;enum QuoteStage { PRE_PURCHASE_ESTIMATE, PACKED_MEASUREMENT, FINAL_AMOUNT }&lt;/p&gt;

&lt;p&gt;record QuoteSnapshot {&lt;br&gt;
  country: string&lt;br&gt;
  stage: QuoteStage&lt;br&gt;
  amount: Decimal&lt;br&gt;
  currency: string&lt;br&gt;
  measuredWeightKg: Decimal | null&lt;br&gt;
  observedAt: timestamp&lt;br&gt;
  policyVersion: string&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;This is language-neutral pseudocode. The stage names are not a proposed public API. The useful constraint is that every amount has a country, a meaning, and the policy version used to calculate it. A later calculation creates a new snapshot instead of silently overwriting an older one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three invariants worth testing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do not relabel an estimate as final.&lt;/strong&gt; A UI component should render the stage with the amount. If the packed measurement is unavailable, it must not invent one from the product listing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not reuse a quote across countries.&lt;/strong&gt; A link for one forwarding route does not establish the cost of another. Validate the country code against the quote and the route before showing a comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not compare unlike inputs.&lt;/strong&gt; If one amount includes a warehouse measurement and another uses a seller's stated weight, show that difference before calculating a “savings” figure. A price comparison needs the same scope and included services.&lt;/p&gt;

&lt;p&gt;A review table makes the boundary explicit:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;What the UI can say&lt;/th&gt;
&lt;th&gt;What it must not infer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Product URL only&lt;/td&gt;
&lt;td&gt;Estimated amount under stated assumptions&lt;/td&gt;
&lt;td&gt;Final packed weight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warehouse measurement&lt;/td&gt;
&lt;td&gt;Amount based on observed package dimensions&lt;/td&gt;
&lt;td&gt;Customs outcome or delivery date&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confirmed final amount&lt;/td&gt;
&lt;td&gt;Charge and the applicable conditions&lt;/td&gt;
&lt;td&gt;That all future orders cost the same&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Make change observable
&lt;/h2&gt;

&lt;p&gt;Keep the old snapshot when a pricing rule changes. The support team should be able to answer: which inputs produced the figure the customer saw, when was it shown, and which version of the rules was in effect? That is more useful than storing only the last amount.&lt;/p&gt;

&lt;p&gt;The public &lt;a href="https://kimfast.com/spain/" rel="noopener noreferrer"&gt;Kimfast Spain forwarding guide&lt;/a&gt; describes an order flow in which the parcel is measured before payment. It is a reference for the user-facing sequence, not evidence that Kimfast has adopted the record structure above. Any quoted price or service condition should be checked against the current guide before a product screen or an external article repeats it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to start
&lt;/h2&gt;

&lt;p&gt;Before changing code, collect one example of each stage from the current product flow without exposing customer data. Label every UI amount with its source, units, and date. Then test transitions with missing measurements, changed country selection, and a price-rule update. If the UI cannot explain why two amounts differ, adding more decimal places will not resolve the underlying ambiguity.&lt;/p&gt;

&lt;p&gt;A forwarding form becomes more trustworthy when the system can explain the number it shows and the conditions under which that number may change.&lt;/p&gt;

</description>
      <category>architecture</category>
    </item>
    <item>
      <title>Backlinks as structured observations: designing a conservative link-audit result</title>
      <dc:creator>hoyeon</dc:creator>
      <pubDate>Mon, 21 Sep 2026 15:34:32 +0000</pubDate>
      <link>https://dev.to/hoyeon/backlinks-as-structured-observations-designing-a-conservative-link-audit-result-3opl</link>
      <guid>https://dev.to/hoyeon/backlinks-as-structured-observations-designing-a-conservative-link-audit-result-3opl</guid>
      <description>&lt;p&gt;A backlink audit is easier to debug when it returns observations rather than a single success flag. A page can be reachable while its link is missing; a link can exist while the checker has no evidence about indexing.&lt;/p&gt;

&lt;p&gt;This article proposes a small data contract for that distinction. It is a design note prepared with AI assistance, not a production benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep evidence separate from conclusions
&lt;/h2&gt;

&lt;p&gt;For each check, record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;source URL and intended destination;&lt;/li&gt;
&lt;li&gt;check timestamp and observation method (raw HTML or rendered DOM);&lt;/li&gt;
&lt;li&gt;fetch outcome, including timeout and access denial;&lt;/li&gt;
&lt;li&gt;observed href, anchor text, and rel tokens;&lt;/li&gt;
&lt;li&gt;redirect destination, if actually followed;&lt;/li&gt;
&lt;li&gt;scope limitations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An empty list of anchors is meaningful only if you successfully inspected the relevant document. If the request failed, an empty list says nothing about the page's links.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use explicit states
&lt;/h2&gt;

&lt;p&gt;A minimal result can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fetch_state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"document_scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"raw_html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"link_state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"present"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"observed_href"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.org/guide"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ugc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nofollow"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"index_state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not_checked"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"referral_state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not_measured"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this contract, present means the expected anchor was observed. It does not mean that the link passes ranking value or has generated visits. Likewise, nofollow belongs in metadata, not in a missing-link bucket.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reducer with a conservative failure path
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;link_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fetch_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_complete&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;matching_anchor_count&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fetch_state&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;document_complete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;present&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;matching_anchor_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;absent_in_observed_document&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;link_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&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="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;link_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;denied&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&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="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;link_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&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="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;link_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;present&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;link_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&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="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;absent_in_observed_document&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are illustrative assertions for the reducer, not evidence about any external website. The caller must decide what complete means. Raw HTML can be complete as an HTTP response while still lacking links added by JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to test around the reducer
&lt;/h2&gt;

&lt;p&gt;Use fixtures for plain URL text, an anchor with href, a relative link, a different subdomain, a redirect, and a lookalike hostname. Match hosts using a URL parser rather than substring checks. Decide explicitly whether www and other subdomains belong to the same target set. Resolve relative links against the observed page URL before matching.&lt;/p&gt;

&lt;p&gt;In a scheduled job, preserve the last successful observation and the latest attempt separately. An outage should not silently overwrite a previously observed link with a deletion claim. Retry conservatively and honor the site's access rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where human review still matters
&lt;/h2&gt;

&lt;p&gt;A technically present anchor can point to the wrong guide or appear in an irrelevant paragraph. A reviewer should assess the surrounding explanation and whether the destination answers the reader's question. That is an editorial decision, not a parser feature.&lt;/p&gt;

&lt;p&gt;Google's &lt;a href="https://developers.google.com/search/docs/crawling-indexing/links-crawlable" rel="noopener noreferrer"&gt;link documentation&lt;/a&gt; provides the crawlable-anchor guidance. The contract above is one implementation proposal; it does not describe Google's internal systems.&lt;/p&gt;

&lt;p&gt;Disclosure: I am involved in &lt;a href="https://keyserp.io/" rel="noopener noreferrer"&gt;Keyserp&lt;/a&gt;. This example is shared as a technical design note about backlinks and evidence handling. It was prepared with AI assistance and makes no ranking or conversion claims.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>debugging</category>
      <category>seo</category>
    </item>
    <item>
      <title>스페인·미국·영국 배송대행 콘텐츠의 데이터 설계: 검색어와 운영 사실 분리하기</title>
      <dc:creator>hoyeon</dc:creator>
      <pubDate>Mon, 21 Sep 2026 15:34:18 +0000</pubDate>
      <link>https://dev.to/hoyeon/seupeinmigugyeonggug-baesongdaehaeng-kontenceuyi-deiteo-seolgye-geomsaegeowa-unyeong-sasil-bunrihagi-4lp1</link>
      <guid>https://dev.to/hoyeon/seupeinmigugyeonggug-baesongdaehaeng-kontenceuyi-deiteo-seolgye-geomsaegeowa-unyeong-sasil-bunrihagi-4lp1</guid>
      <description>&lt;p&gt;배송대행 안내 페이지를 여러 국가로 나눌 때 URL 경로만 바꾸면 운영 정보가 섞이기 쉽습니다. 이 글은 김빠른을 예시로, 검색어와 검증된 운영 정보를 서로 다른 필드로 관리하는 데이터 설계 제안입니다. 실제 운영 시스템에 배포했다는 사례나 성과 보고는 아닙니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  키워드는 사실 데이터가 아닙니다
&lt;/h2&gt;

&lt;p&gt;스페인 배송대행, 미국 배송대행, 영국 배송대행은 콘텐츠 기획에 쓰는 표현입니다. LA 배송대행도 검색어 후보가 될 수 있지만, 그 문자열만으로 LA 거점이 존재한다고 추론해서는 안 됩니다.&lt;/p&gt;

&lt;p&gt;따라서 검색어를 warehouse_city 같은 운영 필드에 자동으로 복사하지 않습니다. 이 원고에서는 김빠른의 LA 거점을 검증하지 않았습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  데이터 구조를 분리합니다
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;make_brief&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verified_city&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;editorial&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keyword&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;country&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;operations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verified_city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;verified_city&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city_claim_allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;verified_city&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;brief&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_brief&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LA 배송대행&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;brief&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;operations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verified_city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;brief&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city_claim_allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;위 코드는 최소 설계 예시입니다. 실무에서는 verified_city라는 변수명만 믿으면 안 됩니다. 출처 URL, 확인 시각, 검토자, 유효 기간까지 함께 저장해야 합니다. 오래된 확인은 최신 사실과 같지 않습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  생성 프롬프트에는 미확인 상태를 전달합니다
&lt;/h2&gt;

&lt;p&gt;“editorial은 독자의 관심사이고 operations만 운영 사실이다. verified_city가 null이면 도시 거점을 단정하지 마라. 부족한 정보를 채우지 말고 review_required에 기록하라.”&lt;/p&gt;

&lt;p&gt;이 지시도 모델의 오류를 완전히 막지는 못합니다. 생성 후 도시·가격·기간 같은 주장을 추출해 근거표와 대조하는 검수 단계가 필요합니다. 문장에 링크가 붙어 있다는 이유만으로 근거가 일치한다고 판단하면 안 됩니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  페이지 연결은 별도 매핑으로 둡니다
&lt;/h2&gt;

&lt;p&gt;실제 국가별 안내의 예시는 &lt;a href="https://kimfast.com/spain/" rel="noopener noreferrer"&gt;김빠른 스페인 배송대행&lt;/a&gt;, &lt;a href="https://kimfast.com/usa/" rel="noopener noreferrer"&gt;미국 배송대행&lt;/a&gt;, &lt;a href="https://kimfast.com/uk/" rel="noopener noreferrer"&gt;영국 배송대행&lt;/a&gt;입니다. 링크는 각 국가의 안내 경로를 보여주는 예시이며, 이 글의 코드가 해당 사이트에 적용됐다는 뜻은 아닙니다.&lt;/p&gt;

&lt;p&gt;국가 코드를 URL에 매핑할 때 기본값을 아무 국가로 두지 않는 편이 좋습니다. 알 수 없는 국가 코드라면 오류를 반환하거나 사람이 확인하도록 해야 합니다. 누락된 국가를 미국으로 처리하면 잘못된 안내로 이어질 수 있습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  테스트 계획
&lt;/h2&gt;

&lt;p&gt;확인 자료가 없는 도시, 만료된 자료, 알 수 없는 국가 코드, 빈 키워드, 국가와 목적지 URL이 다른 입력을 각각 테스트 대상으로 둡니다. 위 예시 두 assertion은 설계 의도를 보여주지만, 실제 서비스 검증을 대신하지는 않습니다.&lt;/p&gt;

&lt;p&gt;이해관계 공개: 작성자는 김빠른 운영에 참여합니다. AI의 도움으로 작성한 개발 설계 제안이며, 배포 완료나 고객 성과를 주장하지 않습니다.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>data</category>
      <category>python</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>A link checker needs an unknown state, not just true or false</title>
      <dc:creator>hoyeon</dc:creator>
      <pubDate>Fri, 18 Sep 2026 10:58:02 +0000</pubDate>
      <link>https://dev.to/hoyeon/a-link-checker-needs-an-unknown-state-not-just-true-or-false-1l58</link>
      <guid>https://dev.to/hoyeon/a-link-checker-needs-an-unknown-state-not-just-true-or-false-1l58</guid>
      <description>&lt;p&gt;Disclosure: I work on Keyserp. This article was generated by an AI agent at the account owner’s request, using the design of our internal article-monitoring tool as context. It has not received independent human editorial review. It is a design note, not a claim about search rankings or a public product release.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boolean that loses information
&lt;/h2&gt;

&lt;p&gt;A published-page checker seems like a simple function: fetch a URL and return true or false. But a timeout, a login wall, and a missing article are three different observations. Mapping all three to false gives an operator the wrong next action.&lt;/p&gt;

&lt;p&gt;Our internal workflow tracks whether an external article remains readable and whether its expected reference is present. The important design decision was to preserve uncertainty instead of hiding it behind a green or red badge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep observations separate from conclusions
&lt;/h2&gt;

&lt;p&gt;A useful result has at least these fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;requested URL and final URL after redirects&lt;/li&gt;
&lt;li&gt;checked-at timestamp&lt;/li&gt;
&lt;li&gt;transport outcome and HTTP status, if available&lt;/li&gt;
&lt;li&gt;whether the article body was identified&lt;/li&gt;
&lt;li&gt;whether an article-specific text marker matched&lt;/li&gt;
&lt;li&gt;whether the expected link was found inside that body&lt;/li&gt;
&lt;li&gt;a reason for any unknown result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use null for a link that could not be checked. Reserve false for a completed inspection that did not find it. A missing observation should not silently become negative evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small decision table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Observation&lt;/th&gt;
&lt;th&gt;Classification&lt;/th&gt;
&lt;th&gt;Next action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Timeout or access denial&lt;/td&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Retry later within the site's limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Successful response showing a login page&lt;/td&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Check permitted access manually&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expected article body and expected link&lt;/td&gt;
&lt;td&gt;Verified at this time&lt;/td&gt;
&lt;td&gt;Schedule the next check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expected article body, complete inspection, no expected link&lt;/td&gt;
&lt;td&gt;Link not found&lt;/td&gt;
&lt;td&gt;Review the article&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;404 or 410&lt;/td&gt;
&lt;td&gt;Missing response observed&lt;/td&gt;
&lt;td&gt;Recheck before escalating&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Body selector no longer matches&lt;/td&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;Review the site adapter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are illustrative fixtures, not measured incident counts. A repeated missing response may justify a removal-suspected state; it still does not prove why the publisher removed anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Link matching deserves its own contract
&lt;/h2&gt;

&lt;p&gt;A domain appearing somewhere in the HTML is weak evidence. Navigation, comments, related articles, and advertising can all contain links outside the author's text.&lt;/p&gt;

&lt;p&gt;Scope the inspection to the identified article body. Resolve relative URLs against the final page URL. Then compare the parsed destination with the explicitly configured target. Do not use substring matching: an unrelated hostname can contain your domain as text.&lt;/p&gt;

&lt;p&gt;Decide deliberately whether trailing slashes, query parameters, or fragments are significant for each target. Do not strip every query parameter by default; some applications use them to identify the resource. If a publisher wraps links through a redirect service, report that wrapper as an observation and review it separately instead of declaring a direct match.&lt;/p&gt;

&lt;p&gt;Store the observed anchor text and rel attribute as evidence. Finding a link does not establish that a search engine indexed it or assigned it value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests that challenge the happy path
&lt;/h2&gt;

&lt;p&gt;Before connecting a classifier to live pages, I would give it fixtures that deliberately look almost correct:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A 200 login page with the expected domain in its footer.&lt;/li&gt;
&lt;li&gt;The correct article with the target link only in a reader comment.&lt;/li&gt;
&lt;li&gt;The correct article with a relative link that resolves to a different destination.&lt;/li&gt;
&lt;li&gt;An article shell whose body is rendered only after JavaScript runs.&lt;/li&gt;
&lt;li&gt;The expected article with no link after a complete body inspection.&lt;/li&gt;
&lt;li&gt;A transient missing response followed by a successful check.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The expected outcome for the first four is unknown or a narrowly described mismatch, depending on what the adapter actually inspected. It should never be verified merely because a string was found.&lt;/p&gt;

&lt;p&gt;Fixture tests establish classifier behavior. They do not establish that a live publisher's current markup is supported. Record those two checks separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checker should not become a publisher
&lt;/h2&gt;

&lt;p&gt;An unknown or missing result should create a review task, not automatically repost the article. Otherwise a temporary outage can turn into duplicate content and unwanted community activity.&lt;/p&gt;

&lt;p&gt;The same boundary helps with alerting: keep the last verified observation visible alongside the latest failed attempt. That tells an operator what was known, when it was known, and what changed.&lt;/p&gt;

&lt;p&gt;Our tool is still internal, and long-term resilience to layout changes remains an open operational question. The practical lesson is to design the unknown state before automating the next action.&lt;/p&gt;

&lt;p&gt;Affiliation: &lt;a href="https://keyserp.io/" rel="noopener noreferrer"&gt;Keyserp&lt;/a&gt;. I have an operational interest in the service mentioned here.&lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>softwareengineering</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
