File size: 44,010 Bytes
384e178
bfc6323
 
 
 
c13e6ee
 
bfc6323
 
 
 
 
 
 
 
 
 
 
 
 
 
c13e6ee
bfc6323
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c13e6ee
bfc6323
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c13e6ee
 
bfc6323
c13e6ee
bfc6323
 
 
 
 
 
 
 
 
 
 
 
 
 
c13e6ee
384e178
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>WordPress GDPR Privacy hooks — check privacy duties in your wordpress</title>
<meta name="description" content="This browser tool checks plugin and theme PHP for the listed privacy duties, data storage, external requests, and pre-consent cookies.">
<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/wordpress-privacy-lint"></head>
<body>
<div class="wrap">
<h1>WordPress GDPR Privacy hooks — check privacy duties in your wordpress</h1>
<p class="lede">Lints WordPress plugin and theme PHP for the GDPR/DSGVO duties the plugin author owns: missing privacy exporter/eraser hooks, raw IP storage, server-side Google Fonts, phone-home and pre-consent cookies. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false">&lt;?php
/**
 * Plugin Name: Acme Contact Forms
 * Description: Collects enquiries and shows a map.
 */
defined( 'ABSPATH' ) || exit;

function acme_store_lead( $email, $message ) {
    global $wpdb;
    $ip = $_SERVER['REMOTE_ADDR'];
    $ua = $_SERVER['HTTP_USER_AGENT'];
    $wpdb-&gt;insert( $wpdb-&gt;prefix . 'acme_leads', array(
        'email'   =&gt; sanitize_email( $email ),
        'message' =&gt; wp_kses_post( $message ),
        'ip'      =&gt; $ip,
        'agent'   =&gt; $ua,
    ) );
    setcookie( 'acme_seen_form', '1', time() + 2592000, '/' );
}

function acme_assets() {
    wp_enqueue_style( 'acme-font', 'https://fonts.googleapis.com/css2?family=Inter&amp;display=swap' );
    wp_enqueue_script( 'acme-vendor', 'https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.js' );
    wp_enqueue_script( 'acme-ga', 'https://www.googletagmanager.com/gtag/js?id=G-ACME1' );
}
add_action( 'wp_enqueue_scripts', 'acme_assets' );

function acme_report_install() {
    wp_remote_post( 'https://stats.acme-plugins.example/collect', array(
        'body' =&gt; array( 'site' =&gt; home_url(), 'admin' =&gt; get_option( 'admin_email' ) ),
    ) );
}
add_action( 'admin_init', 'acme_report_install' );

