<?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: Știuriuc Sorin-Marian</title>
    <description>The latest articles on DEV Community by Știuriuc Sorin-Marian (@sorin_stiuriuc).</description>
    <link>https://dev.to/sorin_stiuriuc</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%2F3942973%2F039666fd-da2a-4038-a866-9d0924925532.jpg</url>
      <title>DEV Community: Știuriuc Sorin-Marian</title>
      <link>https://dev.to/sorin_stiuriuc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sorin_stiuriuc"/>
    <language>en</language>
    <item>
      <title>Auditing a Next.js Salary Calculator After a 4.5x Search Visibility Spike</title>
      <dc:creator>Știuriuc Sorin-Marian</dc:creator>
      <pubDate>Wed, 29 Jul 2026 04:59:44 +0000</pubDate>
      <link>https://dev.to/sorin_stiuriuc/auditing-a-nextjs-salary-calculator-after-a-45x-search-visibility-spike-426j</link>
      <guid>https://dev.to/sorin_stiuriuc/auditing-a-nextjs-salary-calculator-after-a-45x-search-visibility-spike-426j</guid>
      <description>&lt;p&gt;At the end of June, a Romanian salary calculator I built entered a very different search environment.&lt;/p&gt;

&lt;p&gt;Google Search Console showed roughly 4.5 times more impressions and clicks during the next 28-day period:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Period&lt;/th&gt;
&lt;th&gt;Clicks&lt;/th&gt;
&lt;th&gt;Impressions&lt;/th&gt;
&lt;th&gt;CTR&lt;/th&gt;
&lt;th&gt;Weighted position&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;June 1–28, 2026&lt;/td&gt;
&lt;td&gt;495&lt;/td&gt;
&lt;td&gt;41,281&lt;/td&gt;
&lt;td&gt;1.20%&lt;/td&gt;
&lt;td&gt;8.94&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;June 29–July 26, 2026&lt;/td&gt;
&lt;td&gt;2,237&lt;/td&gt;
&lt;td&gt;184,388&lt;/td&gt;
&lt;td&gt;1.21%&lt;/td&gt;
&lt;td&gt;7.77&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is a 351.9% increase in clicks and a 346.7% increase in impressions.&lt;/p&gt;

&lt;p&gt;It would make a great case study if I could honestly say: “I changed a few tags, Google rewarded the site, and traffic exploded.”&lt;/p&gt;

&lt;p&gt;That is not what happened.&lt;/p&gt;

&lt;p&gt;The more useful story is how I audited the site &lt;em&gt;after&lt;/em&gt; the spike, separated seasonal demand from technical improvements, and decided what not to change while the data was still incomplete.&lt;/p&gt;

&lt;p&gt;The project is &lt;a href="https://salariile.ro" rel="noopener noreferrer"&gt;salariile.ro&lt;/a&gt;, a Next.js and TypeScript calculator for Romanian salaries and payroll taxes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first clue was the nearly unchanged CTR
&lt;/h2&gt;

&lt;p&gt;Clicks grew by 4.52x. Impressions grew by 4.47x. CTR moved from 1.20% to 1.21%.&lt;/p&gt;

&lt;p&gt;That pattern matters.&lt;/p&gt;

&lt;p&gt;If the increase had primarily come from dramatically better titles, snippets, or rankings, I would expect a clearer CTR change. Instead, Google showed the site much more often, and clicks increased almost proportionally.&lt;/p&gt;

&lt;p&gt;There was a ranking improvement: weighted average position moved from 8.94 to 7.77. That is useful progress, especially around the first results page, but it does not explain a four-and-a-half-fold increase by itself.&lt;/p&gt;

&lt;p&gt;The stronger explanation was external demand.&lt;/p&gt;

&lt;p&gt;Romania’s statutory minimum gross salary changed from 4,050 lei to 4,325 lei on July 1, 2026. That created a predictable wave of searches around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the new minimum net salary;&lt;/li&gt;
&lt;li&gt;employer costs;&lt;/li&gt;
&lt;li&gt;payroll tax calculations;&lt;/li&gt;
&lt;li&gt;gross-to-net conversions;&lt;/li&gt;
&lt;li&gt;the effective date of the change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The comparison window starts on June 29, immediately before the change. It therefore captures the search peak around a national fiscal event, while the previous period does not.&lt;/p&gt;

