openvibers/openvibe.tools Commits
Host routing: text keeps json/markdown/escape/diff/compare/slug, gateway dev tools move to unique hosts
Changes
@@@ -151,12 +151,12 @@ H=s=>s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/const T={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']},++jsonfmt:{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']},++md:{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']},@@@ -164,16 +164,16 @@ jwt:{n:'JWT Decoder',i:'fa-key',c:'enc',d:'JWT decoder \u2014 header, payload &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']},++entities:{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']},++codediff:{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']},++slugify:{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']},++curlconvert:{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']}@@@ -232,7 +232,7 @@ function setMode(el){document.querySelectorAll('.mb').forEach(b=>b.classList.remfunction buildRel(){const cat=T[ACT]?.c;let h='';for(const[id,t]of Object.entries(T)){if(id===ACT||t.hub||t.c!==cat)continue;h+='<a href="https://'+id+'.openvibe.tools"><i class="fa-solid '+t.i+'"></i> '+t.n+'</a>'}-- if(['curl','webhook'].includes(ACT))h+='<a href="https://headers.openvibe.tools"><i class="fa-solid fa-heading"></i> Headers</a><a href="https://ssl.openvibe.tools"><i class="fa-solid fa-lock"></i> SSL</a>';++ if(['curlconvert','webhook'].includes(ACT))h+='<a href="https://headers.openvibe.tools"><i class="fa-solid fa-heading"></i> Headers</a><a href="https://ssl.openvibe.tools"><i class="fa-solid fa-lock"></i> SSL</a>';if(ACT==='opengraph')h+='<a href="https://logo.openvibe.tools"><i class="fa-solid fa-wand-magic-sparkles"></i> Logo.OpenVibe</a>';$('rel').innerHTML=h;}@@@ -245,7 +245,7 @@ async function run(){}const P={};--P.json=(s,m)=>{const o=JSON.parse(s);if(m==='Minify')return JSON.stringify(o);if(m==='Validate')return'\u2713 Valid JSON \u2014 '+(Array.isArray(o)?o.length+' items':typeof o==='object'?Object.keys(o).length+' keys':typeof o);return JSON.stringify(o,null,2)};++P.jsonfmt=(s,m)=>{const o=JSON.parse(s);if(m==='Minify')return JSON.stringify(o);if(m==='Validate')return'\u2713 Valid JSON \u2014 '+(Array.isArray(o)?o.length+' items':typeof o==='object'?Object.keys(o).length+' keys':typeof o);return JSON.stringify(o,null,2)};P.yaml=(s,m)=>{if(m.includes('JSON\u2192')){const o=JSON.parse(s);return j2y(o,'')}@@@ -277,7 +277,7 @@ P.sql=(s,m)=>{return r.replace(/^\n+/,'').replace(/\n{2,}/g,'\n');};--P.markdown=(s,m)=>{++P.md=(s,m)=>{let h=s.replace(/^######\s(.+)/gm,'<h6>$1</h6>').replace(/^#####\s(.+)/gm,'<h5>$1</h5>').replace(/^####\s(.+)/gm,'<h4>$1</h4>').replace(/^###\s(.+)/gm,'<h3>$1</h3>').replace(/^##\s(.+)/gm,'<h2>$1</h2>').replace(/^#\s(.+)/gm,'<h1>$1</h1>').replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/\*(.+?)\*/g,'<em>$1</em>')@@@ -320,7 +320,7 @@ P.hash=async(s,m)=>{const a=m==='SHA-1'?'SHA-1':m==='SHA-512'?'SHA-512':'SHA-256P.hex=(s,m)=>{if(m==='From Hex')return s.trim().replace(/\s+/g,'').match(/.{2}/g).map(h=>String.fromCharCode(parseInt(h,16))).join('');return[...s].map(c=>c.charCodeAt(0).toString(16).padStart(2,'0')).join(' ')};--P.escape=(s,m)=>{++P.entities=(s,m)=>{if(m==='HTML Enc')return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');if(m==='HTML Dec'){const d=document.createElement('div');d.innerHTML=s;return d.textContent}if(m==='JS Esc')return s.replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/"/g,'\\"').replace(/\n/g,'\\n').replace(/\r/g,'\\r').replace(/\t/g,'\\t');@@@ -355,7 +355,7 @@ P.beautify=(s,m)=>{P.minify=(s,m)=>{if(m==='JSON')return JSON.stringify(JSON.parse(s));if(m==='CSS')return s.replace(/\/\*[\s\S]*?\*\//g,'').replace(/\s+/g,' ').replace(/\s*([{}:;,])\s*/g,'$1').trim();if(m==='HTML')return s.replace(/\s+/g,' ').replace(/>\s+</g,'><').trim();return s.replace(/\/\/.*$/gm,'').replace(/\/\*[\s\S]*?\*\//g,'').replace(/\s+/g,' ').trim()};--P.diff=(a,m,b)=>{++P.codediff=(a,m,b)=>{const al=a.split('\n'),bl=b.split('\n'),r=[];let ai=0,bi=0;while(ai<al.length||bi<bl.length){if(ai<al.length&&bi<bl.length&&al[ai]===bl[bi]){r.push({t:'c',n:ai+1,l:al[ai]});ai++;bi++}@@@ -376,7 +376,7 @@ P.regex=(pat,m,test)=>{return{html:info};};--P.slug=s=>s.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');++P.slugify=s=>s.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');P.lorem=(s,m)=>{const W='lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua enim ad minim veniam quis nostrud exercitation ullamco laboris nisi aliquip ex ea commodo consequat duis aute irure in reprehenderit voluptate velit esse cillum fugiat nulla pariatur excepteur sint occaecat cupidatat non proident sunt culpa qui officia deserunt mollit anim id est laborum'.split(' ');@@@ -388,7 +388,7 @@ P.lorem=(s,m)=>{return Array.from({length:parseInt(m)||3},para).join('\n\n');};--P.curl=(s,m)=>{++P.curlconvert=(s,m)=>{const cmd=s.trim().replace(/\\\n/g,' ');let method='GET',url='',hd={},body='';const tok=[];let cur='',inQ=false,qc='';for(let i=0;i<cmd.length;i++){const c=cmd[i];if(!inQ&&(c==="'"||c==='"')){inQ=true;qc=c}else if(inQ&&c===qc){inQ=false}else if(!inQ&&c===' '){if(cur){tok.push(cur);cur=''}continue}else cur+=c}@@@ -493,7 +493,7 @@{ name: 'OpenVibeMerge', domain: 'merge.openvibe.tools', icon: 'fa-code-merge' },{ name: 'OpenVibePitch', domain: 'pitch.openvibe.tools', icon: 'fa-wave-square' },{ name: 'OpenVibeSpeed', domain: 'speed.openvibe.tools', icon: 'fa-gauge-high' },-- { name: 'OpenVibeReverse', domain: 'reverse.openvibe.tools', icon: 'fa-backward' },++ { name: 'OpenVibeReverse Text', domain: 'reversetext.openvibe.tools', icon: 'fa-backward' },{ name: 'OpenVibeNormalize', domain: 'normalize.openvibe.tools', icon: 'fa-chart-line' },{ name: 'OpenVibeFade', domain: 'fade.openvibe.tools', icon: 'fa-volume-low' },{ name: 'OpenVibeLoop', domain: 'loop.openvibe.tools', icon: 'fa-repeat' },@@@ -632,13 +632,13 @@{ name: 'OpenVibeSitemap', domain: 'sitemap.openvibe.tools', icon: 'fa-sitemap' },]},{ cat: 'Developer — Data & Formats', icon: 'fa-database', color: '#a78bfa', tools: [-- { name: 'OpenVibeJSON', domain: 'json.openvibe.tools', icon: 'fa-code' },++ { name: 'OpenVibeJSON', domain: 'jsonfmt.openvibe.tools', icon: 'fa-code' },{ name: 'OpenVibeYAML', domain: 'yaml.openvibe.tools', icon: 'fa-file-code' },{ name: 'OpenVibeXML', domain: 'xml.openvibe.tools', icon: 'fa-file-code' },{ name: 'OpenVibeCSV', domain: 'csv.openvibe.tools', icon: 'fa-table' },{ name: 'OpenVibeSQL', domain: 'sql.openvibe.tools', icon: 'fa-database' },{ name: 'OpenVibeHTML', domain: 'html.openvibe.tools', icon: 'fa-file-code' },-- { name: 'OpenVibeMarkdown', domain: 'markdown.openvibe.tools', icon: 'fa-file-lines' },++ { name: 'OpenVibeMarkdown', domain: 'md.openvibe.tools', icon: 'fa-file-lines' },]},{ cat: 'Developer — Encoding & Crypto', icon: 'fa-lock', color: '#a78bfa', tools: [{ name: 'OpenVibeBase64', domain: 'base64.openvibe.tools', icon: 'fa-lock' },@@@ -647,20 +647,20 @@{ name: 'OpenVibeUUID', domain: 'uuid.openvibe.tools', icon: 'fa-fingerprint' },{ name: 'OpenVibeHash', domain: 'hash.openvibe.tools', icon: 'fa-hashtag' },{ name: 'OpenVibeHex', domain: 'hex.openvibe.tools', icon: 'fa-barcode' },-- { name: 'OpenVibeEscape', domain: 'escape.openvibe.tools', icon: 'fa-shield-halved' },++ { name: 'OpenVibeEscape', domain: 'entities.openvibe.tools', icon: 'fa-shield-halved' },]},{ cat: 'Developer — Quality & Time', icon: 'fa-wand-magic-sparkles', color: '#a78bfa', tools: [{ name: 'OpenVibeBeautify', domain: 'beautify.openvibe.tools', icon: 'fa-wand-magic-sparkles' },{ name: 'OpenVibeMinify', domain: 'minify.openvibe.tools', icon: 'fa-compress' },-- { name: 'OpenVibeDiff', domain: 'diff.openvibe.tools', icon: 'fa-code-compare' },++ { name: 'OpenVibeDiff', domain: 'codediff.openvibe.tools', icon: 'fa-code-compare' },{ name: 'OpenVibeRegex', domain: 'regex.openvibe.tools', icon: 'fa-magnifying-glass' },-- { name: 'OpenVibeSlug', domain: 'slug.openvibe.tools', icon: 'fa-link' },++ { name: 'OpenVibeSlug', domain: 'slugify.openvibe.tools', icon: 'fa-link' },{ name: 'OpenVibeLorem', domain: 'lorem.openvibe.tools', icon: 'fa-paragraph' },{ name: 'OpenVibeTimestamp', domain: 'timestamp.openvibe.tools', icon: 'fa-clock' },{ name: 'OpenVibeCron', domain: 'cron.openvibe.tools', icon: 'fa-calendar-check' },]},{ cat: 'Developer — HTTP & SEO', icon: 'fa-terminal', color: '#a78bfa', tools: [-- { name: 'OpenVibeCurl Converter', domain: 'curl.openvibe.tools', icon: 'fa-terminal' },++ { name: 'OpenVibeCurl Converter', domain: 'curlconvert.openvibe.tools', icon: 'fa-terminal' },{ name: 'OpenVibeWebhook', domain: 'webhook.openvibe.tools', icon: 'fa-satellite-dish' },{ name: 'OpenVibeColor', domain: 'color.openvibe.tools', icon: 'fa-palette' },{ name: 'OpenVibeOpenGraph', domain: 'opengraph.openvibe.tools', icon: 'fa-share-nodes' },@@@ -2,6 +2,12 @@// 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.++//++// Host ownership: json., markdown., escape., diff., compare. and slug.openvibe.tools belong to++// the Text app (apps/text), whose dedicated pages are the richer implementations and whose++// nginx server_name wins over this gateway's wildcard anyway. The generic dev.html versions++// of those tools live on their own hosts (jsonfmt., md., entities., codediff., slugify.) so no++// host is claimed twice. curl. is the Net tool; the dev-side curl converter is curlconvert.const { seoFor } = require('../seo/catalog');// ═══════════════════════════════════════════════════════════════@@@ -13,12 +19,12 @@ const DEV_TOOLS = [{ id: 'dev', subdomain: 'dev', name: 'Dev.OpenVibe', icon: 'fa-code', desc: 'Developer & SEO Tools Hub', hub: true, category: 'hub' },// Data & Formats-- { id: 'json', subdomain: 'json', name: 'OpenVibeJSON', icon: 'fa-code', desc: 'JSON formatter, validator, minifier & converter', category: 'data' },++ { id: 'json', subdomain: 'jsonfmt', name: 'OpenVibeJSON', icon: 'fa-code', desc: 'JSON formatter, validator, minifier & converter', category: 'data' },{ id: 'yaml', subdomain: 'yaml', name: 'OpenVibeYAML', icon: 'fa-file-code', desc: 'YAML formatter, validator & JSON converter', category: 'data' },{ id: 'xml', subdomain: 'xml', name: 'OpenVibeXML', icon: 'fa-file-code', desc: 'XML formatter, validator & converter', category: 'data' },{ id: 'csv', subdomain: 'csv', name: 'OpenVibeCSV', icon: 'fa-table', desc: 'CSV viewer, converter & JSON transformer', category: 'data' },{ id: 'sql', subdomain: 'sql', name: 'OpenVibeSQL', icon: 'fa-database', desc: 'SQL formatter & syntax highlighter', category: 'data' },-- { id: 'markdown', subdomain: 'markdown', name: 'OpenVibeMarkdown', icon: 'fa-file-lines', desc: 'Markdown editor with live HTML preview', category: 'data' },++ { id: 'markdown', subdomain: 'md', name: 'OpenVibeMarkdown', icon: 'fa-file-lines', desc: 'Markdown editor with live HTML preview', category: 'data' },{ id: 'html', subdomain: 'html', name: 'OpenVibeHTML', icon: 'fa-file-code', desc: 'HTML formatter, minifier & entity encoder', category: 'data' },// Encoding & Crypto@@@ -28,7 +34,7 @@ const DEV_TOOLS = [{ id: 'uuid', subdomain: 'uuid', name: 'OpenVibeUUID', icon: 'fa-fingerprint', desc: 'UUID v4 generator, bulk generate & validator', category: 'encoding' },{ id: 'hash', subdomain: 'hash', name: 'OpenVibeHash', icon: 'fa-hashtag', desc: 'SHA-1, SHA-256 & SHA-512 hash generator', category: 'encoding' },{ id: 'hex', subdomain: 'hex', name: 'OpenVibeHex', icon: 'fa-barcode', desc: 'Hex encoder/decoder & binary converter', category: 'encoding' },-- { id: 'escape', subdomain: 'escape', name: 'OpenVibeEscape', icon: 'fa-shield-halved', desc: 'HTML, JavaScript & URL escape/unescape', category: 'encoding' },++ { id: 'escape', subdomain: 'entities', name: 'OpenVibeEscape', icon: 'fa-shield-halved', desc: 'HTML, JavaScript & URL escape/unescape', category: 'encoding' },// Time & Scheduling{ id: 'timestamp', subdomain: 'timestamp', name: 'OpenVibeTimestamp', icon: 'fa-clock', desc: 'Unix timestamp ↔ date converter with timezones', category: 'time' },@@@ -37,13 +43,13 @@ const DEV_TOOLS = [// Code Quality{ id: 'beautify', subdomain: 'beautify', name: 'OpenVibeBeautify', icon: 'fa-wand-magic-sparkles', desc: 'Beautify & format JS, HTML, CSS, JSON & SQL', category: 'quality' },{ id: 'minify', subdomain: 'minify', name: 'OpenVibeMinify', icon: 'fa-compress', desc: 'Minify JS, HTML, CSS & JSON', category: 'quality' },-- { id: 'diff', subdomain: 'diff', name: 'OpenVibeDiff', icon: 'fa-code-compare', desc: 'Side-by-side text & code diff checker', category: 'quality' },++ { id: 'diff', subdomain: 'codediff', name: 'OpenVibeDiff', icon: 'fa-code-compare', desc: 'Side-by-side text & code diff checker', category: 'quality' },{ id: 'regex', subdomain: 'regex', name: 'OpenVibeRegex', icon: 'fa-magnifying-glass', desc: 'Regex tester with match highlighting & capture groups', category: 'quality' },-- { id: 'slug', subdomain: 'slug', name: 'OpenVibeSlug', icon: 'fa-link', desc: 'URL slug generator from any text', category: 'quality' },++ { id: 'slug', subdomain: 'slugify', name: 'OpenVibeSlug', icon: 'fa-link', desc: 'URL slug generator from any text', category: 'quality' },{ id: 'lorem', subdomain: 'lorem', name: 'OpenVibeLorem', icon: 'fa-paragraph', desc: 'Lorem ipsum & placeholder text generator', category: 'quality' },// HTTP & API-- { id: 'curl', subdomain: 'curl', name: 'OpenVibeCurl', icon: 'fa-terminal', desc: 'Parse curl commands & convert to fetch, Python, Node.js', category: 'http' },++ { id: 'curl', subdomain: 'curlconvert', name: 'OpenVibeCurl', icon: 'fa-terminal', desc: 'Parse curl commands & convert to fetch, Python, Node.js', category: 'http' },{ id: 'webhook', subdomain: 'webhook', name: 'OpenVibeWebhook', icon: 'fa-satellite-dish', desc: 'Webhook request inspector & debugging bin', category: 'http' },// Frontend & SEO@@@ -60,10 +66,9 @@ const DEV_ALIASES = {code: 'dev',build: 'dev',debug: 'dev',-- compare: 'diff',format: 'beautify',prettier: 'beautify',-- md: 'markdown',++ textdiff: 'codediff',colours: 'color',colors: 'color',og: 'opengraph',@@@ -75,17 +80,16 @@ const DEV_ALIASES = {b64: 'base64',urlencode: 'url',urldecode: 'url',-- entities: 'escape',-- htmlentities: 'escape',++ htmlentities: 'entities',checksum: 'hash',sha256: 'hash',sha1: 'hash',sha512: 'hash',-- ini: 'json',-- toml: 'json',-- env: 'json',-- request: 'curl',-- http: 'curl',++ ini: 'jsonfmt',++ toml: 'jsonfmt',++ env: 'jsonfmt',++ request: 'curlconvert',++ http: 'curlconvert',};module.exports = { DEV_TOOLS, DEV_TOOL_MAP, DEV_ALIASES };@@@ -30,7 +30,7 @@ const DEV = {bullets: ['Every tool works client-side: your data never leaves the browser', 'No signup and 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: {++ jsonfmt: {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',@@@ -70,7 +70,7 @@ const DEV = {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: {++ md: {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',@@@ -134,7 +134,7 @@ const DEV = {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: {++ entities: {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',@@@ -174,7 +174,7 @@ const DEV = {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: {++ codediff: {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',@@@ -190,7 +190,7 @@ const DEV = {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: {++ slugify: {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',@@@ -206,7 +206,7 @@ const DEV = {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: {++ curlconvert: {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',@@@ -165,7 +165,8 @@ function buildSitemap(extraUrls = []) {/** 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',++ 'case', 'braille', 'binary', 'fancy', 'json', 'markdown', 'escape', 'diff', 'slug',++ 'wordcount', 'reverse', 'reversetext',].map(sub => ({ loc: `https://${sub}.openvibe.tools/`, priority: '0.8', changefreq: 'weekly' }));module.exports = { renderTool, buildSitemap, SATELLITES, toolUrls, SITE };@@@ -1,6 +1,12 @@# ═══════════════════════════════════════════════════════════════# Text.OpenVibe + Logo.OpenVibe — Nginx Vhost# Single backend serving all text + logo subdomains.++#++# These exact server_names win over the gateway's *.openvibe.tools wildcard, so++# json./markdown./escape./diff./compare./slug. resolve here (the dedicated text++# pages) and the gateway's generic dev versions live on jsonfmt./md./entities./++# codediff./slugify. instead. reverse.openvibe.tools belongs to the Audio vhost;++# the text flipper is reversetext. / mirror. Keep in step with server/index.js.# ═══════════════════════════════════════════════════════════════# HTTP → HTTPS redirect@@@ -15,6 +21,7 @@ server {gothic.openvibe.tools wide.openvibe.tools monospaced.openvibe.toolsbraille.openvibe.tools morse.openvibe.tools binary.openvibe.toolscase.openvibe.tools caps.openvibe.tools titlecase.openvibe.tools++ reversetext.openvibe.tools mirror.openvibe.toolsclean.openvibe.tools strip.openvibe.toolscount.openvibe.tools lines.openvibe.tools sort.openvibe.toolsdedupe.openvibe.tools slug.openvibe.tools compare.openvibe.tools@@@ -49,6 +56,7 @@ server {gothic.openvibe.tools wide.openvibe.tools monospaced.openvibe.toolsbraille.openvibe.tools morse.openvibe.tools binary.openvibe.toolscase.openvibe.tools caps.openvibe.tools titlecase.openvibe.tools++ reversetext.openvibe.tools mirror.openvibe.toolsclean.openvibe.tools strip.openvibe.toolscount.openvibe.tools lines.openvibe.tools sort.openvibe.toolsdedupe.openvibe.tools slug.openvibe.tools compare.openvibe.tools@@@ -96,11 +96,11 @@<p>Fix smart quotes, remove invisible characters, strip Zalgo, normalize spacing, clean HTML tags.</p><span class="card-domain">clean.openvibe.tools</span></a>-- <a href="https://reverse.openvibe.tools" class="tool-card">++ <a href="https://reversetext.openvibe.tools" class="tool-card"><div class="card-icon"><i class="fas fa-rotate-left"></i></div><h3>Reverse & Flip Text</h3><p>Reverse, flip upside down, mirror text. Works with full Unicode support.</p>-- <span class="card-domain">reverse.openvibe.tools</span>++ <span class="card-domain">reversetext.openvibe.tools</span></a><a href="https://kaomoji.openvibe.tools" class="tool-card"><div class="card-icon"><i class="fas fa-face-smile-beam"></i></div>@@@ -3,7 +3,7 @@<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">-- <title>Reverse & Flip Text — Upside Down, Mirror | reverse.openvibe.tools</title>++ <title>Reverse & Flip Text — Upside Down, Mirror | reversetext.openvibe.tools</title><meta name="description" content="Reverse text, flip it upside down, or mirror it. Full Unicode support with combining character awareness."><meta name="theme-color" content="#0b0c12"><link rel="preconnect" href="https://fonts.googleapis.com">@@@ -62,6 +62,10 @@ app.use(analytics.middleware());// ── Hostname → HTML file mapping ─────────────────────────────// Each subdomain gets its own static HTML page for SEO + focused UX.++// Every host here must also be in deploy/nginx/text.openvibe.tools.conf, and must not be++// claimed by another app's vhost or by the gateway's net/dev catalogs: json., markdown.,++// escape., diff., compare. and slug. are ours (the gateway's generic versions moved to++// jsonfmt., md., entities., codediff. and slugify.).const HOSTNAME_MAP = {// Text hubs'text.openvibe.tools': 'index.html',@@@ -87,7 +91,10 @@ const HOSTNAME_MAP = {'case.openvibe.tools': 'case.html','caps.openvibe.tools': 'case.html','titlecase.openvibe.tools': 'case.html',-- 'reverse.openvibe.tools': 'reverse.html',++ // reverse.openvibe.tools is the Audio app's (reverse an audio file); the text flipper++ // lives on reversetext. + mirror. instead. Keep this in step with deploy/nginx.++ 'reversetext.openvibe.tools':'reverse.html',++ 'mirror.openvibe.tools': 'reverse.html','clean.openvibe.tools': 'clean.html','strip.openvibe.tools': 'clean.html','count.openvibe.tools': 'count.html',
@@ -151,12 +151,12 @@ H=s=>s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/
@@ -151,12 +151,12 @@ H=s=>s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/
const T={
const T={
dev:{n:'Developer Tools',i:'fa-code',c:'hub',d:'Developer & SEO Tools Hub',hub:1},
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']},
+jsonfmt:{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']},
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']},
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']},
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']},
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']},
+md:{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']},
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']},
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']},
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']},
@@ -164,16 +164,16 @@ jwt:{n:'JWT Decoder',i:'fa-key',c:'enc',d:'JWT decoder \u2014 header, payload &
@@ -164,16 +164,16 @@ jwt:{n:'JWT Decoder',i:'fa-key',c:'enc',d:'JWT decoder \u2014 header, payload &
uuid:{n:'UUID Generator',i:'fa-fingerprint',c:'enc',d:'UUID v4 generator & validator',p:'',m:['Generate','Bulk 10','Bulk 100','Validate']},
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']},
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']},
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']},
+entities:{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']},
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']},
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']},
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']},
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']},
+codediff:{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']},
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']},
+slugify:{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']},
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']},
+curlconvert:{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']},
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']},
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']}
opengraph:{n:'Open Graph Preview',i:'fa-share-nodes',c:'fe',d:'Open Graph & Twitter Card preview',p:'https://openvibe.live',m:['Preview']}
@@ -232,7 +232,7 @@ function setMode(el){document.querySelectorAll('.mb').forEach(b=>b.classList.rem
@@ -232,7 +232,7 @@ function setMode(el){document.querySelectorAll('.mb').forEach(b=>b.classList.rem
function buildRel(){
function buildRel(){
const cat=T[ACT]?.c;let h='';
const cat=T[ACT]?.c;let h='';
for(const[id,t]of Object.entries(T)){if(id===ACT||t.hub||t.c!==cat)continue;h+='<a href="https://'+id+'.openvibe.tools"><i class="fa-solid '+t.i+'"></i> '+t.n+'</a>'}
for(const[id,t]of Object.entries(T)){if(id===ACT||t.hub||t.c!==cat)continue;h+='<a href="https://'+id+'.openvibe.tools"><i class="fa-solid '+t.i+'"></i> '+t.n+'</a>'}
- if(['curl','webhook'].includes(ACT))h+='<a href="https://headers.openvibe.tools"><i class="fa-solid fa-heading"></i> Headers</a><a href="https://ssl.openvibe.tools"><i class="fa-solid fa-lock"></i> SSL</a>';
+ if(['curlconvert','webhook'].includes(ACT))h+='<a href="https://headers.openvibe.tools"><i class="fa-solid fa-heading"></i> Headers</a><a href="https://ssl.openvibe.tools"><i class="fa-solid fa-lock"></i> SSL</a>';
if(ACT==='opengraph')h+='<a href="https://logo.openvibe.tools"><i class="fa-solid fa-wand-magic-sparkles"></i> Logo.OpenVibe</a>';
if(ACT==='opengraph')h+='<a href="https://logo.openvibe.tools"><i class="fa-solid fa-wand-magic-sparkles"></i> Logo.OpenVibe</a>';
$('rel').innerHTML=h;
$('rel').innerHTML=h;
}
}
@@ -245,7 +245,7 @@ async function run(){
@@ -245,7 +245,7 @@ async function run(){
}
}
const P={};
const P={};
-P.json=(s,m)=>{const o=JSON.parse(s);if(m==='Minify')return JSON.stringify(o);if(m==='Validate')return'\u2713 Valid JSON \u2014 '+(Array.isArray(o)?o.length+' items':typeof o==='object'?Object.keys(o).length+' keys':typeof o);return JSON.stringify(o,null,2)};
+P.jsonfmt=(s,m)=>{const o=JSON.parse(s);if(m==='Minify')return JSON.stringify(o);if(m==='Validate')return'\u2713 Valid JSON \u2014 '+(Array.isArray(o)?o.length+' items':typeof o==='object'?Object.keys(o).length+' keys':typeof o);return JSON.stringify(o,null,2)};
P.yaml=(s,m)=>{
P.yaml=(s,m)=>{
if(m.includes('JSON\u2192')){const o=JSON.parse(s);return j2y(o,'')}
if(m.includes('JSON\u2192')){const o=JSON.parse(s);return j2y(o,'')}
@@ -277,7 +277,7 @@ P.sql=(s,m)=>{
@@ -277,7 +277,7 @@ P.sql=(s,m)=>{
return r.replace(/^\n+/,'').replace(/\n{2,}/g,'\n');
return r.replace(/^\n+/,'').replace(/\n{2,}/g,'\n');
};
};
-P.markdown=(s,m)=>{
+P.md=(s,m)=>{
let h=s.replace(/^######\s(.+)/gm,'<h6>$1</h6>').replace(/^#####\s(.+)/gm,'<h5>$1</h5>').replace(/^####\s(.+)/gm,'<h4>$1</h4>')
let h=s.replace(/^######\s(.+)/gm,'<h6>$1</h6>').replace(/^#####\s(.+)/gm,'<h5>$1</h5>').replace(/^####\s(.+)/gm,'<h4>$1</h4>')
.replace(/^###\s(.+)/gm,'<h3>$1</h3>').replace(/^##\s(.+)/gm,'<h2>$1</h2>').replace(/^#\s(.+)/gm,'<h1>$1</h1>')
.replace(/^###\s(.+)/gm,'<h3>$1</h3>').replace(/^##\s(.+)/gm,'<h2>$1</h2>').replace(/^#\s(.+)/gm,'<h1>$1</h1>')
.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/\*(.+?)\*/g,'<em>$1</em>')
.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/\*(.+?)\*/g,'<em>$1</em>')
@@ -320,7 +320,7 @@ P.hash=async(s,m)=>{const a=m==='SHA-1'?'SHA-1':m==='SHA-512'?'SHA-512':'SHA-256
@@ -320,7 +320,7 @@ P.hash=async(s,m)=>{const a=m==='SHA-1'?'SHA-1':m==='SHA-512'?'SHA-512':'SHA-256
P.hex=(s,m)=>{if(m==='From Hex')return s.trim().replace(/\s+/g,'').match(/.{2}/g).map(h=>String.fromCharCode(parseInt(h,16))).join('');
P.hex=(s,m)=>{if(m==='From Hex')return s.trim().replace(/\s+/g,'').match(/.{2}/g).map(h=>String.fromCharCode(parseInt(h,16))).join('');
return[...s].map(c=>c.charCodeAt(0).toString(16).padStart(2,'0')).join(' ')};
return[...s].map(c=>c.charCodeAt(0).toString(16).padStart(2,'0')).join(' ')};
-P.escape=(s,m)=>{
+P.entities=(s,m)=>{
if(m==='HTML Enc')return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
if(m==='HTML Enc')return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
if(m==='HTML Dec'){const d=document.createElement('div');d.innerHTML=s;return d.textContent}
if(m==='HTML Dec'){const d=document.createElement('div');d.innerHTML=s;return d.textContent}
if(m==='JS Esc')return s.replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/"/g,'\\"').replace(/\n/g,'\\n').replace(/\r/g,'\\r').replace(/\t/g,'\\t');
if(m==='JS Esc')return s.replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/"/g,'\\"').replace(/\n/g,'\\n').replace(/\r/g,'\\r').replace(/\t/g,'\\t');
@@ -355,7 +355,7 @@ P.beautify=(s,m)=>{
@@ -355,7 +355,7 @@ P.beautify=(s,m)=>{
P.minify=(s,m)=>{if(m==='JSON')return JSON.stringify(JSON.parse(s));if(m==='CSS')return s.replace(/\/\*[\s\S]*?\*\//g,'').replace(/\s+/g,' ').replace(/\s*([{}:;,])\s*/g,'$1').trim();
P.minify=(s,m)=>{if(m==='JSON')return JSON.stringify(JSON.parse(s));if(m==='CSS')return s.replace(/\/\*[\s\S]*?\*\//g,'').replace(/\s+/g,' ').replace(/\s*([{}:;,])\s*/g,'$1').trim();
if(m==='HTML')return s.replace(/\s+/g,' ').replace(/>\s+</g,'><').trim();return s.replace(/\/\/.*$/gm,'').replace(/\/\*[\s\S]*?\*\//g,'').replace(/\s+/g,' ').trim()};
if(m==='HTML')return s.replace(/\s+/g,' ').replace(/>\s+</g,'><').trim();return s.replace(/\/\/.*$/gm,'').replace(/\/\*[\s\S]*?\*\//g,'').replace(/\s+/g,' ').trim()};
-P.diff=(a,m,b)=>{
+P.codediff=(a,m,b)=>{
const al=a.split('\n'),bl=b.split('\n'),r=[];let ai=0,bi=0;
const al=a.split('\n'),bl=b.split('\n'),r=[];let ai=0,bi=0;
while(ai<al.length||bi<bl.length){
while(ai<al.length||bi<bl.length){
if(ai<al.length&&bi<bl.length&&al[ai]===bl[bi]){r.push({t:'c',n:ai+1,l:al[ai]});ai++;bi++}
if(ai<al.length&&bi<bl.length&&al[ai]===bl[bi]){r.push({t:'c',n:ai+1,l:al[ai]});ai++;bi++}
@@ -376,7 +376,7 @@ P.regex=(pat,m,test)=>{
@@ -376,7 +376,7 @@ P.regex=(pat,m,test)=>{
return{html:info};
return{html:info};
};
};
-P.slug=s=>s.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');
+P.slugify=s=>s.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');
P.lorem=(s,m)=>{
P.lorem=(s,m)=>{
const W='lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua enim ad minim veniam quis nostrud exercitation ullamco laboris nisi aliquip ex ea commodo consequat duis aute irure in reprehenderit voluptate velit esse cillum fugiat nulla pariatur excepteur sint occaecat cupidatat non proident sunt culpa qui officia deserunt mollit anim id est laborum'.split(' ');
const W='lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua enim ad minim veniam quis nostrud exercitation ullamco laboris nisi aliquip ex ea commodo consequat duis aute irure in reprehenderit voluptate velit esse cillum fugiat nulla pariatur excepteur sint occaecat cupidatat non proident sunt culpa qui officia deserunt mollit anim id est laborum'.split(' ');
@@ -388,7 +388,7 @@ P.lorem=(s,m)=>{
@@ -388,7 +388,7 @@ P.lorem=(s,m)=>{
return Array.from({length:parseInt(m)||3},para).join('\n\n');
return Array.from({length:parseInt(m)||3},para).join('\n\n');
};
};
-P.curl=(s,m)=>{
+P.curlconvert=(s,m)=>{
const cmd=s.trim().replace(/\\\n/g,' ');let method='GET',url='',hd={},body='';
const cmd=s.trim().replace(/\\\n/g,' ');let method='GET',url='',hd={},body='';
const tok=[];let cur='',inQ=false,qc='';
const tok=[];let cur='',inQ=false,qc='';
for(let i=0;i<cmd.length;i++){const c=cmd[i];if(!inQ&&(c==="'"||c==='"')){inQ=true;qc=c}else if(inQ&&c===qc){inQ=false}else if(!inQ&&c===' '){if(cur){tok.push(cur);cur=''}continue}else cur+=c}
for(let i=0;i<cmd.length;i++){const c=cmd[i];if(!inQ&&(c==="'"||c==='"')){inQ=true;qc=c}else if(inQ&&c===qc){inQ=false}else if(!inQ&&c===' '){if(cur){tok.push(cur);cur=''}continue}else cur+=c}
@@ -493,7 +493,7 @@
@@ -493,7 +493,7 @@
{ name: 'OpenVibeMerge', domain: 'merge.openvibe.tools', icon: 'fa-code-merge' },
{ name: 'OpenVibeMerge', domain: 'merge.openvibe.tools', icon: 'fa-code-merge' },
{ name: 'OpenVibePitch', domain: 'pitch.openvibe.tools', icon: 'fa-wave-square' },
{ name: 'OpenVibePitch', domain: 'pitch.openvibe.tools', icon: 'fa-wave-square' },
{ name: 'OpenVibeSpeed', domain: 'speed.openvibe.tools', icon: 'fa-gauge-high' },
{ name: 'OpenVibeSpeed', domain: 'speed.openvibe.tools', icon: 'fa-gauge-high' },
- { name: 'OpenVibeReverse', domain: 'reverse.openvibe.tools', icon: 'fa-backward' },
+ { name: 'OpenVibeReverse Text', domain: 'reversetext.openvibe.tools', icon: 'fa-backward' },
{ name: 'OpenVibeNormalize', domain: 'normalize.openvibe.tools', icon: 'fa-chart-line' },
{ name: 'OpenVibeNormalize', domain: 'normalize.openvibe.tools', icon: 'fa-chart-line' },
{ name: 'OpenVibeFade', domain: 'fade.openvibe.tools', icon: 'fa-volume-low' },
{ name: 'OpenVibeFade', domain: 'fade.openvibe.tools', icon: 'fa-volume-low' },
{ name: 'OpenVibeLoop', domain: 'loop.openvibe.tools', icon: 'fa-repeat' },
{ name: 'OpenVibeLoop', domain: 'loop.openvibe.tools', icon: 'fa-repeat' },
@@ -632,13 +632,13 @@
@@ -632,13 +632,13 @@
{ name: 'OpenVibeSitemap', domain: 'sitemap.openvibe.tools', icon: 'fa-sitemap' },
{ name: 'OpenVibeSitemap', domain: 'sitemap.openvibe.tools', icon: 'fa-sitemap' },
]},
]},
{ cat: 'Developer — Data & Formats', icon: 'fa-database', color: '#a78bfa', tools: [
{ cat: 'Developer — Data & Formats', icon: 'fa-database', color: '#a78bfa', tools: [
- { name: 'OpenVibeJSON', domain: 'json.openvibe.tools', icon: 'fa-code' },
+ { name: 'OpenVibeJSON', domain: 'jsonfmt.openvibe.tools', icon: 'fa-code' },
{ name: 'OpenVibeYAML', domain: 'yaml.openvibe.tools', icon: 'fa-file-code' },
{ name: 'OpenVibeYAML', domain: 'yaml.openvibe.tools', icon: 'fa-file-code' },
{ name: 'OpenVibeXML', domain: 'xml.openvibe.tools', icon: 'fa-file-code' },
{ name: 'OpenVibeXML', domain: 'xml.openvibe.tools', icon: 'fa-file-code' },
{ name: 'OpenVibeCSV', domain: 'csv.openvibe.tools', icon: 'fa-table' },
{ name: 'OpenVibeCSV', domain: 'csv.openvibe.tools', icon: 'fa-table' },
{ name: 'OpenVibeSQL', domain: 'sql.openvibe.tools', icon: 'fa-database' },
{ name: 'OpenVibeSQL', domain: 'sql.openvibe.tools', icon: 'fa-database' },
{ name: 'OpenVibeHTML', domain: 'html.openvibe.tools', icon: 'fa-file-code' },
{ name: 'OpenVibeHTML', domain: 'html.openvibe.tools', icon: 'fa-file-code' },
- { name: 'OpenVibeMarkdown', domain: 'markdown.openvibe.tools', icon: 'fa-file-lines' },
+ { name: 'OpenVibeMarkdown', domain: 'md.openvibe.tools', icon: 'fa-file-lines' },
]},
]},
{ cat: 'Developer — Encoding & Crypto', icon: 'fa-lock', color: '#a78bfa', tools: [
{ cat: 'Developer — Encoding & Crypto', icon: 'fa-lock', color: '#a78bfa', tools: [
{ name: 'OpenVibeBase64', domain: 'base64.openvibe.tools', icon: 'fa-lock' },
{ name: 'OpenVibeBase64', domain: 'base64.openvibe.tools', icon: 'fa-lock' },
@@ -647,20 +647,20 @@
@@ -647,20 +647,20 @@
{ name: 'OpenVibeUUID', domain: 'uuid.openvibe.tools', icon: 'fa-fingerprint' },
{ name: 'OpenVibeUUID', domain: 'uuid.openvibe.tools', icon: 'fa-fingerprint' },
{ name: 'OpenVibeHash', domain: 'hash.openvibe.tools', icon: 'fa-hashtag' },
{ name: 'OpenVibeHash', domain: 'hash.openvibe.tools', icon: 'fa-hashtag' },
{ name: 'OpenVibeHex', domain: 'hex.openvibe.tools', icon: 'fa-barcode' },
{ name: 'OpenVibeHex', domain: 'hex.openvibe.tools', icon: 'fa-barcode' },
- { name: 'OpenVibeEscape', domain: 'escape.openvibe.tools', icon: 'fa-shield-halved' },
+ { name: 'OpenVibeEscape', domain: 'entities.openvibe.tools', icon: 'fa-shield-halved' },
]},
]},
{ cat: 'Developer — Quality & Time', icon: 'fa-wand-magic-sparkles', color: '#a78bfa', tools: [
{ cat: 'Developer — Quality & Time', icon: 'fa-wand-magic-sparkles', color: '#a78bfa', tools: [
{ name: 'OpenVibeBeautify', domain: 'beautify.openvibe.tools', icon: 'fa-wand-magic-sparkles' },
{ name: 'OpenVibeBeautify', domain: 'beautify.openvibe.tools', icon: 'fa-wand-magic-sparkles' },
{ name: 'OpenVibeMinify', domain: 'minify.openvibe.tools', icon: 'fa-compress' },
{ name: 'OpenVibeMinify', domain: 'minify.openvibe.tools', icon: 'fa-compress' },
- { name: 'OpenVibeDiff', domain: 'diff.openvibe.tools', icon: 'fa-code-compare' },
+ { name: 'OpenVibeDiff', domain: 'codediff.openvibe.tools', icon: 'fa-code-compare' },
{ name: 'OpenVibeRegex', domain: 'regex.openvibe.tools', icon: 'fa-magnifying-glass' },
{ name: 'OpenVibeRegex', domain: 'regex.openvibe.tools', icon: 'fa-magnifying-glass' },
- { name: 'OpenVibeSlug', domain: 'slug.openvibe.tools', icon: 'fa-link' },
+ { name: 'OpenVibeSlug', domain: 'slugify.openvibe.tools', icon: 'fa-link' },
{ name: 'OpenVibeLorem', domain: 'lorem.openvibe.tools', icon: 'fa-paragraph' },
{ name: 'OpenVibeLorem', domain: 'lorem.openvibe.tools', icon: 'fa-paragraph' },
{ name: 'OpenVibeTimestamp', domain: 'timestamp.openvibe.tools', icon: 'fa-clock' },
{ name: 'OpenVibeTimestamp', domain: 'timestamp.openvibe.tools', icon: 'fa-clock' },
{ name: 'OpenVibeCron', domain: 'cron.openvibe.tools', icon: 'fa-calendar-check' },
{ name: 'OpenVibeCron', domain: 'cron.openvibe.tools', icon: 'fa-calendar-check' },
]},
]},
{ cat: 'Developer — HTTP & SEO', icon: 'fa-terminal', color: '#a78bfa', tools: [
{ cat: 'Developer — HTTP & SEO', icon: 'fa-terminal', color: '#a78bfa', tools: [
- { name: 'OpenVibeCurl Converter', domain: 'curl.openvibe.tools', icon: 'fa-terminal' },
+ { name: 'OpenVibeCurl Converter', domain: 'curlconvert.openvibe.tools', icon: 'fa-terminal' },
{ name: 'OpenVibeWebhook', domain: 'webhook.openvibe.tools', icon: 'fa-satellite-dish' },
{ name: 'OpenVibeWebhook', domain: 'webhook.openvibe.tools', icon: 'fa-satellite-dish' },
{ name: 'OpenVibeColor', domain: 'color.openvibe.tools', icon: 'fa-palette' },
{ name: 'OpenVibeColor', domain: 'color.openvibe.tools', icon: 'fa-palette' },
{ name: 'OpenVibeOpenGraph', domain: 'opengraph.openvibe.tools', icon: 'fa-share-nodes' },
{ name: 'OpenVibeOpenGraph', domain: 'opengraph.openvibe.tools', icon: 'fa-share-nodes' },
@@ -2,6 +2,12 @@
@@ -2,6 +2,12 @@
// Tool names and descriptions live in server/seo/catalog.js so the UI, the page <title> and
// 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.
// the structured data can never disagree. Entries here keep their id/subdomain/icon/category.
+//
+// Host ownership: json., markdown., escape., diff., compare. and slug.openvibe.tools belong to
+// the Text app (apps/text), whose dedicated pages are the richer implementations and whose
+// nginx server_name wins over this gateway's wildcard anyway. The generic dev.html versions
+// of those tools live on their own hosts (jsonfmt., md., entities., codediff., slugify.) so no
+// host is claimed twice. curl. is the Net tool; the dev-side curl converter is curlconvert.
const { seoFor } = require('../seo/catalog');
const { seoFor } = require('../seo/catalog');
// ═══════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════
@@ -13,12 +19,12 @@ const DEV_TOOLS = [
@@ -13,12 +19,12 @@ const DEV_TOOLS = [
{ id: 'dev', subdomain: 'dev', name: 'Dev.OpenVibe', icon: 'fa-code', desc: 'Developer & SEO Tools Hub', hub: true, category: 'hub' },
{ id: 'dev', subdomain: 'dev', name: 'Dev.OpenVibe', icon: 'fa-code', desc: 'Developer & SEO Tools Hub', hub: true, category: 'hub' },
// Data & Formats
// Data & Formats
- { id: 'json', subdomain: 'json', name: 'OpenVibeJSON', icon: 'fa-code', desc: 'JSON formatter, validator, minifier & converter', category: 'data' },
+ { id: 'json', subdomain: 'jsonfmt', name: 'OpenVibeJSON', icon: 'fa-code', desc: 'JSON formatter, validator, minifier & converter', category: 'data' },
{ id: 'yaml', subdomain: 'yaml', name: 'OpenVibeYAML', icon: 'fa-file-code', desc: 'YAML formatter, validator & JSON converter', category: 'data' },
{ id: 'yaml', subdomain: 'yaml', name: 'OpenVibeYAML', icon: 'fa-file-code', desc: 'YAML formatter, validator & JSON converter', category: 'data' },
{ id: 'xml', subdomain: 'xml', name: 'OpenVibeXML', icon: 'fa-file-code', desc: 'XML formatter, validator & converter', category: 'data' },
{ id: 'xml', subdomain: 'xml', name: 'OpenVibeXML', icon: 'fa-file-code', desc: 'XML formatter, validator & converter', category: 'data' },
{ id: 'csv', subdomain: 'csv', name: 'OpenVibeCSV', icon: 'fa-table', desc: 'CSV viewer, converter & JSON transformer', category: 'data' },
{ id: 'csv', subdomain: 'csv', name: 'OpenVibeCSV', icon: 'fa-table', desc: 'CSV viewer, converter & JSON transformer', category: 'data' },
{ id: 'sql', subdomain: 'sql', name: 'OpenVibeSQL', icon: 'fa-database', desc: 'SQL formatter & syntax highlighter', category: 'data' },
{ id: 'sql', subdomain: 'sql', name: 'OpenVibeSQL', icon: 'fa-database', desc: 'SQL formatter & syntax highlighter', category: 'data' },
- { id: 'markdown', subdomain: 'markdown', name: 'OpenVibeMarkdown', icon: 'fa-file-lines', desc: 'Markdown editor with live HTML preview', category: 'data' },
+ { id: 'markdown', subdomain: 'md', name: 'OpenVibeMarkdown', icon: 'fa-file-lines', desc: 'Markdown editor with live HTML preview', category: 'data' },
{ id: 'html', subdomain: 'html', name: 'OpenVibeHTML', icon: 'fa-file-code', desc: 'HTML formatter, minifier & entity encoder', category: 'data' },
{ id: 'html', subdomain: 'html', name: 'OpenVibeHTML', icon: 'fa-file-code', desc: 'HTML formatter, minifier & entity encoder', category: 'data' },
// Encoding & Crypto
// Encoding & Crypto
@@ -28,7 +34,7 @@ const DEV_TOOLS = [
@@ -28,7 +34,7 @@ const DEV_TOOLS = [
{ id: 'uuid', subdomain: 'uuid', name: 'OpenVibeUUID', icon: 'fa-fingerprint', desc: 'UUID v4 generator, bulk generate & validator', category: 'encoding' },
{ id: 'uuid', subdomain: 'uuid', name: 'OpenVibeUUID', icon: 'fa-fingerprint', desc: 'UUID v4 generator, bulk generate & validator', category: 'encoding' },
{ id: 'hash', subdomain: 'hash', name: 'OpenVibeHash', icon: 'fa-hashtag', desc: 'SHA-1, SHA-256 & SHA-512 hash generator', category: 'encoding' },
{ id: 'hash', subdomain: 'hash', name: 'OpenVibeHash', icon: 'fa-hashtag', desc: 'SHA-1, SHA-256 & SHA-512 hash generator', category: 'encoding' },
{ id: 'hex', subdomain: 'hex', name: 'OpenVibeHex', icon: 'fa-barcode', desc: 'Hex encoder/decoder & binary converter', category: 'encoding' },
{ id: 'hex', subdomain: 'hex', name: 'OpenVibeHex', icon: 'fa-barcode', desc: 'Hex encoder/decoder & binary converter', category: 'encoding' },
- { id: 'escape', subdomain: 'escape', name: 'OpenVibeEscape', icon: 'fa-shield-halved', desc: 'HTML, JavaScript & URL escape/unescape', category: 'encoding' },
+ { id: 'escape', subdomain: 'entities', name: 'OpenVibeEscape', icon: 'fa-shield-halved', desc: 'HTML, JavaScript & URL escape/unescape', category: 'encoding' },
// Time & Scheduling
// Time & Scheduling
{ id: 'timestamp', subdomain: 'timestamp', name: 'OpenVibeTimestamp', icon: 'fa-clock', desc: 'Unix timestamp ↔ date converter with timezones', category: 'time' },
{ id: 'timestamp', subdomain: 'timestamp', name: 'OpenVibeTimestamp', icon: 'fa-clock', desc: 'Unix timestamp ↔ date converter with timezones', category: 'time' },
@@ -37,13 +43,13 @@ const DEV_TOOLS = [
@@ -37,13 +43,13 @@ const DEV_TOOLS = [
// Code Quality
// Code Quality
{ id: 'beautify', subdomain: 'beautify', name: 'OpenVibeBeautify', icon: 'fa-wand-magic-sparkles', desc: 'Beautify & format JS, HTML, CSS, JSON & SQL', category: 'quality' },
{ id: 'beautify', subdomain: 'beautify', name: 'OpenVibeBeautify', icon: 'fa-wand-magic-sparkles', desc: 'Beautify & format JS, HTML, CSS, JSON & SQL', category: 'quality' },
{ id: 'minify', subdomain: 'minify', name: 'OpenVibeMinify', icon: 'fa-compress', desc: 'Minify JS, HTML, CSS & JSON', category: 'quality' },
{ id: 'minify', subdomain: 'minify', name: 'OpenVibeMinify', icon: 'fa-compress', desc: 'Minify JS, HTML, CSS & JSON', category: 'quality' },
- { id: 'diff', subdomain: 'diff', name: 'OpenVibeDiff', icon: 'fa-code-compare', desc: 'Side-by-side text & code diff checker', category: 'quality' },
+ { id: 'diff', subdomain: 'codediff', name: 'OpenVibeDiff', icon: 'fa-code-compare', desc: 'Side-by-side text & code diff checker', category: 'quality' },
{ id: 'regex', subdomain: 'regex', name: 'OpenVibeRegex', icon: 'fa-magnifying-glass', desc: 'Regex tester with match highlighting & capture groups', category: 'quality' },
{ id: 'regex', subdomain: 'regex', name: 'OpenVibeRegex', icon: 'fa-magnifying-glass', desc: 'Regex tester with match highlighting & capture groups', category: 'quality' },
- { id: 'slug', subdomain: 'slug', name: 'OpenVibeSlug', icon: 'fa-link', desc: 'URL slug generator from any text', category: 'quality' },
+ { id: 'slug', subdomain: 'slugify', name: 'OpenVibeSlug', icon: 'fa-link', desc: 'URL slug generator from any text', category: 'quality' },
{ id: 'lorem', subdomain: 'lorem', name: 'OpenVibeLorem', icon: 'fa-paragraph', desc: 'Lorem ipsum & placeholder text generator', category: 'quality' },
{ id: 'lorem', subdomain: 'lorem', name: 'OpenVibeLorem', icon: 'fa-paragraph', desc: 'Lorem ipsum & placeholder text generator', category: 'quality' },
// HTTP & API
// HTTP & API
- { id: 'curl', subdomain: 'curl', name: 'OpenVibeCurl', icon: 'fa-terminal', desc: 'Parse curl commands & convert to fetch, Python, Node.js', category: 'http' },
+ { id: 'curl', subdomain: 'curlconvert', name: 'OpenVibeCurl', icon: 'fa-terminal', desc: 'Parse curl commands & convert to fetch, Python, Node.js', category: 'http' },
{ id: 'webhook', subdomain: 'webhook', name: 'OpenVibeWebhook', icon: 'fa-satellite-dish', desc: 'Webhook request inspector & debugging bin', category: 'http' },
{ id: 'webhook', subdomain: 'webhook', name: 'OpenVibeWebhook', icon: 'fa-satellite-dish', desc: 'Webhook request inspector & debugging bin', category: 'http' },
// Frontend & SEO
// Frontend & SEO
@@ -60,10 +66,9 @@ const DEV_ALIASES = {
@@ -60,10 +66,9 @@ const DEV_ALIASES = {
code: 'dev',
code: 'dev',
build: 'dev',
build: 'dev',
debug: 'dev',
debug: 'dev',
- compare: 'diff',
format: 'beautify',
format: 'beautify',
prettier: 'beautify',
prettier: 'beautify',
- md: 'markdown',
+ textdiff: 'codediff',
colours: 'color',
colours: 'color',
colors: 'color',
colors: 'color',
og: 'opengraph',
og: 'opengraph',
@@ -75,17 +80,16 @@ const DEV_ALIASES = {
@@ -75,17 +80,16 @@ const DEV_ALIASES = {
b64: 'base64',
b64: 'base64',
urlencode: 'url',
urlencode: 'url',
urldecode: 'url',
urldecode: 'url',
- entities: 'escape',
- htmlentities: 'escape',
+ htmlentities: 'entities',
checksum: 'hash',
checksum: 'hash',
sha256: 'hash',
sha256: 'hash',
sha1: 'hash',
sha1: 'hash',
sha512: 'hash',
sha512: 'hash',
- ini: 'json',
- toml: 'json',
- env: 'json',
- request: 'curl',
- http: 'curl',
+ ini: 'jsonfmt',
+ toml: 'jsonfmt',
+ env: 'jsonfmt',
+ request: 'curlconvert',
+ http: 'curlconvert',
};
};
module.exports = { DEV_TOOLS, DEV_TOOL_MAP, DEV_ALIASES };
module.exports = { DEV_TOOLS, DEV_TOOL_MAP, DEV_ALIASES };
@@ -30,7 +30,7 @@ const DEV = {
@@ -30,7 +30,7 @@ const DEV = {
bullets: ['Every tool works client-side: your data never leaves the browser', 'No signup and no rate limits on the offline tools', 'Open source and part of the wider OpenVibe network'],
bullets: ['Every tool works client-side: your data never leaves the browser', 'No signup and 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.']],
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: {
+ jsonfmt: {
name: 'JSON Formatter', title: 'JSON Formatter & Validator — Free Online JSON Beautifier',
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.',
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',
kw: 'json formatter, json validator, json beautifier, json minifier, pretty print json, json parser online',
@@ -70,7 +70,7 @@ const DEV = {
@@ -70,7 +70,7 @@ const DEV = {
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'],
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.']],
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: {
+ md: {
name: 'Markdown Editor', title: 'Markdown Editor with Live Preview — Free Online Markdown to HTML',
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.',
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',
kw: 'markdown editor, markdown to html, markdown preview, online markdown, readme preview',
@@ -134,7 +134,7 @@ const DEV = {
@@ -134,7 +134,7 @@ const DEV = {
bullets: ['Text ⇄ hex with UTF-8 handling', 'Hex ⇄ binary and decimal conversion', 'Accepts spaced, prefixed or continuous hex input'],
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.']],
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: {
+ entities: {
name: 'String Escape Tool', title: 'HTML, JavaScript & URL Escape Tool — Free Online Escaper',
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.',
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',
kw: 'escape html, unescape string, javascript escape, escape characters online, string escaper',
@@ -174,7 +174,7 @@ const DEV = {
@@ -174,7 +174,7 @@ const DEV = {
bullets: ['JavaScript, CSS, HTML and JSON in one place', 'Shows the size saved by the minification', 'Runs locally — no upload, no build step required'],
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.']],
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: {
+ codediff: {
name: 'Diff Checker', title: 'Diff Checker — Compare Text & Code Online, Free',
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.',
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',
kw: 'diff checker, compare text, text comparison, code diff, file compare online, difference checker',
@@ -190,7 +190,7 @@ const DEV = {
@@ -190,7 +190,7 @@ const DEV = {
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'],
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.']],
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: {
+ slugify: {
name: 'Slug Generator', title: 'URL Slug Generator — Free SEO-Friendly Slug Maker',
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.',
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',
kw: 'slug generator, url slug, seo friendly url, permalink generator, slugify text online',
@@ -206,7 +206,7 @@ const DEV = {
@@ -206,7 +206,7 @@ const DEV = {
bullets: ['Generate by word, sentence or paragraph count', 'Classic lorem ipsum text for familiar typographic colour', 'Copy straight into a design tool or HTML'],
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.']],
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: {
+ curlconvert: {
name: 'curl Converter', title: 'curl Converter — Convert curl to fetch, Python & Node.js, Free',
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.',
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',
kw: 'curl to fetch, curl converter, curl to python, curl to javascript, convert curl command',
@@ -165,7 +165,8 @@ function buildSitemap(extraUrls = []) {
@@ -165,7 +165,8 @@ function buildSitemap(extraUrls = []) {
/** Satellite apps that live outside the dev/net catalogs but belong in the sitemap. */
/** Satellite apps that live outside the dev/net catalogs but belong in the sitemap. */
const SATELLITES = [
const SATELLITES = [
'img', 'audio', 'docs', 'food', 'maps', 'yt', 'text', 'logo', 'pastes',
'img', 'audio', 'docs', 'food', 'maps', 'yt', 'text', 'logo', 'pastes',
- 'case', 'braille', 'binary', 'fancy', 'json', 'wordcount', 'reverse',
+ 'case', 'braille', 'binary', 'fancy', 'json', 'markdown', 'escape', 'diff', 'slug',
+ 'wordcount', 'reverse', 'reversetext',
].map(sub => ({ loc: `https://${sub}.openvibe.tools/`, priority: '0.8', changefreq: 'weekly' }));
].map(sub => ({ loc: `https://${sub}.openvibe.tools/`, priority: '0.8', changefreq: 'weekly' }));
module.exports = { renderTool, buildSitemap, SATELLITES, toolUrls, SITE };
module.exports = { renderTool, buildSitemap, SATELLITES, toolUrls, SITE };
@@ -1,6 +1,12 @@
@@ -1,6 +1,12 @@
# ═══════════════════════════════════════════════════════════════
# ═══════════════════════════════════════════════════════════════
# Text.OpenVibe + Logo.OpenVibe — Nginx Vhost
# Text.OpenVibe + Logo.OpenVibe — Nginx Vhost
# Single backend serving all text + logo subdomains.
# Single backend serving all text + logo subdomains.
+#
+# These exact server_names win over the gateway's *.openvibe.tools wildcard, so
+# json./markdown./escape./diff./compare./slug. resolve here (the dedicated text
+# pages) and the gateway's generic dev versions live on jsonfmt./md./entities./
+# codediff./slugify. instead. reverse.openvibe.tools belongs to the Audio vhost;
+# the text flipper is reversetext. / mirror. Keep in step with server/index.js.
# ═══════════════════════════════════════════════════════════════
# ═══════════════════════════════════════════════════════════════
# HTTP → HTTPS redirect
# HTTP → HTTPS redirect
@@ -15,6 +21,7 @@ server {
@@ -15,6 +21,7 @@ server {
gothic.openvibe.tools wide.openvibe.tools monospaced.openvibe.tools
gothic.openvibe.tools wide.openvibe.tools monospaced.openvibe.tools
braille.openvibe.tools morse.openvibe.tools binary.openvibe.tools
braille.openvibe.tools morse.openvibe.tools binary.openvibe.tools
case.openvibe.tools caps.openvibe.tools titlecase.openvibe.tools
case.openvibe.tools caps.openvibe.tools titlecase.openvibe.tools
+ reversetext.openvibe.tools mirror.openvibe.tools
clean.openvibe.tools strip.openvibe.tools
clean.openvibe.tools strip.openvibe.tools
count.openvibe.tools lines.openvibe.tools sort.openvibe.tools
count.openvibe.tools lines.openvibe.tools sort.openvibe.tools
dedupe.openvibe.tools slug.openvibe.tools compare.openvibe.tools
dedupe.openvibe.tools slug.openvibe.tools compare.openvibe.tools
@@ -49,6 +56,7 @@ server {
@@ -49,6 +56,7 @@ server {
gothic.openvibe.tools wide.openvibe.tools monospaced.openvibe.tools
gothic.openvibe.tools wide.openvibe.tools monospaced.openvibe.tools
braille.openvibe.tools morse.openvibe.tools binary.openvibe.tools
braille.openvibe.tools morse.openvibe.tools binary.openvibe.tools
case.openvibe.tools caps.openvibe.tools titlecase.openvibe.tools
case.openvibe.tools caps.openvibe.tools titlecase.openvibe.tools
+ reversetext.openvibe.tools mirror.openvibe.tools
clean.openvibe.tools strip.openvibe.tools
clean.openvibe.tools strip.openvibe.tools
count.openvibe.tools lines.openvibe.tools sort.openvibe.tools
count.openvibe.tools lines.openvibe.tools sort.openvibe.tools
dedupe.openvibe.tools slug.openvibe.tools compare.openvibe.tools
dedupe.openvibe.tools slug.openvibe.tools compare.openvibe.tools
@@ -96,11 +96,11 @@
@@ -96,11 +96,11 @@
<p>Fix smart quotes, remove invisible characters, strip Zalgo, normalize spacing, clean HTML tags.</p>
<p>Fix smart quotes, remove invisible characters, strip Zalgo, normalize spacing, clean HTML tags.</p>
<span class="card-domain">clean.openvibe.tools</span>
<span class="card-domain">clean.openvibe.tools</span>
</a>
</a>
- <a href="https://reverse.openvibe.tools" class="tool-card">
+ <a href="https://reversetext.openvibe.tools" class="tool-card">
<div class="card-icon"><i class="fas fa-rotate-left"></i></div>
<div class="card-icon"><i class="fas fa-rotate-left"></i></div>
<h3>Reverse & Flip Text</h3>
<h3>Reverse & Flip Text</h3>
<p>Reverse, flip upside down, mirror text. Works with full Unicode support.</p>
<p>Reverse, flip upside down, mirror text. Works with full Unicode support.</p>
- <span class="card-domain">reverse.openvibe.tools</span>
+ <span class="card-domain">reversetext.openvibe.tools</span>
</a>
</a>
<a href="https://kaomoji.openvibe.tools" class="tool-card">
<a href="https://kaomoji.openvibe.tools" class="tool-card">
<div class="card-icon"><i class="fas fa-face-smile-beam"></i></div>
<div class="card-icon"><i class="fas fa-face-smile-beam"></i></div>
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
<head>
<head>
<meta charset="UTF-8">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Reverse & Flip Text — Upside Down, Mirror | reverse.openvibe.tools</title>
+ <title>Reverse & Flip Text — Upside Down, Mirror | reversetext.openvibe.tools</title>
<meta name="description" content="Reverse text, flip it upside down, or mirror it. Full Unicode support with combining character awareness.">
<meta name="description" content="Reverse text, flip it upside down, or mirror it. Full Unicode support with combining character awareness.">
<meta name="theme-color" content="#0b0c12">
<meta name="theme-color" content="#0b0c12">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.googleapis.com">
@@ -62,6 +62,10 @@ app.use(analytics.middleware());
@@ -62,6 +62,10 @@ app.use(analytics.middleware());
// ── Hostname → HTML file mapping ─────────────────────────────
// ── Hostname → HTML file mapping ─────────────────────────────
// Each subdomain gets its own static HTML page for SEO + focused UX.
// Each subdomain gets its own static HTML page for SEO + focused UX.
+// Every host here must also be in deploy/nginx/text.openvibe.tools.conf, and must not be
+// claimed by another app's vhost or by the gateway's net/dev catalogs: json., markdown.,
+// escape., diff., compare. and slug. are ours (the gateway's generic versions moved to
+// jsonfmt., md., entities., codediff. and slugify.).
const HOSTNAME_MAP = {
const HOSTNAME_MAP = {
// Text hubs
// Text hubs
'text.openvibe.tools': 'index.html',
'text.openvibe.tools': 'index.html',
@@ -87,7 +91,10 @@ const HOSTNAME_MAP = {
@@ -87,7 +91,10 @@ const HOSTNAME_MAP = {
'case.openvibe.tools': 'case.html',
'case.openvibe.tools': 'case.html',
'caps.openvibe.tools': 'case.html',
'caps.openvibe.tools': 'case.html',
'titlecase.openvibe.tools': 'case.html',
'titlecase.openvibe.tools': 'case.html',
- 'reverse.openvibe.tools': 'reverse.html',
+ // reverse.openvibe.tools is the Audio app's (reverse an audio file); the text flipper
+ // lives on reversetext. + mirror. instead. Keep this in step with deploy/nginx.
+ 'reversetext.openvibe.tools':'reverse.html',
+ 'mirror.openvibe.tools': 'reverse.html',
'clean.openvibe.tools': 'clean.html',
'clean.openvibe.tools': 'clean.html',
'strip.openvibe.tools': 'clean.html',
'strip.openvibe.tools': 'clean.html',
'count.openvibe.tools': 'count.html',
'count.openvibe.tools': 'count.html',