function acme_map_shortcode() {
    return '&lt;iframe src="https://www.google.com/maps/embed?pb=acme" width="600"&gt;&lt;/iframe&gt;';
}
add_shortcode( 'acme_map', 'acme_map_shortcode' );</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> Audits the PHP file you have open against all 12 author-side GDPR/DSGVO rules and names the fix for each hit. <strong>In the editor:</strong> Scans every PHP file in the whole plugin in one run and writes a dated audit report you keep (Markdown/JSON) for the WordPress.org review reply and your Art. 30 records. — that part asks for a licence key.<br><a href="https://getreadystack.com/api/buy/cl/polar_cl_sZ2Bgyx7lAEhKmhzceABGE3rRlKWXaJxUrjPm48Q1KV" style="display:inline-block;margin-top:10px;font-weight:700;color:#8a3d12">Whole-plugin sweep + audit report — $29 once</a><br><span class="note">$29 once · one licence key per person or team seat · A DSGVO code review by a German IT-law firm is commonly billed at EUR 200 per hour.</span></div>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var WPPL_RULES = [
  {"id": "privacy-exporter-missing", "mode": "absent", "sev": "error",
   "trigger": "\\$wpdb-\u003einsert|dbDelta\\s*\\(|update_user_meta\\s*\\(|add_option\\s*\\(|update_option\\s*\\(",
   "require": "wp_privacy_personal_data_exporters",
   "msg": "Plugin stores user data but never registers a wp_privacy_personal_data_exporters filter. A GDPR Art. 15 export request cannot be answered from the WP admin. Add add_filter('wp_privacy_personal_data_exporters', ...) (WordPress 4.9.6 privacy tools API)."},

  {"id": "privacy-eraser-missing", "mode": "absent", "sev": "error",
   "trigger": "\\$wpdb-\u003einsert|dbDelta\\s*\\(|update_user_meta\\s*\\(|add_option\\s*\\(|update_option\\s*\\(",
   "require": "wp_privacy_personal_data_erasers",
   "msg": "Plugin stores user data but never registers a wp_privacy_personal_data_erasers filter. Tools \u003e Erase Personal Data will silently leave your rows behind on a GDPR Art. 17 request. Add add_filter('wp_privacy_personal_data_erasers', ...)."},

  {"id": "policy-content-missing", "mode": "absent", "sev": "warn",
   "trigger": "\\$wpdb-\u003einsert|dbDelta\\s*\\(|update_user_meta\\s*\\(|setcookie\\s*\\(",
   "require": "wp_add_privacy_policy_content",
   "msg": "Plugin collects data but adds no suggested privacy policy text. Call wp_add_privacy_policy_content('Plugin Name', $text) on admin_init so the site owner's policy can name what you store."},

  {"id": "raw-ip-stored", "mode": "line", "sev": "error",
   "pattern": "REMOTE_ADDR|HTTP_X_FORWARDED_FOR",
   "unless": "wp_privacy_anonymize_ip",
   "msg": "Raw visitor IP is read without anonymisation. An IP address is personal data (CJEU Breyer, C-582/14). Wrap it: wp_privacy_anonymize_ip( $ip )."},

  {"id": "user-agent-stored", "mode": "line", "sev": "warn",
   "pattern": "HTTP_USER_AGENT",
   "unless": "wp_privacy_anonymize",
   "msg": "Raw User-Agent string is read. Combined with an IP it is a device fingerprint under GDPR Art. 4(1); store a hash or drop it."},

  {"id": "google-fonts-server-side", "mode": "line", "sev": "error",
   "pattern": "fonts\\.googleapis\\.com|fonts\\.gstatic\\.com",
   "msg": "Google Fonts is loaded from Google's servers, which hands every visitor IP to a US provider. LG Munchen I, 3 O 17493/20 (20 Jan 2022) awarded EUR 100 damages for exactly this. Bundle the .woff2 files with the plugin instead."},

  {"id": "third-party-cdn-enqueue", "mode": "line", "sev": "error",
   "pattern": "ajax\\.googleapis\\.com|cdn\\.jsdelivr\\.net|unpkg\\.com|cdnjs\\.cloudflare\\.com|code\\.jquery\\.com|stackpath\\.bootstrapcdn\\.com",
   "msg": "Asset is enqueued from a third-party CDN, so the visitor's IP leaves the site before any consent. This is also a WordPress.org plugin review blocker (all scripts must be bundled). Enqueue a local copy."},

  {"id": "analytics-injected", "mode": "line", "sev": "error",
   "pattern": "googletagmanager\\.com|google-analytics\\.com|gtag\\s*\\(|fbq\\s*\\(|connect\\.facebook\\.net",
   "msg": "Analytics or pixel code is injected from PHP with no consent gate. Under ePrivacy Art. 5(3) and TDDDG Sec. 25 this needs opt-in before the request fires. Gate it behind the site's consent signal."},

  {"id": "phone-home-no-consent", "mode": "line", "sev": "error",
   "pattern": "wp_remote_(post|get|request)\\s*\\(\\s*['\"]https?://",
   "unless": "api\\.wordpress\\.org|downloads\\.wordpress\\.org",
   "msg": "Plugin calls an external endpoint with a hardcoded URL. Sending site or user data off-site without explicit opt-in is WordPress.org Detailed Plugin Guideline 7 (no phoning home) and a GDPR Art. 44 transfer. Ask first, and disclose the service in readme.txt."},

  {"id": "cookie-before-consent", "mode": "line", "sev": "error",
   "pattern": "setcookie\\s*\\(",
   "unless": "consent|has_consent|cookielaw",
   "msg": "A cookie is set with no consent check on the same line. Non-essential cookies need prior opt-in (ePrivacy Art. 5(3), TDDDG Sec. 25 in Germany). Wrap the call in your consent check."},

  {"id": "external-embed-no-consent", "mode": "line", "sev": "warn",
   "pattern": "youtube\\.com/embed|player\\.vimeo\\.com|google\\.com/maps|maps\\.googleapis\\.com",
   "msg": "A third-party embed is printed directly, which contacts the provider on page load. German courts treat this like Google Fonts; use a click-to-load placeholder."},

  {"id": "uninstall-cleanup-missing", "mode": "absent", "sev": "warn",
   "trigger": "dbDelta\\s*\\(|\\$wpdb-\u003einsert|add_option\\s*\\(",
   "require": "register_uninstall_hook|uninstall\\.php|WP_UNINSTALL_PLUGIN",
   "msg": "Plugin creates storage but registers no uninstall cleanup, so personal data outlives the plugin. That breaks GDPR Art. 5(1)(e) storage limitation. Add register_uninstall_hook() or an uninstall.php."}
]
;</script>
<script>// WordPress Privacy Lint - one brain, used by the VS Code extension and by the free web page.
const RULES = (typeof module !== 'undefined') ? require('./rules.json') : window.WPPL_RULES;