&lt;p&gt;SEO improvements may have helped the site capture some of that demand. They did not create the demand.&lt;/p&gt;

&lt;p&gt;That distinction changes how I interpret every metric that follows.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we cannot conclude
&lt;/h2&gt;

&lt;p&gt;This dataset does &lt;strong&gt;not&lt;/strong&gt; prove that a particular deployment caused the increase.&lt;/p&gt;

&lt;p&gt;It does not provide a clean experiment, because several variables changed at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;search demand rose sharply around July 1;&lt;/li&gt;
&lt;li&gt;the query mix changed;&lt;/li&gt;
&lt;li&gt;Google crawled and reevaluated pages on its own schedule;&lt;/li&gt;
&lt;li&gt;some site improvements were deployed during or after the peak;&lt;/li&gt;
&lt;li&gt;Search Console data was not fully finalized through the audit date.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I ran the analysis on July 29, the latest final Search Console date was July 26. Changes made close to that date did not yet have a complete post-deployment observation window.&lt;/p&gt;

&lt;p&gt;A responsible conclusion is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The site gained substantially more visibility during a major seasonal event, while average position improved and CTR remained almost flat.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A less responsible conclusion would be:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Our latest SEO changes increased organic traffic by 352%.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I am waiting for complete seven-, fourteen-, and twenty-eight-day post-change windows before attributing results to specific work.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. SSR is an indexability requirement, not just an architecture choice
&lt;/h2&gt;

&lt;p&gt;A calculator is naturally interactive, so it is easy to push too much of it into client-side rendering.&lt;/p&gt;

&lt;p&gt;That can produce a page whose browser experience looks correct while its initial HTML contains little more than controls, placeholders, or a loading state.&lt;/p&gt;

&lt;p&gt;For the important landing pages, I want the server-rendered response to contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the page title and primary heading;&lt;/li&gt;
&lt;li&gt;an explanation of the calculation;&lt;/li&gt;
&lt;li&gt;the current fiscal values;&lt;/li&gt;
&lt;li&gt;useful results or examples;&lt;/li&gt;
&lt;li&gt;internal links;&lt;/li&gt;
&lt;li&gt;canonical and structured-data signals.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;JavaScript should enhance the calculator. It should not be responsible for creating the entire indexable document.&lt;/p&gt;

&lt;p&gt;The practical test is simple: fetch the production URL and inspect the returned HTML before hydration. If its main subject is not understandable there, the page is not finished from an SEO perspective.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Dynamic routes need an indexation policy
&lt;/h2&gt;

&lt;p&gt;Salary calculators can generate almost unlimited combinations. A route such as &lt;code&gt;/calculator/[value]&lt;/code&gt; could theoretically create a page for every possible salary.&lt;/p&gt;

&lt;p&gt;That does not mean every value deserves an indexable URL.&lt;/p&gt;

&lt;p&gt;Thousands of near-identical pages would fragment internal authority, consume crawl attention, and create little unique value. The project therefore uses an allowlist for salary-value landing pages.&lt;/p&gt;

&lt;p&gt;A simplified Next.js pattern looks like this:&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;indexedSalaryValues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4050&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4325&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateStaticParams&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;indexedSalaryValues&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;value&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;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&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;The exact implementation is less important than the rule: a new indexable value must have demonstrated search demand or a distinct fiscal purpose.&lt;/p&gt;

&lt;p&gt;Users can still calculate arbitrary salaries interactively. User input simply does not create an unlimited indexable URL inventory.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Canonicals should be tested as contracts
&lt;/h2&gt;

&lt;p&gt;Every indexable page emits a self-referencing canonical.&lt;/p&gt;

&lt;p&gt;That sounds basic, but canonical regressions often appear during route refactors, metadata abstraction, query-string handling, or trailing-slash changes.&lt;/p&gt;

