Read the code before you install it.

This page is the whole product. Not a summary of it, not a security badge someone sold us — the actual file, in full, below. Read it, hash it, paste it into any AI you trust and ask what it does. Install it only after that.

1. What you are about to check

One file of plain JavaScript. No build step, no minifier, no dependencies, no package manager. The listing at the bottom of this page is that file, and the hash below is its hash — the same bytes ship as js/auto-year.js inside the zip and inside the WordPress plugin.

File
auto-year.js · auto-year-free/js/auto-year.js in the zip
Size
8,130 bytes
SHA-256
7e18e734a94a085ec12fd661e4f0c10f2ddf8b8d4ae66cd86655b4a7b12badf3

Unzip what you downloaded and check that the file is the one we published:

macOS / Linux   shasum -a 256 auto-year-free/js/auto-year.js
Windows         certutil -hashfile auto-year-free\js\auto-year.js SHA256

Two honest footnotes. The hash is of the script, not of the zip: zip archives record build timestamps, so their digest changes without a single byte of code changing — the script is what runs on your site, so the script is what we pin. And if you take the snippet by copying it off the install guide rather than by unzipping the file, you get the same code but not necessarily the same hash: a pasted block carries whatever trailing whitespace your editor decides to add. Hash the file, not the paste.

2. What it does — and what it cannot do

Every line below is in the source further down; nothing here is a promise made outside the code.

It does

  • Walk the text of your page and look at 4-digit numbers. <script> and <style> are skipped.
  • Update such a number only if it is a year between 1990 and today and one of two gates is open: the text carries ©, Copyright or (c), or the element sits in the bottom 15% of the page.
  • In a range like 2019–2024, change the end year only.
  • Leave the current year alone, so running twice changes nothing.
  • Re-run every 250 ms for about ten seconds after load, then stop for good. Tilda and similar builders inject the footer after first paint; without this the script would arrive before the footer does.
  • Sit inside try/catch from top to bottom, so a failure is silence, never a broken page.
  • Skip any element you mark data-ayu-ignore, and everything inside it.

It cannot

  • Talk to us, or to anyone. There is no fetch, no XMLHttpRequest, no sendBeacon, no tracking pixel — no network call of any kind, to our servers or to a third party.
  • Load anything external. It pulls in no library, no font, no CDN.
  • Store anything: no cookies, no localStorage, no sessionStorage.
  • See your visitors. It reads no form fields, no page URL, no addresses, no identity of any kind. It never learns a site is yours.
  • Build new markup or run generated code: no innerHTML, no eval, no new Function, no createElement. It assigns text to text nodes, and that is the only write it performs.
  • Touch anything but that text: no attributes, links, images, styles, prices or layout.
  • Phone home about a licence. There is no key, no activation, no expiry. Cut us off tomorrow and it keeps working.

Those absences are greppable. On the file you downloaded, this prints nothing:

grep -nE "fetch|XMLHttpRequest|sendBeacon|localStorage|sessionStorage|document\.cookie|innerHTML|eval\(|new Function|createElement" auto-year.js

3. The limits, stated by us first

4. Don't take our word for it — ask a machine that has no stake

Copy the source below, paste it into ChatGPT, Claude, Gemini or whatever you already use, and paste this after it:

This JavaScript is about to go into the footer of my website. Read it and tell me plainly: does it send any data anywhere, does it load anything from another server, does it store anything in my visitors' browsers, can it break my page, and is there anything in it that is not needed for the job it claims to do? Quote the lines that back each answer.

An AI reading this file has no reason to be kind to us, and the file is short enough to read in full — that is the point of shipping it unminified. If an answer contradicts anything on this page, we would genuinely like to hear about it: [email protected].

Prefer tools over opinions? Upload the file to VirusTotal for 70+ engines, or paste it into JSHint — it is ordinary, standards-compliant code with no syntax anyone has to be clever about.

5. The whole thing, nothing removed

Comments included — they explain the awkward decisions, which is where a script like this would hide something if it wanted to.