function isComment(line) {
  const t = line.trim();
  return t.startsWith('//') || t.startsWith('#') || t.startsWith('*') || t.startsWith('/*');
}

function check(text, opts) {
  opts = opts || {};
  const src = String(text == null ? '' : text);
  const lines = src.split(/\r?\n/);
  // code-only view: comment lines blanked so a rule name written in a comment is not a hit
  const code = lines.map(function (l) { return isComment(l) ? '' : l; });
  const codeText = code.join('\n');
  const findings = [];

  RULES.forEach(function (rule) {
    if (rule.mode === 'absent') {
      const trig = new RegExp(rule.trigger, 'i');
      const req = new RegExp(rule.require, 'i');
      if (!trig.test(codeText) || req.test(codeText)) return;
      let at = 1;
      for (let i = 0; i < code.length; i++) { if (trig.test(code[i])) { at = i + 1; break; } }
      findings.push({ check: rule.id, sev: rule.sev, msg: rule.msg, line: at });
      return;
    }
    const pat = new RegExp(rule.pattern, 'i');
    const unless = rule.unless ? new RegExp(rule.unless, 'i') : null;
    for (let i = 0; i < code.length; i++) {
      const l = code[i];
      if (!l || !pat.test(l)) continue;
      if (unless && unless.test(l)) continue;
      findings.push({ check: rule.id, sev: rule.sev, msg: rule.msg, line: i + 1 });
    }
  });

  findings.sort(function (a, b) { return a.line - b.line; });
  return { findings: findings };
}

const engine = { check: check };
const RULE_COUNT = RULES.length;