&lt;p&gt;I do not treat “the component contains a canonical declaration” as sufficient. The rendered production output should answer three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is the canonical present?&lt;/li&gt;
&lt;li&gt;Does it resolve to the intended public URL?&lt;/li&gt;
&lt;li&gt;Is that URL indexable and successful?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A canonical pointing to a redirect, a different salary variant, or the wrong hostname can quietly undermine an otherwise good page.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Rendered tests catch SEO failures that unit tests miss
&lt;/h2&gt;

&lt;p&gt;Calculation tests verify fiscal logic. They do not verify what a crawler receives.&lt;/p&gt;

&lt;p&gt;The project also tests rendered routes for SEO-critical output, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP status;&lt;/li&gt;
&lt;li&gt;title and description;&lt;/li&gt;
&lt;li&gt;one meaningful H1;&lt;/li&gt;
&lt;li&gt;canonical URL;&lt;/li&gt;
&lt;li&gt;indexability;&lt;/li&gt;
&lt;li&gt;expected content;&lt;/li&gt;
&lt;li&gt;valid JSON-LD;&lt;/li&gt;
&lt;li&gt;treatment of unsupported calculator values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This turns important SEO behavior into a regression boundary.&lt;/p&gt;

&lt;p&gt;If a refactor accidentally removes an H1, changes a canonical, or makes a fiscal page render an empty client shell, the test should fail before deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. JSON-LD is useful only when it describes the visible page
&lt;/h2&gt;

&lt;p&gt;Structured data is not a place to list every schema type that might generate a rich result.&lt;/p&gt;

&lt;p&gt;The site uses page-appropriate entities such as &lt;code&gt;WebApplication&lt;/code&gt;, &lt;code&gt;Article&lt;/code&gt;, &lt;code&gt;FAQPage&lt;/code&gt;, &lt;code&gt;BreadcrumbList&lt;/code&gt;, &lt;code&gt;WebPage&lt;/code&gt;, and &lt;code&gt;Organization&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The rules I follow are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the markup must parse;&lt;/li&gt;
&lt;li&gt;the type must match the page;&lt;/li&gt;
&lt;li&gt;claims must agree with visible content;&lt;/li&gt;
&lt;li&gt;dates and URLs must be consistent;&lt;/li&gt;
&lt;li&gt;FAQ markup must correspond to questions users can actually read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Schema validation passing is necessary, but it is not evidence that the page deserves stronger rankings.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Crawl the live deployment, not only the repository
&lt;/h2&gt;

&lt;p&gt;The production crawl covered all 63 URLs present in the live sitemap.&lt;/p&gt;

&lt;p&gt;The result was 63 successful responses out of 63, with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no missing or mismatched canonicals;&lt;/li&gt;
&lt;li&gt;no accidental &lt;code&gt;noindex&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;no invalid JSON-LD;&lt;/li&gt;
&lt;li&gt;no missing primary headings;&lt;/li&gt;
&lt;li&gt;no duplicate titles or descriptions;&lt;/li&gt;
&lt;li&gt;no true orphan pages;&lt;/li&gt;
&lt;li&gt;no unexpectedly thin pages under the audit threshold.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a useful technical baseline. It is not an SEO victory by itself.&lt;/p&gt;

&lt;p&gt;A perfectly crawlable page can still rank poorly because its content is less useful, its intent is unclear, its result is unattractive, or competing domains have much stronger authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Keep Core Web Vitals in the release gate
&lt;/h2&gt;

&lt;p&gt;Markup and crawlability are only part of the audit.&lt;/p&gt;

&lt;p&gt;For Core Web Vitals, I monitor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LCP&lt;/strong&gt; for delayed hero content, fonts, and render-blocking assets;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;INP&lt;/strong&gt; for calculator interactions and expensive client-side handlers;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLS&lt;/strong&gt; for results, notices, and controls that shift after hydration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Field data and lab data serve different purposes. Lab runs are excellent regression alarms, while field data shows what real users experience at the 75th percentile.&lt;/p&gt;

&lt;p&gt;I do not use a green Lighthouse run to explain an organic traffic spike. Likewise, I would not ignore a real-user performance regression just because rankings had not moved yet.&lt;/p&gt;

&lt;p&gt;Performance is a product-quality requirement first and a search signal second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the next bottleneck is probably off-page
&lt;/h2&gt;

