gpsr-listing-lint / index.html
getreadystack's picture
Upload index.html with huggingface_hub
e5cddc7 verified
Raw History Blame Contribute Delete
52.7 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>GPSR Product Listing requirements — check your product feed free</title>
<style>
:root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512}
.wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px}
textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box}
.bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer}
input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px}
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65}
.sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600}
.note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa}
</style>
<script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script>
<!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/gpsr-listing-lint"></head>
<body>
<div class="wrap">
<h1>GPSR Product Listing requirements — check your product feed free</h1>
<p class="lede">Audits product feed files (CSV/TSV/XML) against GPSR Article 19: manufacturer address, EU Responsible Person, product picture and identifier, warnings. 12 rules, offline. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false">id,title,product_type,gtin,mpn,image_link,manufacturer,manufacturer_address,manufacturer_email,country_of_origin,responsible_person,responsible_person_address,responsible_person_email,warning,target_country
SKU-2001,Cotton Tea Towel 50x70,Home &gt; Kitchen textiles,4012345678901,TT-5070,https://cdn.example.eu/img/2001.jpg,Weberei Lindau GmbH,Seestrasse 14 88131 Lindau DE,kontakt@weberei-lindau.de,DE,,,,,DE
SKU-2002,Ceramic Mug 350 ml,Home &gt; Tableware,4012345678918,MUG-350,https://cdn.example.eu/img/2002.jpg,,Via Roma 9 20121 Milano IT,info@ceramica.example,IT,,,,,IT
SKU-2003,Linen Napkin Set of 4,Home &gt; Kitchen textiles,4012345678925,NAP-4L,https://cdn.example.eu/img/2003.jpg,Tessitura Brescia Srl,N/A,info@tessiturabrescia.it,IT,,,,,IT
SKU-2004,Glass Carafe 1 L,Home &gt; Tableware,4012345678932,CAR-1L,https://cdn.example.eu/img/2004.jpg,Verrerie Lorraine SAS,7 Rue Haute 54000 Nancy FR,noreply@verrerie-lorraine.fr,FR,,,,,FR
SKU-2005,Stainless Steel Bottle 500 ml,Home &gt; Drinkware,4012345678949,BT-500,https://cdn.example.eu/img/2005.jpg,Generic,Unit 4 Business Park Dublin IE,sales@bottles.example,IE,,,,,IE
SKU-2006,Bamboo Cutting Board,Home &gt; Kitchen,4012345678956,CB-BAM,https://cdn.example.eu/img/2006.jpg,Guangxi Bamboo Works Ltd,No 88 Jinan Road Nanning CN,sales@gx-bamboo.example,CN,,,,,DE
SKU-2007,Silicone Baking Mat,Home &gt; Bakeware,4012345678963,BM-SIL,https://cdn.example.eu/img/2007.jpg,Ningbo Kitchen Tools Co,12 Yinzhou Ningbo CN,export@nbkitchen.example,CN,Ningbo Kitchen Tools Co,12 Yinzhou Ningbo CN,export@nbkitchen.example,,DE
SKU-2008,Cast Iron Skillet 26 cm,Home &gt; Cookware,4012345678970,SK-26,https://cdn.example.eu/img/2008.jpg,Ohio Ironworks Inc,1400 Grand Ave Columbus OH US,hello@ohioiron.example,US,Rhein Compliance GmbH,Domstrasse 5 50668 Koeln DE,,,DE
SKU-2009,Woven Storage Basket,Home &gt; Storage,4012345678987,BK-WOV,,Rattan Craft Portugal Lda,Rua do Sol 12 4000 Porto PT,geral@rattancraft.pt,PT,,,,,PT
SKU-2010,Marble Coaster Set,Home &gt; Tableware,,,https://cdn.example.eu/img/2010.jpg,Pietra Carrara Srl,Via Cave 3 54033 Carrara IT,info@pietracarrara.it,IT,,,,,IT
SKU-2011,Wooden Pull-Along Duck 3+,Toys &gt; Wooden toys,4012345678994,DK-PULL,https://cdn.example.eu/img/2011.jpg,Holzspiel Werk GmbH,Hauptstrasse 8 96450 Coburg DE,service@holzspielwerk.de,DE,,,,,DE
SKU-2012,LED Desk Lamp with Lithium Battery,Electronics &gt; Lighting,4012345679007,LMP-LI,https://cdn.example.eu/img/2012.jpg,Lumen Nord AB,Storgatan 2 11151 Stockholm SE,info@lumennord.se,SE,,,,"Warning. Do not short-circuit the battery. Keep away from fire.",DE
SKU-2013,Enamel Mixing Bowl 24 cm,Home &gt; Kitchen,4012345679014,BW-24E,https://cdn.example.eu/img/2013.jpg,Emaille Werk Krems GmbH,Wiener Strasse 3 3500 Krems AT,,AT,,,,,AT</textarea>
<div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div>
<div id="out" aria-live="polite"></div>
<div id="paid"><strong>Free here:</strong> every listing row in this feed that is missing a GPSR Article 19 field, with its line number and the article it fails. <strong>In the editor:</strong> export a dated evidence pack for the whole workspace catalogue — every feed file, every failing row, per-rule counts, as CSV and Markdown for a marketplace, an importer or an auditor. <a href="https://getreadystack.com/api/buy/cl/polar_cl_MeEOXqXAf3FoF5aXVJjeeyaoJJNIODsmsm5CK0skgvG">Get the full version — $29</a><br><small>$29 once · one licence key per person or team seat · An EU Responsible Person service is sold as a monthly subscription, per brand; hand-checking a feed at ten seconds a row is about fourteen hours for five thousand rows.</small></div>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var GPSR_RULES = [
{"id": "A19a-NAME", "sev": "error", "article": "GPSR Art. 19(a)",
"msg": "No manufacturer name in the offer. Art. 19(a) wants the name, registered trade name or trade mark of the manufacturer on the listing itself."},
{"id": "A19a-POST", "sev": "error", "article": "GPSR Art. 19(a)",
"msg": "No manufacturer postal address. Art. 19(a) wants a postal address at which the manufacturer can be contacted, shown in the offer."},
{"id": "A19a-ELEC", "sev": "error", "article": "GPSR Art. 19(a)",
"msg": "No manufacturer electronic address. Art. 19(a) wants an e-mail address or contact URL next to the postal one."},
{"id": "A19a-NOREPLY", "sev": "warn", "article": "GPSR Art. 19(a)",
"msg": "Electronic address is an unmonitored mailbox. Art. 19(a) asks for an address at which the manufacturer can be contacted, not one that bounces."},
{"id": "A19a-NOBRAND", "sev": "warn", "article": "GPSR Art. 19(a)",
"msg": "Manufacturer field holds a placeholder brand (generic / unbranded / OEM / the shop's own name). That is not a manufacturer identity."},
{"id": "A19b-RP", "sev": "error", "article": "GPSR Art. 19(b)",
"msg": "Manufacturer is established outside the EU/EEA and no Responsible Person is named. Art. 19(b) requires the name, postal and electronic address of the responsible person under Art. 16(1) GPSR or Art. 4(1) of Reg. (EU) 2019/1020."},
{"id": "A19b-RP-EU", "sev": "error", "article": "GPSR Art. 19(b)",
"msg": "Responsible Person is named but their address is outside the EU/EEA. The responsible person must be established in the Union."},
{"id": "A19b-RP-ELEC", "sev": "error", "article": "GPSR Art. 19(b)",
"msg": "Responsible Person has no electronic address. Art. 19(b) asks for name, postal AND electronic address."},
{"id": "A19c-PIC", "sev": "error", "article": "GPSR Art. 19(c)",
"msg": "No picture of the product. Art. 19(c) names the picture as part of the information allowing identification of the product."},
{"id": "A19c-ID", "sev": "error", "article": "GPSR Art. 19(c)",
"msg": "No product identifier. Art. 19(c) wants the type and any other product identifier (model, MPN, GTIN) in the offer."},
{"id": "A19d-WARN", "sev": "error", "article": "GPSR Art. 19(d)",
"msg": "Product carries warning-bearing signals (age limit, battery, CE, chemical, electrical) but the offer shows no warning or safety information. Art. 19(d) requires it in the offer, not only on the box."},
{"id": "A19d-LANG", "sev": "warn", "article": "GPSR Art. 19(d)",
"msg": "Warning text is not in the language of the target market. Art. 19(d) requires a language easily understood by consumers in the Member State where the product is made available."}
]
;</script>
<script>// GPSR Listing Lint — engine. Same file runs in Node (extension) and in the browser (free web page).
var RULES = (typeof module !== 'undefined') ? require('./rules.json') : window.GPSR_RULES;
var RULE_BY_ID = {};
RULES.forEach(function (r) { RULE_BY_ID[r.id] = r; });
// ---- column vocabulary -------------------------------------------------
var COLS = {
mfr_name: ['manufacturer', 'manufacturername', 'manufacturerinfo', 'manufacturerinfoname', 'brand', 'productbrand', 'hersteller', 'fabricant', 'marque', 'fabricante'],
mfr_post: ['manufactureraddress', 'manufacturerpostaladdress', 'manufacturerinfoaddress', 'manufacturerpostal', 'brandaddress', 'herstelleradresse', 'adressefabricant'],
mfr_elec: ['manufactureremail', 'manufacturercontact', 'manufacturercontacturl', 'manufacturerurl', 'manufacturerinfoemail', 'herstelleremail', 'contactemail', 'emailfabricant'],
rp_name: ['responsibleperson', 'responsiblepersonname', 'euresponsibleperson', 'rpname', 'authorisedrepresentative', 'authorizedrepresentative', 'eurepresentative', 'bevollmaechtigter', 'importer', 'importerinfo', 'importername', 'personneresponsable'],
rp_post: ['responsiblepersonaddress', 'rpaddress', 'euresponsiblepersonaddress', 'importeraddress', 'importerinfoaddress', 'representativeaddress'],
rp_elec: ['responsiblepersonemail', 'rpemail', 'importeremail', 'representativeemail', 'responsiblepersoncontact'],
origin: ['countryoforigin', 'manufacturercountry', 'origin', 'madein', 'productioncountry', 'herkunftsland'],
pic: ['imagelink', 'image', 'imageurl', 'images', 'imagelink1', 'mainimage', 'picture', 'pictureurl', 'bild'],
ident: ['gtin', 'ean', 'upc', 'mpn', 'model', 'modelnumber', 'type', 'sku', 'productid', 'partnumber', 'itemnumber', 'artikelnummer'],
warn: ['warning', 'warnings', 'warningtext', 'safetyinformation', 'safetyinfo', 'safetywarning', 'warnhinweise', 'agewarning', 'hazardwarning'],
market: ['targetcountry', 'contentlanguage', 'language', 'market', 'locale', 'shippingcountry', 'sellingcountry', 'country'],
title: ['title', 'name', 'productname', 'producttitle', 'itemtitle', 'description'],
category: ['producttype', 'googleproductcategory', 'category', 'kategorie', 'categorie']
};
var EU = ('AT BE BG HR CY CZ DK EE FI FR DE GR EL HU IE IT LV LT LU MT NL PL PT RO SK SI ES SE IS LI NO ' +
'AUSTRIA BELGIUM BULGARIA CROATIA CYPRUS CZECHIA CZECHREPUBLIC DENMARK ESTONIA FINLAND FRANCE GERMANY DEUTSCHLAND GREECE HUNGARY IRELAND ITALY ITALIA LATVIA LITHUANIA LUXEMBOURG MALTA NETHERLANDS POLAND POLSKA PORTUGAL ROMANIA SLOVAKIA SLOVENIA SPAIN ESPANA SWEDEN ICELAND LIECHTENSTEIN NORWAY EU EEA').split(/\s+/);
var PLACEHOLDER = /^(n\/?a|na|tbd|tba|none|null|nil|-+|\.+|0|unknown|see packaging|on packaging|see box|see label|as above|same as above|refer to packaging|contact seller|not applicable|xxx+|todo)$/i;
var WARN_SIGNAL = /(\b\d\s*\+\s*(years|yrs)?|not suitable for children|choking|small parts|lithium|li-ion|battery|batteries|rechargeable|ce mark|ce-mark|\b(110|120|220|230|240)\s*v\b|mains|charger|power adapter|flammable|corrosive|toxic|irritant|aerosol|bleach|solvent|laser|toy\b|toys\b|plush|ride-on|scooter|helmet|cosmetic|candle)/i;
var LANG_OF = { DE: 'de', AT: 'de', CH: 'de', FR: 'fr', BE: 'fr', LU: 'fr', ES: 'es', IT: 'it', PL: 'pl', NL: 'nl', PT: 'pt', SE: 'sv', DK: 'da', FI: 'fi', CZ: 'cs', RO: 'ro', HU: 'hu', GR: 'el', EL: 'el' };
var LANG_MARK = {
de: /(achtung|warnung|warnhinweis|vorsicht|nicht geeignet|erstickungsgefahr)/i,
fr: /(attention|avertissement|ne convient pas|danger d|risque)/i,
es: /(advertencia|atenci|no apto|peligro)/i,
it: /(avvertenz|attenzione|non adatto|pericolo)/i,
pl: /(ostrze|uwaga|nie nadaje)/i,
nl: /(waarschuwing|let op|niet geschikt)/i,
pt: /(aviso|aten|n.o adequado|perigo)/i,
sv: /(varning|observera|ej l.mplig)/i,
da: /(advarsel|bem.rk|ikke egnet)/i,
fi: /(varoitus|huomio|ei sovellu)/i,
cs: /(varov.n|upozorn|nen. vhodn)/i,
ro: /(avertisment|aten|nu este potrivit)/i,
hu: /(figyelmeztet|vigy.zat|nem alkalmas)/i,
el: /(προσοχ|προειδοπο)/i
};
var EN_MARK = /(warning|caution|not suitable|choking hazard|keep away from)/i;
var NOREPLY = /^(no-?reply|donotreply|do-not-reply|noreply)@/i;
// ---- parsing -----------------------------------------------------------
function norm(s) { return String(s || '').toLowerCase().replace(/[^a-z0-9]/g, ''); }
function blank(v) { var s = String(v == null ? '' : v).trim(); return s === '' || PLACEHOLDER.test(s); }
function splitRow(line, d) {
var out = [], cur = '', q = false;
for (var i = 0; i < line.length; i++) {
var c = line[i];
if (q) {
if (c === '"' && line[i + 1] === '"') { cur += '"'; i++; }
else if (c === '"') q = false;
else cur += c;
} else if (c === '"') q = true;
else if (c === d) { out.push(cur); cur = ''; }
else cur += c;
}
out.push(cur);
return out.map(function (s) { return s.trim(); });
}
function delimOf(head) {
var best = ',', n = -1;
[',', ';', '\t', '|'].forEach(function (d) {
var c = splitRow(head, d).length;
if (c > n) { n = c; best = d; }
});
return best;
}
function parseCSV(text) {
var lines = text.replace(/\r/g, '').split('\n');
var hi = 0;
while (hi < lines.length && lines[hi].trim() === '') hi++;
if (hi >= lines.length) return null;
var d = delimOf(lines[hi]);
var header = splitRow(lines[hi], d).map(norm);
var rows = [];
for (var i = hi + 1; i < lines.length; i++) {
if (lines[i].trim() === '') continue;
var cells = splitRow(lines[i], d), rec = {};
for (var j = 0; j < header.length; j++) rec[header[j]] = cells[j] === undefined ? '' : cells[j];
rows.push({ line: i + 1, rec: rec });
}
return { header: header, rows: rows, headerLine: hi + 1 };
}
function parseXML(text) {
var lines = text.replace(/\r/g, '').split('\n');
var rows = [], header = {}, cur = null, start = 0;
for (var i = 0; i < lines.length; i++) {
var L = lines[i];
if (/<(item|entry|product)[\s>]/i.test(L)) { cur = {}; start = i + 1; continue; }
if (/<\/(item|entry|product)>/i.test(L)) { if (cur) rows.push({ line: start, rec: cur }); cur = null; continue; }
if (cur) {
var m = L.match(/<([A-Za-z0-9_:-]+)[^>]*>([\s\S]*?)<\/\1>/);
if (m) {
var k = norm(m[1].replace(/^.*:/, ''));
var v = m[2].replace(/<!\[CDATA\[([\s\S]*?)\]\]>/, '$1').trim();
cur[k] = v; header[k] = 1;
}
}
}
return { header: Object.keys(header), rows: rows, headerLine: 1 };
}
// ---- helpers -----------------------------------------------------------
function colFor(header, group) {
for (var i = 0; i < COLS[group].length; i++) if (header.indexOf(COLS[group][i]) >= 0) return COLS[group][i];
return null;
}
function val(rec, header, group) {
var syn = COLS[group], first = null;
for (var i = 0; i < syn.length; i++) {
if (header.indexOf(syn[i]) >= 0) {
var v = String(rec[syn[i]] == null ? '' : rec[syn[i]]);
if (first === null) first = v;
if (!blank(v)) return v; // any filled synonym column satisfies the field
}
}
return first === null ? '' : first;
}
function anyVal(rec, header, groups) {
for (var i = 0; i < groups.length; i++) { var v = val(rec, header, groups[i]); if (!blank(v)) return v; }
return '';
}
function isEU(s) {
var t = String(s || '').toUpperCase().replace(/[^A-Z]/g, '');
if (!t) return null;
if (EU.indexOf(t) >= 0) return true;
for (var i = 0; i < EU.length; i++) if (EU[i].length > 2 && t.indexOf(EU[i]) >= 0) return true;
var codes = String(s).toUpperCase().match(/\b[A-Z]{2}\b/g) || [];
for (var j = 0; j < codes.length; j++) if (EU.indexOf(codes[j]) >= 0) return true;
return false;
}
function label(rec, header) {
var t = val(rec, header, 'title') || anyVal(rec, header, ['ident']);
t = t.replace(/\s+/g, ' ').trim();
return t ? ' — "' + (t.length > 46 ? t.slice(0, 46) + '…' : t) + '"' : '';
}
// ---- check -------------------------------------------------------------
function check(text, opts) {
opts = opts || {};
var findings = [];
var src = String(text || '');
var doc = /^\s*</.test(src) ? parseXML(src) : parseCSV(src);
if (!doc || !doc.rows.length) return { findings: findings };
var H = doc.header, rows = doc.rows, HL = doc.headerLine;
function add(id, line, extra) {
var r = RULE_BY_ID[id];
findings.push({ check: id, sev: r.sev, msg: r.msg + (extra || ''), line: line });
}
// file-level: a required column is not in the feed at all
[['mfr_name', 'A19a-NAME'], ['mfr_post', 'A19a-POST'], ['mfr_elec', 'A19a-ELEC'],
['pic', 'A19c-PIC'], ['ident', 'A19c-ID']].forEach(function (p) {
if (!colFor(H, p[0])) add(p[1], HL, ' No such column in this feed — all ' + rows.length + ' rows fail.');
});
var nonEU = rows.filter(function (r) { return isEU(val(r.rec, H, 'origin')) === false; });
var rpCol = colFor(H, 'rp_name');
if (nonEU.length && !rpCol) add('A19b-RP', HL, ' No Responsible Person column in this feed — ' + nonEU.length + ' non-EU row(s) fail.');
var warnCol = colFor(H, 'warn');
var warnRows = rows.filter(function (r) {
var hay = val(r.rec, H, 'title') + ' ' + val(r.rec, H, 'category');
return WARN_SIGNAL.test(hay);
});
if (warnRows.length && !warnCol) add('A19d-WARN', HL, ' No warning/safety column in this feed — ' + warnRows.length + ' warning-bearing row(s) fail.');
rows.forEach(function (row) {
var rec = row.rec, ln = row.line, tag = label(rec, H);
if (colFor(H, 'mfr_name')) {
var mn = val(rec, H, 'mfr_name');
if (blank(mn)) add('A19a-NAME', ln, tag);
else if (/^(generic|unbranded|no ?brand|oem|noname|no name|assorted|various|own brand)$/i.test(mn.trim())) add('A19a-NOBRAND', ln, ' Found "' + mn.trim() + '".' + tag);
}
if (colFor(H, 'mfr_post') && blank(val(rec, H, 'mfr_post'))) add('A19a-POST', ln, tag);
if (colFor(H, 'mfr_elec')) {
var me = val(rec, H, 'mfr_elec');
if (blank(me)) add('A19a-ELEC', ln, tag);
else if (NOREPLY.test(me.trim())) add('A19a-NOREPLY', ln, ' Found "' + me.trim() + '".' + tag);
}
if (colFor(H, 'pic') && blank(val(rec, H, 'pic'))) add('A19c-PIC', ln, tag);
if (colFor(H, 'ident') && blank(anyVal(rec, H, ['ident']))) add('A19c-ID', ln, tag);
var eu = isEU(val(rec, H, 'origin'));
if (eu === false) {
var rp = rpCol ? val(rec, H, 'rp_name') : '';
if (blank(rp)) { if (rpCol) add('A19b-RP', ln, ' Origin "' + val(rec, H, 'origin') + '".' + tag); }
else {
var rpAddr = val(rec, H, 'rp_post');
if (!blank(rpAddr) && isEU(rpAddr) === false) add('A19b-RP-EU', ln, ' Responsible Person address "' + rpAddr + '".' + tag);
if (colFor(H, 'rp_elec') && blank(val(rec, H, 'rp_elec'))) add('A19b-RP-ELEC', ln, tag);
}
}
var hay = val(rec, H, 'title') + ' ' + val(rec, H, 'category');
if (WARN_SIGNAL.test(hay) && warnCol) {
var w = val(rec, H, 'warn');
if (blank(w)) add('A19d-WARN', ln, tag);
else {
var mk = val(rec, H, 'market').toUpperCase().replace(/[^A-Z]/g, '').slice(0, 2);
var want = LANG_OF[mk];
if (want && LANG_MARK[want] && !LANG_MARK[want].test(w) && EN_MARK.test(w)) {
add('A19d-LANG', ln, ' Market ' + mk + ' expects ' + want + ', warning reads English.' + tag);
}
}
}
});
findings.sort(function (a, b) { return a.line - b.line; });
return { findings: findings };
}
var API = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length };
if (typeof module !== 'undefined') module.exports = API;
if (typeof window !== 'undefined') window.GPSRENGINE = API;
</script>
<script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
'use strict';
function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
function summaryLine(res) {
const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length;
return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.';
}
function toText(res, label) {
const f = (res && res.findings) || [];
const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)];
f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); });
return lines.join('\n');
}
function toHtml(res) {
const f = (res && res.findings) || [];
if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>';
const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join('');
return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
var GPSRREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = GPSRREPORT;
else window.GPSRREPORT = GPSRREPORT;
</script>
<script>
(function(){
var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today');
today.value=new Date().toISOString().slice(0,10);
function run(){ try { var res=window.GPSRENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.GPSRREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } }
go.onclick=run; if (src.value.trim()) run();
})();
</script>
<section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://marketplace.visualstudio.com/items?itemName=ReadyStack.gpsr-listing-lint" rel="noopener" data-yj-install-door="mp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'gpsr-listing-lint',src:'mp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">VS Code Marketplace</a><a href="https://open-vsx.org/extension/readystack/gpsr-listing-lint" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'gpsr-listing-lint',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><a href="https://www.npmjs.com/package/@readystack/gpsr-listing-lint" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'gpsr-listing-lint',src:'npm',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">npm</a><a href="https://www.npmjs.com/package/@readystack/gpsr-listing-lint" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'gpsr-listing-lint',src:'mcp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">MCP server (npx)</a><a href="https://hub.docker.com/r/getreadystack/gpsr-listing-lint" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'gpsr-listing-lint',src:'docker',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Docker Hub</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">ext install ReadyStack.gpsr-listing-lint
npx @readystack/gpsr-listing-lint &lt;file&gt;
npx @readystack/gpsr-listing-lint --mcp</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/cl/polar_cl_l6iN1uWt0FwWu7tBsczD0jWpP2vxFM54Wdwqb3KPi1G" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get the full rule table as a PDF</b><div style="opacity:.78;margin:.35rem 0 .8rem">All 12 rules GPSR Listing Lint checks, each with its source and date, on printable pages you can hand to your team or auditor. We email it now, and once more only if a rule changes. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Email me the PDF</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">Sent. It arrives within the hour - check spam too.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"gpsr-listing-lint",lang:"en",source:"rule_table"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);})();</script></section>
<section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">Audits a product feed row by row against Article 19 of the EU General Product Safety Regulation</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">An EU Responsible Person / authorised-representative service is sold as a monthly subscription, per brand; hand-checking a feed at ten seconds a row is about fourteen hours for five thousand rows. This is $29 once.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_MeEOXqXAf3FoF5aXVJjeeyaoJJNIODsmsm5CK0skgvG" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>
<!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div>
<script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script>
<script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script>
<section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-14">Updated 2026-09-14</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-14","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku57790_result_card.jpg" alt="GPSR Listing Lint" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>Thirteen rows of a Shopify export, twelve findings: that is what a feed manager shipping to Germany and Italy got back from the sample catalogue bundled with GPSR Listing Lint — nine blockers and three warnings, each one pinned to a line number and an article.</p>
<p>Article 19 of the EU General Product Safety Regulation, Regulation (EU) 2023/988, is short and unusually literal. Where a product is offered at a distance, the offer itself must clearly and visibly indicate: the name, registered trade name or trade mark of the manufacturer together with a postal and an electronic address at which they can be contacted; where the manufacturer is not established in the Union, the name, postal and electronic address of the responsible person under Article 16(1) GPSR or Article 4(1) of Regulation (EU) 2019/1020; information allowing the identification of the product, including a picture of it, its type and any other product identifier; and any warning or safety information, in a language easily understood by consumers in the Member State where the product is made available.</p>
<p>The word that costs money is &quot;offer&quot;. Not the box, not the PIM, not the importer&#x27;s e-mail. In a marketplace catalogue the offer is a row in a CSV or an item in an XML feed, and that row is almost never where compliance data was collected.</p>
<p>The sample feed shows how ordinary the failures are. One row simply has an empty manufacturer column. One has N/A where the postal address should be — a placeholder passes a filter looking for blanks, because it is not blank. One has noreply@ as the contact address, which is an address that cannot be contacted. Two rows are made in China: the first names no responsible person at all, the second names the Chinese manufacturer as its own responsible person, which is precisely what Article 19(b) exists to prevent. One row has no image link, and Article 19(c) names the picture explicitly. One has neither GTIN nor MPN. A wooden pull-along duck marked 3+ carries no warning text. An LED lamp with a lithium battery sells into the German market with a warning written in English.</p>
<p>Twelve rules, four articles, one pass over the file. The clean version of the same catalogue returns zero findings, which is the other half of a useful linter: it has to be quiet when the data is right.</p>
<p>Column names are matched by vocabulary rather than by one fixed schema, because no two exports agree. manufacturer, brand, hersteller and fabricant resolve to the same field; responsible_person, eu_representative and importer resolve to another. Comma, semicolon, tab and pipe delimiters are detected from the header line. Placeholders — N/A, TBD, unknown, a bare hyphen, see packaging, same as above — count as empty, and see packaging is the instructive one: the consumer reading the offer has not got the packaging yet.</p>
<p>When a required column is missing from the header entirely, the finding is reported once against the header line with the number of rows it condemns, rather than once per row. A five-thousand-row export missing the responsible person column is one finding, not five thousand.</p>
<p>Two dates belong in the calendar. GPSR has applied since 13 December 2024. The new EU product liability rules, Directive (EU) 2024/2853, must be transposed by 9 December 2026 and apply to products placed on the market from that date — eighty-seven days from today.</p>
<p>The scan is free and complete for the file you have open: every failing row, every line number, nothing uploaded. The paid layer is a different job — one dated evidence pack for the whole workspace catalogue, every feed file and every failing row, as CSV and Markdown, for the marketplace or the importer who asked.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku57790_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku57790_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does GPSR Listing Lint actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads a product feed file — CSV, TSV or a Merchant-style XML — and checks every row against Article 19 of the EU General Product Safety Regulation: manufacturer name, postal address and electronic address, an EU Responsible Person when the manufacturer sits outside the Union, a product picture and identifier, and warning text. It reports the failing line numbers.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is this for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Feed managers, e-commerce developers and agency operators who maintain catalogues for Amazon, eBay, Etsy, Shopify or Google Merchant Center in the EU and EEA. If you edit products_export.csv or an XML feed in your editor, and someone upstream asks whether the catalogue is GPSR-ready, this answers that question offline, row by row.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why can&#x27;t I do this with a spreadsheet filter or a chatbot?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Spreadsheet filters find empty cells. They do not know that &quot;see packaging&quot; fails Article 19, that a Responsible Person in Shenzhen is not established in the Union, or that a German-market toy needs German warning text. A chatbot cannot read a forty-megabyte feed row by row, and pasting a customer catalogue into one is its own problem.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what costs money?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free: open any feed file and run the scan. You get every failing row, its line number and the article it fails, for that file, with no licence key and nothing uploaded. Paid, twenty-nine dollars once: export a dated evidence pack — every feed file in the workspace, every failing row, per-rule counts — as CSV and Markdown.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What would this cost me without the extension?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Hand-checking a catalogue costs about ten seconds a row, so five thousand rows is roughly fourteen hours of someone&#x27;s week, repeated every time the feed changes. An EU Responsible Person service is billed monthly, per brand. This extension is twenty-nine dollars once and runs offline in the editor you already have.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just ask ChatGPT or another AI chat?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. GPSR Listing Lint checks the file you open against 12 rules, each naming its legal or official basis, from a rule set dated 2026-09-20, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does GPSR Listing Lint actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads a product feed file — CSV, TSV or a Merchant-style XML — and checks every row against Article 19 of the EU General Product Safety Regulation: manufacturer name, postal address and electronic address, an EU Responsible Person when the manufacturer sits outside the Union, a product picture and identifier, and warning text. It reports the failing line numbers."}}, {"@type": "Question", "name": "Who is this for?", "acceptedAnswer": {"@type": "Answer", "text": "Feed managers, e-commerce developers and agency operators who maintain catalogues for Amazon, eBay, Etsy, Shopify or Google Merchant Center in the EU and EEA. If you edit products_export.csv or an XML feed in your editor, and someone upstream asks whether the catalogue is GPSR-ready, this answers that question offline, row by row."}}, {"@type": "Question", "name": "Why can't I do this with a spreadsheet filter or a chatbot?", "acceptedAnswer": {"@type": "Answer", "text": "Spreadsheet filters find empty cells. They do not know that \"see packaging\" fails Article 19, that a Responsible Person in Shenzhen is not established in the Union, or that a German-market toy needs German warning text. A chatbot cannot read a forty-megabyte feed row by row, and pasting a customer catalogue into one is its own problem."}}, {"@type": "Question", "name": "What is free and what costs money?", "acceptedAnswer": {"@type": "Answer", "text": "Free: open any feed file and run the scan. You get every failing row, its line number and the article it fails, for that file, with no licence key and nothing uploaded. Paid, twenty-nine dollars once: export a dated evidence pack — every feed file in the workspace, every failing row, per-rule counts — as CSV and Markdown."}}, {"@type": "Question", "name": "What would this cost me without the extension?", "acceptedAnswer": {"@type": "Answer", "text": "Hand-checking a catalogue costs about ten seconds a row, so five thousand rows is roughly fourteen hours of someone's week, repeated every time the feed changes. An EU Responsible Person service is billed monthly, per brand. This extension is twenty-nine dollars once and runs offline in the editor you already have."}}, {"@type": "Question", "name": "Why not just ask ChatGPT or another AI chat?", "acceptedAnswer": {"@type": "Answer", "text": "A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. GPSR Listing Lint checks the file you open against 12 rules, each naming its legal or official basis, from a rule set dated 2026-09-20, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:57790,slug:"gpsr-listing-lint",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/gpsr-eu-listing-risk-2026"><span class="yj-rel-t">GPSR EU Listing Risk Calculator 2026</span><span class="yj-rel-p">$60</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/single-product-store-shopify-theme"><span class="yj-rel-t">Single Product Theme for Shopify — One-Product Store, Pre-Order &amp; Restock Read</span><span class="yj-rel-p">$180</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/ai-companion-safety-lint"><span class="yj-rel-t">AI Companion Safety Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/robots-txt-ai-crawler-audit"><span class="yj-rel-t">AI Crawler Rules - robots.txt Audit for AI Search</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/age-gate-lint"><span class="yj-rel-t">Age Gate Lint — UK Online Safety Act</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/cra-24-72-14-reporting-lint"><span class="yj-rel-t">CRA 24/72/14 Reporting Lint (Article 14)</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body>
</html>