if (typeof module !== 'undefined') module.exports = { engine: engine, RULES: RULES, RULE_COUNT: RULE_COUNT };
if (typeof window !== 'undefined') window.WPPLENGINE = { engine: engine, RULES: RULES, RULE_COUNT: RULE_COUNT };
</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 WPPLREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = WPPLREPORT;
else window.WPPLREPORT = WPPLREPORT;
</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.WPPLENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.WPPLREPORT.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.wordpress-privacy-lint" rel="noopener" data-yj-install-door="mp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'wordpress-privacy-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/wordpress-privacy-lint" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'wordpress-privacy-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/wordpress-privacy-lint" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'wordpress-privacy-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/wordpress-privacy-lint" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'wordpress-privacy-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/wordpress-privacy-lint" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'wordpress-privacy-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.wordpress-privacy-lint
npx @readystack/wordpress-privacy-lint &lt;file&gt;
npx @readystack/wordpress-privacy-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 WordPress Privacy Lint (GDPR / DSGVO) 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:"wordpress-privacy-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">Finds the 12 author-side privacy duties your plugin PHP breaks, with the article and the fix on every line.</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">A DSGVO code review by a German IT-law firm is commonly billed at EUR 200 per hour, and a Google Fonts Abmahnung letter is typically settled at EUR 100 plus fees.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_sZ2Bgyx7lAEhKmhzceABGE3rRlKWXaJxUrjPm48Q1KV" 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-13">Updated 2026-09-13</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-13","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/sku49970_result_card.jpg" alt="WordPress Privacy Lint (GDPR / DSGVO)" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>Twelve findings in a 38-line WordPress contact-form plugin — eight of them errors — is what a German agency developer gets back the first time an AI-written plugin is checked against the privacy duties that fall on the plugin author.</p>
<p>It is what the bundled fixture produces. Ask any assistant for &quot;a WordPress form plugin that logs submissions&quot; and you get roughly the same file every time: a custom table, the visitor&#x27;s IP written straight into it, a cookie so the form is not shown twice, a Google Font for the labels, a charting library from a CDN, a GA tag, and a small call home on admin_init so the author can count installs. Every one of those is a duty the plugin author owns, and nothing in the WordPress toolchain tells you so.</p>
<p>When a site receives an Art. 15 export request, WordPress loops over every callback registered on the wp_privacy_personal_data_exporters filter. If your plugin never registered one, the rows in your table are simply absent from the export the site owner hands over. The same holds for Art. 17: Tools &gt; Erase Personal Data runs the wp_privacy_personal_data_erasers callbacks, and yours does not exist, so your rows quietly survive a deletion request. That API has shipped with WordPress since 4.9.6. Almost nobody wires it up.</p>
<p>The German exposure is sharper still. On 20 January 2022 the Landgericht Muenchen I, in case 3 O 17493/20, awarded a visitor EUR 100 in damages because a site loaded a font from fonts.googleapis.com, which sends the visitor&#x27;s IP to a US provider without consent. That entire liability, for every site running your plugin, is one line inside wp_enqueue_style. The same reasoning covers jsDelivr, unpkg, cdnjs and code.jquery.com, and it covers a YouTube or Google Maps embed printed directly into the page. An IP address is personal data in the first place because of CJEU Breyer, C-582/14.</p>
<p>Then there is the directory. The WordPress.org Detailed Plugin Guidelines make calling an external endpoint without explicit user consent — phoning home — a standing review blocker, and they require all scripts to be bundled rather than loaded remotely. A rejection does not cost money; it costs your place in the review queue, which is where a launch date goes to die.</p>
<p>So why does PHPCS not catch this? Because the WordPress Coding Standards sniffs are about escaping, nonces, prefixes and formatting. There is no sniff for a missing privacy exporter filter. There is none for a hostname inside an enqueue call. There is none for a hardcoded wp_remote_post URL. And none of them cites an article, which matters the moment a client asks you to put the answer in writing.</p>
<p>WordPress Privacy Lint is a static check for exactly those twelve rules, and nothing else. It reads your PHP and reports each hit with the article and the fix. It makes no network calls. Run it on the bundled clean fixture, which is the same plugin with its duties met, and you get zero findings. That pair is the yardstick: check it yourself before trusting the rule set on your own code.</p>
<p>The free check covers one file completely — all twelve rules, no watermark, no countdown, no locked result — and the same twelve rules run in the browser with nothing installed. The paid tier moves along a different axis: one run across every PHP file in the plugin, plus a dated Markdown and JSON audit report you keep for the WordPress.org review reply and your Art. 30 records. For comparison, a DSGVO code review by a German IT-law firm is commonly billed around EUR 200 per hour.</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/sku49970_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku49970_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 WordPress Privacy Lint actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads WordPress plugin or theme PHP and reports the 12 privacy duties that fall on the plugin author, not on the site owner: missing wp_privacy_personal_data_exporters and erasers filters, raw REMOTE_ADDR storage, server-side Google Fonts, third-party CDN enqueues, analytics without a consent gate, hardcoded phone-home calls, cookies set before consent, and no uninstall cleanup. Every finding carries the article and the fix.</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 it for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Freelance and agency WordPress developers who ship plugins and themes to clients in Germany, Austria and the rest of the EU, and anyone submitting to the WordPress.org plugin directory, where phoning home and remote-loaded assets are standing review blockers. It is aimed at the person writing the PHP, including PHP generated by an AI assistant.</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 will a free linter not do this?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">PHPCS with the WordPress Coding Standards sniffs checks escaping, nonces, prefixes and formatting. It has no sniff for a missing privacy exporter filter, none for fonts.googleapis.com inside wp_enqueue_style, and none for a hardcoded wp_remote_post URL. It also cites no GDPR article, so you cannot hand its output to a client or a reviewer.</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 needs the licence key?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free covers one file completely: open any PHP file, run the check, and you get every one of the 12 rules with the article and the fix. The same 12 rules also run in the browser with nothing installed. The licence key adds scope and ownership: one run across every PHP file in the plugin, plus a dated audit report in Markdown and JSON that you keep.</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 otherwise?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A DSGVO code review by a German IT-law firm is commonly billed around EUR 200 per hour. A Google Fonts Abmahnung is typically settled at about EUR 100 in damages plus the other side&#x27;s fees, after LG Muenchen I, 3 O 17493/20, awarded that sum on 20 January 2022. A WordPress.org rejection costs you a fresh wait in the review queue.</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. WordPress GDPR Lint - DSGVO privacy checks for PHP themes an checks the file you open against 12 rules 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 WordPress Privacy Lint actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads WordPress plugin or theme PHP and reports the 12 privacy duties that fall on the plugin author, not on the site owner: missing wp_privacy_personal_data_exporters and erasers filters, raw REMOTE_ADDR storage, server-side Google Fonts, third-party CDN enqueues, analytics without a consent gate, hardcoded phone-home calls, cookies set before consent, and no uninstall cleanup. Every finding carries the article and the fix."}}, {"@type": "Question", "name": "Who is it for?", "acceptedAnswer": {"@type": "Answer", "text": "Freelance and agency WordPress developers who ship plugins and themes to clients in Germany, Austria and the rest of the EU, and anyone submitting to the WordPress.org plugin directory, where phoning home and remote-loaded assets are standing review blockers. It is aimed at the person writing the PHP, including PHP generated by an AI assistant."}}, {"@type": "Question", "name": "Why will a free linter not do this?", "acceptedAnswer": {"@type": "Answer", "text": "PHPCS with the WordPress Coding Standards sniffs checks escaping, nonces, prefixes and formatting. It has no sniff for a missing privacy exporter filter, none for fonts.googleapis.com inside wp_enqueue_style, and none for a hardcoded wp_remote_post URL. It also cites no GDPR article, so you cannot hand its output to a client or a reviewer."}}, {"@type": "Question", "name": "What is free and what needs the licence key?", "acceptedAnswer": {"@type": "Answer", "text": "Free covers one file completely: open any PHP file, run the check, and you get every one of the 12 rules with the article and the fix. The same 12 rules also run in the browser with nothing installed. The licence key adds scope and ownership: one run across every PHP file in the plugin, plus a dated audit report in Markdown and JSON that you keep."}}, {"@type": "Question", "name": "What would this cost me otherwise?", "acceptedAnswer": {"@type": "Answer", "text": "A DSGVO code review by a German IT-law firm is commonly billed around EUR 200 per hour. A Google Fonts Abmahnung is typically settled at about EUR 100 in damages plus the other side's fees, after LG Muenchen I, 3 O 17493/20, awarded that sum on 20 January 2022. A WordPress.org rejection costs you a fresh wait in the review queue."}}, {"@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. WordPress GDPR Lint - DSGVO privacy checks for PHP themes an checks the file you open against 12 rules 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:49970,slug:"wordpress-privacy-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/wp-plugin-review-gate"><span class="yj-rel-t">WP Plugin Review Gate</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/portfolio-wordpress-template"><span class="yj-rel-t">Portfolio WordPress Template: Commission Enquiry Plugin for Freelance Designer</span><span class="yj-rel-p">$129</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/gdpr-privacy-notice-lint"><span class="yj-rel-t">GDPR Privacy Notice Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/personal-data-map-dsar-audit"><span class="yj-rel-t">Personal Data Map: find every column a GDPR subject access request has to reac</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/wordpress-ada-compliance-plugin"><span class="yj-rel-t">ADA Compliance Plugin for WordPress: Title II Grievance Form</span><span class="yj-rel-p">$60</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/charter-boat-booking-wordpress-plugin"><span class="yj-rel-t">Charter Boat Booking Requests — 6-Passenger Limit</span><span class="yj-rel-p">$60</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>