&lt;p&gt;The audit did not uncover a technical failure large enough to explain the remaining ranking ceiling.&lt;/p&gt;

&lt;p&gt;Several high-volume generic queries sit around positions 6–10. The strongest pages already have sound rendering, canonicals, internal linking, structured data, and substantial content.&lt;/p&gt;

&lt;p&gt;At that point, repeatedly rewriting healthy pages can create more risk than value.&lt;/p&gt;

&lt;p&gt;The larger gap is external authority: relevant websites independently citing the project as a useful source.&lt;/p&gt;

&lt;p&gt;The next phase is therefore not mass link building. It is producing assets that deserve references:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transparent calculation methodology;&lt;/li&gt;
&lt;li&gt;reusable fiscal tables and datasets;&lt;/li&gt;
&lt;li&gt;reproducible examples;&lt;/li&gt;
&lt;li&gt;an embeddable calculator or widget;&lt;/li&gt;
&lt;li&gt;original analyses that HR, accounting, and employment publications can verify.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project’s &lt;a href="https://salariile.ro/metodologie" rel="noopener noreferrer"&gt;methodology page&lt;/a&gt; exists so readers can inspect assumptions and official sources rather than trust an unexplained number.&lt;/p&gt;

&lt;p&gt;For developers who want to inspect the implementation, the &lt;a href="https://github.com/xsagul/salariile-ro" rel="noopener noreferrer"&gt;repository is public on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Relevant editorial citations are useful. Purchased links, automated directory submissions, and unrelated guest-post networks are not a durable strategy, especially for salary and tax information.&lt;/p&gt;

&lt;h2&gt;
  
  
  My post-spike audit checklist
&lt;/h2&gt;

&lt;p&gt;When a search spike appears, I now ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did clicks grow faster than impressions?&lt;/li&gt;
&lt;li&gt;Did CTR or average position materially change?&lt;/li&gt;
&lt;li&gt;Was there a seasonal, legal, or news event?&lt;/li&gt;
&lt;li&gt;Is the latest Search Console data final?&lt;/li&gt;
&lt;li&gt;Does the initial HTML contain the page’s real content?&lt;/li&gt;
&lt;li&gt;Are dynamic indexable URLs intentionally limited?&lt;/li&gt;
&lt;li&gt;Do rendered pages pass canonical, heading, schema, and status tests?&lt;/li&gt;
&lt;li&gt;Are Core Web Vitals healthy in field data?&lt;/li&gt;
&lt;li&gt;Is the bottleneck still technical, or has authority become the limiting factor?&lt;/li&gt;
&lt;li&gt;What observation window is required before making another change?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The hardest SEO decision can be to wait
&lt;/h2&gt;

&lt;p&gt;A large spike creates pressure to optimize everything immediately.&lt;/p&gt;

&lt;p&gt;In this case, the better decision is to preserve the strongest pages, document the baseline, and wait for complete post-event data. The homepage remains the owner of the generic calculator intent until the seasonal wave normalizes and there is enough evidence to justify any migration.&lt;/p&gt;

&lt;p&gt;The audit confirmed that the technical foundation is healthy.&lt;/p&gt;

&lt;p&gt;It also showed why that is not enough.&lt;/p&gt;

&lt;p&gt;Search growth came from the intersection of real-world demand, crawlable implementation, useful content, and improving visibility. The next challenge is earning enough trust outside the site to move important queries from the lower half of page one into the positions where people actually click.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>seo</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The fiscal edge cases Romanian payroll software gets wrong (and how I modeled them in TypeScript)</title>
      <dc:creator>Știuriuc Sorin-Marian</dc:creator>
      <pubDate>Thu, 02 Jul 2026 18:24:45 +0000</pubDate>
      <link>https://dev.to/sorin_stiuriuc/the-fiscal-edge-cases-romanian-payroll-software-gets-wrong-and-how-i-modeled-them-in-typescript-152</link>
      <guid>https://dev.to/sorin_stiuriuc/the-fiscal-edge-cases-romanian-payroll-software-gets-wrong-and-how-i-modeled-them-in-typescript-152</guid>
      <description>&lt;p&gt;Three weeks ago I wrote about &lt;a href="https://dev.to/sorin_stiuriuc"&gt;modeling Romania's salary taxes in TypeScript&lt;/a&gt; for &lt;a href="https://salariile.ro" rel="noopener noreferrer"&gt;salariile.ro&lt;/a&gt;. Since then, Romania's minimum wage changed (July 1st: 4,050 → 4,325 lei gross), and I shipped a payslip generator on top of the same fiscal engine: &lt;a href="https://salariile.ro/fluturas-salariu" rel="noopener noreferrer"&gt;salariile.ro/fluturas-salariu&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Building it forced me to confront edge cases that, as far as I can tell from comparing outputs, even commercial payroll software gets wrong. Here are three of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The tax-free allowance survives bonuses (up to a ceiling)
