openvibers/openvibe.tools Commits
SEO: per-subdomain metadata and real content for every tool page
Changes
@@@ -150,33 +150,33 @@ const API='https://openvibe.tools/api/dev',$=s=>document.getElementById(s),H=s=>s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');const T={--dev:{n:'Dev.OpenVibe',i:'fa-code',c:'hub',d:'Developer & SEO Tools Hub',hub:1},--json:{n:'OpenVibeJSON',i:'fa-code',c:'data',d:'Format, validate & minify JSON',p:'{"name":"openvibe","version":1,"active":true}',m:['Format','Validate','Minify']},--yaml:{n:'OpenVibeYAML',i:'fa-file-code',c:'data',d:'YAML \u2194 JSON converter',p:'name: openvibe\nversion: 1\nactive: true',m:['YAML\u2192JSON','JSON\u2192YAML']},--xml:{n:'OpenVibeXML',i:'fa-file-code',c:'data',d:'XML formatter & validator',p:'<root><item id="1"><name>OpenVibe</name></item></root>',m:['Format','Validate','Minify']},--csv:{n:'OpenVibeCSV',i:'fa-table',c:'data',d:'CSV viewer & JSON converter',p:'name,age,city\nAlice,30,NYC\nBob,25,LA',m:['Table','To JSON']},--sql:{n:'OpenVibeSQL',i:'fa-database',c:'data',d:'SQL formatter & highlighter',p:'select u.id, u.name, o.total from users u join orders o on u.id = o.user_id where o.total > 100 order by o.total desc limit 10',m:['Format','Uppercase','Compact']},--markdown:{n:'OpenVibeMarkdown',i:'fa-file-lines',c:'data',d:'Markdown editor with live preview',p:'# Hello World\n\nThis is **bold** and *italic* text.\n\n- Item one\n- Item two\n\n> A blockquote\n\n`inline code`\n\n[Link](https://openvibe.network)',m:['Preview','To HTML']},--html:{n:'OpenVibeHTML',i:'fa-file-code',c:'data',d:'HTML formatter, minifier & entity encoder',p:'<div><p>Hello <strong>World</strong></p></div>',m:['Format','Minify','Entities','Unescape']},--base64:{n:'OpenVibeBase64',i:'fa-lock',c:'enc',d:'Base64 encode & decode',p:'Hello, World!',m:['Encode','Decode']},--url:{n:'OpenVibeURL',i:'fa-link',c:'enc',d:'URL encode, decode & parser',p:'https://openvibe.tools/search?q=hello world&lang=en#results',m:['Encode','Decode','Parse']},--jwt:{n:'OpenVibeJWT',i:'fa-key',c:'enc',d:'JWT decoder \u2014 header, payload & expiry',p:'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhvYm8iLCJpYXQiOjE1MTYyMzkwMjJ9.4S5J5Z5mSp5F8Kv3K5qFJpQ5Y5jF5p5t5m5k5e5h5d4',m:['Decode']},--uuid:{n:'OpenVibeUUID',i:'fa-fingerprint',c:'enc',d:'UUID v4 generator & validator',p:'',m:['Generate','Bulk 10','Bulk 100','Validate']},--hash:{n:'OpenVibeHash',i:'fa-hashtag',c:'enc',d:'SHA-256, SHA-1 & SHA-512 hash generator',p:'Hello, World!',m:['SHA-256','SHA-1','SHA-512']},--hex:{n:'OpenVibeHex',i:'fa-barcode',c:'enc',d:'Hex encoder / decoder',p:'Hello',m:['To Hex','From Hex']},--escape:{n:'OpenVibeEscape',i:'fa-shield-halved',c:'enc',d:'HTML, JS & URL escape / unescape',p:'<script>alert("hello")<\/script>',m:['HTML Enc','HTML Dec','JS Esc','JS Unesc','URL Enc','URL Dec']},--timestamp:{n:'OpenVibeTimestamp',i:'fa-clock',c:'time',d:'Unix timestamp \u2194 date converter',p:'',m:['Now','To Date','To Unix']},--cron:{n:'OpenVibeCron',i:'fa-calendar-check',c:'time',d:'Cron expression parser & next runs',p:'*/15 9-17 * * 1-5',m:['Parse']},--beautify:{n:'OpenVibeBeautify',i:'fa-wand-magic-sparkles',c:'qual',d:'Beautify JS, CSS, HTML & JSON',p:'{"a":1,"b":[2,3],"c":{"d":true}}',m:['JSON','HTML','CSS']},--minify:{n:'OpenVibeMinify',i:'fa-compress',c:'qual',d:'Minify JS, CSS, HTML & JSON',p:'{\n "name": "openvibe",\n "version": 1\n}',m:['JSON','HTML','CSS']},--diff:{n:'OpenVibeDiff',i:'fa-code-compare',c:'qual',d:'Side-by-side text diff checker',p:'Line one\nLine two\nLine three\nLine four',p2:'Line one\nLine 2 modified\nLine three\nLine five\nLine six',dual:1,m:['Compare']},--regex:{n:'OpenVibeRegex',i:'fa-magnifying-glass',c:'qual',d:'Regex tester with match highlighting',p:'\\b[A-Z][a-z]+\\b',p2:'Hello World from OpenVibe.Tools today',dual:1,m:['Test']},--slug:{n:'OpenVibeSlug',i:'fa-link',c:'qual',d:'URL slug generator',p:'Hello World! This is a Test -- 2024',m:['Generate']},--lorem:{n:'OpenVibeLorem',i:'fa-paragraph',c:'qual',d:'Lorem ipsum generator',p:'',m:['1 Para','3 Para','5 Para','10 Sent','50 Words']},--curl:{n:'OpenVibeCurl',i:'fa-terminal',c:'http',d:'Parse curl \u2192 fetch, Python, Node.js',p:"curl -X POST https://api.example.com/users -H 'Content-Type: application/json' -H 'Authorization: Bearer tok123' -d '{\"name\":\"openvibe\"}'",m:['To fetch','To Python','To Node','Parsed']},--webhook:{n:'OpenVibeWebhook',i:'fa-satellite-dish',c:'http',d:'Webhook request inspector bin',p:'',m:['Create Bin']},--color:{n:'OpenVibeColor',i:'fa-palette',c:'fe',d:'Color picker & HEX/RGB/HSL converter',p:'#a78bfa',m:['Convert']},--opengraph:{n:'OpenVibeOpenGraph',i:'fa-share-nodes',c:'fe',d:'Open Graph & Twitter Card preview',p:'https://openvibe.live',m:['Preview']}++dev:{n:'Developer Tools',i:'fa-code',c:'hub',d:'Developer & SEO Tools Hub',hub:1},++json:{n:'JSON Formatter',i:'fa-code',c:'data',d:'Format, validate & minify JSON',p:'{"name":"openvibe","version":1,"active":true}',m:['Format','Validate','Minify']},++yaml:{n:'YAML to JSON Converter',i:'fa-file-code',c:'data',d:'YAML \u2194 JSON converter',p:'name: openvibe\nversion: 1\nactive: true',m:['YAML\u2192JSON','JSON\u2192YAML']},++xml:{n:'XML Formatter',i:'fa-file-code',c:'data',d:'XML formatter & validator',p:'<root><item id="1"><name>OpenVibe</name></item></root>',m:['Format','Validate','Minify']},++csv:{n:'CSV to JSON Converter',i:'fa-table',c:'data',d:'CSV viewer & JSON converter',p:'name,age,city\nAlice,30,NYC\nBob,25,LA',m:['Table','To JSON']},++sql:{n:'SQL Formatter',i:'fa-database',c:'data',d:'SQL formatter & highlighter',p:'select u.id, u.name, o.total from users u join orders o on u.id = o.user_id where o.total > 100 order by o.total desc limit 10',m:['Format','Uppercase','Compact']},++markdown:{n:'Markdown Editor',i:'fa-file-lines',c:'data',d:'Markdown editor with live preview',p:'# Hello World\n\nThis is **bold** and *italic* text.\n\n- Item one\n- Item two\n\n> A blockquote\n\n`inline code`\n\n[Link](https://openvibe.network)',m:['Preview','To HTML']},++html:{n:'HTML Formatter',i:'fa-file-code',c:'data',d:'HTML formatter, minifier & entity encoder',p:'<div><p>Hello <strong>World</strong></p></div>',m:['Format','Minify','Entities','Unescape']},++base64:{n:'Base64 Encoder & Decoder',i:'fa-lock',c:'enc',d:'Base64 encode & decode',p:'Hello, World!',m:['Encode','Decode']},++url:{n:'URL Encoder & Decoder',i:'fa-link',c:'enc',d:'URL encode, decode & parser',p:'https://openvibe.tools/search?q=hello world&lang=en#results',m:['Encode','Decode','Parse']},++jwt:{n:'JWT Decoder',i:'fa-key',c:'enc',d:'JWT decoder \u2014 header, payload & expiry',p:'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhvYm8iLCJpYXQiOjE1MTYyMzkwMjJ9.4S5J5Z5mSp5F8Kv3K5qFJpQ5Y5jF5p5t5m5k5e5h5d4',m:['Decode']},++uuid:{n:'UUID Generator',i:'fa-fingerprint',c:'enc',d:'UUID v4 generator & validator',p:'',m:['Generate','Bulk 10','Bulk 100','Validate']},++hash:{n:'Hash Generator',i:'fa-hashtag',c:'enc',d:'SHA-256, SHA-1 & SHA-512 hash generator',p:'Hello, World!',m:['SHA-256','SHA-1','SHA-512']},++hex:{n:'Hex Converter',i:'fa-barcode',c:'enc',d:'Hex encoder / decoder',p:'Hello',m:['To Hex','From Hex']},++escape:{n:'String Escape Tool',i:'fa-shield-halved',c:'enc',d:'HTML, JS & URL escape / unescape',p:'<script>alert("hello")<\/script>',m:['HTML Enc','HTML Dec','JS Esc','JS Unesc','URL Enc','URL Dec']},++timestamp:{n:'Unix Timestamp Converter',i:'fa-clock',c:'time',d:'Unix timestamp \u2194 date converter',p:'',m:['Now','To Date','To Unix']},++cron:{n:'Cron Expression Parser',i:'fa-calendar-check',c:'time',d:'Cron expression parser & next runs',p:'*/15 9-17 * * 1-5',m:['Parse']},++beautify:{n:'Code Beautifier',i:'fa-wand-magic-sparkles',c:'qual',d:'Beautify JS, CSS, HTML & JSON',p:'{"a":1,"b":[2,3],"c":{"d":true}}',m:['JSON','HTML','CSS']},++minify:{n:'Code Minifier',i:'fa-compress',c:'qual',d:'Minify JS, CSS, HTML & JSON',p:'{\n "name": "openvibe",\n "version": 1\n}',m:['JSON','HTML','CSS']},++diff:{n:'Diff Checker',i:'fa-code-compare',c:'qual',d:'Side-by-side text diff checker',p:'Line one\nLine two\nLine three\nLine four',p2:'Line one\nLine 2 modified\nLine three\nLine five\nLine six',dual:1,m:['Compare']},++regex:{n:'Regex Tester',i:'fa-magnifying-glass',c:'qual',d:'Regex tester with match highlighting',p:'\\b[A-Z][a-z]+\\b',p2:'Hello World from OpenVibe.Tools today',dual:1,m:['Test']},++slug:{n:'Slug Generator',i:'fa-link',c:'qual',d:'URL slug generator',p:'Hello World! This is a Test -- 2024',m:['Generate']},++lorem:{n:'Lorem Ipsum Generator',i:'fa-paragraph',c:'qual',d:'Lorem ipsum generator',p:'',m:['1 Para','3 Para','5 Para','10 Sent','50 Words']},++curl:{n:'Online curl',i:'fa-terminal',c:'http',d:'Parse curl \u2192 fetch, Python, Node.js',p:"curl -X POST https://api.example.com/users -H 'Content-Type: application/json' -H 'Authorization: Bearer tok123' -d '{\"name\":\"openvibe\"}'",m:['To fetch','To Python','To Node','Parsed']},++webhook:{n:'Webhook Tester',i:'fa-satellite-dish',c:'http',d:'Webhook request inspector bin',p:'',m:['Create Bin']},++color:{n:'Color Converter',i:'fa-palette',c:'fe',d:'Color picker & HEX/RGB/HSL converter',p:'#a78bfa',m:['Convert']},++opengraph:{n:'Open Graph Preview',i:'fa-share-nodes',c:'fe',d:'Open Graph & Twitter Card preview',p:'https://openvibe.live',m:['Preview']}};const CATS={data:{l:'Data & Formats',i:'fa-database'},enc:{l:'Encoding & Crypto',i:'fa-lock'},time:{l:'Time & Scheduling',i:'fa-clock'},qual:{l:'Code Quality',i:'fa-wand-magic-sparkles'},http:{l:'HTTP & API',i:'fa-terminal'},fe:{l:'Frontend & SEO',i:'fa-palette'}};@@@ -208,7 +208,8 @@ function showTool(){const t=T[ACT];if(!t)return showHub();$('hi').innerHTML='<i class="fa-solid '+t.i+'"></i>';$('ht').textContent=t.n;$('hd').textContent=t.d;-- document.title=t.n+' \u2014 Free Online '+t.d;++ // Title comes from the server (server/seo) so crawlers and the tab agree — don't clobber it.++ if(!document.querySelector('link[rel=canonical][href*="'+ACT+'.openvibe"]'))document.title=t.n+' \u2014 '+t.d;let mh='';if(t.m)t.m.forEach((m,i)=>{mh+='<button class="mb'+(i===0?' on':'')+'" data-m="'+m+'" onclick="setMode(this)">'+m+'</button>'});$('modes').innerHTML=mh;MODE=t.m?t.m[0]:'';@@@ -651,42 +651,42 @@// ── Tool definitions (mirrors server/net/config.js) ──────const TOOLS = {// Hub-- net: { name: 'Net.OpenVibe', icon: 'fa-network-wired', cat: 'hub', endpoint: null, placeholder: 'Enter a domain, IP, or URL…' },-- lookup: { name: 'OpenVibeLookup', icon: 'fa-magnifying-glass', cat: 'hub', endpoint: '/lookup', placeholder: 'Domain, IP, or URL — we\'ll look up everything', btnText: 'Lookup All' },++ net: { name: 'Network Tools', icon: 'fa-network-wired', cat: 'hub', endpoint: null, placeholder: 'Enter a domain, IP, or URL…' },++ lookup: { name: 'Domain & IP Lookup', icon: 'fa-magnifying-glass', cat: 'hub', endpoint: '/lookup', placeholder: 'Domain, IP, or URL — we\'ll look up everything', btnText: 'Lookup All' },// IP / Identity-- myip: { name: 'OpenVibeMyIP', icon: 'fa-location-crosshairs', cat: 'ip', endpoint: '/myip', placeholder: null, auto: true },-- ip: { name: 'OpenVibeIP', icon: 'fa-at', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },-- ipv4: { name: 'OpenVibeIPv4', icon: 'fa-hashtag', cat: 'ip', endpoint: '/ipv4', placeholder: 'IPv4 address or domain' },-- ipv6: { name: 'OpenVibeIPv6', icon: 'fa-code', cat: 'ip', endpoint: '/ipv6', placeholder: 'IPv6 address or domain' },-- geoip: { name: 'OpenVibeGeoIP', icon: 'fa-earth-americas', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },-- hostname: { name: 'OpenVibeHostname', icon: 'fa-server', cat: 'ip', endpoint: '/rdns', placeholder: 'IP address to resolve hostname' },-- isp: { name: 'OpenVibeISP', icon: 'fa-building', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },-- asn: { name: 'OpenVibeASN', icon: 'fa-diagram-project', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },-- rdns: { name: 'OpenVibeReverseDNS', icon: 'fa-rotate-left', cat: 'ip', endpoint: '/rdns', placeholder: 'IP address for reverse DNS' },-- whois: { name: 'OpenVibeWhois', icon: 'fa-address-book', cat: 'ip', endpoint: '/rdap', placeholder: 'Domain or IP' },-- rdap: { name: 'OpenVibeRDAP', icon: 'fa-id-card', cat: 'ip', endpoint: '/rdap', placeholder: 'Domain or IP for RDAP lookup' },++ myip: { name: 'What Is My IP', icon: 'fa-location-crosshairs', cat: 'ip', endpoint: '/myip', placeholder: null, auto: true },++ ip: { name: 'IP Address Lookup', icon: 'fa-at', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },++ ipv4: { name: 'IPv4 Lookup & CIDR Calculator', icon: 'fa-hashtag', cat: 'ip', endpoint: '/ipv4', placeholder: 'IPv4 address or domain' },++ ipv6: { name: 'IPv6 Lookup', icon: 'fa-code', cat: 'ip', endpoint: '/ipv6', placeholder: 'IPv6 address or domain' },++ geoip: { name: 'IP Geolocation', icon: 'fa-earth-americas', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },++ hostname: { name: 'Hostname Lookup', icon: 'fa-server', cat: 'ip', endpoint: '/rdns', placeholder: 'IP address to resolve hostname' },++ isp: { name: 'ISP Lookup', icon: 'fa-building', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },++ asn: { name: 'ASN Lookup', icon: 'fa-diagram-project', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },++ rdns: { name: 'Reverse DNS Lookup', icon: 'fa-rotate-left', cat: 'ip', endpoint: '/rdns', placeholder: 'IP address for reverse DNS' },++ whois: { name: 'Whois Lookup', icon: 'fa-address-book', cat: 'ip', endpoint: '/rdap', placeholder: 'Domain or IP' },++ rdap: { name: 'RDAP Lookup', icon: 'fa-id-card', cat: 'ip', endpoint: '/rdap', placeholder: 'Domain or IP for RDAP lookup' },// DNS-- dns: { name: 'OpenVibeDNS', icon: 'fa-sitemap', cat: 'dns', endpoint: '/dns', placeholder: 'Domain to query' },-- dig: { name: 'OpenVibeDig', icon: 'fa-terminal', cat: 'dns', endpoint: '/dns', placeholder: 'Domain (dig-style query)', dnsTypes: true },-- nslookup: { name: 'OpenVibeNSLookup', icon: 'fa-magnifying-glass-arrow-right', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for nslookup' },-- dnspropagation: { name: 'OpenVibeDNSPropagation', icon: 'fa-globe', cat: 'dns', endpoint: '/dns', placeholder: 'Domain to check propagation' },-- mx: { name: 'OpenVibeMX', icon: 'fa-envelope', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for MX records', defaultTypes: 'MX' },-- txt: { name: 'OpenVibeTXT', icon: 'fa-file-lines', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for TXT records', defaultTypes: 'TXT' },-- ns: { name: 'OpenVibeNS', icon: 'fa-server', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for NS records', defaultTypes: 'NS' },-- spf: { name: 'OpenVibeSPF', icon: 'fa-shield-halved', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for SPF check', defaultTypes: 'TXT' },-- dkim: { name: 'OpenVibeDKIM', icon: 'fa-key', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for DKIM', defaultTypes: 'TXT' },-- dmarc: { name: 'OpenVibeDMARC', icon: 'fa-user-shield', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for DMARC', defaultTypes: 'TXT' },++ dns: { name: 'DNS Lookup', icon: 'fa-sitemap', cat: 'dns', endpoint: '/dns', placeholder: 'Domain to query' },++ dig: { name: 'Online dig', icon: 'fa-terminal', cat: 'dns', endpoint: '/dns', placeholder: 'Domain (dig-style query)', dnsTypes: true },++ nslookup: { name: 'NSLookup', icon: 'fa-magnifying-glass-arrow-right', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for nslookup' },++ dnspropagation: { name: 'DNS Propagation Checker', icon: 'fa-globe', cat: 'dns', endpoint: '/dns', placeholder: 'Domain to check propagation' },++ mx: { name: 'MX Record Lookup', icon: 'fa-envelope', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for MX records', defaultTypes: 'MX' },++ txt: { name: 'TXT Record Lookup', icon: 'fa-file-lines', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for TXT records', defaultTypes: 'TXT' },++ ns: { name: 'Nameserver Lookup', icon: 'fa-server', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for NS records', defaultTypes: 'NS' },++ spf: { name: 'SPF Record Checker', icon: 'fa-shield-halved', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for SPF check', defaultTypes: 'TXT' },++ dkim: { name: 'DKIM Record Lookup', icon: 'fa-key', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for DKIM', defaultTypes: 'TXT' },++ dmarc: { name: 'DMARC Checker', icon: 'fa-user-shield', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for DMARC', defaultTypes: 'TXT' },// Active diagnostics-- ping: { name: 'OpenVibePing', icon: 'fa-satellite-dish', cat: 'diag', endpoint: '/ping', placeholder: 'Host to ping' },-- traceroute: { name: 'OpenVibeTraceroute', icon: 'fa-route', cat: 'diag', endpoint: '/ping', placeholder: 'Host for traceroute' },-- mtr: { name: 'OpenVibeMTR', icon: 'fa-chart-line', cat: 'diag', endpoint: '/ping', placeholder: 'Host for MTR' },-- port: { name: 'OpenVibePortCheck', icon: 'fa-door-open', cat: 'diag', endpoint: '/port', placeholder: 'Host to scan ports' },-- headers: { name: 'OpenVibeHeaders', icon: 'fa-list', cat: 'diag', endpoint: '/headers', placeholder: 'URL or domain' },-- redirects: { name: 'OpenVibeRedirects', icon: 'fa-share', cat: 'diag', endpoint: '/redirects', placeholder: 'URL to follow redirects' },-- ssl: { name: 'OpenVibeSSL', icon: 'fa-lock', cat: 'diag', endpoint: '/ssl', placeholder: 'Domain for SSL check' },-- curl: { name: 'OpenVibeCurl', icon: 'fa-download', cat: 'diag', endpoint: '/headers', placeholder: 'URL' },-- httpstatus: { name: 'OpenVibeHTTPStatus', icon: 'fa-circle-check', cat: 'diag', endpoint: '/headers', placeholder: 'URL to check' },-- latency: { name: 'OpenVibeLatency', icon: 'fa-gauge-high', cat: 'diag', endpoint: '/ping', placeholder: 'Host to measure latency' },++ ping: { name: 'Ping Test', icon: 'fa-satellite-dish', cat: 'diag', endpoint: '/ping', placeholder: 'Host to ping' },++ traceroute: { name: 'Traceroute', icon: 'fa-route', cat: 'diag', endpoint: '/ping', placeholder: 'Host for traceroute' },++ mtr: { name: 'MTR Report', icon: 'fa-chart-line', cat: 'diag', endpoint: '/ping', placeholder: 'Host for MTR' },++ port: { name: 'Port Checker', icon: 'fa-door-open', cat: 'diag', endpoint: '/port', placeholder: 'Host to scan ports' },++ headers: { name: 'HTTP Header Checker', icon: 'fa-list', cat: 'diag', endpoint: '/headers', placeholder: 'URL or domain' },++ redirects: { name: 'Redirect Checker', icon: 'fa-share', cat: 'diag', endpoint: '/redirects', placeholder: 'URL to follow redirects' },++ ssl: { name: 'SSL Certificate Checker', icon: 'fa-lock', cat: 'diag', endpoint: '/ssl', placeholder: 'Domain for SSL check' },++ curl: { name: 'Online curl', icon: 'fa-download', cat: 'diag', endpoint: '/headers', placeholder: 'URL' },++ httpstatus: { name: 'HTTP Status Codes', icon: 'fa-circle-check', cat: 'diag', endpoint: '/headers', placeholder: 'URL to check' },++ latency: { name: 'Latency Test', icon: 'fa-gauge-high', cat: 'diag', endpoint: '/ping', placeholder: 'Host to measure latency' },};const CATS = {@@@ -1,5 +1,9 @@'use strict';++// Tool names and descriptions live in server/seo/catalog.js so the UI, the page <title> and++// the structured data can never disagree. Entries here keep their id/subdomain/icon/category.++const { seoFor } = require('../seo/catalog');++// ═══════════════════════════════════════════════════════════════// Dev.OpenVibe — Developer & SEO Tools Configuration// ═══════════════════════════════════════════════════════════════@@@ -47,6 +51,8 @@ const DEV_TOOLS = [{ id: 'opengraph', subdomain: 'opengraph', name: 'OpenVibeOpenGraph', icon: 'fa-share-nodes', desc: 'Open Graph & Twitter Card preview & validator', category: 'frontend' },];++for (const t of DEV_TOOLS) { const seo = seoFor(t.subdomain); if (seo) { t.name = seo.name; t.desc = seo.desc; t.seo = seo; } }++const DEV_TOOL_MAP = new Map();for (const t of DEV_TOOLS) DEV_TOOL_MAP.set(t.subdomain, t);@@@ -178,12 +178,36 @@ function isStaticPath(reqPath) {return /\.(js|css|ico|png|svg|jpg|xml|txt|woff2?)$/.test(reqPath);}++// Every tool subdomain gets its OWN title, description, canonical, structured data and a++// crawlable content block (server/seo). They used to share the hub's <head>, which told search++// engines that ~65 distinct tools were one page.++const { renderTool, buildSitemap, SATELLITES } = require('./seo/render');++const NET_HTML = path.join(__dirname, '..', 'public', 'net.html');++const DEV_HTML = path.join(__dirname, '..', 'public', 'dev.html');++function sendTool(req, res, file) {++ try {++ res.set('Content-Type', 'text/html; charset=utf-8');++ return res.send(renderTool(file, subdomainOf(req)));++ } catch (err) {++ console.error('[SEO] render failed:', err.message);++ return res.sendFile(file);++ }++}++++// Generated sitemap — covers both hubs, every tool subdomain and the satellite apps, so it can++// never drift from the tool catalogs the way a hand-maintained file does.++app.get('/sitemap.xml', (_req, res) => {++ res.set('Content-Type', 'application/xml; charset=utf-8');++ res.set('Cache-Control', 'public, max-age=3600');++ res.send(buildSitemap(SATELLITES));++});++// Net tool subdomains → net.html SPAapp.use((req, res, next) => {if (!isNetHost(req)) return next();if (req.path.startsWith('/api/') || req.path.startsWith('/auth/')) return next();if (isStaticPath(req.path)) return next();-- return res.sendFile(path.join(__dirname, '..', 'public', 'net.html'));++ return sendTool(req, res, NET_HTML);});// Paste subdomain → paste.html SPA@@@ -199,7 +223,7 @@ app.use((req, res, next) => {if (!isDevHost(req)) return next();if (req.path.startsWith('/api/') || req.path.startsWith('/auth/')) return next();if (isStaticPath(req.path)) return next();-- return res.sendFile(path.join(__dirname, '..', 'public', 'dev.html'));++ return sendTool(req, res, DEV_HTML);});// ── Web-push service worker (same-origin, scope "/") ─────────@@@ -1,5 +1,9 @@'use strict';++// Tool names and descriptions live in server/seo/catalog.js so the UI, the page <title> and++// the structured data can never disagree. Entries here keep their id/subdomain/icon/category.++const { seoFor } = require('../seo/catalog');++// ═══════════════════════════════════════════════════════════════// Net.OpenVibe — Network Tools Configuration// API keys, rate limits, probe regions, and tool definitions.@@@ -130,4 +134,6 @@ const NET_ALIASES = {'ipv6lookup': 'ipv6',};++for (const t of NET_TOOLS) { const seo = seoFor(t.subdomain); if (seo) { t.name = seo.name; t.desc = seo.desc; t.seo = seo; } }++module.exports = { getSetting, getNetConfig, NET_TOOLS, NET_TOOL_MAP, NET_ALIASES };nnew file mode 100644@@@ -0,0 +1,316 @@++'use strict';++++// ═══════════════════════════════════════════════════════════════++// SEO catalog — one record per tool subdomain.++//++// Every dev.* and net.* tool used to share a single SPA file, which meant ~65 subdomains++// all served the same <title>, the same description and a canonical pointing at the hub.++// Search engines saw one page, not sixty-five. This catalog gives each tool its own++// search-friendly identity, and seo/render.js stamps it into the HTML per request.++//++// name what the tool is called everywhere (no "OpenVibe" prefix — people search++// for "json formatter", not for our brand)++// title <title> — primary keyword first, qualifier second, brand last++// desc <meta name="description"> — one sentence, ~150 chars, says what you get++// kw comma-separated keywords (also used to pick related tools)++// about visible intro paragraph under the tool (real content, not hidden text)++// bullets visible feature list++// faq visible Q&A + FAQPage structured data++// ═══════════════════════════════════════════════════════════════++++const BRAND = 'OpenVibe';++++// ── Dev & SEO tools (dev.openvibe.tools) ─────────────────────++const DEV = {++ dev: {++ name: 'Developer Tools', title: 'Free Online Developer Tools — Formatters, Encoders & Validators',++ desc: 'A fast, free developer toolbox: JSON, YAML and XML formatters, Base64 and JWT decoders, regex tester, diff checker, hash generator and more. No signup, nothing leaves your browser.',++ kw: 'developer tools, online tools, free dev tools, formatter, encoder, decoder, validator',++ about: 'A collection of small, fast developer utilities that run entirely in your browser. Nothing is uploaded, nothing is logged, and there is no account to create — open a tool, paste your data, get your answer.',++ bullets: ['Every tool works client-side: your data never leaves the browser', 'No signup, no ads, no rate limits on the offline tools', 'Open source and part of the wider OpenVibe network'],++ faq: [['Are these developer tools really free?', 'Yes. Every tool here is free with no account, no trial and no usage cap. The project is community-run and open source.'], ['Is my data sent to a server?', 'The formatting, encoding and generation tools run entirely in your browser, so your data never leaves your machine. Only tools that must reach the network (like the Open Graph preview and webhook inspector) send a request, and they say so on the page.']],++ },++ json: {++ name: 'JSON Formatter', title: 'JSON Formatter & Validator — Free Online JSON Beautifier',++ desc: 'Format, validate and minify JSON online. Pretty-print messy JSON, catch syntax errors with line numbers, and compact it back down. Free, instant, runs in your browser.',++ kw: 'json formatter, json validator, json beautifier, json minifier, pretty print json, json parser online',++ about: 'Paste any JSON to pretty-print it with consistent indentation, validate it against the JSON spec, or minify it for production. Errors are reported with the exact position so you can find the stray comma fast.',++ bullets: ['Beautify with clean indentation, or minify to the smallest valid output', 'Validation errors point at the line and character that broke the parse', 'Handles large documents without uploading them anywhere'],++ faq: [['How do I fix "Unexpected token" errors in JSON?', 'Paste the JSON here and press Validate. The error message names the exact character position, which is almost always a trailing comma, a single quote instead of a double quote, or an unquoted key.'], ['Does this JSON formatter upload my data?', 'No. Parsing and formatting happen in your browser, so the JSON never reaches our servers.']],++ },++ yaml: {++ name: 'YAML to JSON Converter', title: 'YAML to JSON Converter & YAML Validator — Free Online',++ desc: 'Convert YAML to JSON and JSON to YAML online, and validate YAML syntax instantly. Ideal for Kubernetes manifests, CI pipelines and config files. Free, no signup.',++ kw: 'yaml to json, json to yaml, yaml converter, yaml validator, yaml parser online, kubernetes yaml',++ about: 'Convert between YAML and JSON in either direction and check that your YAML actually parses. Useful whenever you are moving between a config file and an API payload, or debugging an indentation mistake in a CI or Kubernetes file.',++ bullets: ['YAML → JSON and JSON → YAML with one click', 'Catches the indentation and tab errors that break YAML parsers', 'Keeps nested structures, lists and multi-line strings intact'],++ faq: [['Why does my YAML fail to parse?', 'YAML forbids tab characters for indentation and is strict about consistent spacing. Paste it here and the parser reports the line where the structure breaks.'], ['Can I convert Kubernetes manifests to JSON?', 'Yes. Kubernetes accepts both formats, and converting to JSON is a quick way to verify exactly what a manifest resolves to.']],++ },++ xml: {++ name: 'XML Formatter', title: 'XML Formatter & Validator — Free Online XML Beautifier',++ desc: 'Format, validate and minify XML online. Pretty-print nested XML, check that tags are well-formed, and compact documents. Free and instant in your browser.',++ kw: 'xml formatter, xml validator, xml beautifier, pretty print xml, xml parser online, format xml',++ about: 'Pretty-print XML with proper nesting, confirm a document is well-formed, or strip whitespace to minify it. Handy for SOAP payloads, RSS feeds, sitemaps and configuration files.',++ bullets: ['Readable indentation for deeply nested documents', 'Well-formedness checks with the position of the offending tag', 'Minify mode strips insignificant whitespace'],++ faq: [['What does "not well-formed" mean?', 'It means the document breaks a structural rule: an unclosed tag, mismatched case, or a stray angle bracket. The validator names the position so you can fix it.'], ['Can I format an RSS feed or sitemap with this?', 'Yes. Both are XML, so paste the feed or sitemap and it formats like any other document.']],++ },++ csv: {++ name: 'CSV to JSON Converter', title: 'CSV to JSON Converter & CSV Viewer — Free Online',++ desc: 'Convert CSV to JSON and view CSV as a sortable table online. Handles quoted fields, commas inside values and custom headers. Free, instant, no upload.',++ kw: 'csv to json, csv viewer, csv converter, csv parser online, convert csv, csv table viewer',++ about: 'Paste CSV to see it as a clean table or convert it straight to a JSON array of objects using the first row as keys. Quoted fields and embedded commas are parsed correctly.',++ bullets: ['Table view for scanning a file quickly', 'CSV → JSON using the header row as object keys', 'Correctly handles quoted values containing commas and newlines'],++ faq: [['How does the converter handle commas inside a field?', 'Fields wrapped in double quotes are parsed as single values, so embedded commas and line breaks survive the conversion.'], ['Can I convert a spreadsheet export?', 'Yes. Export the sheet as CSV, paste it here, and convert to JSON or read it as a table.']],++ },++ sql: {++ name: 'SQL Formatter', title: 'SQL Formatter & Beautifier — Free Online Query Formatter',++ desc: 'Format and beautify SQL queries online. Turn one-line queries into readable, indented SQL with keyword highlighting. Free, works with any SQL dialect.',++ kw: 'sql formatter, sql beautifier, format sql online, sql pretty print, query formatter',++ about: 'Turn a long single-line query into properly indented SQL with keywords aligned, so joins and nested conditions are readable in a code review or a bug report.',++ bullets: ['Readable indentation for joins, subqueries and CTEs', 'Uppercase keyword mode for a consistent house style', 'Compact mode to put a query back on one line'],++ faq: [['Which SQL dialects are supported?', 'The formatter works on standard SQL syntax, which covers the vast majority of PostgreSQL, MySQL, SQLite and SQL Server queries.'], ['Is my query sent anywhere?', 'No. Formatting happens in your browser, so queries containing table or column names never leave your machine.']],++ },++ markdown: {++ name: 'Markdown Editor', title: 'Markdown Editor with Live Preview — Free Online Markdown to HTML',++ desc: 'Write Markdown and see the rendered HTML live. Convert Markdown to HTML, preview README files and copy the output. Free, no signup, runs in your browser.',++ kw: 'markdown editor, markdown to html, markdown preview, online markdown, readme preview',++ about: 'Type Markdown on one side and watch the rendered result on the other. Useful for drafting a README, checking how a comment will render, or converting Markdown into HTML you can paste elsewhere.',++ bullets: ['Live side-by-side preview as you type', 'Headings, lists, tables, code blocks and links', 'Copy the generated HTML in one click'],++ faq: [['Does it support GitHub-flavoured Markdown?', 'The common extensions are covered: tables, fenced code blocks, task lists and strikethrough.'], ['Can I use this to preview a README before pushing?', 'Yes. Paste the file contents and the preview shows how the structure will render.']],++ },++ html: {++ name: 'HTML Formatter', title: 'HTML Formatter, Minifier & Entity Encoder — Free Online',++ desc: 'Format, minify and escape HTML online. Beautify messy markup, compress it for production, or encode HTML entities safely. Free and instant.',++ kw: 'html formatter, html minifier, html beautifier, html entity encoder, format html online',++ about: 'Clean up unreadable markup, shrink it for production, or convert characters to HTML entities so user content can be displayed without breaking the page.',++ bullets: ['Beautify with consistent tag indentation', 'Minify by stripping comments and redundant whitespace', 'Entity encoding for safely displaying markup as text'],++ faq: [['When should I encode HTML entities?', 'Any time you display text that a user supplied. Encoding turns characters like < and > into entities so the browser renders them as text instead of executing them as markup.'], ['Does minifying HTML break my page?', 'The minifier only removes whitespace and comments that do not affect rendering. Content inside pre and textarea elements is preserved.']],++ },++ base64: {++ name: 'Base64 Encoder & Decoder', title: 'Base64 Encode & Decode Online — Free Base64 Converter',++ desc: 'Encode text to Base64 and decode Base64 back to text online. UTF-8 safe, handles data URIs, and runs entirely in your browser. Free, no signup.',++ kw: 'base64 encode, base64 decode, base64 converter, base64 online, decode base64 string',++ about: 'Convert text to Base64 and back. Useful for reading an encoded token, building a data URI, or inspecting a value copied out of a config file or HTTP header.',++ bullets: ['Encode and decode with full UTF-8 support', 'Decodes padded and unpadded input', 'Everything runs locally — safe for tokens and credentials'],++ faq: [['Is Base64 encryption?', 'No. Base64 is an encoding, not encryption — anyone can decode it. It exists to move binary data safely through text-only channels.'], ['Is it safe to decode a token here?', 'Yes. Decoding happens in your browser and nothing is transmitted or stored.']],++ },++ url: {++ name: 'URL Encoder & Decoder', title: 'URL Encode & Decode Online — Free Percent-Encoding Tool',++ desc: 'Encode and decode URLs online, and break any URL into its parts. Handles percent-encoding, query strings and parameters. Free and instant.',++ kw: 'url encode, url decode, percent encoding, url parser, query string parser, urlencode online',++ about: 'Percent-encode a string for safe use in a URL, decode one you have been handed, or split a full URL into protocol, host, path, query parameters and fragment.',++ bullets: ['Encode and decode with correct percent-encoding rules', 'Parse a URL into its components and query parameters', 'Useful for debugging redirects, callbacks and tracking links'],++ faq: [['What is the difference between encodeURI and encodeURIComponent?', 'encodeURI leaves characters that have meaning in a URL (like / and ?) alone, while encodeURIComponent encodes them too. Use the component version for individual query values.'], ['Why does my query parameter break?', 'Usually an unencoded ampersand or equals sign inside a value. Encoding the value fixes it.']],++ },++ jwt: {++ name: 'JWT Decoder', title: 'JWT Decoder — Decode JSON Web Tokens Online, Free',++ desc: 'Decode a JWT to inspect its header, payload and expiry. See claims, issued-at and expiration times in readable form. Runs in your browser — tokens are never sent.',++ kw: 'jwt decoder, decode jwt, json web token decoder, jwt parser, jwt payload viewer',++ about: 'Paste a JSON Web Token to read its header and payload, with timestamps rendered as dates and the expiry checked against the current time. Decoding happens locally, so tokens stay on your machine.',++ bullets: ['Header, payload and signature split out clearly', 'Expiry and issued-at shown as human-readable dates', 'Nothing is transmitted — safe for real access tokens'],++ faq: [['Does decoding a JWT verify its signature?', 'No. Decoding only reads the token contents. Verifying the signature needs the issuer\'s secret or public key, which should only ever happen on your server.'], ['Is it safe to paste a live token here?', 'Yes. The token is parsed in your browser and never sent over the network.']],++ },++ uuid: {++ name: 'UUID Generator', title: 'UUID Generator — Free Online UUID v4 Generator & Validator',++ desc: 'Generate random UUID v4 identifiers, create them in bulk, and validate existing UUIDs. Cryptographically random, instant, free, no signup.',++ kw: 'uuid generator, guid generator, uuid v4, random uuid, uuid validator, generate uuid online',++ about: 'Generate version 4 UUIDs using your browser\'s cryptographic random source, produce a batch for seeding a database, or check whether a string is a valid UUID.',++ bullets: ['Cryptographically random UUID v4 values', 'Bulk generation for test data and migrations', 'Validation for UUIDs of any version'],++ faq: [['Are these UUIDs safe to use in production?', 'Yes. They come from the browser\'s crypto random generator, the same source server-side libraries use.'], ['What is the difference between a UUID and a GUID?', 'They are the same thing. GUID is Microsoft\'s name for the same 128-bit identifier format.']],++ },++ hash: {++ name: 'Hash Generator', title: 'SHA-256 Hash Generator — Free Online SHA-1, SHA-256 & SHA-512',++ desc: 'Generate SHA-1, SHA-256 and SHA-512 hashes from any text online. Useful for checksums, fingerprints and integrity checks. Free and computed in your browser.',++ kw: 'sha256 generator, hash generator, sha1 hash, sha512, checksum generator, hash text online',++ about: 'Produce a cryptographic hash of any text using SHA-1, SHA-256 or SHA-512. Handy for verifying a checksum, generating a fingerprint, or comparing two values without storing the originals.',++ bullets: ['SHA-1, SHA-256 and SHA-512 from the same input', 'Uses the browser\'s native Web Crypto implementation', 'Nothing is uploaded — safe for sensitive strings'],++ faq: [['Can a hash be reversed?', 'No. Hashes are one-way. Identical input always produces identical output, but you cannot recover the input from the hash.'], ['Which algorithm should I use?', 'SHA-256 for almost everything. SHA-1 is only useful for compatibility with older systems, as it is no longer considered collision-resistant.']],++ },++ hex: {++ name: 'Hex Converter', title: 'Hex to Text Converter — Free Online Hex, Binary & ASCII Tool',++ desc: 'Convert text to hexadecimal, decode hex back to text, and switch between hex, binary and decimal. Free, instant, runs in your browser.',++ kw: 'hex to text, text to hex, hex converter, hex decoder, binary converter, ascii to hex',++ about: 'Convert between text, hexadecimal and binary. Useful when reading a memory dump, decoding a protocol payload, or converting a byte sequence you found in a log.',++ bullets: ['Text ⇄ hex with UTF-8 handling', 'Hex ⇄ binary and decimal conversion', 'Accepts spaced, prefixed or continuous hex input'],++ faq: [['What do the 0x prefixes mean?', 'They mark a number as hexadecimal in most programming languages. The converter accepts input with or without them.'], ['Why do some characters become several hex bytes?', 'Non-ASCII characters take more than one byte in UTF-8, so an emoji or accented letter maps to several hex pairs.']],++ },++ escape: {++ name: 'String Escape Tool', title: 'HTML, JavaScript & URL Escape Tool — Free Online Escaper',++ desc: 'Escape and unescape strings for HTML, JavaScript and URLs. Prevent injection bugs and safely embed text in code. Free and instant.',++ kw: 'escape html, unescape string, javascript escape, escape characters online, string escaper',++ about: 'Escape a string so it can be safely embedded in HTML, in a JavaScript literal, or in a URL — and reverse the process when you need the original text back.',++ bullets: ['HTML entity escaping for safe display of user content', 'JavaScript string escaping including quotes and newlines', 'URL escaping for query values and path segments'],++ faq: [['Why does escaping matter for security?', 'Unescaped user input is how cross-site scripting happens. Escaping ensures text is displayed as text rather than executed as markup or code.'], ['Which mode should I use?', 'Match the destination: HTML escaping for page content, JavaScript escaping for code literals, URL escaping for anything going into a link.']],++ },++ timestamp: {++ name: 'Unix Timestamp Converter', title: 'Unix Timestamp Converter — Epoch to Date, Free Online',++ desc: 'Convert Unix timestamps to human-readable dates and back, with timezone support. Handles seconds and milliseconds. Free, instant, no signup.',++ kw: 'unix timestamp converter, epoch converter, timestamp to date, date to timestamp, epoch time',++ about: 'Turn an epoch timestamp into a readable date in any timezone, or convert a date back into a timestamp. Both seconds and milliseconds are detected automatically.',++ bullets: ['Epoch ⇄ date in both directions', 'Automatic detection of seconds vs milliseconds', 'Timezone-aware output including UTC and local time'],++ faq: [['Is my timestamp in seconds or milliseconds?', 'Ten digits is seconds, thirteen is milliseconds. The converter detects which you pasted and labels the result.'], ['What is the Unix epoch?', 'Midnight UTC on 1 January 1970. A Unix timestamp counts the seconds elapsed since that moment.']],++ },++ cron: {++ name: 'Cron Expression Parser', title: 'Cron Expression Parser — Explain Cron Schedules Online, Free',++ desc: 'Decode any cron expression into plain English and see the next run times. Validate crontab syntax before you deploy it. Free and instant.',++ kw: 'cron expression parser, crontab generator, cron schedule explained, next run time, cron validator',++ about: 'Paste a cron expression to read it in plain English and see exactly when it will fire next. Saves guessing whether a schedule means every five minutes or five past the hour.',++ bullets: ['Plain-English description of any cron expression', 'A list of upcoming run times so you can sanity-check the schedule', 'Validates the syntax before you commit it to a crontab'],++ faq: [['What does */5 * * * * mean?', 'Every five minutes, on every hour, day and month. Paste any expression here for a written explanation and the next run times.'], ['Why did my cron job not run?', 'Most often the field order is wrong (minute, hour, day of month, month, day of week) or the day-of-week and day-of-month fields conflict. The parser shows the interpreted schedule so you can spot it.']],++ },++ beautify: {++ name: 'Code Beautifier', title: 'Code Beautifier — Free Online JS, CSS, HTML, JSON & SQL Formatter',++ desc: 'Beautify JavaScript, CSS, HTML, JSON and SQL online. Turn minified or messy code into clean, indented, readable source. Free, instant, no signup.',++ kw: 'code beautifier, javascript formatter, css formatter, unminify, code formatter online, prettify code',++ about: 'Paste minified or badly formatted code and get readable, consistently indented source back. Works across JavaScript, CSS, HTML, JSON and SQL, which makes it useful for reading third-party bundles.',++ bullets: ['One tool for JS, CSS, HTML, JSON and SQL', 'Consistent indentation and line breaks', 'Great for reading minified production code'],++ faq: [['Can this recover minified code?', 'It restores structure and indentation so the code is readable, but names that a minifier shortened cannot be recovered.'], ['Does beautifying change what the code does?', 'No. Only whitespace and line breaks change; the code is functionally identical.']],++ },++ minify: {++ name: 'Code Minifier', title: 'Code Minifier — Free Online JS, CSS, HTML & JSON Minifier',++ desc: 'Minify JavaScript, CSS, HTML and JSON online to cut file size and speed up your site. Free, instant, runs entirely in your browser.',++ kw: 'minify javascript, css minifier, html minifier, json minifier, compress code online',++ about: 'Strip comments and unnecessary whitespace to shrink files before shipping them. Smaller payloads mean faster page loads, which is one of the ranking signals search engines actually measure.',++ bullets: ['JavaScript, CSS, HTML and JSON in one place', 'Shows the size saved by the minification', 'Runs locally — no upload, no build step required'],++ faq: [['How much smaller will my file get?', 'Typically 20–60% before compression, depending on how much whitespace and commenting the source contains. The tool reports the exact saving.'], ['Should I minify if my server uses gzip?', 'Yes. Minification and gzip compound: minifying first removes content gzip would otherwise have to encode.']],++ },++ diff: {++ name: 'Diff Checker', title: 'Diff Checker — Compare Text & Code Online, Free',++ desc: 'Compare two blocks of text or code side by side and see every addition, deletion and change highlighted. Free, instant, private to your browser.',++ kw: 'diff checker, compare text, text comparison, code diff, file compare online, difference checker',++ about: 'Paste two versions of a file, a config or a paragraph and see exactly what changed, line by line, with additions and removals highlighted.',++ bullets: ['Side-by-side comparison with highlighted changes', 'Works on prose, code, config files and logs', 'Nothing is uploaded, so it is safe for private files'],++ faq: [['Can I compare two config files?', 'Yes. Paste each version into one side; the differences are highlighted line by line.'], ['Is there a size limit?', 'The comparison runs in your browser, so the practical limit is your machine\'s memory rather than an upload cap.']],++ },++ regex: {++ name: 'Regex Tester', title: 'Regex Tester — Test Regular Expressions Online, Free',++ desc: 'Test regular expressions against sample text with live match highlighting and capture groups. Debug patterns before shipping them. Free and instant.',++ kw: 'regex tester, regular expression tester, regex online, test regex, regex match, regex debugger',++ about: 'Write a pattern, paste sample text, and watch matches highlight as you type. Capture groups are listed separately so you can confirm a pattern extracts what you expect before it reaches production.',++ bullets: ['Live highlighting of every match as you type', 'Named and numbered capture groups listed per match', 'Flag toggles for global, case-insensitive and multiline'],++ faq: [['Which regex flavour does this use?', 'JavaScript regular expressions, which are close to PCRE for everyday patterns. Lookbehind and named groups are supported in modern browsers.'], ['Why does my pattern match nothing?', 'Common causes are an unescaped special character, a missing global flag when you expect several matches, or anchors that assume single-line input.']],++ },++ slug: {++ name: 'Slug Generator', title: 'URL Slug Generator — Free SEO-Friendly Slug Maker',++ desc: 'Turn any title into a clean, SEO-friendly URL slug. Removes accents and punctuation, lowercases and hyphenates. Free and instant.',++ kw: 'slug generator, url slug, seo friendly url, permalink generator, slugify text online',++ about: 'Convert a headline into a tidy URL slug: lowercase, hyphen-separated, accents folded and punctuation removed. Readable slugs help both people and search engines understand a page before they open it.',++ bullets: ['Accent folding so international titles produce clean ASCII slugs', 'Punctuation and stop-character stripping', 'Consistent hyphenation with no double separators'],++ faq: [['Do slugs affect SEO?', 'A short, descriptive slug containing your main keyword helps search engines and gives people a readable link, which improves click-through from search results.'], ['Should slugs contain dates or IDs?', 'Usually not. Keep them short and descriptive; dates age the URL and IDs mean nothing to a reader.']],++ },++ lorem: {++ name: 'Lorem Ipsum Generator', title: 'Lorem Ipsum Generator — Free Placeholder Text Generator',++ desc: 'Generate lorem ipsum placeholder text by words, sentences or paragraphs. Perfect for mockups, layouts and design drafts. Free, instant, no signup.',++ kw: 'lorem ipsum generator, placeholder text, dummy text generator, filler text, lipsum',++ about: 'Produce as much placeholder copy as your mockup needs, measured in words, sentences or paragraphs, so a layout can be reviewed without waiting on the real content.',++ bullets: ['Generate by word, sentence or paragraph count', 'Classic lorem ipsum text for familiar typographic colour', 'Copy straight into a design tool or HTML'],++ faq: [['Why use lorem ipsum instead of real text?', 'It has roughly the same letter distribution as natural prose, so a layout reads realistically without reviewers getting distracted by the wording.'], ['Can I generate a specific number of words?', 'Yes. Choose the unit and the amount, and the generator produces exactly that much text.']],++ },++ curl: {++ name: 'curl Converter', title: 'curl Converter — Convert curl to fetch, Python & Node.js, Free',++ desc: 'Paste a curl command and convert it to JavaScript fetch, Python requests or Node.js code. Great for turning browser "Copy as cURL" into working code.',++ kw: 'curl to fetch, curl converter, curl to python, curl to javascript, convert curl command',++ about: 'Take a curl command — including one copied straight from your browser\'s network tab — and turn it into equivalent code, with headers, method and body carried across.',++ bullets: ['Converts to fetch, Python requests and Node.js', 'Preserves headers, method, body and authentication', 'Works with "Copy as cURL" output from browser devtools'],++ faq: [['Where do I get a curl command to convert?', 'In any browser\'s devtools, right-click a request in the Network tab and choose "Copy as cURL". Paste it here to get code.'], ['Are my headers sent anywhere?', 'No. The conversion is pure text processing in your browser, so authorization headers stay local.']],++ },++ webhook: {++ name: 'Webhook Tester', title: 'Webhook Tester — Free Online Request Bin & Inspector',++ desc: 'Get a temporary webhook URL and inspect every request it receives: headers, body, method and query. Debug integrations without deploying anything.',++ kw: 'webhook tester, request bin, webhook debugger, http request inspector, test webhook online',++ about: 'Create a throwaway endpoint, point your integration at it, and read exactly what arrives — method, headers, query string and body — so you can confirm a provider is sending what its docs claim.',++ bullets: ['Instant URL with no account required', 'Full request detail: headers, body, method and query', 'Bins expire automatically so nothing lingers'],++ faq: [['How long does a webhook bin last?', 'Bins expire after about an hour and hold a capped number of recent requests, which is plenty for debugging an integration.'], ['Can I use this for production traffic?', 'No. It is a debugging aid — bins are temporary and publicly addressable by anyone with the URL.']],++ },++ color: {++ name: 'Color Converter', title: 'Color Picker & HEX to RGB Converter — Free Online Color Tool',++ desc: 'Pick colors and convert between HEX, RGB and HSL, then generate matching palettes. Free, instant, perfect for CSS and design work.',++ kw: 'hex to rgb, color converter, color picker, rgb to hex, hsl converter, color palette generator',++ about: 'Convert a color between HEX, RGB and HSL notation, pick one visually, and generate a palette of related shades for a design system or a CSS variable set.',++ bullets: ['HEX, RGB and HSL conversion in every direction', 'Visual picker with live preview', 'Palette generation for tints, shades and complements'],++ faq: [['What is the difference between HEX and RGB?', 'They describe the same color in different notation — HEX is a compact hexadecimal form of the same red, green and blue values. HSL instead describes hue, saturation and lightness, which is easier to adjust by hand.'], ['How do I make a lighter version of a brand color?', 'Convert it to HSL and raise the lightness value. The palette generator does this for you.']],++ },++ opengraph: {++ name: 'Open Graph Preview', title: 'Open Graph Preview & Meta Tag Checker — Free Social Card Tester',++ desc: 'Preview how any URL appears when shared on social media and validate its Open Graph and Twitter Card tags. Spot missing images and titles before you post.',++ kw: 'open graph preview, og tag checker, twitter card validator, social media preview, meta tag checker',++ about: 'Enter a URL to fetch its Open Graph and Twitter Card tags and see the link preview a social platform would build from them, along with a list of any tags that are missing or truncated.',++ bullets: ['Live preview of the social card for any public URL', 'Lists Open Graph, Twitter Card and standard meta tags', 'Flags missing titles, descriptions and images'],++ faq: [['Why does my link preview show no image?', 'Usually a missing og:image tag, or an image URL that is relative rather than absolute. Social platforms need a full https URL.'], ['Why has my preview not updated after I fixed the tags?', 'Platforms cache previews aggressively. Most offer a debugger that forces a re-scrape; this tool always fetches live so you can confirm the tags themselves are correct.']],++ },++};++++// ── Network & diagnostics tools (net.openvibe.tools) ─────────++const NET = {++ net: {++ name: 'Network Tools', title: 'Free Online Network Tools — Ping, DNS, Whois & SSL Diagnostics',++ desc: 'Run ping, traceroute, DNS lookups, Whois, SSL checks, port scans and HTTP header tests from the browser. Free network diagnostics with no signup.',++ kw: 'network tools, online network diagnostics, ping test, dns lookup, whois, ssl checker',++ about: 'A set of network diagnostics that run from our servers so you can test a host from outside your own connection — useful when you need to know whether a problem is yours or theirs.',++ bullets: ['Tests run from our infrastructure, not your browser', 'Covers DNS, email deliverability, HTTP, TLS and routing', 'Free with no account required'],++ faq: [['Why test from a server instead of my machine?', 'Testing from outside your network rules out your own DNS cache, firewall or ISP, which is exactly what you need when a site works for you but not for someone else.'], ['Are these tools free?', 'Yes, with reasonable rate limits to keep them available for everyone.']],++ },++ lookup: { name: 'Domain & IP Lookup', title: 'Domain & IP Lookup — All-in-One Free Online Lookup Tool', desc: 'Look up everything about a domain or IP in one go: DNS records, Whois, geolocation, SSL and HTTP headers. Free, instant, no signup.', kw: 'domain lookup, ip lookup, whois lookup, dns lookup, all in one lookup tool', about: 'Enter a domain or IP address and get a complete report in one pass: registration details, DNS records, hosting location, certificate status and HTTP response headers.', bullets: ['One query returns DNS, Whois, geolocation and TLS detail', 'Works with domains, IPv4 and IPv6 addresses', 'A fast first step when diagnosing any site problem'], faq: [['What can I learn from a domain lookup?', 'Who registered the domain and when it expires, which nameservers and mail servers it uses, where it is hosted, and whether its certificate is valid.'], ['Does this work for IP addresses too?', 'Yes. Enter an IP to see its owner, network range, geolocation and reverse DNS name.']] },++ myip: { name: 'What Is My IP', title: 'What Is My IP Address — Free IP, ISP & Location Checker', desc: 'See your public IP address, ISP, approximate location and reverse DNS instantly. Works for both IPv4 and IPv6. Free, no signup.', kw: 'what is my ip, my ip address, public ip, ip location, my isp', about: 'Shows the public IP address the internet sees for your connection, along with the network operator, approximate location and reverse DNS name.', bullets: ['Detects both your IPv4 and IPv6 address', 'ISP, autonomous system and approximate location', 'Confirms at a glance whether a VPN is actually active'], faq: [['Why does my IP differ from the one in my router?', 'Your router shows a private address used inside your network. The public address shown here is what websites actually see.'], ['How do I check whether my VPN is working?', 'Load this page with the VPN off, then on. If the address and location change, traffic is going through the VPN.']] },++ ip: { name: 'IP Address Lookup', title: 'IP Address Lookup — Free IPv4 & IPv6 Information Tool', desc: 'Look up any IP address to see its owner, network, geolocation, reverse DNS and abuse contact. Supports IPv4 and IPv6. Free and instant.', kw: 'ip address lookup, ip checker, ip information, ip details, ip owner lookup', about: 'Enter any IP address to find out which organisation announces it, which network block it belongs to, roughly where it is, and what its reverse DNS name says.', bullets: ['Owner, network range and autonomous system', 'Geolocation with country, region and city', 'Reverse DNS and abuse contact where published'], faq: [['Can an IP address identify a person?', 'No. It identifies a network connection and usually resolves only to a city-level area and an internet provider.'], ['Why is the location wrong?', 'Geolocation is inferred from registry and routing data. Mobile networks and VPNs often report the location of a gateway hundreds of miles away.']] },++ ipv4: { name: 'IPv4 Lookup & CIDR Calculator', title: 'IPv4 Lookup & CIDR Subnet Calculator — Free Online', desc: 'Look up IPv4 addresses and calculate CIDR subnets: network range, broadcast address, usable hosts and mask. Free and instant.', kw: 'ipv4 lookup, cidr calculator, subnet calculator, ip range calculator, netmask calculator', about: 'Inspect an IPv4 address or work out a subnet: given a CIDR block you get the network address, broadcast address, usable host range and the total address count.', bullets: ['CIDR to range, mask and host count', 'Address class and reserved-range detection', 'Useful for firewall rules and allowlists'], faq: [['How many hosts are in a /24?', '256 addresses, of which 254 are usable — the first is the network address and the last is the broadcast address.'], ['What does the /number in CIDR mean?', 'It is the count of leading bits that identify the network. A larger number means a smaller subnet.']] },++ ipv6: { name: 'IPv6 Lookup', title: 'IPv6 Address Lookup & Expander — Free Online IPv6 Tool', desc: 'Look up IPv6 addresses, expand or compress notation, and identify address type and scope. Free, instant, no signup.', kw: 'ipv6 lookup, ipv6 expander, ipv6 address checker, expand ipv6, ipv6 notation', about: 'Expand a shortened IPv6 address to its full form, compress it back, and see what kind of address it is — global unicast, link-local, unique local or multicast.', bullets: ['Expand and compress IPv6 notation', 'Address type, scope and prefix detection', 'Owner and routing information for global addresses'], faq: [['What does the :: in an IPv6 address mean?', 'It replaces one run of consecutive zero groups. It can appear only once in an address, which is why expansion is unambiguous.'], ['Why do I have several IPv6 addresses?', 'Devices normally hold a link-local address plus one or more global addresses, often including a rotating privacy address.']] },++ geoip: { name: 'IP Geolocation', title: 'IP Geolocation Lookup — Find the Location of Any IP, Free', desc: 'Find the country, region, city, timezone and coordinates behind any IP address, shown on a map. Free IP geolocation with no signup.', kw: 'ip geolocation, ip location lookup, find ip location, geoip, ip to location', about: 'Resolve an IP address to its most likely physical location, including country, region, city, timezone and approximate coordinates plotted on a map.', bullets: ['Country, region, city and timezone', 'Coordinates plotted on a map', 'Network operator shown alongside the location'], faq: [['How accurate is IP geolocation?', 'Country level is highly reliable; city level is a good guess and often off for mobile and satellite connections. It should never be treated as a precise location.'], ['Why does my own IP show the wrong city?', 'Your provider may route traffic through a regional hub, so the registered location of the gateway is what gets reported.']] },++ hostname: { name: 'Hostname Lookup', title: 'Hostname Lookup — Resolve Hostnames to IP Addresses, Free', desc: 'Resolve a hostname to its IP addresses or find the hostname behind an IP. Forward and reverse lookups in one tool. Free and instant.', kw: 'hostname lookup, resolve hostname, host to ip, ip to hostname, dns resolve', about: 'Resolve a hostname to every address it answers with, or go the other way and find the name registered for an IP.', bullets: ['Forward resolution to IPv4 and IPv6 addresses', 'Reverse resolution from an address to a name', 'Shows every record when a host is load balanced'], faq: [['Why does one hostname return several IPs?', 'Large sites publish multiple addresses for load balancing and redundancy; your resolver picks one.'], ['What if the reverse lookup returns nothing?', 'Reverse records are optional and set by whoever controls the IP block, so many addresses simply have none.']] },++ isp: { name: 'ISP Lookup', title: 'ISP Lookup — Find the Internet Provider Behind Any IP, Free', desc: 'Identify the ISP, hosting company or organisation that owns any IP address, plus its network range. Free and instant.', kw: 'isp lookup, ip owner, who owns this ip, hosting provider lookup, organization lookup', about: 'Find out which company is responsible for an address — a residential ISP, a mobile carrier, a cloud provider or a hosting company — along with the network block it sits in.', bullets: ['Provider and organisation name', 'Network block and autonomous system', 'Distinguishes residential, mobile and datacentre addresses'], faq: [['How can I tell if traffic came from a datacentre?', 'The owner name is the giveaway: cloud and hosting providers appear instead of a consumer ISP, which usually means automated traffic or a VPN.'], ['Is the ISP always accurate?', 'It reflects registry data, which is reliable for the owning organisation but may lag when address blocks are transferred or leased.']] },++ asn: { name: 'ASN Lookup', title: 'ASN Lookup — Autonomous System Number Info, Free Online', desc: 'Look up any autonomous system number to see its operator, announced prefixes and peers. Free BGP and routing information.', kw: 'asn lookup, autonomous system number, bgp lookup, as number, network prefix lookup', about: 'Inspect an autonomous system: who operates it, which address prefixes it announces to the internet, and how it connects to the wider routing table.', bullets: ['Operator name, country and registry', 'Announced IPv4 and IPv6 prefixes', 'Useful for routing investigations and abuse reports'], faq: [['What is an autonomous system?', 'A group of IP networks under one operator with a single routing policy. Every ISP and large hosting provider has at least one, identified by an AS number.'], ['How do I find the ASN for a website?', 'Look up the site\'s IP address first — the result includes the AS number announcing it.']] },++ rdns: { name: 'Reverse DNS Lookup', title: 'Reverse DNS Lookup — Find the Hostname of an IP (PTR), Free', desc: 'Run a reverse DNS lookup to find the PTR hostname for any IPv4 or IPv6 address. Essential for mail server checks. Free and instant.', kw: 'reverse dns lookup, ptr record lookup, rdns, ip to hostname, reverse ip lookup', about: 'Query the PTR record for an address to find the hostname its operator has registered. Mail servers check this, which makes it a required step when diagnosing delivery problems.', bullets: ['PTR lookups for both IPv4 and IPv6', 'Shows whether forward and reverse records agree', 'Key check for mail server configuration'], faq: [['Why does my mail get rejected for missing reverse DNS?', 'Receiving mail servers treat a missing or mismatched PTR record as a spam signal. Your hosting provider sets this record for you.'], ['What is forward-confirmed reverse DNS?', 'It means the PTR name resolves back to the same IP. Mail servers prefer it, and this tool shows whether it matches.']] },++ whois: { name: 'Whois Lookup', title: 'Whois Lookup — Free Domain Registration & Ownership Checker', desc: 'Look up domain registration details: registrar, creation and expiry dates, nameservers and status codes. Free Whois lookup, no signup.', kw: 'whois lookup, domain whois, domain registration lookup, check domain owner, domain expiry', about: 'Query registration records for a domain to see who registered it, when it was created, when it expires, which registrar holds it and what status codes are applied.', bullets: ['Registrar, creation, update and expiry dates', 'Nameservers and domain status codes', 'Works for domains and IP address blocks'], faq: [['Why is the registrant hidden?', 'Privacy rules mean most registrars replace personal details with a privacy service. Registrar, dates and nameservers remain public.'], ['How do I tell when a domain expires?', 'The expiry date is in the record here, though domains usually stay in redemption for weeks afterwards before becoming available again.']] },++ rdap: { name: 'RDAP Lookup', title: 'RDAP Lookup — Modern Domain & IP Registration Data, Free', desc: 'Query RDAP for structured domain and IP registration data — the modern replacement for Whois. Free, instant, standards-compliant.', kw: 'rdap lookup, rdap query, domain registration data, whois alternative, registration data access', about: 'RDAP is the structured successor to Whois: the same registration data delivered as predictable JSON, with consistent fields across registries.', bullets: ['Structured, machine-readable registration data', 'Consistent field names across every registry', 'Covers both domains and IP allocations'], faq: [['How is RDAP different from Whois?', 'Whois returns free-form text that differs by registry. RDAP returns structured data with standard field names, so it is far easier to parse reliably.'], ['Is RDAP replacing Whois?', 'Yes, gradually. Registries are required to run RDAP, and Whois is being retired over time.']] },++ dns: { name: 'DNS Lookup', title: 'DNS Lookup — Free Online DNS Record Checker (A, MX, TXT)', desc: 'Look up DNS records for any domain: A, AAAA, MX, TXT, CNAME, NS, SOA and more. Free DNS lookup with instant results.', kw: 'dns lookup, dns record checker, check dns records, a record lookup, mx lookup, txt record', about: 'Query any DNS record type for a domain and see the answers exactly as a resolver receives them, including TTL values so you know how long a change will take to spread.', bullets: ['All common record types: A, AAAA, MX, TXT, CNAME, NS, SOA', 'TTL values shown so you can predict propagation', 'Queried from our resolvers, bypassing your local cache'], faq: [['Why do I still see the old DNS record?', 'Resolvers cache answers for the length of the TTL. This tool queries directly, so it usually shows the new value before your machine does.'], ['Which record do I need for a website?', 'An A record (or AAAA for IPv6) pointing at your server\'s IP, or a CNAME pointing at another hostname.']] },++ dig: { name: 'Online dig', title: 'Online dig Command — Free Web-Based DNS Query Tool', desc: 'Run dig from your browser to query any DNS record type and read the full answer, authority and additional sections. Free, no install.', kw: 'online dig, dig command online, dns query tool, dig web interface, dns debugging', about: 'The dig command without a terminal: choose a record type, query any domain, and read the complete response including the authority and additional sections.', bullets: ['Full response sections, not just the answer', 'Any record type, including less common ones', 'No local install — useful from a locked-down machine'], faq: [['What is the difference between dig and nslookup?', 'dig shows the complete DNS response including flags and section detail, which makes it the better tool for debugging. nslookup is simpler and shows just the answer.'], ['Can I query a specific nameserver?', 'Yes — that is the usual way to confirm an authoritative server has your change before caches expire.']] },++ nslookup: { name: 'NSLookup', title: 'NSLookup Online — Free Web-Based DNS Lookup Tool', desc: 'Run nslookup from your browser to resolve domains and check DNS records. Simple, fast, free and no install required.', kw: 'nslookup online, nslookup tool, dns lookup command, resolve domain online', about: 'A simple DNS query in the familiar nslookup style: enter a domain, get the addresses and records it resolves to.', bullets: ['Straightforward answers without protocol noise', 'Common record types covered', 'Runs from our resolvers, so it ignores your local cache'], faq: [['Why does nslookup show a different answer than my browser?', 'Your browser and operating system cache DNS answers. A direct query shows the current published record.'], ['Is nslookup still worth using?', 'For a quick "what does this resolve to" check, yes. For debugging, dig gives far more detail.']] },++ dnspropagation: { name: 'DNS Propagation Checker', title: 'DNS Propagation Checker — Test DNS Worldwide, Free', desc: 'Check whether a DNS change has propagated by querying resolvers around the world and comparing the answers. Free and instant.', kw: 'dns propagation checker, dns propagation test, global dns check, dns worldwide, check dns update', about: 'Query the same record from resolvers in different regions to see which ones have picked up your change and which are still serving the cached answer.', bullets: ['Answers from multiple global regions side by side', 'Highlights resolvers still serving the old value', 'Works for any record type, not just A records'], faq: [['How long does DNS propagation take?', 'Up to the record\'s previous TTL — commonly an hour, sometimes 24 or 48 hours if the TTL was set high. Lowering the TTL before a planned change makes the next one faster.'], ['Why do some regions still show the old IP?', 'Those resolvers cached the previous answer and will keep serving it until that cache entry expires.']] },++ mx: { name: 'MX Record Lookup', title: 'MX Record Lookup — Find Mail Servers for Any Domain, Free', desc: 'Look up the MX records for a domain to see which mail servers handle its email and in what priority order. Free and instant.', kw: 'mx record lookup, mail server lookup, mx check, email dns records, find mail server', about: 'See which servers accept mail for a domain and their priority order — the first thing to check when email is bouncing or arriving somewhere unexpected.', bullets: ['All MX records with priority values', 'Resolves each mail server to its address', 'Flags domains with no MX record at all'], faq: [['What does the MX priority number mean?', 'Lower numbers are tried first. Equal values are used in rotation, which spreads load across servers.'], ['Why is my email bouncing?', 'Start here: a missing MX record, or one pointing at a host that no longer exists, stops delivery outright.']] },++ txt: { name: 'TXT Record Lookup', title: 'TXT Record Lookup — Check SPF, DKIM & Verification Records', desc: 'Look up TXT records for any domain, including SPF, DKIM, DMARC and site verification strings. Free, instant DNS TXT checker.', kw: 'txt record lookup, spf record check, dns txt records, domain verification record, check txt record', about: 'Read every TXT record published for a domain — the place where SPF policies, domain verification tokens and assorted service strings all live.', bullets: ['Every TXT record, shown in full', 'Highlights SPF, DKIM and verification entries', 'Useful for confirming a verification token is live'], faq: [['Why is my domain verification failing?', 'Usually the record was added to the wrong host name, or the provider wrapped it in extra quotes. This shows exactly what is published.'], ['Can a domain have more than one TXT record?', 'Yes, and most do. The exception is SPF: exactly one SPF record is allowed, and multiple ones cause failures.']] },++ ns: { name: 'Nameserver Lookup', title: 'NS Record Lookup — Find the Nameservers for Any Domain, Free', desc: 'Look up the NS records for a domain to see which nameservers are authoritative for it. Free and instant nameserver checker.', kw: 'ns record lookup, nameserver lookup, find nameservers, authoritative dns, dns delegation check', about: 'Find which nameservers a domain is delegated to, and confirm the delegation at the registry matches what the nameservers themselves claim.', bullets: ['Authoritative nameservers for the domain', 'Reveals mismatches between registry and zone', 'First check when a DNS change has no effect'], faq: [['Why do my DNS changes have no effect?', 'Often the domain is delegated to different nameservers than the ones you edited. This shows which servers are actually authoritative.'], ['How many nameservers should a domain have?', 'At least two, on separate networks, so the domain still resolves if one goes down.']] },++ spf: { name: 'SPF Record Checker', title: 'SPF Record Checker — Validate Your Email SPF Policy, Free', desc: 'Check and validate a domain\'s SPF record. See which servers may send mail, catch syntax errors and count DNS lookups. Free and instant.', kw: 'spf record checker, spf validator, check spf, email authentication, spf lookup limit', about: 'Read a domain\'s SPF policy, expand every include, and confirm it stays inside the ten-lookup limit that causes silent failures when exceeded.', bullets: ['Full expansion of includes and redirects', 'Counts DNS lookups against the limit of ten', 'Flags syntax errors and multiple SPF records'], faq: [['What happens if SPF exceeds ten DNS lookups?', 'Receiving servers return a permanent error and may treat your mail as unauthenticated, so legitimate messages can land in spam.'], ['Can I have two SPF records?', 'No. A domain must publish exactly one; two records cause validation to fail entirely.']] },++ dkim: { name: 'DKIM Record Lookup', title: 'DKIM Record Lookup & Validator — Free Email Auth Checker', desc: 'Look up and validate DKIM records by selector. Confirm your signing key is published correctly for email authentication. Free and instant.', kw: 'dkim record lookup, dkim checker, dkim validator, dkim selector, email authentication', about: 'Fetch the DKIM public key for a given selector and confirm it is published and well-formed, so signed mail can be verified by receiving servers.', bullets: ['Selector-based lookup for any domain', 'Validates key format and flags', 'Confirms the record is reachable before you go live'], faq: [['What is a DKIM selector?', 'A label that identifies which key to use, published at selector._domainkey.yourdomain.com. Your mail provider tells you which selector it signs with.'], ['Why is DKIM failing after a provider change?', 'The new provider signs with a different selector and key, so the old record no longer matches. Publish the new one they supply.']] },++ dmarc: { name: 'DMARC Checker', title: 'DMARC Record Checker — Validate Your DMARC Policy, Free', desc: 'Check a domain\'s DMARC record and policy. See enforcement level, reporting addresses and alignment settings. Free and instant.', kw: 'dmarc checker, dmarc record lookup, dmarc validator, dmarc policy, email security check', about: 'Read a domain\'s DMARC policy: whether failures are monitored, quarantined or rejected, where reports are sent, and how strictly alignment is enforced.', bullets: ['Policy level, percentage and alignment settings', 'Reporting addresses for aggregate and forensic reports', 'Syntax validation with clear explanations'], faq: [['What DMARC policy should I start with?', 'Start at p=none with a reporting address. That monitors without affecting delivery, so you can find legitimate senders before moving to quarantine and then reject.'], ['Do I need SPF and DKIM for DMARC to work?', 'Yes. DMARC builds on both, and requires that at least one passes and aligns with the visible From domain.']] },++ ping: { name: 'Ping Test', title: 'Online Ping Test — Check if a Host Is Up, Free', desc: 'Ping any host or IP from multiple global regions to check reachability and latency. Free online ping test with no install.', kw: 'ping test, online ping, ping ip, check if server is up, latency test, ping tool', about: 'Send ICMP pings from our servers to check whether a host answers and how long the round trip takes, from several regions at once.', bullets: ['Results from multiple global regions', 'Round-trip time, jitter and packet loss', 'Confirms whether an outage is local to you or global'], faq: [['Why does a site load but not respond to ping?', 'Many hosts and firewalls block ICMP on purpose. A failed ping does not always mean a site is down — check HTTP headers instead.'], ['What is a good ping time?', 'Under 50 ms feels instant, under 100 ms is fine for most uses, and over 200 ms is noticeable in calls and games.']] },++ traceroute: { name: 'Traceroute', title: 'Online Traceroute — Trace the Network Path to Any Host, Free', desc: 'Run a traceroute from our servers to see every hop between us and a host, with latency per hop. Free, no install required.', kw: 'traceroute online, trace route, network path, tracert online, hop by hop latency', about: 'Map the route packets take to a destination, hop by hop, with timing at each step so you can see where latency is introduced or where the path stops.', bullets: ['Every hop with per-hop latency', 'Reverse DNS names to identify the networks in the path', 'Shows exactly where a broken path terminates'], faq: [['Why do some hops show stars?', 'Those routers are configured not to reply to probes. It is normal and does not mean the path is broken.'], ['How do I read a traceroute?', 'Look for the point where latency jumps sharply or replies stop. The hop before it usually names the network where the trouble starts.']] },++ mtr: { name: 'MTR Report', title: 'Online MTR — Combined Ping & Traceroute Report, Free', desc: 'Run MTR to combine traceroute and ping into one report with per-hop packet loss and latency over time. Free network path analysis.', kw: 'mtr online, mtr report, my traceroute, packet loss test, network path analysis', about: 'MTR repeats a traceroute continuously and aggregates the results, so intermittent packet loss at a single hop shows up clearly instead of hiding behind one lucky probe.', bullets: ['Per-hop loss percentage over many probes', 'Best, average and worst latency for each hop', 'The report network operators ask for in support tickets'], faq: [['Why is MTR better than traceroute for support tickets?', 'It samples repeatedly, so it distinguishes a genuinely lossy hop from a single dropped probe. That is the evidence a network operator needs.'], ['Loss shows at one hop but not later ones — is that a problem?', 'No. That router is just rate-limiting its own replies. Real trouble shows as loss that continues at every hop after it.']] },++ port: { name: 'Port Checker', title: 'Open Port Checker — Test if a Port Is Open, Free Online', desc: 'Check whether a TCP port is open on any host, from outside your network. Test common services or any custom port. Free and instant.', kw: 'port checker, open port test, port scanner online, check port open, tcp port test', about: 'Test whether a port accepts connections from the public internet — the definitive check on whether a port forward, firewall rule or service binding actually works.', bullets: ['Tests from outside your network, as a real client would', 'Common service ports or any custom port', 'Distinguishes closed ports from filtered ones'], faq: [['My service runs but the port shows closed — why?', 'Either a firewall is dropping the traffic, the port forward is missing, or the service is bound to localhost instead of all interfaces.'], ['What is the difference between closed and filtered?', 'Closed means the host actively refused the connection. Filtered means nothing answered at all, which usually indicates a firewall dropping packets silently.']] },++ headers: { name: 'HTTP Header Checker', title: 'HTTP Header Checker — Inspect Response Headers Free Online', desc: 'Check the HTTP response headers of any URL, including security, caching and compression headers. Free header analysis tool.', kw: 'http header checker, response headers, security headers check, cache headers, view http headers', about: 'Fetch any URL and read the full response headers, with the security and caching headers called out so you can see what a browser is being told to do.', bullets: ['Complete response headers exactly as sent', 'Security header review including HSTS and CSP', 'Cache and compression headers explained'], faq: [['Which security headers should I have?', 'At minimum Strict-Transport-Security, X-Content-Type-Options and a Content-Security-Policy. The report lists what is present and what is missing.'], ['Why is my page not being cached?', 'Look at Cache-Control. A no-store or no-cache directive, or a missing max-age, prevents browsers and CDNs from reusing the response.']] },++ redirects: { name: 'Redirect Checker', title: 'Redirect Checker — Trace Redirect Chains Online, Free', desc: 'Follow every hop in a redirect chain and see each status code and destination. Find redirect loops and SEO-damaging chains. Free and instant.', kw: 'redirect checker, redirect chain, 301 checker, url redirect tracer, redirect loop finder', about: 'Follow a URL through every redirect to its final destination, with the status code at each step, so you can spot long chains and loops that waste crawl budget and lose link value.', bullets: ['Every hop with its status code and target', 'Detects loops and unnecessarily long chains', 'Flags temporary redirects used where permanent ones belong'], faq: [['Do redirect chains hurt SEO?', 'Yes. Each extra hop slows the page and search engines limit how many they follow. Point the first URL straight at the final destination.'], ['301 or 302 — which should I use?', 'Use 301 for permanent moves so ranking signals transfer. Use 302 only when the original URL will genuinely return.']] },++ ssl: { name: 'SSL Certificate Checker', title: 'SSL Certificate Checker — Free Online TLS & Expiry Test', desc: 'Check any site\'s SSL certificate: expiry date, issuer, chain completeness and supported TLS versions. Free SSL checker, instant results.', kw: 'ssl checker, ssl certificate checker, tls check, certificate expiry, ssl chain test', about: 'Inspect a site\'s TLS certificate — who issued it, when it expires, whether the chain is complete and which protocol versions the server negotiates.', bullets: ['Expiry date with days remaining', 'Chain validation that catches missing intermediates', 'Issuer, subject and alternative names'], faq: [['My certificate works in Chrome but fails elsewhere — why?', 'Almost always a missing intermediate certificate. Browsers often cache intermediates from other sites; other clients do not. This check shows whether the chain your server sends is complete.'], ['How early should I renew?', 'Automate renewal at around 30 days remaining. Most issuers allow renewal well before expiry.']] },++ curl: { name: 'Online curl', title: 'Online curl — Free Web-Based HTTP Request Tester', desc: 'Send HTTP requests from your browser and inspect the full response: status, headers and body. Test APIs without installing anything.', kw: 'online curl, http request tester, api tester online, send http request, rest client online', about: 'Make an HTTP request from our servers and read the complete response. Useful for testing an API from outside your network or from a machine where you cannot install tools.', bullets: ['Any method, with custom headers and body', 'Full response: status, headers and body', 'Requests originate from our servers, not your IP'], faq: [['How is this different from the curl converter?', 'This sends a real request and shows the response. The converter turns a curl command into code without sending anything.'], ['Can I test an internal service?', 'No. Requests come from our servers, so the target must be reachable from the public internet.']] },++ httpstatus: { name: 'HTTP Status Codes', title: 'HTTP Status Codes — Full Reference & Online Checker, Free', desc: 'Look up what any HTTP status code means and check the code any URL returns. Complete reference from 100 to 599. Free and instant.', kw: 'http status codes, status code meaning, 404 meaning, 301 vs 302, http response codes list', about: 'A full reference for HTTP status codes with plain-English explanations, plus a live check of what code any URL actually returns.', bullets: ['Every status code with a practical explanation', 'Live check of the code a URL returns', 'Notes on which codes search engines act on'], faq: [['What is the difference between 301 and 302?', '301 is permanent and transfers ranking signals to the new URL. 302 is temporary and tells search engines to keep indexing the original.'], ['Should a missing page return 404 or 410?', '404 means not found and may be retried. 410 means permanently gone and gets pages dropped from an index faster.']] },++ latency: { name: 'Latency Test', title: 'Latency Test — Measure Response Time to Any Host, Free', desc: 'Measure response times to any host from multiple regions and compare latency worldwide. Free online latency and speed check.', kw: 'latency test, response time test, server speed test, ttfb test, network latency check', about: 'Measure how long a host takes to respond from several regions, so you can see where your site feels fast and where it drags.', bullets: ['Response times from multiple global regions', 'Connection versus first-byte timing separated', 'Useful for deciding whether a CDN would help'], faq: [['What is a good time to first byte?', 'Under 200 ms is good, under 500 ms acceptable. Beyond that, visitors notice and search engines record the page as slow.'], ['Why is latency high from one region only?', 'Your server is probably far from it with no edge cache in between. A CDN usually fixes exactly this.']] },++ blacklist: { name: 'Blacklist Checker', title: 'IP & Domain Blacklist Checker — Free RBL Lookup', desc: 'Check whether an IP or domain is listed on major email blacklists and RBLs. Find out why your mail is being rejected. Free and instant.', kw: 'blacklist check, rbl lookup, ip blacklist checker, email blacklist, spam list check', about: 'Query the major real-time blacklists to see whether an address or domain is listed — the usual explanation when mail is being rejected or silently dropped.', bullets: ['Checks many major blacklists in one query', 'Shows which list flagged you and why', 'Includes delisting links where the operator provides them'], faq: [['How do I get removed from a blacklist?', 'Fix the cause first — an open relay, a compromised account or a bad sending pattern — then use the operator\'s delisting form. Requesting removal without fixing the cause gets you relisted.'], ['Does one listing stop all my mail?', 'Not always. Different providers consult different lists, so a single listing often causes partial rather than total failure.']] },++ reputation: { name: 'Domain Reputation Checker', title: 'Domain & IP Reputation Checker — Free Online Trust Score', desc: 'Check the reputation of a domain or IP across blacklists, email authentication and security signals. Free reputation report.', kw: 'domain reputation, ip reputation checker, email reputation, sender score, domain trust check', about: 'A combined view of the signals that decide whether other systems trust you: blacklist status, email authentication records, certificate health and hosting details.', bullets: ['Blacklist status combined with authentication checks', 'SPF, DKIM and DMARC presence in one report', 'Practical notes on what to fix first'], faq: [['What hurts domain reputation most?', 'Sending mail without SPF, DKIM and DMARC, appearing on blacklists, and hosting alongside abusive neighbours on a shared IP.'], ['How long does reputation take to recover?', 'Weeks of consistent, authenticated sending. There is no way to reset it instantly.']] },++ smtp: { name: 'SMTP Server Test', title: 'SMTP Server Test — Check Mail Server Connectivity, Free', desc: 'Test an SMTP server\'s connectivity, banner, STARTTLS support and authentication requirements. Free online mail server check.', kw: 'smtp test, smtp server check, mail server test, starttls check, smtp connectivity', about: 'Connect to a mail server and report what it says: the greeting banner, the extensions it advertises, whether STARTTLS is offered and whether it accepts mail for a domain.', bullets: ['Connection test with the full server greeting', 'STARTTLS and authentication support detection', 'Checks the common submission ports'], faq: [['Which SMTP port should I use?', 'Port 587 with STARTTLS for sending mail from a client, or 465 for implicit TLS. Port 25 is for server-to-server delivery and is blocked by most consumer ISPs.'], ['What does an open relay mean?', 'A server that accepts mail for domains it does not handle. It will be blacklisted quickly, so this test flags it.']] },++ uptime: { name: 'Website Uptime Checker', title: 'Website Uptime Checker — Is a Site Down? Free Online Test', desc: 'Check whether a website is up from multiple regions and see its response time and status code. Free "is it down" checker.', kw: 'uptime checker, is this site down, website down checker, site availability test, server status check', about: 'Check a site from several regions at once to settle the "is it down for everyone or just me" question, with status code and response time for each.', bullets: ['Checks from multiple global regions at once', 'Status code and response time per region', 'Distinguishes a genuine outage from a local problem'], faq: [['The site works for me but not for a friend — what now?', 'Run this check. If some regions fail, it is a routing or DNS propagation issue rather than the server being down.'], ['Does this monitor continuously?', 'No, this is an on-demand check. It tells you the state right now.']] },++ robots: { name: 'Robots.txt Tester', title: 'Robots.txt Tester & Analyzer — Free Online Checker', desc: 'Fetch and analyse any site\'s robots.txt, check which URLs are blocked, and catch rules that hide your site from search engines.', kw: 'robots.txt tester, robots txt checker, crawl rules, disallow checker, seo crawl test', about: 'Read a site\'s robots.txt, see the rules that apply to each crawler, and test whether a specific URL would be allowed or blocked — the file where an accidental line can remove a whole site from search.', bullets: ['Rules grouped by user-agent', 'Test any URL against the live rules', 'Flags the accidental site-wide disallow'], faq: [['Does robots.txt remove a page from Google?', 'No. It stops crawling, not indexing — a blocked URL can still appear without a description. Use a noindex meta tag to remove a page properly.'], ['Should robots.txt list my sitemap?', 'Yes. A Sitemap line is the simplest way to point every crawler at your sitemap without registering anywhere.']] },++ sitemap: { name: 'Sitemap Validator', title: 'Sitemap.xml Validator — Free Online XML Sitemap Checker', desc: 'Validate an XML sitemap, check its structure and URL count, and catch errors that stop search engines indexing your pages. Free.', kw: 'sitemap validator, sitemap.xml checker, xml sitemap test, sitemap errors, seo sitemap check', about: 'Fetch and validate a sitemap against the protocol: structure, URL count, lastmod formatting and index files, so you can be sure crawlers can actually read it.', bullets: ['Structure and schema validation', 'URL count against the 50,000 entry limit', 'Follows sitemap index files to their children'], faq: [['How many URLs can one sitemap hold?', '50,000 URLs or 50 MB uncompressed. Beyond that, split it and reference the parts from a sitemap index.'], ['Does lastmod matter?', 'Yes, when it is accurate. Crawlers use it to prioritise re-crawling, but they learn to ignore it if every URL claims to have changed today.']] },++};++++const CATALOG = new Map();++for (const [sub, rec] of Object.entries(DEV)) CATALOG.set(sub, { ...rec, sub, family: 'dev', hub: sub === 'dev' });++for (const [sub, rec] of Object.entries(NET)) CATALOG.set(sub, { ...rec, sub, family: 'net', hub: sub === 'net' });++++/** SEO record for a subdomain, or null when it is not a catalogued tool. */++function seoFor(sub) { return CATALOG.get(sub) || null; }++++/** Sibling tools in the same family, for the "related tools" block and internal linking. */++function relatedTo(sub, limit = 6) {++ const rec = CATALOG.get(sub);++ if (!rec) return [];++ const words = new Set(String(rec.kw || '').toLowerCase().split(/[,\s]+/).filter(w => w.length > 3));++ const scored = [];++ for (const [otherSub, other] of CATALOG) {++ if (otherSub === sub || other.hub || other.family !== rec.family) continue;++ let score = 0;++ for (const w of String(other.kw || '').toLowerCase().split(/[,\s]+/)) if (words.has(w)) score++;++ scored.push({ sub: otherSub, name: other.name, desc: other.desc, score });++ }++ scored.sort((a, b) => b.score - a.score || a.sub.localeCompare(b.sub));++ return scored.slice(0, limit);++}++++module.exports = { CATALOG, DEV, NET, seoFor, relatedTo, BRAND };nnew file mode 100644@@@ -0,0 +1,171 @@++'use strict';++++// ═══════════════════════════════════════════════════════════════++// Per-subdomain SEO rendering for the shared tool SPAs.++//++// dev.html and net.html are each served for dozens of subdomains. Without this, every one of++// those pages shipped the hub's <title>, the hub's description and a canonical pointing AT the++// hub — telling search engines that sixty-five distinct tools were all the same page, and that++// the real one was somewhere else. Nothing could rank.++//++// This module rewrites the <head> per request and appends a visible, crawlable content block++// (H1, intro, features, FAQ, related tools) so each tool page has genuine text and internal++// links even before the SPA boots. The content is visible rather than hidden: hidden keyword++// text is a penalty risk, and this copy is genuinely useful to a first-time visitor.++// ═══════════════════════════════════════════════════════════════++++const fs = require('fs');++const path = require('path');++const { seoFor, relatedTo, BRAND } = require('./catalog');++++const SITE = 'https://openvibe.tools';++const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));++++// Page HTML is read once and cached; a tiny LRU keeps rendered variants hot.++const _base = new Map();++function baseHtml(file) {++ if (!_base.has(file)) _base.set(file, fs.readFileSync(file, 'utf8'));++ return _base.get(file);++}++const _rendered = new Map();++++function jsonLd(rec, url, related) {++ const app = {++ '@context': 'https://schema.org', '@type': 'SoftwareApplication',++ name: rec.name, url, applicationCategory: 'DeveloperApplication',++ operatingSystem: 'Any (web browser)', description: rec.desc,++ offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },++ isAccessibleForFree: true,++ publisher: { '@type': 'Organization', name: BRAND, url: SITE },++ featureList: rec.bullets || undefined,++ };++ const crumbs = {++ '@context': 'https://schema.org', '@type': 'BreadcrumbList',++ itemListElement: [++ { '@type': 'ListItem', position: 1, name: BRAND, item: SITE },++ { '@type': 'ListItem', position: 2, name: rec.family === 'net' ? 'Network Tools' : 'Developer Tools', item: `https://${rec.family}.openvibe.tools` },++ { '@type': 'ListItem', position: 3, name: rec.name, item: url },++ ],++ };++ const blocks = [app, crumbs];++ if ((rec.faq || []).length) {++ blocks.push({++ '@context': 'https://schema.org', '@type': 'FAQPage',++ mainEntity: rec.faq.map(([q, a]) => ({ '@type': 'Question', name: q, acceptedAnswer: { '@type': 'Answer', text: a } })),++ });++ }++ if (related.length) {++ blocks.push({++ '@context': 'https://schema.org', '@type': 'ItemList',++ name: 'Related tools',++ itemListElement: related.map((r, i) => ({ '@type': 'ListItem', position: i + 1, name: r.name, url: `https://${r.sub}.openvibe.tools` })),++ });++ }++ return blocks.map(b => `<script type="application/ld+json">${JSON.stringify(b).replace(/</g, '\\u003c')}</script>`).join('\n');++}++++/** The visible content block appended under the tool: real copy, real internal links. */++function contentBlock(rec, related) {++ const hubUrl = `https://${rec.family}.openvibe.tools`;++ const hubName = rec.family === 'net' ? 'network tools' : 'developer tools';++ return `++<section class="seo-about" id="about-this-tool">++ <div class="seo-about-inner">++ <h2>About the ${esc(rec.name)}</h2>++ <p>${esc(rec.about)}</p>++ ${(rec.bullets || []).length ? `<ul class="seo-features">${rec.bullets.map(b => `<li>${esc(b)}</li>`).join('')}</ul>` : ''}++ ${(rec.faq || []).length ? `<h2>Frequently asked questions</h2><dl class="seo-faq">${rec.faq.map(([q, a]) => `<dt>${esc(q)}</dt><dd>${esc(a)}</dd>`).join('')}</dl>` : ''}++ ${related.length ? `<h2>Related tools</h2><ul class="seo-related">${related.map(r => `<li><a href="https://${esc(r.sub)}.openvibe.tools"><strong>${esc(r.name)}</strong><span>${esc(r.desc)}</span></a></li>`).join('')}</ul>` : ''}++ <p class="seo-hub"><a href="${hubUrl}">Browse all free ${hubName}</a> · <a href="${SITE}">${BRAND} home</a> · <a href="https://openvibe.live">Live streaming</a></p>++ </div>++</section>++<style>++.seo-about{position:relative;z-index:1;max-width:1100px;margin:48px auto 0;padding:28px 20px 40px;border-top:1px solid var(--brd,rgba(255,255,255,.07));color:var(--tx2,#a0a8c4);font-size:.92rem;line-height:1.65}++.seo-about h2{color:var(--tx,#f1f4fb);font-size:1.05rem;font-weight:700;margin:22px 0 10px}++.seo-about h2:first-child{margin-top:0}++.seo-about p{margin:0 0 12px;max-width:72ch}++.seo-features{margin:0 0 4px;padding-left:20px}.seo-features li{margin:4px 0}++.seo-faq{margin:0}.seo-faq dt{color:var(--tx,#f1f4fb);font-weight:600;margin:12px 0 4px}.seo-faq dd{margin:0 0 8px;max-width:72ch}++.seo-related{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:8px}++.seo-related a{display:block;padding:10px 12px;border-radius:12px;background:var(--bg2,#12131c);border:1px solid var(--brd,rgba(255,255,255,.07));transition:border-color .15s,transform .15s}++.seo-related a:hover{border-color:var(--ac,#a78bfa);transform:translateY(-1px)}++.seo-related strong{display:block;color:var(--tx,#f1f4fb);font-size:.88rem;font-weight:600}++.seo-related span{display:block;font-size:.76rem;color:var(--tx3,#5e6580);margin-top:2px;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}++.seo-hub{margin-top:18px;font-size:.84rem}.seo-hub a{color:var(--ac,#a78bfa)}++</style>`;++}++++/**++ * Render the SPA file for one subdomain with its own metadata and content.++ * Returns the original HTML untouched when the subdomain is not catalogued.++ */++function renderTool(file, sub) {++ const rec = seoFor(sub);++ if (!rec) return baseHtml(file);++ const key = `${file}::${sub}`;++ if (_rendered.has(key)) return _rendered.get(key);++++ const url = `https://${sub}.openvibe.tools`;++ const related = relatedTo(sub, 6);++ const title = `${rec.title} | ${BRAND}`;++ let html = baseHtml(file);++++ // <head>: title, description, canonical, social cards — each replaced rather than appended++ // so we never ship two competing values.++ html = html.replace(/<title>[\s\S]*?<\/title>/i, `<title>${esc(title)}</title>`);++ html = html.replace(/<meta\s+name=["']description["'][^>]*>/i, `<meta name="description" content="${esc(rec.desc)}">`);++ html = html.replace(/<meta\s+property=["']og:title["'][^>]*>/i, `<meta property="og:title" content="${esc(rec.title)}">`);++ html = html.replace(/<meta\s+property=["']og:description["'][^>]*>/i, `<meta property="og:description" content="${esc(rec.desc)}">`);++ html = html.replace(/<meta\s+property=["']og:url["'][^>]*>/i, `<meta property="og:url" content="${url}">`);++ html = html.replace(/<link\s+rel=["']canonical["'][^>]*>/i, `<link rel="canonical" href="${url}">`);++++ const extraHead = [++ `<meta name="keywords" content="${esc(rec.kw || '')}">`,++ '<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">',++ '<meta name="twitter:card" content="summary_large_image">',++ `<meta name="twitter:title" content="${esc(rec.title)}">`,++ `<meta name="twitter:description" content="${esc(rec.desc)}">`,++ `<meta property="og:site_name" content="${BRAND}">`,++ '<meta property="og:locale" content="en_US">',++ jsonLd(rec, url, related),++ ].join('\n');++ html = html.replace(/<\/head>/i, `${extraHead}\n</head>`);++++ // Visible content + internal links, appended before </body> so it renders under the tool.++ html = html.replace(/<\/body>/i, `${contentBlock(rec, related)}\n</body>`);++++ if (_rendered.size > 200) _rendered.clear();++ _rendered.set(key, html);++ return html;++}++++/** Hub pages list every tool, which gives crawlers a clean path into all of them. */++function toolUrls() {++ const { CATALOG } = require('./catalog');++ const urls = [];++ for (const [sub, rec] of CATALOG) {++ urls.push({ loc: `https://${sub}.openvibe.tools/`, priority: rec.hub ? '0.9' : '0.8', changefreq: rec.hub ? 'weekly' : 'monthly' });++ }++ return urls;++}++++/** Full sitemap: the hubs, every tool subdomain, and the satellite apps. */++function buildSitemap(extraUrls = []) {++ const today = new Date().toISOString().slice(0, 10);++ const seen = new Set();++ const all = [++ { loc: `${SITE}/`, priority: '1.0', changefreq: 'weekly' },++ ...toolUrls(),++ ...extraUrls,++ ].filter(u => { if (seen.has(u.loc)) return false; seen.add(u.loc); return true; });++ const body = all.map(u => ` <url>\n <loc>${u.loc}</loc>\n <lastmod>${u.lastmod || today}</lastmod>\n <changefreq>${u.changefreq || 'monthly'}</changefreq>\n <priority>${u.priority || '0.7'}</priority>\n </url>`).join('\n');++ return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${body}\n</urlset>\n`;++}++++/** Satellite apps that live outside the dev/net catalogs but belong in the sitemap. */++const SATELLITES = [++ 'img', 'audio', 'docs', 'food', 'maps', 'yt', 'text', 'logo', 'pastes',++ 'case', 'braille', 'binary', 'fancy', 'json', 'wordcount', 'reverse',++].map(sub => ({ loc: `https://${sub}.openvibe.tools/`, priority: '0.8', changefreq: 'weekly' }));++++module.exports = { renderTool, buildSitemap, SATELLITES, toolUrls, SITE };
@@ -150,33 +150,33 @@ const API='https://openvibe.tools/api/dev',$=s=>document.getElementById(s),
@@ -150,33 +150,33 @@ const API='https://openvibe.tools/api/dev',$=s=>document.getElementById(s),
H=s=>s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
H=s=>s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
const T={
const T={
-dev:{n:'Dev.OpenVibe',i:'fa-code',c:'hub',d:'Developer & SEO Tools Hub',hub:1},
-json:{n:'OpenVibeJSON',i:'fa-code',c:'data',d:'Format, validate & minify JSON',p:'{"name":"openvibe","version":1,"active":true}',m:['Format','Validate','Minify']},
-yaml:{n:'OpenVibeYAML',i:'fa-file-code',c:'data',d:'YAML \u2194 JSON converter',p:'name: openvibe\nversion: 1\nactive: true',m:['YAML\u2192JSON','JSON\u2192YAML']},
-xml:{n:'OpenVibeXML',i:'fa-file-code',c:'data',d:'XML formatter & validator',p:'<root><item id="1"><name>OpenVibe</name></item></root>',m:['Format','Validate','Minify']},
-csv:{n:'OpenVibeCSV',i:'fa-table',c:'data',d:'CSV viewer & JSON converter',p:'name,age,city\nAlice,30,NYC\nBob,25,LA',m:['Table','To JSON']},
-sql:{n:'OpenVibeSQL',i:'fa-database',c:'data',d:'SQL formatter & highlighter',p:'select u.id, u.name, o.total from users u join orders o on u.id = o.user_id where o.total > 100 order by o.total desc limit 10',m:['Format','Uppercase','Compact']},
-markdown:{n:'OpenVibeMarkdown',i:'fa-file-lines',c:'data',d:'Markdown editor with live preview',p:'# Hello World\n\nThis is **bold** and *italic* text.\n\n- Item one\n- Item two\n\n> A blockquote\n\n`inline code`\n\n[Link](https://openvibe.network)',m:['Preview','To HTML']},
-html:{n:'OpenVibeHTML',i:'fa-file-code',c:'data',d:'HTML formatter, minifier & entity encoder',p:'<div><p>Hello <strong>World</strong></p></div>',m:['Format','Minify','Entities','Unescape']},
-base64:{n:'OpenVibeBase64',i:'fa-lock',c:'enc',d:'Base64 encode & decode',p:'Hello, World!',m:['Encode','Decode']},
-url:{n:'OpenVibeURL',i:'fa-link',c:'enc',d:'URL encode, decode & parser',p:'https://openvibe.tools/search?q=hello world&lang=en#results',m:['Encode','Decode','Parse']},
-jwt:{n:'OpenVibeJWT',i:'fa-key',c:'enc',d:'JWT decoder \u2014 header, payload & expiry',p:'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhvYm8iLCJpYXQiOjE1MTYyMzkwMjJ9.4S5J5Z5mSp5F8Kv3K5qFJpQ5Y5jF5p5t5m5k5e5h5d4',m:['Decode']},
-uuid:{n:'OpenVibeUUID',i:'fa-fingerprint',c:'enc',d:'UUID v4 generator & validator',p:'',m:['Generate','Bulk 10','Bulk 100','Validate']},
-hash:{n:'OpenVibeHash',i:'fa-hashtag',c:'enc',d:'SHA-256, SHA-1 & SHA-512 hash generator',p:'Hello, World!',m:['SHA-256','SHA-1','SHA-512']},
-hex:{n:'OpenVibeHex',i:'fa-barcode',c:'enc',d:'Hex encoder / decoder',p:'Hello',m:['To Hex','From Hex']},
-escape:{n:'OpenVibeEscape',i:'fa-shield-halved',c:'enc',d:'HTML, JS & URL escape / unescape',p:'<script>alert("hello")<\/script>',m:['HTML Enc','HTML Dec','JS Esc','JS Unesc','URL Enc','URL Dec']},
-timestamp:{n:'OpenVibeTimestamp',i:'fa-clock',c:'time',d:'Unix timestamp \u2194 date converter',p:'',m:['Now','To Date','To Unix']},
-cron:{n:'OpenVibeCron',i:'fa-calendar-check',c:'time',d:'Cron expression parser & next runs',p:'*/15 9-17 * * 1-5',m:['Parse']},
-beautify:{n:'OpenVibeBeautify',i:'fa-wand-magic-sparkles',c:'qual',d:'Beautify JS, CSS, HTML & JSON',p:'{"a":1,"b":[2,3],"c":{"d":true}}',m:['JSON','HTML','CSS']},
-minify:{n:'OpenVibeMinify',i:'fa-compress',c:'qual',d:'Minify JS, CSS, HTML & JSON',p:'{\n "name": "openvibe",\n "version": 1\n}',m:['JSON','HTML','CSS']},
-diff:{n:'OpenVibeDiff',i:'fa-code-compare',c:'qual',d:'Side-by-side text diff checker',p:'Line one\nLine two\nLine three\nLine four',p2:'Line one\nLine 2 modified\nLine three\nLine five\nLine six',dual:1,m:['Compare']},
-regex:{n:'OpenVibeRegex',i:'fa-magnifying-glass',c:'qual',d:'Regex tester with match highlighting',p:'\\b[A-Z][a-z]+\\b',p2:'Hello World from OpenVibe.Tools today',dual:1,m:['Test']},
-slug:{n:'OpenVibeSlug',i:'fa-link',c:'qual',d:'URL slug generator',p:'Hello World! This is a Test -- 2024',m:['Generate']},
-lorem:{n:'OpenVibeLorem',i:'fa-paragraph',c:'qual',d:'Lorem ipsum generator',p:'',m:['1 Para','3 Para','5 Para','10 Sent','50 Words']},
-curl:{n:'OpenVibeCurl',i:'fa-terminal',c:'http',d:'Parse curl \u2192 fetch, Python, Node.js',p:"curl -X POST https://api.example.com/users -H 'Content-Type: application/json' -H 'Authorization: Bearer tok123' -d '{\"name\":\"openvibe\"}'",m:['To fetch','To Python','To Node','Parsed']},
-webhook:{n:'OpenVibeWebhook',i:'fa-satellite-dish',c:'http',d:'Webhook request inspector bin',p:'',m:['Create Bin']},
-color:{n:'OpenVibeColor',i:'fa-palette',c:'fe',d:'Color picker & HEX/RGB/HSL converter',p:'#a78bfa',m:['Convert']},
-opengraph:{n:'OpenVibeOpenGraph',i:'fa-share-nodes',c:'fe',d:'Open Graph & Twitter Card preview',p:'https://openvibe.live',m:['Preview']}
+dev:{n:'Developer Tools',i:'fa-code',c:'hub',d:'Developer & SEO Tools Hub',hub:1},
+json:{n:'JSON Formatter',i:'fa-code',c:'data',d:'Format, validate & minify JSON',p:'{"name":"openvibe","version":1,"active":true}',m:['Format','Validate','Minify']},
+yaml:{n:'YAML to JSON Converter',i:'fa-file-code',c:'data',d:'YAML \u2194 JSON converter',p:'name: openvibe\nversion: 1\nactive: true',m:['YAML\u2192JSON','JSON\u2192YAML']},
+xml:{n:'XML Formatter',i:'fa-file-code',c:'data',d:'XML formatter & validator',p:'<root><item id="1"><name>OpenVibe</name></item></root>',m:['Format','Validate','Minify']},
+csv:{n:'CSV to JSON Converter',i:'fa-table',c:'data',d:'CSV viewer & JSON converter',p:'name,age,city\nAlice,30,NYC\nBob,25,LA',m:['Table','To JSON']},
+sql:{n:'SQL Formatter',i:'fa-database',c:'data',d:'SQL formatter & highlighter',p:'select u.id, u.name, o.total from users u join orders o on u.id = o.user_id where o.total > 100 order by o.total desc limit 10',m:['Format','Uppercase','Compact']},
+markdown:{n:'Markdown Editor',i:'fa-file-lines',c:'data',d:'Markdown editor with live preview',p:'# Hello World\n\nThis is **bold** and *italic* text.\n\n- Item one\n- Item two\n\n> A blockquote\n\n`inline code`\n\n[Link](https://openvibe.network)',m:['Preview','To HTML']},
+html:{n:'HTML Formatter',i:'fa-file-code',c:'data',d:'HTML formatter, minifier & entity encoder',p:'<div><p>Hello <strong>World</strong></p></div>',m:['Format','Minify','Entities','Unescape']},
+base64:{n:'Base64 Encoder & Decoder',i:'fa-lock',c:'enc',d:'Base64 encode & decode',p:'Hello, World!',m:['Encode','Decode']},
+url:{n:'URL Encoder & Decoder',i:'fa-link',c:'enc',d:'URL encode, decode & parser',p:'https://openvibe.tools/search?q=hello world&lang=en#results',m:['Encode','Decode','Parse']},
+jwt:{n:'JWT Decoder',i:'fa-key',c:'enc',d:'JWT decoder \u2014 header, payload & expiry',p:'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkhvYm8iLCJpYXQiOjE1MTYyMzkwMjJ9.4S5J5Z5mSp5F8Kv3K5qFJpQ5Y5jF5p5t5m5k5e5h5d4',m:['Decode']},
+uuid:{n:'UUID Generator',i:'fa-fingerprint',c:'enc',d:'UUID v4 generator & validator',p:'',m:['Generate','Bulk 10','Bulk 100','Validate']},
+hash:{n:'Hash Generator',i:'fa-hashtag',c:'enc',d:'SHA-256, SHA-1 & SHA-512 hash generator',p:'Hello, World!',m:['SHA-256','SHA-1','SHA-512']},
+hex:{n:'Hex Converter',i:'fa-barcode',c:'enc',d:'Hex encoder / decoder',p:'Hello',m:['To Hex','From Hex']},
+escape:{n:'String Escape Tool',i:'fa-shield-halved',c:'enc',d:'HTML, JS & URL escape / unescape',p:'<script>alert("hello")<\/script>',m:['HTML Enc','HTML Dec','JS Esc','JS Unesc','URL Enc','URL Dec']},
+timestamp:{n:'Unix Timestamp Converter',i:'fa-clock',c:'time',d:'Unix timestamp \u2194 date converter',p:'',m:['Now','To Date','To Unix']},
+cron:{n:'Cron Expression Parser',i:'fa-calendar-check',c:'time',d:'Cron expression parser & next runs',p:'*/15 9-17 * * 1-5',m:['Parse']},
+beautify:{n:'Code Beautifier',i:'fa-wand-magic-sparkles',c:'qual',d:'Beautify JS, CSS, HTML & JSON',p:'{"a":1,"b":[2,3],"c":{"d":true}}',m:['JSON','HTML','CSS']},
+minify:{n:'Code Minifier',i:'fa-compress',c:'qual',d:'Minify JS, CSS, HTML & JSON',p:'{\n "name": "openvibe",\n "version": 1\n}',m:['JSON','HTML','CSS']},
+diff:{n:'Diff Checker',i:'fa-code-compare',c:'qual',d:'Side-by-side text diff checker',p:'Line one\nLine two\nLine three\nLine four',p2:'Line one\nLine 2 modified\nLine three\nLine five\nLine six',dual:1,m:['Compare']},
+regex:{n:'Regex Tester',i:'fa-magnifying-glass',c:'qual',d:'Regex tester with match highlighting',p:'\\b[A-Z][a-z]+\\b',p2:'Hello World from OpenVibe.Tools today',dual:1,m:['Test']},
+slug:{n:'Slug Generator',i:'fa-link',c:'qual',d:'URL slug generator',p:'Hello World! This is a Test -- 2024',m:['Generate']},
+lorem:{n:'Lorem Ipsum Generator',i:'fa-paragraph',c:'qual',d:'Lorem ipsum generator',p:'',m:['1 Para','3 Para','5 Para','10 Sent','50 Words']},
+curl:{n:'Online curl',i:'fa-terminal',c:'http',d:'Parse curl \u2192 fetch, Python, Node.js',p:"curl -X POST https://api.example.com/users -H 'Content-Type: application/json' -H 'Authorization: Bearer tok123' -d '{\"name\":\"openvibe\"}'",m:['To fetch','To Python','To Node','Parsed']},
+webhook:{n:'Webhook Tester',i:'fa-satellite-dish',c:'http',d:'Webhook request inspector bin',p:'',m:['Create Bin']},
+color:{n:'Color Converter',i:'fa-palette',c:'fe',d:'Color picker & HEX/RGB/HSL converter',p:'#a78bfa',m:['Convert']},
+opengraph:{n:'Open Graph Preview',i:'fa-share-nodes',c:'fe',d:'Open Graph & Twitter Card preview',p:'https://openvibe.live',m:['Preview']}
};
};
const CATS={data:{l:'Data & Formats',i:'fa-database'},enc:{l:'Encoding & Crypto',i:'fa-lock'},time:{l:'Time & Scheduling',i:'fa-clock'},qual:{l:'Code Quality',i:'fa-wand-magic-sparkles'},http:{l:'HTTP & API',i:'fa-terminal'},fe:{l:'Frontend & SEO',i:'fa-palette'}};
const CATS={data:{l:'Data & Formats',i:'fa-database'},enc:{l:'Encoding & Crypto',i:'fa-lock'},time:{l:'Time & Scheduling',i:'fa-clock'},qual:{l:'Code Quality',i:'fa-wand-magic-sparkles'},http:{l:'HTTP & API',i:'fa-terminal'},fe:{l:'Frontend & SEO',i:'fa-palette'}};
@@ -208,7 +208,8 @@ function showTool(){
@@ -208,7 +208,8 @@ function showTool(){
const t=T[ACT];if(!t)return showHub();
const t=T[ACT];if(!t)return showHub();
$('hi').innerHTML='<i class="fa-solid '+t.i+'"></i>';
$('hi').innerHTML='<i class="fa-solid '+t.i+'"></i>';
$('ht').textContent=t.n;$('hd').textContent=t.d;
$('ht').textContent=t.n;$('hd').textContent=t.d;
- document.title=t.n+' \u2014 Free Online '+t.d;
+ // Title comes from the server (server/seo) so crawlers and the tab agree — don't clobber it.
+ if(!document.querySelector('link[rel=canonical][href*="'+ACT+'.openvibe"]'))document.title=t.n+' \u2014 '+t.d;
let mh='';
let mh='';
if(t.m)t.m.forEach((m,i)=>{mh+='<button class="mb'+(i===0?' on':'')+'" data-m="'+m+'" onclick="setMode(this)">'+m+'</button>'});
if(t.m)t.m.forEach((m,i)=>{mh+='<button class="mb'+(i===0?' on':'')+'" data-m="'+m+'" onclick="setMode(this)">'+m+'</button>'});
$('modes').innerHTML=mh;MODE=t.m?t.m[0]:'';
$('modes').innerHTML=mh;MODE=t.m?t.m[0]:'';
@@ -651,42 +651,42 @@
@@ -651,42 +651,42 @@
// ── Tool definitions (mirrors server/net/config.js) ──────
// ── Tool definitions (mirrors server/net/config.js) ──────
const TOOLS = {
const TOOLS = {
// Hub
// Hub
- net: { name: 'Net.OpenVibe', icon: 'fa-network-wired', cat: 'hub', endpoint: null, placeholder: 'Enter a domain, IP, or URL…' },
- lookup: { name: 'OpenVibeLookup', icon: 'fa-magnifying-glass', cat: 'hub', endpoint: '/lookup', placeholder: 'Domain, IP, or URL — we\'ll look up everything', btnText: 'Lookup All' },
+ net: { name: 'Network Tools', icon: 'fa-network-wired', cat: 'hub', endpoint: null, placeholder: 'Enter a domain, IP, or URL…' },
+ lookup: { name: 'Domain & IP Lookup', icon: 'fa-magnifying-glass', cat: 'hub', endpoint: '/lookup', placeholder: 'Domain, IP, or URL — we\'ll look up everything', btnText: 'Lookup All' },
// IP / Identity
// IP / Identity
- myip: { name: 'OpenVibeMyIP', icon: 'fa-location-crosshairs', cat: 'ip', endpoint: '/myip', placeholder: null, auto: true },
- ip: { name: 'OpenVibeIP', icon: 'fa-at', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
- ipv4: { name: 'OpenVibeIPv4', icon: 'fa-hashtag', cat: 'ip', endpoint: '/ipv4', placeholder: 'IPv4 address or domain' },
- ipv6: { name: 'OpenVibeIPv6', icon: 'fa-code', cat: 'ip', endpoint: '/ipv6', placeholder: 'IPv6 address or domain' },
- geoip: { name: 'OpenVibeGeoIP', icon: 'fa-earth-americas', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
- hostname: { name: 'OpenVibeHostname', icon: 'fa-server', cat: 'ip', endpoint: '/rdns', placeholder: 'IP address to resolve hostname' },
- isp: { name: 'OpenVibeISP', icon: 'fa-building', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
- asn: { name: 'OpenVibeASN', icon: 'fa-diagram-project', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
- rdns: { name: 'OpenVibeReverseDNS', icon: 'fa-rotate-left', cat: 'ip', endpoint: '/rdns', placeholder: 'IP address for reverse DNS' },
- whois: { name: 'OpenVibeWhois', icon: 'fa-address-book', cat: 'ip', endpoint: '/rdap', placeholder: 'Domain or IP' },
- rdap: { name: 'OpenVibeRDAP', icon: 'fa-id-card', cat: 'ip', endpoint: '/rdap', placeholder: 'Domain or IP for RDAP lookup' },
+ myip: { name: 'What Is My IP', icon: 'fa-location-crosshairs', cat: 'ip', endpoint: '/myip', placeholder: null, auto: true },
+ ip: { name: 'IP Address Lookup', icon: 'fa-at', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
+ ipv4: { name: 'IPv4 Lookup & CIDR Calculator', icon: 'fa-hashtag', cat: 'ip', endpoint: '/ipv4', placeholder: 'IPv4 address or domain' },
+ ipv6: { name: 'IPv6 Lookup', icon: 'fa-code', cat: 'ip', endpoint: '/ipv6', placeholder: 'IPv6 address or domain' },
+ geoip: { name: 'IP Geolocation', icon: 'fa-earth-americas', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
+ hostname: { name: 'Hostname Lookup', icon: 'fa-server', cat: 'ip', endpoint: '/rdns', placeholder: 'IP address to resolve hostname' },
+ isp: { name: 'ISP Lookup', icon: 'fa-building', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
+ asn: { name: 'ASN Lookup', icon: 'fa-diagram-project', cat: 'ip', endpoint: '/ip', placeholder: 'IP address or domain' },
+ rdns: { name: 'Reverse DNS Lookup', icon: 'fa-rotate-left', cat: 'ip', endpoint: '/rdns', placeholder: 'IP address for reverse DNS' },
+ whois: { name: 'Whois Lookup', icon: 'fa-address-book', cat: 'ip', endpoint: '/rdap', placeholder: 'Domain or IP' },
+ rdap: { name: 'RDAP Lookup', icon: 'fa-id-card', cat: 'ip', endpoint: '/rdap', placeholder: 'Domain or IP for RDAP lookup' },
// DNS
// DNS
- dns: { name: 'OpenVibeDNS', icon: 'fa-sitemap', cat: 'dns', endpoint: '/dns', placeholder: 'Domain to query' },
- dig: { name: 'OpenVibeDig', icon: 'fa-terminal', cat: 'dns', endpoint: '/dns', placeholder: 'Domain (dig-style query)', dnsTypes: true },
- nslookup: { name: 'OpenVibeNSLookup', icon: 'fa-magnifying-glass-arrow-right', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for nslookup' },
- dnspropagation: { name: 'OpenVibeDNSPropagation', icon: 'fa-globe', cat: 'dns', endpoint: '/dns', placeholder: 'Domain to check propagation' },
- mx: { name: 'OpenVibeMX', icon: 'fa-envelope', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for MX records', defaultTypes: 'MX' },
- txt: { name: 'OpenVibeTXT', icon: 'fa-file-lines', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for TXT records', defaultTypes: 'TXT' },
- ns: { name: 'OpenVibeNS', icon: 'fa-server', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for NS records', defaultTypes: 'NS' },
- spf: { name: 'OpenVibeSPF', icon: 'fa-shield-halved', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for SPF check', defaultTypes: 'TXT' },
- dkim: { name: 'OpenVibeDKIM', icon: 'fa-key', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for DKIM', defaultTypes: 'TXT' },
- dmarc: { name: 'OpenVibeDMARC', icon: 'fa-user-shield', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for DMARC', defaultTypes: 'TXT' },
+ dns: { name: 'DNS Lookup', icon: 'fa-sitemap', cat: 'dns', endpoint: '/dns', placeholder: 'Domain to query' },
+ dig: { name: 'Online dig', icon: 'fa-terminal', cat: 'dns', endpoint: '/dns', placeholder: 'Domain (dig-style query)', dnsTypes: true },
+ nslookup: { name: 'NSLookup', icon: 'fa-magnifying-glass-arrow-right', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for nslookup' },
+ dnspropagation: { name: 'DNS Propagation Checker', icon: 'fa-globe', cat: 'dns', endpoint: '/dns', placeholder: 'Domain to check propagation' },
+ mx: { name: 'MX Record Lookup', icon: 'fa-envelope', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for MX records', defaultTypes: 'MX' },
+ txt: { name: 'TXT Record Lookup', icon: 'fa-file-lines', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for TXT records', defaultTypes: 'TXT' },
+ ns: { name: 'Nameserver Lookup', icon: 'fa-server', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for NS records', defaultTypes: 'NS' },
+ spf: { name: 'SPF Record Checker', icon: 'fa-shield-halved', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for SPF check', defaultTypes: 'TXT' },
+ dkim: { name: 'DKIM Record Lookup', icon: 'fa-key', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for DKIM', defaultTypes: 'TXT' },
+ dmarc: { name: 'DMARC Checker', icon: 'fa-user-shield', cat: 'dns', endpoint: '/dns', placeholder: 'Domain for DMARC', defaultTypes: 'TXT' },
// Active diagnostics
// Active diagnostics
- ping: { name: 'OpenVibePing', icon: 'fa-satellite-dish', cat: 'diag', endpoint: '/ping', placeholder: 'Host to ping' },
- traceroute: { name: 'OpenVibeTraceroute', icon: 'fa-route', cat: 'diag', endpoint: '/ping', placeholder: 'Host for traceroute' },
- mtr: { name: 'OpenVibeMTR', icon: 'fa-chart-line', cat: 'diag', endpoint: '/ping', placeholder: 'Host for MTR' },
- port: { name: 'OpenVibePortCheck', icon: 'fa-door-open', cat: 'diag', endpoint: '/port', placeholder: 'Host to scan ports' },
- headers: { name: 'OpenVibeHeaders', icon: 'fa-list', cat: 'diag', endpoint: '/headers', placeholder: 'URL or domain' },
- redirects: { name: 'OpenVibeRedirects', icon: 'fa-share', cat: 'diag', endpoint: '/redirects', placeholder: 'URL to follow redirects' },
- ssl: { name: 'OpenVibeSSL', icon: 'fa-lock', cat: 'diag', endpoint: '/ssl', placeholder: 'Domain for SSL check' },
- curl: { name: 'OpenVibeCurl', icon: 'fa-download', cat: 'diag', endpoint: '/headers', placeholder: 'URL' },
- httpstatus: { name: 'OpenVibeHTTPStatus', icon: 'fa-circle-check', cat: 'diag', endpoint: '/headers', placeholder: 'URL to check' },
- latency: { name: 'OpenVibeLatency', icon: 'fa-gauge-high', cat: 'diag', endpoint: '/ping', placeholder: 'Host to measure latency' },
+ ping: { name: 'Ping Test', icon: 'fa-satellite-dish', cat: 'diag', endpoint: '/ping', placeholder: 'Host to ping' },
+ traceroute: { name: 'Traceroute', icon: 'fa-route', cat: 'diag', endpoint: '/ping', placeholder: 'Host for traceroute' },
+ mtr: { name: 'MTR Report', icon: 'fa-chart-line', cat: 'diag', endpoint: '/ping', placeholder: 'Host for MTR' },
+ port: { name: 'Port Checker', icon: 'fa-door-open', cat: 'diag', endpoint: '/port', placeholder: 'Host to scan ports' },
+ headers: { name: 'HTTP Header Checker', icon: 'fa-list', cat: 'diag', endpoint: '/headers', placeholder: 'URL or domain' },
+ redirects: { name: 'Redirect Checker', icon: 'fa-share', cat: 'diag', endpoint: '/redirects', placeholder: 'URL to follow redirects' },
+ ssl: { name: 'SSL Certificate Checker', icon: 'fa-lock', cat: 'diag', endpoint: '/ssl', placeholder: 'Domain for SSL check' },
+ curl: { name: 'Online curl', icon: 'fa-download', cat: 'diag', endpoint: '/headers', placeholder: 'URL' },
+ httpstatus: { name: 'HTTP Status Codes', icon: 'fa-circle-check', cat: 'diag', endpoint: '/headers', placeholder: 'URL to check' },
+ latency: { name: 'Latency Test', icon: 'fa-gauge-high', cat: 'diag', endpoint: '/ping', placeholder: 'Host to measure latency' },
};
};
const CATS = {
const CATS = {
@@ -1,5 +1,9 @@
@@ -1,5 +1,9 @@
'use strict';
'use strict';
+// Tool names and descriptions live in server/seo/catalog.js so the UI, the page <title> and
+// the structured data can never disagree. Entries here keep their id/subdomain/icon/category.
+const { seoFor } = require('../seo/catalog');
+
// ═══════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════
// Dev.OpenVibe — Developer & SEO Tools Configuration
// Dev.OpenVibe — Developer & SEO Tools Configuration
// ═══════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════
@@ -47,6 +51,8 @@ const DEV_TOOLS = [
@@ -47,6 +51,8 @@ const DEV_TOOLS = [
{ id: 'opengraph', subdomain: 'opengraph', name: 'OpenVibeOpenGraph', icon: 'fa-share-nodes', desc: 'Open Graph & Twitter Card preview & validator', category: 'frontend' },
{ id: 'opengraph', subdomain: 'opengraph', name: 'OpenVibeOpenGraph', icon: 'fa-share-nodes', desc: 'Open Graph & Twitter Card preview & validator', category: 'frontend' },
];
];
+for (const t of DEV_TOOLS) { const seo = seoFor(t.subdomain); if (seo) { t.name = seo.name; t.desc = seo.desc; t.seo = seo; } }
+
const DEV_TOOL_MAP = new Map();
const DEV_TOOL_MAP = new Map();
for (const t of DEV_TOOLS) DEV_TOOL_MAP.set(t.subdomain, t);
for (const t of DEV_TOOLS) DEV_TOOL_MAP.set(t.subdomain, t);
@@ -178,12 +178,36 @@ function isStaticPath(reqPath) {
@@ -178,12 +178,36 @@ function isStaticPath(reqPath) {
return /\.(js|css|ico|png|svg|jpg|xml|txt|woff2?)$/.test(reqPath);
return /\.(js|css|ico|png|svg|jpg|xml|txt|woff2?)$/.test(reqPath);
}
}
+// Every tool subdomain gets its OWN title, description, canonical, structured data and a
+// crawlable content block (server/seo). They used to share the hub's <head>, which told search
+// engines that ~65 distinct tools were one page.
+const { renderTool, buildSitemap, SATELLITES } = require('./seo/render');
+const NET_HTML = path.join(__dirname, '..', 'public', 'net.html');
+const DEV_HTML = path.join(__dirname, '..', 'public', 'dev.html');
+function sendTool(req, res, file) {
+ try {
+ res.set('Content-Type', 'text/html; charset=utf-8');
+ return res.send(renderTool(file, subdomainOf(req)));
+ } catch (err) {
+ console.error('[SEO] render failed:', err.message);
+ return res.sendFile(file);
+ }
+}
+
+// Generated sitemap — covers both hubs, every tool subdomain and the satellite apps, so it can
+// never drift from the tool catalogs the way a hand-maintained file does.
+app.get('/sitemap.xml', (_req, res) => {
+ res.set('Content-Type', 'application/xml; charset=utf-8');
+ res.set('Cache-Control', 'public, max-age=3600');
+ res.send(buildSitemap(SATELLITES));
+});
+
// Net tool subdomains → net.html SPA
// Net tool subdomains → net.html SPA
app.use((req, res, next) => {
app.use((req, res, next) => {
if (!isNetHost(req)) return next();
if (!isNetHost(req)) return next();
if (req.path.startsWith('/api/') || req.path.startsWith('/auth/')) return next();
if (req.path.startsWith('/api/') || req.path.startsWith('/auth/')) return next();
if (isStaticPath(req.path)) return next();
if (isStaticPath(req.path)) return next();
- return res.sendFile(path.join(__dirname, '..', 'public', 'net.html'));
+ return sendTool(req, res, NET_HTML);
});
});
// Paste subdomain → paste.html SPA
// Paste subdomain → paste.html SPA
@@ -199,7 +223,7 @@ app.use((req, res, next) => {
@@ -199,7 +223,7 @@ app.use((req, res, next) => {
if (!isDevHost(req)) return next();
if (!isDevHost(req)) return next();
if (req.path.startsWith('/api/') || req.path.startsWith('/auth/')) return next();
if (req.path.startsWith('/api/') || req.path.startsWith('/auth/')) return next();
if (isStaticPath(req.path)) return next();
if (isStaticPath(req.path)) return next();
- return res.sendFile(path.join(__dirname, '..', 'public', 'dev.html'));
+ return sendTool(req, res, DEV_HTML);
});
});
// ── Web-push service worker (same-origin, scope "/") ─────────
// ── Web-push service worker (same-origin, scope "/") ─────────
@@ -1,5 +1,9 @@
@@ -1,5 +1,9 @@
'use strict';
'use strict';
+// Tool names and descriptions live in server/seo/catalog.js so the UI, the page <title> and
+// the structured data can never disagree. Entries here keep their id/subdomain/icon/category.
+const { seoFor } = require('../seo/catalog');
+
// ═══════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════
// Net.OpenVibe — Network Tools Configuration
// Net.OpenVibe — Network Tools Configuration
// API keys, rate limits, probe regions, and tool definitions.
// API keys, rate limits, probe regions, and tool definitions.
@@ -130,4 +134,6 @@ const NET_ALIASES = {
@@ -130,4 +134,6 @@ const NET_ALIASES = {
'ipv6lookup': 'ipv6',
'ipv6lookup': 'ipv6',
};
};
+for (const t of NET_TOOLS) { const seo = seoFor(t.subdomain); if (seo) { t.name = seo.name; t.desc = seo.desc; t.seo = seo; } }
+
module.exports = { getSetting, getNetConfig, NET_TOOLS, NET_TOOL_MAP, NET_ALIASES };
module.exports = { getSetting, getNetConfig, NET_TOOLS, NET_TOOL_MAP, NET_ALIASES };
new file mode 100644
new file mode 100644
@@ -0,0 +1,316 @@
@@ -0,0 +1,316 @@
+'use strict';
+
+// ═══════════════════════════════════════════════════════════════
+// SEO catalog — one record per tool subdomain.
+//
+// Every dev.* and net.* tool used to share a single SPA file, which meant ~65 subdomains
+// all served the same <title>, the same description and a canonical pointing at the hub.
+// Search engines saw one page, not sixty-five. This catalog gives each tool its own
+// search-friendly identity, and seo/render.js stamps it into the HTML per request.
+//
+// name what the tool is called everywhere (no "OpenVibe" prefix — people search
+// for "json formatter", not for our brand)
+// title <title> — primary keyword first, qualifier second, brand last
+// desc <meta name="description"> — one sentence, ~150 chars, says what you get
+// kw comma-separated keywords (also used to pick related tools)
+// about visible intro paragraph under the tool (real content, not hidden text)
+// bullets visible feature list
+// faq visible Q&A + FAQPage structured data
+// ═══════════════════════════════════════════════════════════════
+
+const BRAND = 'OpenVibe';
+
+// ── Dev & SEO tools (dev.openvibe.tools) ─────────────────────
+const DEV = {
+ dev: {
+ name: 'Developer Tools', title: 'Free Online Developer Tools — Formatters, Encoders & Validators',
+ desc: 'A fast, free developer toolbox: JSON, YAML and XML formatters, Base64 and JWT decoders, regex tester, diff checker, hash generator and more. No signup, nothing leaves your browser.',
+ kw: 'developer tools, online tools, free dev tools, formatter, encoder, decoder, validator',
+ about: 'A collection of small, fast developer utilities that run entirely in your browser. Nothing is uploaded, nothing is logged, and there is no account to create — open a tool, paste your data, get your answer.',
+ bullets: ['Every tool works client-side: your data never leaves the browser', 'No signup, no ads, no rate limits on the offline tools', 'Open source and part of the wider OpenVibe network'],
+ faq: [['Are these developer tools really free?', 'Yes. Every tool here is free with no account, no trial and no usage cap. The project is community-run and open source.'], ['Is my data sent to a server?', 'The formatting, encoding and generation tools run entirely in your browser, so your data never leaves your machine. Only tools that must reach the network (like the Open Graph preview and webhook inspector) send a request, and they say so on the page.']],
+ },
+ json: {
+ name: 'JSON Formatter', title: 'JSON Formatter & Validator — Free Online JSON Beautifier',
+ desc: 'Format, validate and minify JSON online. Pretty-print messy JSON, catch syntax errors with line numbers, and compact it back down. Free, instant, runs in your browser.',
+ kw: 'json formatter, json validator, json beautifier, json minifier, pretty print json, json parser online',
+ about: 'Paste any JSON to pretty-print it with consistent indentation, validate it against the JSON spec, or minify it for production. Errors are reported with the exact position so you can find the stray comma fast.',
+ bullets: ['Beautify with clean indentation, or minify to the smallest valid output', 'Validation errors point at the line and character that broke the parse', 'Handles large documents without uploading them anywhere'],
+ faq: [['How do I fix "Unexpected token" errors in JSON?', 'Paste the JSON here and press Validate. The error message names the exact character position, which is almost always a trailing comma, a single quote instead of a double quote, or an unquoted key.'], ['Does this JSON formatter upload my data?', 'No. Parsing and formatting happen in your browser, so the JSON never reaches our servers.']],
+ },
+ yaml: {
+ name: 'YAML to JSON Converter', title: 'YAML to JSON Converter & YAML Validator — Free Online',
+ desc: 'Convert YAML to JSON and JSON to YAML online, and validate YAML syntax instantly. Ideal for Kubernetes manifests, CI pipelines and config files. Free, no signup.',
+ kw: 'yaml to json, json to yaml, yaml converter, yaml validator, yaml parser online, kubernetes yaml',
+ about: 'Convert between YAML and JSON in either direction and check that your YAML actually parses. Useful whenever you are moving between a config file and an API payload, or debugging an indentation mistake in a CI or Kubernetes file.',
+ bullets: ['YAML → JSON and JSON → YAML with one click', 'Catches the indentation and tab errors that break YAML parsers', 'Keeps nested structures, lists and multi-line strings intact'],
+ faq: [['Why does my YAML fail to parse?', 'YAML forbids tab characters for indentation and is strict about consistent spacing. Paste it here and the parser reports the line where the structure breaks.'], ['Can I convert Kubernetes manifests to JSON?', 'Yes. Kubernetes accepts both formats, and converting to JSON is a quick way to verify exactly what a manifest resolves to.']],
+ },
+ xml: {
+ name: 'XML Formatter', title: 'XML Formatter & Validator — Free Online XML Beautifier',
+ desc: 'Format, validate and minify XML online. Pretty-print nested XML, check that tags are well-formed, and compact documents. Free and instant in your browser.',
+ kw: 'xml formatter, xml validator, xml beautifier, pretty print xml, xml parser online, format xml',
+ about: 'Pretty-print XML with proper nesting, confirm a document is well-formed, or strip whitespace to minify it. Handy for SOAP payloads, RSS feeds, sitemaps and configuration files.',
+ bullets: ['Readable indentation for deeply nested documents', 'Well-formedness checks with the position of the offending tag', 'Minify mode strips insignificant whitespace'],
+ faq: [['What does "not well-formed" mean?', 'It means the document breaks a structural rule: an unclosed tag, mismatched case, or a stray angle bracket. The validator names the position so you can fix it.'], ['Can I format an RSS feed or sitemap with this?', 'Yes. Both are XML, so paste the feed or sitemap and it formats like any other document.']],
+ },
+ csv: {
+ name: 'CSV to JSON Converter', title: 'CSV to JSON Converter & CSV Viewer — Free Online',
+ desc: 'Convert CSV to JSON and view CSV as a sortable table online. Handles quoted fields, commas inside values and custom headers. Free, instant, no upload.',
+ kw: 'csv to json, csv viewer, csv converter, csv parser online, convert csv, csv table viewer',
+ about: 'Paste CSV to see it as a clean table or convert it straight to a JSON array of objects using the first row as keys. Quoted fields and embedded commas are parsed correctly.',
+ bullets: ['Table view for scanning a file quickly', 'CSV → JSON using the header row as object keys', 'Correctly handles quoted values containing commas and newlines'],
+ faq: [['How does the converter handle commas inside a field?', 'Fields wrapped in double quotes are parsed as single values, so embedded commas and line breaks survive the conversion.'], ['Can I convert a spreadsheet export?', 'Yes. Export the sheet as CSV, paste it here, and convert to JSON or read it as a table.']],
+ },
+ sql: {
+ name: 'SQL Formatter', title: 'SQL Formatter & Beautifier — Free Online Query Formatter',
+ desc: 'Format and beautify SQL queries online. Turn one-line queries into readable, indented SQL with keyword highlighting. Free, works with any SQL dialect.',
+ kw: 'sql formatter, sql beautifier, format sql online, sql pretty print, query formatter',
+ about: 'Turn a long single-line query into properly indented SQL with keywords aligned, so joins and nested conditions are readable in a code review or a bug report.',
+ bullets: ['Readable indentation for joins, subqueries and CTEs', 'Uppercase keyword mode for a consistent house style', 'Compact mode to put a query back on one line'],
+ faq: [['Which SQL dialects are supported?', 'The formatter works on standard SQL syntax, which covers the vast majority of PostgreSQL, MySQL, SQLite and SQL Server queries.'], ['Is my query sent anywhere?', 'No. Formatting happens in your browser, so queries containing table or column names never leave your machine.']],
+ },
+ markdown: {
+ name: 'Markdown Editor', title: 'Markdown Editor with Live Preview — Free Online Markdown to HTML',
+ desc: 'Write Markdown and see the rendered HTML live. Convert Markdown to HTML, preview README files and copy the output. Free, no signup, runs in your browser.',
+ kw: 'markdown editor, markdown to html, markdown preview, online markdown, readme preview',
+ about: 'Type Markdown on one side and watch the rendered result on the other. Useful for drafting a README, checking how a comment will render, or converting Markdown into HTML you can paste elsewhere.',
+ bullets: ['Live side-by-side preview as you type', 'Headings, lists, tables, code blocks and links', 'Copy the generated HTML in one click'],
+ faq: [['Does it support GitHub-flavoured Markdown?', 'The common extensions are covered: tables, fenced code blocks, task lists and strikethrough.'], ['Can I use this to preview a README before pushing?', 'Yes. Paste the file contents and the preview shows how the structure will render.']],
+ },
+ html: {
+ name: 'HTML Formatter', title: 'HTML Formatter, Minifier & Entity Encoder — Free Online',
+ desc: 'Format, minify and escape HTML online. Beautify messy markup, compress it for production, or encode HTML entities safely. Free and instant.',
+ kw: 'html formatter, html minifier, html beautifier, html entity encoder, format html online',
+ about: 'Clean up unreadable markup, shrink it for production, or convert characters to HTML entities so user content can be displayed without breaking the page.',
+ bullets: ['Beautify with consistent tag indentation', 'Minify by stripping comments and redundant whitespace', 'Entity encoding for safely displaying markup as text'],
+ faq: [['When should I encode HTML entities?', 'Any time you display text that a user supplied. Encoding turns characters like < and > into entities so the browser renders them as text instead of executing them as markup.'], ['Does minifying HTML break my page?', 'The minifier only removes whitespace and comments that do not affect rendering. Content inside pre and textarea elements is preserved.']],
+ },
+ base64: {
+ name: 'Base64 Encoder & Decoder', title: 'Base64 Encode & Decode Online — Free Base64 Converter',
+ desc: 'Encode text to Base64 and decode Base64 back to text online. UTF-8 safe, handles data URIs, and runs entirely in your browser. Free, no signup.',
+ kw: 'base64 encode, base64 decode, base64 converter, base64 online, decode base64 string',
+ about: 'Convert text to Base64 and back. Useful for reading an encoded token, building a data URI, or inspecting a value copied out of a config file or HTTP header.',
+ bullets: ['Encode and decode with full UTF-8 support', 'Decodes padded and unpadded input', 'Everything runs locally — safe for tokens and credentials'],
+ faq: [['Is Base64 encryption?', 'No. Base64 is an encoding, not encryption — anyone can decode it. It exists to move binary data safely through text-only channels.'], ['Is it safe to decode a token here?', 'Yes. Decoding happens in your browser and nothing is transmitted or stored.']],
+ },
+ url: {
+ name: 'URL Encoder & Decoder', title: 'URL Encode & Decode Online — Free Percent-Encoding Tool',
+ desc: 'Encode and decode URLs online, and break any URL into its parts. Handles percent-encoding, query strings and parameters. Free and instant.',
+ kw: 'url encode, url decode, percent encoding, url parser, query string parser, urlencode online',
+ about: 'Percent-encode a string for safe use in a URL, decode one you have been handed, or split a full URL into protocol, host, path, query parameters and fragment.',
+ bullets: ['Encode and decode with correct percent-encoding rules', 'Parse a URL into its components and query parameters', 'Useful for debugging redirects, callbacks and tracking links'],
+ faq: [['What is the difference between encodeURI and encodeURIComponent?', 'encodeURI leaves characters that have meaning in a URL (like / and ?) alone, while encodeURIComponent encodes them too. Use the component version for individual query values.'], ['Why does my query parameter break?', 'Usually an unencoded ampersand or equals sign inside a value. Encoding the value fixes it.']],
+ },
+ jwt: {
+ name: 'JWT Decoder', title: 'JWT Decoder — Decode JSON Web Tokens Online, Free',
+ desc: 'Decode a JWT to inspect its header, payload and expiry. See claims, issued-at and expiration times in readable form. Runs in your browser — tokens are never sent.',
+ kw: 'jwt decoder, decode jwt, json web token decoder, jwt parser, jwt payload viewer',
+ about: 'Paste a JSON Web Token to read its header and payload, with timestamps rendered as dates and the expiry checked against the current time. Decoding happens locally, so tokens stay on your machine.',
+ bullets: ['Header, payload and signature split out clearly', 'Expiry and issued-at shown as human-readable dates', 'Nothing is transmitted — safe for real access tokens'],
+ faq: [['Does decoding a JWT verify its signature?', 'No. Decoding only reads the token contents. Verifying the signature needs the issuer\'s secret or public key, which should only ever happen on your server.'], ['Is it safe to paste a live token here?', 'Yes. The token is parsed in your browser and never sent over the network.']],
+ },
+ uuid: {
+ name: 'UUID Generator', title: 'UUID Generator — Free Online UUID v4 Generator & Validator',
+ desc: 'Generate random UUID v4 identifiers, create them in bulk, and validate existing UUIDs. Cryptographically random, instant, free, no signup.',
+ kw: 'uuid generator, guid generator, uuid v4, random uuid, uuid validator, generate uuid online',
+ about: 'Generate version 4 UUIDs using your browser\'s cryptographic random source, produce a batch for seeding a database, or check whether a string is a valid UUID.',
+ bullets: ['Cryptographically random UUID v4 values', 'Bulk generation for test data and migrations', 'Validation for UUIDs of any version'],
+ faq: [['Are these UUIDs safe to use in production?', 'Yes. They come from the browser\'s crypto random generator, the same source server-side libraries use.'], ['What is the difference between a UUID and a GUID?', 'They are the same thing. GUID is Microsoft\'s name for the same 128-bit identifier format.']],
+ },
+ hash: {
+ name: 'Hash Generator', title: 'SHA-256 Hash Generator — Free Online SHA-1, SHA-256 & SHA-512',
+ desc: 'Generate SHA-1, SHA-256 and SHA-512 hashes from any text online. Useful for checksums, fingerprints and integrity checks. Free and computed in your browser.',
+ kw: 'sha256 generator, hash generator, sha1 hash, sha512, checksum generator, hash text online',
+ about: 'Produce a cryptographic hash of any text using SHA-1, SHA-256 or SHA-512. Handy for verifying a checksum, generating a fingerprint, or comparing two values without storing the originals.',
+ bullets: ['SHA-1, SHA-256 and SHA-512 from the same input', 'Uses the browser\'s native Web Crypto implementation', 'Nothing is uploaded — safe for sensitive strings'],
+ faq: [['Can a hash be reversed?', 'No. Hashes are one-way. Identical input always produces identical output, but you cannot recover the input from the hash.'], ['Which algorithm should I use?', 'SHA-256 for almost everything. SHA-1 is only useful for compatibility with older systems, as it is no longer considered collision-resistant.']],
+ },
+ hex: {
+ name: 'Hex Converter', title: 'Hex to Text Converter — Free Online Hex, Binary & ASCII Tool',
+ desc: 'Convert text to hexadecimal, decode hex back to text, and switch between hex, binary and decimal. Free, instant, runs in your browser.',
+ kw: 'hex to text, text to hex, hex converter, hex decoder, binary converter, ascii to hex',
+ about: 'Convert between text, hexadecimal and binary. Useful when reading a memory dump, decoding a protocol payload, or converting a byte sequence you found in a log.',
+ bullets: ['Text ⇄ hex with UTF-8 handling', 'Hex ⇄ binary and decimal conversion', 'Accepts spaced, prefixed or continuous hex input'],
+ faq: [['What do the 0x prefixes mean?', 'They mark a number as hexadecimal in most programming languages. The converter accepts input with or without them.'], ['Why do some characters become several hex bytes?', 'Non-ASCII characters take more than one byte in UTF-8, so an emoji or accented letter maps to several hex pairs.']],
+ },
+ escape: {
+ name: 'String Escape Tool', title: 'HTML, JavaScript & URL Escape Tool — Free Online Escaper',
+ desc: 'Escape and unescape strings for HTML, JavaScript and URLs. Prevent injection bugs and safely embed text in code. Free and instant.',
+ kw: 'escape html, unescape string, javascript escape, escape characters online, string escaper',
+ about: 'Escape a string so it can be safely embedded in HTML, in a JavaScript literal, or in a URL — and reverse the process when you need the original text back.',
+ bullets: ['HTML entity escaping for safe display of user content', 'JavaScript string escaping including quotes and newlines', 'URL escaping for query values and path segments'],
+ faq: [['Why does escaping matter for security?', 'Unescaped user input is how cross-site scripting happens. Escaping ensures text is displayed as text rather than executed as markup or code.'], ['Which mode should I use?', 'Match the destination: HTML escaping for page content, JavaScript escaping for code literals, URL escaping for anything going into a link.']],
+ },
+ timestamp: {
+ name: 'Unix Timestamp Converter', title: 'Unix Timestamp Converter — Epoch to Date, Free Online',
+ desc: 'Convert Unix timestamps to human-readable dates and back, with timezone support. Handles seconds and milliseconds. Free, instant, no signup.',
+ kw: 'unix timestamp converter, epoch converter, timestamp to date, date to timestamp, epoch time',
+ about: 'Turn an epoch timestamp into a readable date in any timezone, or convert a date back into a timestamp. Both seconds and milliseconds are detected automatically.',
+ bullets: ['Epoch ⇄ date in both directions', 'Automatic detection of seconds vs milliseconds', 'Timezone-aware output including UTC and local time'],
+ faq: [['Is my timestamp in seconds or milliseconds?', 'Ten digits is seconds, thirteen is milliseconds. The converter detects which you pasted and labels the result.'], ['What is the Unix epoch?', 'Midnight UTC on 1 January 1970. A Unix timestamp counts the seconds elapsed since that moment.']],
+ },
+ cron: {
+ name: 'Cron Expression Parser', title: 'Cron Expression Parser — Explain Cron Schedules Online, Free',
+ desc: 'Decode any cron expression into plain English and see the next run times. Validate crontab syntax before you deploy it. Free and instant.',
+ kw: 'cron expression parser, crontab generator, cron schedule explained, next run time, cron validator',
+ about: 'Paste a cron expression to read it in plain English and see exactly when it will fire next. Saves guessing whether a schedule means every five minutes or five past the hour.',
+ bullets: ['Plain-English description of any cron expression', 'A list of upcoming run times so you can sanity-check the schedule', 'Validates the syntax before you commit it to a crontab'],
+ faq: [['What does */5 * * * * mean?', 'Every five minutes, on every hour, day and month. Paste any expression here for a written explanation and the next run times.'], ['Why did my cron job not run?', 'Most often the field order is wrong (minute, hour, day of month, month, day of week) or the day-of-week and day-of-month fields conflict. The parser shows the interpreted schedule so you can spot it.']],
+ },
+ beautify: {
+ name: 'Code Beautifier', title: 'Code Beautifier — Free Online JS, CSS, HTML, JSON & SQL Formatter',
+ desc: 'Beautify JavaScript, CSS, HTML, JSON and SQL online. Turn minified or messy code into clean, indented, readable source. Free, instant, no signup.',
+ kw: 'code beautifier, javascript formatter, css formatter, unminify, code formatter online, prettify code',
+ about: 'Paste minified or badly formatted code and get readable, consistently indented source back. Works across JavaScript, CSS, HTML, JSON and SQL, which makes it useful for reading third-party bundles.',
+ bullets: ['One tool for JS, CSS, HTML, JSON and SQL', 'Consistent indentation and line breaks', 'Great for reading minified production code'],
+ faq: [['Can this recover minified code?', 'It restores structure and indentation so the code is readable, but names that a minifier shortened cannot be recovered.'], ['Does beautifying change what the code does?', 'No. Only whitespace and line breaks change; the code is functionally identical.']],
+ },
+ minify: {
+ name: 'Code Minifier', title: 'Code Minifier — Free Online JS, CSS, HTML & JSON Minifier',
+ desc: 'Minify JavaScript, CSS, HTML and JSON online to cut file size and speed up your site. Free, instant, runs entirely in your browser.',
+ kw: 'minify javascript, css minifier, html minifier, json minifier, compress code online',
+ about: 'Strip comments and unnecessary whitespace to shrink files before shipping them. Smaller payloads mean faster page loads, which is one of the ranking signals search engines actually measure.',
+ bullets: ['JavaScript, CSS, HTML and JSON in one place', 'Shows the size saved by the minification', 'Runs locally — no upload, no build step required'],
+ faq: [['How much smaller will my file get?', 'Typically 20–60% before compression, depending on how much whitespace and commenting the source contains. The tool reports the exact saving.'], ['Should I minify if my server uses gzip?', 'Yes. Minification and gzip compound: minifying first removes content gzip would otherwise have to encode.']],
+ },
+ diff: {
+ name: 'Diff Checker', title: 'Diff Checker — Compare Text & Code Online, Free',
+ desc: 'Compare two blocks of text or code side by side and see every addition, deletion and change highlighted. Free, instant, private to your browser.',
+ kw: 'diff checker, compare text, text comparison, code diff, file compare online, difference checker',
+ about: 'Paste two versions of a file, a config or a paragraph and see exactly what changed, line by line, with additions and removals highlighted.',
+ bullets: ['Side-by-side comparison with highlighted changes', 'Works on prose, code, config files and logs', 'Nothing is uploaded, so it is safe for private files'],
+ faq: [['Can I compare two config files?', 'Yes. Paste each version into one side; the differences are highlighted line by line.'], ['Is there a size limit?', 'The comparison runs in your browser, so the practical limit is your machine\'s memory rather than an upload cap.']],
+ },
+ regex: {
+ name: 'Regex Tester', title: 'Regex Tester — Test Regular Expressions Online, Free',
+ desc: 'Test regular expressions against sample text with live match highlighting and capture groups. Debug patterns before shipping them. Free and instant.',
+ kw: 'regex tester, regular expression tester, regex online, test regex, regex match, regex debugger',
+ about: 'Write a pattern, paste sample text, and watch matches highlight as you type. Capture groups are listed separately so you can confirm a pattern extracts what you expect before it reaches production.',
+ bullets: ['Live highlighting of every match as you type', 'Named and numbered capture groups listed per match', 'Flag toggles for global, case-insensitive and multiline'],
+ faq: [['Which regex flavour does this use?', 'JavaScript regular expressions, which are close to PCRE for everyday patterns. Lookbehind and named groups are supported in modern browsers.'], ['Why does my pattern match nothing?', 'Common causes are an unescaped special character, a missing global flag when you expect several matches, or anchors that assume single-line input.']],
+ },
+ slug: {
+ name: 'Slug Generator', title: 'URL Slug Generator — Free SEO-Friendly Slug Maker',
+ desc: 'Turn any title into a clean, SEO-friendly URL slug. Removes accents and punctuation, lowercases and hyphenates. Free and instant.',
+ kw: 'slug generator, url slug, seo friendly url, permalink generator, slugify text online',
+ about: 'Convert a headline into a tidy URL slug: lowercase, hyphen-separated, accents folded and punctuation removed. Readable slugs help both people and search engines understand a page before they open it.',
+ bullets: ['Accent folding so international titles produce clean ASCII slugs', 'Punctuation and stop-character stripping', 'Consistent hyphenation with no double separators'],
+ faq: [['Do slugs affect SEO?', 'A short, descriptive slug containing your main keyword helps search engines and gives people a readable link, which improves click-through from search results.'], ['Should slugs contain dates or IDs?', 'Usually not. Keep them short and descriptive; dates age the URL and IDs mean nothing to a reader.']],
+ },
+ lorem: {
+ name: 'Lorem Ipsum Generator', title: 'Lorem Ipsum Generator — Free Placeholder Text Generator',
+ desc: 'Generate lorem ipsum placeholder text by words, sentences or paragraphs. Perfect for mockups, layouts and design drafts. Free, instant, no signup.',
+ kw: 'lorem ipsum generator, placeholder text, dummy text generator, filler text, lipsum',
+ about: 'Produce as much placeholder copy as your mockup needs, measured in words, sentences or paragraphs, so a layout can be reviewed without waiting on the real content.',
+ bullets: ['Generate by word, sentence or paragraph count', 'Classic lorem ipsum text for familiar typographic colour', 'Copy straight into a design tool or HTML'],
+ faq: [['Why use lorem ipsum instead of real text?', 'It has roughly the same letter distribution as natural prose, so a layout reads realistically without reviewers getting distracted by the wording.'], ['Can I generate a specific number of words?', 'Yes. Choose the unit and the amount, and the generator produces exactly that much text.']],
+ },
+ curl: {
+ name: 'curl Converter', title: 'curl Converter — Convert curl to fetch, Python & Node.js, Free',
+ desc: 'Paste a curl command and convert it to JavaScript fetch, Python requests or Node.js code. Great for turning browser "Copy as cURL" into working code.',
+ kw: 'curl to fetch, curl converter, curl to python, curl to javascript, convert curl command',
+ about: 'Take a curl command — including one copied straight from your browser\'s network tab — and turn it into equivalent code, with headers, method and body carried across.',
+ bullets: ['Converts to fetch, Python requests and Node.js', 'Preserves headers, method, body and authentication', 'Works with "Copy as cURL" output from browser devtools'],
+ faq: [['Where do I get a curl command to convert?', 'In any browser\'s devtools, right-click a request in the Network tab and choose "Copy as cURL". Paste it here to get code.'], ['Are my headers sent anywhere?', 'No. The conversion is pure text processing in your browser, so authorization headers stay local.']],
+ },
+ webhook: {
+ name: 'Webhook Tester', title: 'Webhook Tester — Free Online Request Bin & Inspector',
+ desc: 'Get a temporary webhook URL and inspect every request it receives: headers, body, method and query. Debug integrations without deploying anything.',
+ kw: 'webhook tester, request bin, webhook debugger, http request inspector, test webhook online',
+ about: 'Create a throwaway endpoint, point your integration at it, and read exactly what arrives — method, headers, query string and body — so you can confirm a provider is sending what its docs claim.',
+ bullets: ['Instant URL with no account required', 'Full request detail: headers, body, method and query', 'Bins expire automatically so nothing lingers'],
+ faq: [['How long does a webhook bin last?', 'Bins expire after about an hour and hold a capped number of recent requests, which is plenty for debugging an integration.'], ['Can I use this for production traffic?', 'No. It is a debugging aid — bins are temporary and publicly addressable by anyone with the URL.']],
+ },
+ color: {
+ name: 'Color Converter', title: 'Color Picker & HEX to RGB Converter — Free Online Color Tool',
+ desc: 'Pick colors and convert between HEX, RGB and HSL, then generate matching palettes. Free, instant, perfect for CSS and design work.',
+ kw: 'hex to rgb, color converter, color picker, rgb to hex, hsl converter, color palette generator',
+ about: 'Convert a color between HEX, RGB and HSL notation, pick one visually, and generate a palette of related shades for a design system or a CSS variable set.',
+ bullets: ['HEX, RGB and HSL conversion in every direction', 'Visual picker with live preview', 'Palette generation for tints, shades and complements'],
+ faq: [['What is the difference between HEX and RGB?', 'They describe the same color in different notation — HEX is a compact hexadecimal form of the same red, green and blue values. HSL instead describes hue, saturation and lightness, which is easier to adjust by hand.'], ['How do I make a lighter version of a brand color?', 'Convert it to HSL and raise the lightness value. The palette generator does this for you.']],
+ },
+ opengraph: {
+ name: 'Open Graph Preview', title: 'Open Graph Preview & Meta Tag Checker — Free Social Card Tester',
+ desc: 'Preview how any URL appears when shared on social media and validate its Open Graph and Twitter Card tags. Spot missing images and titles before you post.',
+ kw: 'open graph preview, og tag checker, twitter card validator, social media preview, meta tag checker',
+ about: 'Enter a URL to fetch its Open Graph and Twitter Card tags and see the link preview a social platform would build from them, along with a list of any tags that are missing or truncated.',
+ bullets: ['Live preview of the social card for any public URL', 'Lists Open Graph, Twitter Card and standard meta tags', 'Flags missing titles, descriptions and images'],
+ faq: [['Why does my link preview show no image?', 'Usually a missing og:image tag, or an image URL that is relative rather than absolute. Social platforms need a full https URL.'], ['Why has my preview not updated after I fixed the tags?', 'Platforms cache previews aggressively. Most offer a debugger that forces a re-scrape; this tool always fetches live so you can confirm the tags themselves are correct.']],
+ },
+};
+
+// ── Network & diagnostics tools (net.openvibe.tools) ─────────
+const NET = {
+ net: {
+ name: 'Network Tools', title: 'Free Online Network Tools — Ping, DNS, Whois & SSL Diagnostics',
+ desc: 'Run ping, traceroute, DNS lookups, Whois, SSL checks, port scans and HTTP header tests from the browser. Free network diagnostics with no signup.',
+ kw: 'network tools, online network diagnostics, ping test, dns lookup, whois, ssl checker',
+ about: 'A set of network diagnostics that run from our servers so you can test a host from outside your own connection — useful when you need to know whether a problem is yours or theirs.',
+ bullets: ['Tests run from our infrastructure, not your browser', 'Covers DNS, email deliverability, HTTP, TLS and routing', 'Free with no account required'],
+ faq: [['Why test from a server instead of my machine?', 'Testing from outside your network rules out your own DNS cache, firewall or ISP, which is exactly what you need when a site works for you but not for someone else.'], ['Are these tools free?', 'Yes, with reasonable rate limits to keep them available for everyone.']],
+ },
+ lookup: { name: 'Domain & IP Lookup', title: 'Domain & IP Lookup — All-in-One Free Online Lookup Tool', desc: 'Look up everything about a domain or IP in one go: DNS records, Whois, geolocation, SSL and HTTP headers. Free, instant, no signup.', kw: 'domain lookup, ip lookup, whois lookup, dns lookup, all in one lookup tool', about: 'Enter a domain or IP address and get a complete report in one pass: registration details, DNS records, hosting location, certificate status and HTTP response headers.', bullets: ['One query returns DNS, Whois, geolocation and TLS detail', 'Works with domains, IPv4 and IPv6 addresses', 'A fast first step when diagnosing any site problem'], faq: [['What can I learn from a domain lookup?', 'Who registered the domain and when it expires, which nameservers and mail servers it uses, where it is hosted, and whether its certificate is valid.'], ['Does this work for IP addresses too?', 'Yes. Enter an IP to see its owner, network range, geolocation and reverse DNS name.']] },
+ myip: { name: 'What Is My IP', title: 'What Is My IP Address — Free IP, ISP & Location Checker', desc: 'See your public IP address, ISP, approximate location and reverse DNS instantly. Works for both IPv4 and IPv6. Free, no signup.', kw: 'what is my ip, my ip address, public ip, ip location, my isp', about: 'Shows the public IP address the internet sees for your connection, along with the network operator, approximate location and reverse DNS name.', bullets: ['Detects both your IPv4 and IPv6 address', 'ISP, autonomous system and approximate location', 'Confirms at a glance whether a VPN is actually active'], faq: [['Why does my IP differ from the one in my router?', 'Your router shows a private address used inside your network. The public address shown here is what websites actually see.'], ['How do I check whether my VPN is working?', 'Load this page with the VPN off, then on. If the address and location change, traffic is going through the VPN.']] },
+ ip: { name: 'IP Address Lookup', title: 'IP Address Lookup — Free IPv4 & IPv6 Information Tool', desc: 'Look up any IP address to see its owner, network, geolocation, reverse DNS and abuse contact. Supports IPv4 and IPv6. Free and instant.', kw: 'ip address lookup, ip checker, ip information, ip details, ip owner lookup', about: 'Enter any IP address to find out which organisation announces it, which network block it belongs to, roughly where it is, and what its reverse DNS name says.', bullets: ['Owner, network range and autonomous system', 'Geolocation with country, region and city', 'Reverse DNS and abuse contact where published'], faq: [['Can an IP address identify a person?', 'No. It identifies a network connection and usually resolves only to a city-level area and an internet provider.'], ['Why is the location wrong?', 'Geolocation is inferred from registry and routing data. Mobile networks and VPNs often report the location of a gateway hundreds of miles away.']] },
+ ipv4: { name: 'IPv4 Lookup & CIDR Calculator', title: 'IPv4 Lookup & CIDR Subnet Calculator — Free Online', desc: 'Look up IPv4 addresses and calculate CIDR subnets: network range, broadcast address, usable hosts and mask. Free and instant.', kw: 'ipv4 lookup, cidr calculator, subnet calculator, ip range calculator, netmask calculator', about: 'Inspect an IPv4 address or work out a subnet: given a CIDR block you get the network address, broadcast address, usable host range and the total address count.', bullets: ['CIDR to range, mask and host count', 'Address class and reserved-range detection', 'Useful for firewall rules and allowlists'], faq: [['How many hosts are in a /24?', '256 addresses, of which 254 are usable — the first is the network address and the last is the broadcast address.'], ['What does the /number in CIDR mean?', 'It is the count of leading bits that identify the network. A larger number means a smaller subnet.']] },
+ ipv6: { name: 'IPv6 Lookup', title: 'IPv6 Address Lookup & Expander — Free Online IPv6 Tool', desc: 'Look up IPv6 addresses, expand or compress notation, and identify address type and scope. Free, instant, no signup.', kw: 'ipv6 lookup, ipv6 expander, ipv6 address checker, expand ipv6, ipv6 notation', about: 'Expand a shortened IPv6 address to its full form, compress it back, and see what kind of address it is — global unicast, link-local, unique local or multicast.', bullets: ['Expand and compress IPv6 notation', 'Address type, scope and prefix detection', 'Owner and routing information for global addresses'], faq: [['What does the :: in an IPv6 address mean?', 'It replaces one run of consecutive zero groups. It can appear only once in an address, which is why expansion is unambiguous.'], ['Why do I have several IPv6 addresses?', 'Devices normally hold a link-local address plus one or more global addresses, often including a rotating privacy address.']] },
+ geoip: { name: 'IP Geolocation', title: 'IP Geolocation Lookup — Find the Location of Any IP, Free', desc: 'Find the country, region, city, timezone and coordinates behind any IP address, shown on a map. Free IP geolocation with no signup.', kw: 'ip geolocation, ip location lookup, find ip location, geoip, ip to location', about: 'Resolve an IP address to its most likely physical location, including country, region, city, timezone and approximate coordinates plotted on a map.', bullets: ['Country, region, city and timezone', 'Coordinates plotted on a map', 'Network operator shown alongside the location'], faq: [['How accurate is IP geolocation?', 'Country level is highly reliable; city level is a good guess and often off for mobile and satellite connections. It should never be treated as a precise location.'], ['Why does my own IP show the wrong city?', 'Your provider may route traffic through a regional hub, so the registered location of the gateway is what gets reported.']] },
+ hostname: { name: 'Hostname Lookup', title: 'Hostname Lookup — Resolve Hostnames to IP Addresses, Free', desc: 'Resolve a hostname to its IP addresses or find the hostname behind an IP. Forward and reverse lookups in one tool. Free and instant.', kw: 'hostname lookup, resolve hostname, host to ip, ip to hostname, dns resolve', about: 'Resolve a hostname to every address it answers with, or go the other way and find the name registered for an IP.', bullets: ['Forward resolution to IPv4 and IPv6 addresses', 'Reverse resolution from an address to a name', 'Shows every record when a host is load balanced'], faq: [['Why does one hostname return several IPs?', 'Large sites publish multiple addresses for load balancing and redundancy; your resolver picks one.'], ['What if the reverse lookup returns nothing?', 'Reverse records are optional and set by whoever controls the IP block, so many addresses simply have none.']] },
+ isp: { name: 'ISP Lookup', title: 'ISP Lookup — Find the Internet Provider Behind Any IP, Free', desc: 'Identify the ISP, hosting company or organisation that owns any IP address, plus its network range. Free and instant.', kw: 'isp lookup, ip owner, who owns this ip, hosting provider lookup, organization lookup', about: 'Find out which company is responsible for an address — a residential ISP, a mobile carrier, a cloud provider or a hosting company — along with the network block it sits in.', bullets: ['Provider and organisation name', 'Network block and autonomous system', 'Distinguishes residential, mobile and datacentre addresses'], faq: [['How can I tell if traffic came from a datacentre?', 'The owner name is the giveaway: cloud and hosting providers appear instead of a consumer ISP, which usually means automated traffic or a VPN.'], ['Is the ISP always accurate?', 'It reflects registry data, which is reliable for the owning organisation but may lag when address blocks are transferred or leased.']] },
+ asn: { name: 'ASN Lookup', title: 'ASN Lookup — Autonomous System Number Info, Free Online', desc: 'Look up any autonomous system number to see its operator, announced prefixes and peers. Free BGP and routing information.', kw: 'asn lookup, autonomous system number, bgp lookup, as number, network prefix lookup', about: 'Inspect an autonomous system: who operates it, which address prefixes it announces to the internet, and how it connects to the wider routing table.', bullets: ['Operator name, country and registry', 'Announced IPv4 and IPv6 prefixes', 'Useful for routing investigations and abuse reports'], faq: [['What is an autonomous system?', 'A group of IP networks under one operator with a single routing policy. Every ISP and large hosting provider has at least one, identified by an AS number.'], ['How do I find the ASN for a website?', 'Look up the site\'s IP address first — the result includes the AS number announcing it.']] },
+ rdns: { name: 'Reverse DNS Lookup', title: 'Reverse DNS Lookup — Find the Hostname of an IP (PTR), Free', desc: 'Run a reverse DNS lookup to find the PTR hostname for any IPv4 or IPv6 address. Essential for mail server checks. Free and instant.', kw: 'reverse dns lookup, ptr record lookup, rdns, ip to hostname, reverse ip lookup', about: 'Query the PTR record for an address to find the hostname its operator has registered. Mail servers check this, which makes it a required step when diagnosing delivery problems.', bullets: ['PTR lookups for both IPv4 and IPv6', 'Shows whether forward and reverse records agree', 'Key check for mail server configuration'], faq: [['Why does my mail get rejected for missing reverse DNS?', 'Receiving mail servers treat a missing or mismatched PTR record as a spam signal. Your hosting provider sets this record for you.'], ['What is forward-confirmed reverse DNS?', 'It means the PTR name resolves back to the same IP. Mail servers prefer it, and this tool shows whether it matches.']] },
+ whois: { name: 'Whois Lookup', title: 'Whois Lookup — Free Domain Registration & Ownership Checker', desc: 'Look up domain registration details: registrar, creation and expiry dates, nameservers and status codes. Free Whois lookup, no signup.', kw: 'whois lookup, domain whois, domain registration lookup, check domain owner, domain expiry', about: 'Query registration records for a domain to see who registered it, when it was created, when it expires, which registrar holds it and what status codes are applied.', bullets: ['Registrar, creation, update and expiry dates', 'Nameservers and domain status codes', 'Works for domains and IP address blocks'], faq: [['Why is the registrant hidden?', 'Privacy rules mean most registrars replace personal details with a privacy service. Registrar, dates and nameservers remain public.'], ['How do I tell when a domain expires?', 'The expiry date is in the record here, though domains usually stay in redemption for weeks afterwards before becoming available again.']] },
+ rdap: { name: 'RDAP Lookup', title: 'RDAP Lookup — Modern Domain & IP Registration Data, Free', desc: 'Query RDAP for structured domain and IP registration data — the modern replacement for Whois. Free, instant, standards-compliant.', kw: 'rdap lookup, rdap query, domain registration data, whois alternative, registration data access', about: 'RDAP is the structured successor to Whois: the same registration data delivered as predictable JSON, with consistent fields across registries.', bullets: ['Structured, machine-readable registration data', 'Consistent field names across every registry', 'Covers both domains and IP allocations'], faq: [['How is RDAP different from Whois?', 'Whois returns free-form text that differs by registry. RDAP returns structured data with standard field names, so it is far easier to parse reliably.'], ['Is RDAP replacing Whois?', 'Yes, gradually. Registries are required to run RDAP, and Whois is being retired over time.']] },
+ dns: { name: 'DNS Lookup', title: 'DNS Lookup — Free Online DNS Record Checker (A, MX, TXT)', desc: 'Look up DNS records for any domain: A, AAAA, MX, TXT, CNAME, NS, SOA and more. Free DNS lookup with instant results.', kw: 'dns lookup, dns record checker, check dns records, a record lookup, mx lookup, txt record', about: 'Query any DNS record type for a domain and see the answers exactly as a resolver receives them, including TTL values so you know how long a change will take to spread.', bullets: ['All common record types: A, AAAA, MX, TXT, CNAME, NS, SOA', 'TTL values shown so you can predict propagation', 'Queried from our resolvers, bypassing your local cache'], faq: [['Why do I still see the old DNS record?', 'Resolvers cache answers for the length of the TTL. This tool queries directly, so it usually shows the new value before your machine does.'], ['Which record do I need for a website?', 'An A record (or AAAA for IPv6) pointing at your server\'s IP, or a CNAME pointing at another hostname.']] },
+ dig: { name: 'Online dig', title: 'Online dig Command — Free Web-Based DNS Query Tool', desc: 'Run dig from your browser to query any DNS record type and read the full answer, authority and additional sections. Free, no install.', kw: 'online dig, dig command online, dns query tool, dig web interface, dns debugging', about: 'The dig command without a terminal: choose a record type, query any domain, and read the complete response including the authority and additional sections.', bullets: ['Full response sections, not just the answer', 'Any record type, including less common ones', 'No local install — useful from a locked-down machine'], faq: [['What is the difference between dig and nslookup?', 'dig shows the complete DNS response including flags and section detail, which makes it the better tool for debugging. nslookup is simpler and shows just the answer.'], ['Can I query a specific nameserver?', 'Yes — that is the usual way to confirm an authoritative server has your change before caches expire.']] },
+ nslookup: { name: 'NSLookup', title: 'NSLookup Online — Free Web-Based DNS Lookup Tool', desc: 'Run nslookup from your browser to resolve domains and check DNS records. Simple, fast, free and no install required.', kw: 'nslookup online, nslookup tool, dns lookup command, resolve domain online', about: 'A simple DNS query in the familiar nslookup style: enter a domain, get the addresses and records it resolves to.', bullets: ['Straightforward answers without protocol noise', 'Common record types covered', 'Runs from our resolvers, so it ignores your local cache'], faq: [['Why does nslookup show a different answer than my browser?', 'Your browser and operating system cache DNS answers. A direct query shows the current published record.'], ['Is nslookup still worth using?', 'For a quick "what does this resolve to" check, yes. For debugging, dig gives far more detail.']] },
+ dnspropagation: { name: 'DNS Propagation Checker', title: 'DNS Propagation Checker — Test DNS Worldwide, Free', desc: 'Check whether a DNS change has propagated by querying resolvers around the world and comparing the answers. Free and instant.', kw: 'dns propagation checker, dns propagation test, global dns check, dns worldwide, check dns update', about: 'Query the same record from resolvers in different regions to see which ones have picked up your change and which are still serving the cached answer.', bullets: ['Answers from multiple global regions side by side', 'Highlights resolvers still serving the old value', 'Works for any record type, not just A records'], faq: [['How long does DNS propagation take?', 'Up to the record\'s previous TTL — commonly an hour, sometimes 24 or 48 hours if the TTL was set high. Lowering the TTL before a planned change makes the next one faster.'], ['Why do some regions still show the old IP?', 'Those resolvers cached the previous answer and will keep serving it until that cache entry expires.']] },
+ mx: { name: 'MX Record Lookup', title: 'MX Record Lookup — Find Mail Servers for Any Domain, Free', desc: 'Look up the MX records for a domain to see which mail servers handle its email and in what priority order. Free and instant.', kw: 'mx record lookup, mail server lookup, mx check, email dns records, find mail server', about: 'See which servers accept mail for a domain and their priority order — the first thing to check when email is bouncing or arriving somewhere unexpected.', bullets: ['All MX records with priority values', 'Resolves each mail server to its address', 'Flags domains with no MX record at all'], faq: [['What does the MX priority number mean?', 'Lower numbers are tried first. Equal values are used in rotation, which spreads load across servers.'], ['Why is my email bouncing?', 'Start here: a missing MX record, or one pointing at a host that no longer exists, stops delivery outright.']] },
+ txt: { name: 'TXT Record Lookup', title: 'TXT Record Lookup — Check SPF, DKIM & Verification Records', desc: 'Look up TXT records for any domain, including SPF, DKIM, DMARC and site verification strings. Free, instant DNS TXT checker.', kw: 'txt record lookup, spf record check, dns txt records, domain verification record, check txt record', about: 'Read every TXT record published for a domain — the place where SPF policies, domain verification tokens and assorted service strings all live.', bullets: ['Every TXT record, shown in full', 'Highlights SPF, DKIM and verification entries', 'Useful for confirming a verification token is live'], faq: [['Why is my domain verification failing?', 'Usually the record was added to the wrong host name, or the provider wrapped it in extra quotes. This shows exactly what is published.'], ['Can a domain have more than one TXT record?', 'Yes, and most do. The exception is SPF: exactly one SPF record is allowed, and multiple ones cause failures.']] },
+ ns: { name: 'Nameserver Lookup', title: 'NS Record Lookup — Find the Nameservers for Any Domain, Free', desc: 'Look up the NS records for a domain to see which nameservers are authoritative for it. Free and instant nameserver checker.', kw: 'ns record lookup, nameserver lookup, find nameservers, authoritative dns, dns delegation check', about: 'Find which nameservers a domain is delegated to, and confirm the delegation at the registry matches what the nameservers themselves claim.', bullets: ['Authoritative nameservers for the domain', 'Reveals mismatches between registry and zone', 'First check when a DNS change has no effect'], faq: [['Why do my DNS changes have no effect?', 'Often the domain is delegated to different nameservers than the ones you edited. This shows which servers are actually authoritative.'], ['How many nameservers should a domain have?', 'At least two, on separate networks, so the domain still resolves if one goes down.']] },
+ spf: { name: 'SPF Record Checker', title: 'SPF Record Checker — Validate Your Email SPF Policy, Free', desc: 'Check and validate a domain\'s SPF record. See which servers may send mail, catch syntax errors and count DNS lookups. Free and instant.', kw: 'spf record checker, spf validator, check spf, email authentication, spf lookup limit', about: 'Read a domain\'s SPF policy, expand every include, and confirm it stays inside the ten-lookup limit that causes silent failures when exceeded.', bullets: ['Full expansion of includes and redirects', 'Counts DNS lookups against the limit of ten', 'Flags syntax errors and multiple SPF records'], faq: [['What happens if SPF exceeds ten DNS lookups?', 'Receiving servers return a permanent error and may treat your mail as unauthenticated, so legitimate messages can land in spam.'], ['Can I have two SPF records?', 'No. A domain must publish exactly one; two records cause validation to fail entirely.']] },
+ dkim: { name: 'DKIM Record Lookup', title: 'DKIM Record Lookup & Validator — Free Email Auth Checker', desc: 'Look up and validate DKIM records by selector. Confirm your signing key is published correctly for email authentication. Free and instant.', kw: 'dkim record lookup, dkim checker, dkim validator, dkim selector, email authentication', about: 'Fetch the DKIM public key for a given selector and confirm it is published and well-formed, so signed mail can be verified by receiving servers.', bullets: ['Selector-based lookup for any domain', 'Validates key format and flags', 'Confirms the record is reachable before you go live'], faq: [['What is a DKIM selector?', 'A label that identifies which key to use, published at selector._domainkey.yourdomain.com. Your mail provider tells you which selector it signs with.'], ['Why is DKIM failing after a provider change?', 'The new provider signs with a different selector and key, so the old record no longer matches. Publish the new one they supply.']] },
+ dmarc: { name: 'DMARC Checker', title: 'DMARC Record Checker — Validate Your DMARC Policy, Free', desc: 'Check a domain\'s DMARC record and policy. See enforcement level, reporting addresses and alignment settings. Free and instant.', kw: 'dmarc checker, dmarc record lookup, dmarc validator, dmarc policy, email security check', about: 'Read a domain\'s DMARC policy: whether failures are monitored, quarantined or rejected, where reports are sent, and how strictly alignment is enforced.', bullets: ['Policy level, percentage and alignment settings', 'Reporting addresses for aggregate and forensic reports', 'Syntax validation with clear explanations'], faq: [['What DMARC policy should I start with?', 'Start at p=none with a reporting address. That monitors without affecting delivery, so you can find legitimate senders before moving to quarantine and then reject.'], ['Do I need SPF and DKIM for DMARC to work?', 'Yes. DMARC builds on both, and requires that at least one passes and aligns with the visible From domain.']] },
+ ping: { name: 'Ping Test', title: 'Online Ping Test — Check if a Host Is Up, Free', desc: 'Ping any host or IP from multiple global regions to check reachability and latency. Free online ping test with no install.', kw: 'ping test, online ping, ping ip, check if server is up, latency test, ping tool', about: 'Send ICMP pings from our servers to check whether a host answers and how long the round trip takes, from several regions at once.', bullets: ['Results from multiple global regions', 'Round-trip time, jitter and packet loss', 'Confirms whether an outage is local to you or global'], faq: [['Why does a site load but not respond to ping?', 'Many hosts and firewalls block ICMP on purpose. A failed ping does not always mean a site is down — check HTTP headers instead.'], ['What is a good ping time?', 'Under 50 ms feels instant, under 100 ms is fine for most uses, and over 200 ms is noticeable in calls and games.']] },
+ traceroute: { name: 'Traceroute', title: 'Online Traceroute — Trace the Network Path to Any Host, Free', desc: 'Run a traceroute from our servers to see every hop between us and a host, with latency per hop. Free, no install required.', kw: 'traceroute online, trace route, network path, tracert online, hop by hop latency', about: 'Map the route packets take to a destination, hop by hop, with timing at each step so you can see where latency is introduced or where the path stops.', bullets: ['Every hop with per-hop latency', 'Reverse DNS names to identify the networks in the path', 'Shows exactly where a broken path terminates'], faq: [['Why do some hops show stars?', 'Those routers are configured not to reply to probes. It is normal and does not mean the path is broken.'], ['How do I read a traceroute?', 'Look for the point where latency jumps sharply or replies stop. The hop before it usually names the network where the trouble starts.']] },
+ mtr: { name: 'MTR Report', title: 'Online MTR — Combined Ping & Traceroute Report, Free', desc: 'Run MTR to combine traceroute and ping into one report with per-hop packet loss and latency over time. Free network path analysis.', kw: 'mtr online, mtr report, my traceroute, packet loss test, network path analysis', about: 'MTR repeats a traceroute continuously and aggregates the results, so intermittent packet loss at a single hop shows up clearly instead of hiding behind one lucky probe.', bullets: ['Per-hop loss percentage over many probes', 'Best, average and worst latency for each hop', 'The report network operators ask for in support tickets'], faq: [['Why is MTR better than traceroute for support tickets?', 'It samples repeatedly, so it distinguishes a genuinely lossy hop from a single dropped probe. That is the evidence a network operator needs.'], ['Loss shows at one hop but not later ones — is that a problem?', 'No. That router is just rate-limiting its own replies. Real trouble shows as loss that continues at every hop after it.']] },
+ port: { name: 'Port Checker', title: 'Open Port Checker — Test if a Port Is Open, Free Online', desc: 'Check whether a TCP port is open on any host, from outside your network. Test common services or any custom port. Free and instant.', kw: 'port checker, open port test, port scanner online, check port open, tcp port test', about: 'Test whether a port accepts connections from the public internet — the definitive check on whether a port forward, firewall rule or service binding actually works.', bullets: ['Tests from outside your network, as a real client would', 'Common service ports or any custom port', 'Distinguishes closed ports from filtered ones'], faq: [['My service runs but the port shows closed — why?', 'Either a firewall is dropping the traffic, the port forward is missing, or the service is bound to localhost instead of all interfaces.'], ['What is the difference between closed and filtered?', 'Closed means the host actively refused the connection. Filtered means nothing answered at all, which usually indicates a firewall dropping packets silently.']] },
+ headers: { name: 'HTTP Header Checker', title: 'HTTP Header Checker — Inspect Response Headers Free Online', desc: 'Check the HTTP response headers of any URL, including security, caching and compression headers. Free header analysis tool.', kw: 'http header checker, response headers, security headers check, cache headers, view http headers', about: 'Fetch any URL and read the full response headers, with the security and caching headers called out so you can see what a browser is being told to do.', bullets: ['Complete response headers exactly as sent', 'Security header review including HSTS and CSP', 'Cache and compression headers explained'], faq: [['Which security headers should I have?', 'At minimum Strict-Transport-Security, X-Content-Type-Options and a Content-Security-Policy. The report lists what is present and what is missing.'], ['Why is my page not being cached?', 'Look at Cache-Control. A no-store or no-cache directive, or a missing max-age, prevents browsers and CDNs from reusing the response.']] },
+ redirects: { name: 'Redirect Checker', title: 'Redirect Checker — Trace Redirect Chains Online, Free', desc: 'Follow every hop in a redirect chain and see each status code and destination. Find redirect loops and SEO-damaging chains. Free and instant.', kw: 'redirect checker, redirect chain, 301 checker, url redirect tracer, redirect loop finder', about: 'Follow a URL through every redirect to its final destination, with the status code at each step, so you can spot long chains and loops that waste crawl budget and lose link value.', bullets: ['Every hop with its status code and target', 'Detects loops and unnecessarily long chains', 'Flags temporary redirects used where permanent ones belong'], faq: [['Do redirect chains hurt SEO?', 'Yes. Each extra hop slows the page and search engines limit how many they follow. Point the first URL straight at the final destination.'], ['301 or 302 — which should I use?', 'Use 301 for permanent moves so ranking signals transfer. Use 302 only when the original URL will genuinely return.']] },
+ ssl: { name: 'SSL Certificate Checker', title: 'SSL Certificate Checker — Free Online TLS & Expiry Test', desc: 'Check any site\'s SSL certificate: expiry date, issuer, chain completeness and supported TLS versions. Free SSL checker, instant results.', kw: 'ssl checker, ssl certificate checker, tls check, certificate expiry, ssl chain test', about: 'Inspect a site\'s TLS certificate — who issued it, when it expires, whether the chain is complete and which protocol versions the server negotiates.', bullets: ['Expiry date with days remaining', 'Chain validation that catches missing intermediates', 'Issuer, subject and alternative names'], faq: [['My certificate works in Chrome but fails elsewhere — why?', 'Almost always a missing intermediate certificate. Browsers often cache intermediates from other sites; other clients do not. This check shows whether the chain your server sends is complete.'], ['How early should I renew?', 'Automate renewal at around 30 days remaining. Most issuers allow renewal well before expiry.']] },
+ curl: { name: 'Online curl', title: 'Online curl — Free Web-Based HTTP Request Tester', desc: 'Send HTTP requests from your browser and inspect the full response: status, headers and body. Test APIs without installing anything.', kw: 'online curl, http request tester, api tester online, send http request, rest client online', about: 'Make an HTTP request from our servers and read the complete response. Useful for testing an API from outside your network or from a machine where you cannot install tools.', bullets: ['Any method, with custom headers and body', 'Full response: status, headers and body', 'Requests originate from our servers, not your IP'], faq: [['How is this different from the curl converter?', 'This sends a real request and shows the response. The converter turns a curl command into code without sending anything.'], ['Can I test an internal service?', 'No. Requests come from our servers, so the target must be reachable from the public internet.']] },
+ httpstatus: { name: 'HTTP Status Codes', title: 'HTTP Status Codes — Full Reference & Online Checker, Free', desc: 'Look up what any HTTP status code means and check the code any URL returns. Complete reference from 100 to 599. Free and instant.', kw: 'http status codes, status code meaning, 404 meaning, 301 vs 302, http response codes list', about: 'A full reference for HTTP status codes with plain-English explanations, plus a live check of what code any URL actually returns.', bullets: ['Every status code with a practical explanation', 'Live check of the code a URL returns', 'Notes on which codes search engines act on'], faq: [['What is the difference between 301 and 302?', '301 is permanent and transfers ranking signals to the new URL. 302 is temporary and tells search engines to keep indexing the original.'], ['Should a missing page return 404 or 410?', '404 means not found and may be retried. 410 means permanently gone and gets pages dropped from an index faster.']] },
+ latency: { name: 'Latency Test', title: 'Latency Test — Measure Response Time to Any Host, Free', desc: 'Measure response times to any host from multiple regions and compare latency worldwide. Free online latency and speed check.', kw: 'latency test, response time test, server speed test, ttfb test, network latency check', about: 'Measure how long a host takes to respond from several regions, so you can see where your site feels fast and where it drags.', bullets: ['Response times from multiple global regions', 'Connection versus first-byte timing separated', 'Useful for deciding whether a CDN would help'], faq: [['What is a good time to first byte?', 'Under 200 ms is good, under 500 ms acceptable. Beyond that, visitors notice and search engines record the page as slow.'], ['Why is latency high from one region only?', 'Your server is probably far from it with no edge cache in between. A CDN usually fixes exactly this.']] },
+ blacklist: { name: 'Blacklist Checker', title: 'IP & Domain Blacklist Checker — Free RBL Lookup', desc: 'Check whether an IP or domain is listed on major email blacklists and RBLs. Find out why your mail is being rejected. Free and instant.', kw: 'blacklist check, rbl lookup, ip blacklist checker, email blacklist, spam list check', about: 'Query the major real-time blacklists to see whether an address or domain is listed — the usual explanation when mail is being rejected or silently dropped.', bullets: ['Checks many major blacklists in one query', 'Shows which list flagged you and why', 'Includes delisting links where the operator provides them'], faq: [['How do I get removed from a blacklist?', 'Fix the cause first — an open relay, a compromised account or a bad sending pattern — then use the operator\'s delisting form. Requesting removal without fixing the cause gets you relisted.'], ['Does one listing stop all my mail?', 'Not always. Different providers consult different lists, so a single listing often causes partial rather than total failure.']] },
+ reputation: { name: 'Domain Reputation Checker', title: 'Domain & IP Reputation Checker — Free Online Trust Score', desc: 'Check the reputation of a domain or IP across blacklists, email authentication and security signals. Free reputation report.', kw: 'domain reputation, ip reputation checker, email reputation, sender score, domain trust check', about: 'A combined view of the signals that decide whether other systems trust you: blacklist status, email authentication records, certificate health and hosting details.', bullets: ['Blacklist status combined with authentication checks', 'SPF, DKIM and DMARC presence in one report', 'Practical notes on what to fix first'], faq: [['What hurts domain reputation most?', 'Sending mail without SPF, DKIM and DMARC, appearing on blacklists, and hosting alongside abusive neighbours on a shared IP.'], ['How long does reputation take to recover?', 'Weeks of consistent, authenticated sending. There is no way to reset it instantly.']] },
+ smtp: { name: 'SMTP Server Test', title: 'SMTP Server Test — Check Mail Server Connectivity, Free', desc: 'Test an SMTP server\'s connectivity, banner, STARTTLS support and authentication requirements. Free online mail server check.', kw: 'smtp test, smtp server check, mail server test, starttls check, smtp connectivity', about: 'Connect to a mail server and report what it says: the greeting banner, the extensions it advertises, whether STARTTLS is offered and whether it accepts mail for a domain.', bullets: ['Connection test with the full server greeting', 'STARTTLS and authentication support detection', 'Checks the common submission ports'], faq: [['Which SMTP port should I use?', 'Port 587 with STARTTLS for sending mail from a client, or 465 for implicit TLS. Port 25 is for server-to-server delivery and is blocked by most consumer ISPs.'], ['What does an open relay mean?', 'A server that accepts mail for domains it does not handle. It will be blacklisted quickly, so this test flags it.']] },
+ uptime: { name: 'Website Uptime Checker', title: 'Website Uptime Checker — Is a Site Down? Free Online Test', desc: 'Check whether a website is up from multiple regions and see its response time and status code. Free "is it down" checker.', kw: 'uptime checker, is this site down, website down checker, site availability test, server status check', about: 'Check a site from several regions at once to settle the "is it down for everyone or just me" question, with status code and response time for each.', bullets: ['Checks from multiple global regions at once', 'Status code and response time per region', 'Distinguishes a genuine outage from a local problem'], faq: [['The site works for me but not for a friend — what now?', 'Run this check. If some regions fail, it is a routing or DNS propagation issue rather than the server being down.'], ['Does this monitor continuously?', 'No, this is an on-demand check. It tells you the state right now.']] },
+ robots: { name: 'Robots.txt Tester', title: 'Robots.txt Tester & Analyzer — Free Online Checker', desc: 'Fetch and analyse any site\'s robots.txt, check which URLs are blocked, and catch rules that hide your site from search engines.', kw: 'robots.txt tester, robots txt checker, crawl rules, disallow checker, seo crawl test', about: 'Read a site\'s robots.txt, see the rules that apply to each crawler, and test whether a specific URL would be allowed or blocked — the file where an accidental line can remove a whole site from search.', bullets: ['Rules grouped by user-agent', 'Test any URL against the live rules', 'Flags the accidental site-wide disallow'], faq: [['Does robots.txt remove a page from Google?', 'No. It stops crawling, not indexing — a blocked URL can still appear without a description. Use a noindex meta tag to remove a page properly.'], ['Should robots.txt list my sitemap?', 'Yes. A Sitemap line is the simplest way to point every crawler at your sitemap without registering anywhere.']] },
+ sitemap: { name: 'Sitemap Validator', title: 'Sitemap.xml Validator — Free Online XML Sitemap Checker', desc: 'Validate an XML sitemap, check its structure and URL count, and catch errors that stop search engines indexing your pages. Free.', kw: 'sitemap validator, sitemap.xml checker, xml sitemap test, sitemap errors, seo sitemap check', about: 'Fetch and validate a sitemap against the protocol: structure, URL count, lastmod formatting and index files, so you can be sure crawlers can actually read it.', bullets: ['Structure and schema validation', 'URL count against the 50,000 entry limit', 'Follows sitemap index files to their children'], faq: [['How many URLs can one sitemap hold?', '50,000 URLs or 50 MB uncompressed. Beyond that, split it and reference the parts from a sitemap index.'], ['Does lastmod matter?', 'Yes, when it is accurate. Crawlers use it to prioritise re-crawling, but they learn to ignore it if every URL claims to have changed today.']] },
+};
+
+const CATALOG = new Map();
+for (const [sub, rec] of Object.entries(DEV)) CATALOG.set(sub, { ...rec, sub, family: 'dev', hub: sub === 'dev' });
+for (const [sub, rec] of Object.entries(NET)) CATALOG.set(sub, { ...rec, sub, family: 'net', hub: sub === 'net' });
+
+/** SEO record for a subdomain, or null when it is not a catalogued tool. */
+function seoFor(sub) { return CATALOG.get(sub) || null; }
+
+/** Sibling tools in the same family, for the "related tools" block and internal linking. */
+function relatedTo(sub, limit = 6) {
+ const rec = CATALOG.get(sub);
+ if (!rec) return [];
+ const words = new Set(String(rec.kw || '').toLowerCase().split(/[,\s]+/).filter(w => w.length > 3));
+ const scored = [];
+ for (const [otherSub, other] of CATALOG) {
+ if (otherSub === sub || other.hub || other.family !== rec.family) continue;
+ let score = 0;
+ for (const w of String(other.kw || '').toLowerCase().split(/[,\s]+/)) if (words.has(w)) score++;
+ scored.push({ sub: otherSub, name: other.name, desc: other.desc, score });
+ }
+ scored.sort((a, b) => b.score - a.score || a.sub.localeCompare(b.sub));
+ return scored.slice(0, limit);
+}
+
+module.exports = { CATALOG, DEV, NET, seoFor, relatedTo, BRAND };
new file mode 100644
new file mode 100644
@@ -0,0 +1,171 @@
@@ -0,0 +1,171 @@
+'use strict';
+
+// ═══════════════════════════════════════════════════════════════
+// Per-subdomain SEO rendering for the shared tool SPAs.
+//
+// dev.html and net.html are each served for dozens of subdomains. Without this, every one of
+// those pages shipped the hub's <title>, the hub's description and a canonical pointing AT the
+// hub — telling search engines that sixty-five distinct tools were all the same page, and that
+// the real one was somewhere else. Nothing could rank.
+//
+// This module rewrites the <head> per request and appends a visible, crawlable content block
+// (H1, intro, features, FAQ, related tools) so each tool page has genuine text and internal
+// links even before the SPA boots. The content is visible rather than hidden: hidden keyword
+// text is a penalty risk, and this copy is genuinely useful to a first-time visitor.
+// ═══════════════════════════════════════════════════════════════
+
+const fs = require('fs');
+const path = require('path');
+const { seoFor, relatedTo, BRAND } = require('./catalog');
+
+const SITE = 'https://openvibe.tools';
+const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
+
+// Page HTML is read once and cached; a tiny LRU keeps rendered variants hot.
+const _base = new Map();
+function baseHtml(file) {
+ if (!_base.has(file)) _base.set(file, fs.readFileSync(file, 'utf8'));
+ return _base.get(file);
+}
+const _rendered = new Map();
+
+function jsonLd(rec, url, related) {
+ const app = {
+ '@context': 'https://schema.org', '@type': 'SoftwareApplication',
+ name: rec.name, url, applicationCategory: 'DeveloperApplication',
+ operatingSystem: 'Any (web browser)', description: rec.desc,
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
+ isAccessibleForFree: true,
+ publisher: { '@type': 'Organization', name: BRAND, url: SITE },
+ featureList: rec.bullets || undefined,
+ };
+ const crumbs = {
+ '@context': 'https://schema.org', '@type': 'BreadcrumbList',
+ itemListElement: [
+ { '@type': 'ListItem', position: 1, name: BRAND, item: SITE },
+ { '@type': 'ListItem', position: 2, name: rec.family === 'net' ? 'Network Tools' : 'Developer Tools', item: `https://${rec.family}.openvibe.tools` },
+ { '@type': 'ListItem', position: 3, name: rec.name, item: url },
+ ],
+ };
+ const blocks = [app, crumbs];
+ if ((rec.faq || []).length) {
+ blocks.push({
+ '@context': 'https://schema.org', '@type': 'FAQPage',
+ mainEntity: rec.faq.map(([q, a]) => ({ '@type': 'Question', name: q, acceptedAnswer: { '@type': 'Answer', text: a } })),
+ });
+ }
+ if (related.length) {
+ blocks.push({
+ '@context': 'https://schema.org', '@type': 'ItemList',
+ name: 'Related tools',
+ itemListElement: related.map((r, i) => ({ '@type': 'ListItem', position: i + 1, name: r.name, url: `https://${r.sub}.openvibe.tools` })),
+ });
+ }
+ return blocks.map(b => `<script type="application/ld+json">${JSON.stringify(b).replace(/</g, '\\u003c')}</script>`).join('\n');
+}
+
+/** The visible content block appended under the tool: real copy, real internal links. */
+function contentBlock(rec, related) {
+ const hubUrl = `https://${rec.family}.openvibe.tools`;
+ const hubName = rec.family === 'net' ? 'network tools' : 'developer tools';
+ return `
+<section class="seo-about" id="about-this-tool">
+ <div class="seo-about-inner">
+ <h2>About the ${esc(rec.name)}</h2>
+ <p>${esc(rec.about)}</p>
+ ${(rec.bullets || []).length ? `<ul class="seo-features">${rec.bullets.map(b => `<li>${esc(b)}</li>`).join('')}</ul>` : ''}
+ ${(rec.faq || []).length ? `<h2>Frequently asked questions</h2><dl class="seo-faq">${rec.faq.map(([q, a]) => `<dt>${esc(q)}</dt><dd>${esc(a)}</dd>`).join('')}</dl>` : ''}
+ ${related.length ? `<h2>Related tools</h2><ul class="seo-related">${related.map(r => `<li><a href="https://${esc(r.sub)}.openvibe.tools"><strong>${esc(r.name)}</strong><span>${esc(r.desc)}</span></a></li>`).join('')}</ul>` : ''}
+ <p class="seo-hub"><a href="${hubUrl}">Browse all free ${hubName}</a> · <a href="${SITE}">${BRAND} home</a> · <a href="https://openvibe.live">Live streaming</a></p>
+ </div>
+</section>
+<style>
+.seo-about{position:relative;z-index:1;max-width:1100px;margin:48px auto 0;padding:28px 20px 40px;border-top:1px solid var(--brd,rgba(255,255,255,.07));color:var(--tx2,#a0a8c4);font-size:.92rem;line-height:1.65}
+.seo-about h2{color:var(--tx,#f1f4fb);font-size:1.05rem;font-weight:700;margin:22px 0 10px}
+.seo-about h2:first-child{margin-top:0}
+.seo-about p{margin:0 0 12px;max-width:72ch}
+.seo-features{margin:0 0 4px;padding-left:20px}.seo-features li{margin:4px 0}
+.seo-faq{margin:0}.seo-faq dt{color:var(--tx,#f1f4fb);font-weight:600;margin:12px 0 4px}.seo-faq dd{margin:0 0 8px;max-width:72ch}
+.seo-related{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:8px}
+.seo-related a{display:block;padding:10px 12px;border-radius:12px;background:var(--bg2,#12131c);border:1px solid var(--brd,rgba(255,255,255,.07));transition:border-color .15s,transform .15s}
+.seo-related a:hover{border-color:var(--ac,#a78bfa);transform:translateY(-1px)}
+.seo-related strong{display:block;color:var(--tx,#f1f4fb);font-size:.88rem;font-weight:600}
+.seo-related span{display:block;font-size:.76rem;color:var(--tx3,#5e6580);margin-top:2px;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
+.seo-hub{margin-top:18px;font-size:.84rem}.seo-hub a{color:var(--ac,#a78bfa)}
+</style>`;
+}
+
+/**
+ * Render the SPA file for one subdomain with its own metadata and content.
+ * Returns the original HTML untouched when the subdomain is not catalogued.
+ */
+function renderTool(file, sub) {
+ const rec = seoFor(sub);
+ if (!rec) return baseHtml(file);
+ const key = `${file}::${sub}`;
+ if (_rendered.has(key)) return _rendered.get(key);
+
+ const url = `https://${sub}.openvibe.tools`;
+ const related = relatedTo(sub, 6);
+ const title = `${rec.title} | ${BRAND}`;
+ let html = baseHtml(file);
+
+ // <head>: title, description, canonical, social cards — each replaced rather than appended
+ // so we never ship two competing values.
+ html = html.replace(/<title>[\s\S]*?<\/title>/i, `<title>${esc(title)}</title>`);
+ html = html.replace(/<meta\s+name=["']description["'][^>]*>/i, `<meta name="description" content="${esc(rec.desc)}">`);
+ html = html.replace(/<meta\s+property=["']og:title["'][^>]*>/i, `<meta property="og:title" content="${esc(rec.title)}">`);
+ html = html.replace(/<meta\s+property=["']og:description["'][^>]*>/i, `<meta property="og:description" content="${esc(rec.desc)}">`);
+ html = html.replace(/<meta\s+property=["']og:url["'][^>]*>/i, `<meta property="og:url" content="${url}">`);
+ html = html.replace(/<link\s+rel=["']canonical["'][^>]*>/i, `<link rel="canonical" href="${url}">`);
+
+ const extraHead = [
+ `<meta name="keywords" content="${esc(rec.kw || '')}">`,
+ '<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">',
+ '<meta name="twitter:card" content="summary_large_image">',
+ `<meta name="twitter:title" content="${esc(rec.title)}">`,
+ `<meta name="twitter:description" content="${esc(rec.desc)}">`,
+ `<meta property="og:site_name" content="${BRAND}">`,
+ '<meta property="og:locale" content="en_US">',
+ jsonLd(rec, url, related),
+ ].join('\n');
+ html = html.replace(/<\/head>/i, `${extraHead}\n</head>`);
+
+ // Visible content + internal links, appended before </body> so it renders under the tool.
+ html = html.replace(/<\/body>/i, `${contentBlock(rec, related)}\n</body>`);
+
+ if (_rendered.size > 200) _rendered.clear();
+ _rendered.set(key, html);
+ return html;
+}
+
+/** Hub pages list every tool, which gives crawlers a clean path into all of them. */
+function toolUrls() {
+ const { CATALOG } = require('./catalog');
+ const urls = [];
+ for (const [sub, rec] of CATALOG) {
+ urls.push({ loc: `https://${sub}.openvibe.tools/`, priority: rec.hub ? '0.9' : '0.8', changefreq: rec.hub ? 'weekly' : 'monthly' });
+ }
+ return urls;
+}
+
+/** Full sitemap: the hubs, every tool subdomain, and the satellite apps. */
+function buildSitemap(extraUrls = []) {
+ const today = new Date().toISOString().slice(0, 10);
+ const seen = new Set();
+ const all = [
+ { loc: `${SITE}/`, priority: '1.0', changefreq: 'weekly' },
+ ...toolUrls(),
+ ...extraUrls,
+ ].filter(u => { if (seen.has(u.loc)) return false; seen.add(u.loc); return true; });
+ const body = all.map(u => ` <url>\n <loc>${u.loc}</loc>\n <lastmod>${u.lastmod || today}</lastmod>\n <changefreq>${u.changefreq || 'monthly'}</changefreq>\n <priority>${u.priority || '0.7'}</priority>\n </url>`).join('\n');
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${body}\n</urlset>\n`;
+}
+
+/** Satellite apps that live outside the dev/net catalogs but belong in the sitemap. */
+const SATELLITES = [
+ 'img', 'audio', 'docs', 'food', 'maps', 'yt', 'text', 'logo', 'pastes',
+ 'case', 'braille', 'binary', 'fancy', 'json', 'wordcount', 'reverse',
+].map(sub => ({ loc: `https://${sub}.openvibe.tools/`, priority: '0.8', changefreq: 'weekly' }));
+
+module.exports = { renderTool, buildSitemap, SATELLITES, toolUrls, SITE };