auto-year.js
/*!
 * YearAlert — standalone (no-backend) copyright-year updater.
 *
 * Drop-in <script> for any site (verified live on Tilda, nosandson.com): keeps the
 * copyright year fresh client-side with ZERO server calls and ZERO content edits.
 *
 * Rule: a 4-digit in-range year is updated to the current year only if it is either
 *   (a) in a COPYRIGHT context  — same text carries ©, "copyright" or "(c)", OR
 *   (b) in the BOTTOM BAND       — its element sits in the lowest `BAND` of the page
 *       (footers live at the bottom; mid-page years like "CROC 2020" are left alone).
 * Ranges (2019–2024) update only the END year, and only when the range really is one —
 * a phone written "3613-2018" is not. Nothing is rewritten when another group of digits
 * sits right beside the candidate: a footer's phone number is not a copyright year, and
 * breaking one on a customer's live site is worse than leaving a year stale (T-018).
 * Idempotent (never re-touches the current year). Opt-out: add data-ayu-ignore to any
 * element to exclude its subtree.
 *
 * Wrapped so it can NEVER throw into the host page. Tilda and other builders inject
 * footer blocks asynchronously, so we re-run on a short interval after first paint.
 */
(function () {
  'use strict';

  var MIN_YEAR = 1990;
  var DASH = '[\\-\\u2013\\u2014]'; // hyphen, en dash, em dash
  var BAND = 0.85;                  // bottom 15% of the page counts as "footer zone"
  var COPY = '©';
  // What may sit between two groups of a phone number. Deliberately NOT '.' or ',':
  // "Copyright 2019. 123 Main St" is a sentence stop and a street number, not a dial code.
  var PHONE_SEP = '[\\s\\u00a0\\-\\u2013\\u2014()/]';
  var SEP_BEFORE = new RegExp('\\d' + PHONE_SEP + '{0,2}$');
  var SEP_AFTER = new RegExp('^' + PHONE_SEP + '{0,2}\\d');

  function inRange(y, cur) { return y >= MIN_YEAR && y <= cur; }

  function hasCopyright(text) {
    if (!text) return false;
    var t = text.toLowerCase();
    return t.indexOf(COPY) !== -1 || t.indexOf('copyright') !== -1 || t.indexOf('(c)') !== -1;
  }

  /*
   * A 4-digit group with ANOTHER group of digits right beside it belongs to a phone
   * number, not to a copyright: "Llámanos: 33 3613 2018". The digit-run gate below only
   * protects digits welded into one run (ИНН, ОГРН) — a phone is written in groups, so
   * every other gate lets it through, and a footer carrying both a phone and a © is the
   * normal case in our market (see GO-TO-MARKET).
   *
   * The lead scanner hit the same bug from the other side and fixed it the same way
   * (`isPhoneFragment` in crm/server/src/checks/_year-detect.js). There a false positive
   * cost us one bad lead; here it rewrites a paying customer's phone number on their own
   * live site, silently. So this errs toward doing nothing: a year we skip is visible and
   * recoverable, a phone we break is neither.
   *
   * `start`/`end` bracket the candidate inside `text`.
   */
  function isPhoneFragment(text, start, end) {
    return SEP_BEFORE.test(text.slice(Math.max(0, start - 14), start))
        || SEP_AFTER.test(text.slice(end, end + 14));
  }

  // A dash between two 4-digit groups is only a year range when the FIRST group is itself
  // a plausible year — a copyright cannot begin in the future. That is what separates
  // "© 2019–2024" from "3613-2018", which the range path used to rewrite to "3613-2026".
  // Below MIN_YEAR the text must actually say copyright: "© 1985–2024" is a real footer,
  // a bare "1234-2018" is a phone number.
  function isYearRange(text, whole, first, index, cur) {
    var start = parseInt(first, 10);
    if (start > cur) return false;
    if (start < MIN_YEAR && !hasCopyright(text)) return false;
    return !isPhoneFragment(text, index, index + whole.length);
  }

  // True if the node's element sits within the bottom BAND of the document.
  function isBottom(node) {
    var el = node.parentElement;
    if (!el || !el.getBoundingClientRect) return false;
    var r = el.getBoundingClientRect();
    var absTop = r.top + (window.pageYOffset || window.scrollY || 0);
    var docH = document.documentElement.scrollHeight;
    return docH > 0 && absTop >= docH * BAND;
  }

  function isIgnored(node) {
    var el = node.parentElement;
    if (!el || !el.closest) return false;
    try { return !!el.closest('[data-ayu-ignore]'); } catch (e) { return false; }
  }

  // Core text-node transform. Exported for tests. `opts` lets tests force the gate
  // (copyright / bottom) without a real layout: { context: <string>, atBottom: <bool> }.
  function fixTextValue(text, cur, gateOpen) {
    if (!text || !/\d{4}/.test(text)) return { text: text, changed: false };
    if (!gateOpen) return { text: text, changed: false };
    var cy = String(cur);
    var changed = false;
    var rangeSrc = '(\\d{4})(\\s*' + DASH + '\\s*)(\\d{4})';

    // Does the text hold a range that is genuinely a range? A dash-separated phone is not
    // one, and must not send us down the range path — that path skips single years, so a
    // rejected phone would otherwise shadow the real copyright later in the same node.
    var scan = new RegExp(rangeSrc, 'g');
    var hit;
    var hasYearRange = false;
    while ((hit = scan.exec(text))) {
      if (isYearRange(text, hit[0], hit[1], hit.index, cur)) { hasYearRange = true; break; }
    }

    if (hasYearRange) {
      text = text.replace(new RegExp(rangeSrc, 'g'), function (m, a, s, b, offset, whole) {
        if (!isYearRange(whole, m, a, offset, cur)) return m;
        var e = parseInt(b, 10);
        if (inRange(e, cur) && e !== cur) { changed = true; return a + s + cy; }
        return m;
      });
    } else {
      // Single year. Require non-digit (or string edge) on BOTH sides so a 4-digit run
      // inside a longer number — Russian ИНН/ОГРН, phone, etc., which frequently share the
      // copyright text node — is never rewritten. No lookbehind (universal browser support).
      text = text.replace(/(\D|^)(\d{4})(?!\d)/g, function (m, pre, y, offset, whole) {
        var n = parseInt(y, 10);
        if (!inRange(n, cur) || n === cur) return m;
        var at = offset + pre.length;
        if (isPhoneFragment(whole, at, at + 4)) return m;
        changed = true;
        return pre + cy;
      });
    }
    return { text: text, changed: changed };
  }

  function fixNode(node, cur) {
    var raw = node.nodeValue;
    if (!raw || !/\d{4}/.test(raw)) return;
    if (isIgnored(node)) return;
    var ctx = raw + ' ' + (node.parentNode ? node.parentNode.textContent : '');
    var gateOpen = hasCopyright(ctx) || isBottom(node);
    var res = fixTextValue(raw, cur, gateOpen);
    if (res.changed) node.nodeValue = res.text;
  }

  function replaceYears(cur) {
    cur = cur || new Date().getFullYear();
    if (typeof document === 'undefined' || !document.body) return;
    var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
      acceptNode: function (nd) {
        var p = nd.parentNode;
        var tg = p && p.nodeName ? p.nodeName.toLowerCase() : '';
        return (tg === 'script' || tg === 'style')
          ? NodeFilter.FILTER_REJECT
          : NodeFilter.FILTER_ACCEPT;
      }
    }, false);
    var nd;
    while ((nd = walker.nextNode())) fixNode(nd, cur);
  }

  function run() { try { replaceYears(); } catch (e) { /* never throw into host */ } }

  // Re-run for ~10s: builder-injected footers (Tilda, etc.) appear after first paint.
  function boot() {
    run();
    var i = 0;
    var iv = setInterval(function () { run(); if (++i > 40) clearInterval(iv); }, 250);
  }

  // Browser auto-boot.
  try {
    if (typeof document !== 'undefined') {
      if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', boot);
      } else {
        boot();
      }
    }
  } catch (e) { /* ignore */ }

  // Node/test surface.
  if (typeof module !== 'undefined' && module.exports) {
    module.exports = { fixTextValue: fixTextValue, hasCopyright: hasCopyright, inRange: inRange, MIN_YEAR: MIN_YEAR, BAND: BAND };
  }
})();

The listing is inside an element marked data-ayu-ignore — the same opt-out you can put on any element of your own site. That is why the years printed in it stay as written.

Happy with what you read?

Get it — it takes two minutes

Already bought? The step-by-step is on the install guide.