&lt;/h2&gt;

&lt;p&gt;Romania gives minimum-wage earners a fixed tax-free slice: 200 lei/month is exempt from all contributions and income tax (OUG 89/2025). The naive implementation:&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;facilitate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;functieDeBaza&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;brut&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;SALARIU_MINIM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;200&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was literally my first version. It's wrong. The actual rule: the allowance applies when the &lt;em&gt;base&lt;/em&gt; salary equals the minimum wage AND total gross income (excluding meal vouchers) stays under a ceiling — 4,600 lei since July. So minimum wage + a 200 lei bonus = allowance stays. Minimum wage + a 400 lei bonus = allowance gone, on the entire amount.&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;salariuDeBaza&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;salariuDeBaza&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;salariuDeBaza&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;brut&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;facilitate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;functieDeBaza&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;salariuDeBaza&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;SALARIU_MINIM&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;brut&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;PLAFON_FACILITATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;DEDUCERE_MINIM&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is 80–100 lei/month in the employee's pocket. I've seen real payslips where software silently dropped the allowance the moment any bonus appeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Personal deduction rounding changed in 2023 — some software never noticed
&lt;/h2&gt;

&lt;p&gt;The personal deduction (a progressive tax shield for lower salaries) used to be rounded up to the nearest 10 lei. Since 2023, the Fiscal Code says you apply the exact computed amount, rounded to the leu.&lt;/p&gt;

&lt;p&gt;For a 4,425 lei gross salary: 19% × 4,325 = 821.75 → &lt;strong&gt;822 lei&lt;/strong&gt;. A well-known Romanian calculator still shows &lt;strong&gt;800&lt;/strong&gt; for this case — the old rounding, cascading into a wrong income tax.&lt;/p&gt;

&lt;p&gt;How do I know mine is right? I validate against ANAF's own D112 declaration validator (DUKIntegrator) — the same software employers' declarations run through. If my numbers diverge from the official validator, my tests fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. There is no such thing as a 168-hour month
&lt;/h2&gt;

&lt;p&gt;Hourly rate = base salary ÷ monthly working hours. The lazy constant is 168 (21 days × 8h). But Romanian months have 19–23 working days once you subtract weekends and legal holidays. July 2026 has 23 → 184 hours → 23.51 lei/hour at minimum wage, not 25.74.&lt;/p&gt;

&lt;p&gt;I generate the working-hours calendar from a single source of truth (a map of legal holidays, shared between the &lt;a href="https://salariile.ro/zile-libere-2026" rel="noopener noreferrer"&gt;public holidays page&lt;/a&gt; and the payslip PDF), so overtime pay is computed on the real month, not an average.&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;zileLucratoareLuna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;an&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;luna0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&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;zileInLuna&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;an&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;luna0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;getUTCDate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lucratoare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;zileInLuna&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dow&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;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;an&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;luna0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;getUTCDay&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;weekend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dow&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;dow&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;6&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;sarbatoare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;an&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SARBATORI_LEGALE_2026&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;luna0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;weekend&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;sarbatoare&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;lucratoare&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;lucratoare&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;h2&gt;
  
  
  The meta-lesson
&lt;/h2&gt;

&lt;p&gt;Fiscal law is a spec written in prose, full of exceptions that only surface when real people hit them. The only defense I've found: one pure calculation module as the single source of truth, tests pinned to the official validator's output, and treating every divergence from another calculator as a research task — sometimes they're wrong, sometimes I am.&lt;/p&gt;

&lt;p&gt;The whole thing is open source: &lt;a href="https://github.com/xsagul/salariile-ro" rel="noopener noreferrer"&gt;github.com/xsagul/salariile-ro&lt;/a&gt;. If you're Romanian and want to check your own payslip against the law, the generator is free, no account: &lt;a href="https://salariile.ro/fluturas-salariu" rel="noopener noreferrer"&gt;salariile.ro/fluturas-salariu&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you've built payroll or tax tools for other countries — do you also validate against the government's own software, or is that a Romanian luxury?&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>Modeling Romania's salary taxes in TypeScript: lessons from building salariile.ro</title>
      <dc:creator>Știuriuc Sorin-Marian</dc:creator>
      <pubDate>Fri, 12 Jun 2026 18:36:15 +0000</pubDate>
      <link>https://dev.to/sorin_stiuriuc/modeling-romanias-salary-taxes-in-typescript-lessons-from-building-salariilero-20o</link>
      <guid>https://dev.to/sorin_stiuriuc/modeling-romanias-salary-taxes-in-typescript-lessons-from-building-salariilero-20o</guid>
      <description>&lt;p&gt;I spent the last few weeks building &lt;a href="https://salariile.ro" rel="noopener noreferrer"&gt;salariile.ro&lt;/a&gt;, a salary calculator for Romania. It sounds like a weekend project: take the gross salary, subtract three percentages, show the net. It was not. Here is what made it interesting, and what I would tell anyone who needs to model payroll or tax rules in code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The domain is the hard part
&lt;/h2&gt;

&lt;p&gt;Romanian salary math in 2026 looks simple on paper: 25% pension contribution (CAS), 10% health insurance (CASS), 10% income tax after deductions, plus 2.25% paid by the employer on top (CAM). The complexity hides in the details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The personal deduction is not a constant. It is a curve that depends on income and the number of dependents, and it phases out completely above a threshold.&lt;/li&gt;
&lt;li&gt;Meal vouchers are taxed differently from cash. CASS and income tax apply to their value, but the tax money is withheld from your cash salary while the full nominal value lands on the card. Many calculators get this subtly wrong, and people then wonder why the payslip does not match the estimate.&lt;/li&gt;
&lt;li&gt;Sector rules (construction, agriculture) and the minimum wage facility change the formula depending on contract details.&lt;/li&gt;
&lt;li&gt;The rules change constantly. The minimum wage changes again on July 1, 2026, and several thresholds are indexed to it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My main takeaway: encode rules as data, not as ifs. Every fiscal period is a config object with rates, thresholds, deduction tables and links to the legal source. The calculation engine is a pure function from (config, input) to a payslip breakdown. When the July 2026 rules land, that will be a new config entry plus a set of tests, not a refactor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test against official examples
&lt;/h2&gt;

&lt;p&gt;The Romanian tax authority publishes norms for Declaration 112 with worked examples. Every rule I implement gets a test case traced back to an official source, and where two sources disagree, a comment links the exact article of law I followed. For a YMYL topic this is the only thing that keeps the project honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The web part: boring on purpose
&lt;/h2&gt;

&lt;p&gt;Next.js 16 App Router, React 19, TypeScript, deployed on Vercel. Server Components everywhere, with the calculator as the only real client component. No cookies, no ads, no tracking scripts. For search visibility the unglamorous things mattered most: correct JSON-LD (Person, FAQPage), a sitemap that actually updates, llms.txt for AI crawlers, and pages that each answer one question instead of one page trying to answer everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;p&gt;While researching the niche I found that the sites ranking top 3 for "calculator salariu" include one with almost zero backlink authority and another still showing rules that expired in 2024. In this niche, search engines seem to reward being current and precise more than raw link mass. Encouraging, if you are building something accurate.&lt;/p&gt;

&lt;p&gt;The calculator is live at &lt;a href="https://salariile.ro" rel="noopener noreferrer"&gt;salariile.ro&lt;/a&gt; and I am happy to answer questions about modeling tax rules in TypeScript. It is a surprisingly fun domain once you accept the chaos.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
