openvibers/openvibe.tools Commits

Host routing: text keeps json/markdown/escape/diff/compare/slug, gateway dev tools move to unique hosts

OP OpenVibers committed 18 Sept 2026 · ca2f6011a1 · 9 files, +66 −46

The Text app's dedicated pages are the richer implementations and its exact
nginx server_names already beat the gateway wildcard, so the gateway's generic
dev.html versions now live on jsonfmt., md., entities., codediff., slugify.
(and the dev curl converter on curlconvert., which was shadowed by the Net
curl tool). reverse.openvibe.tools belongs to the Audio vhost; the text
flipper is reversetext. / mirror. in HOSTNAME_MAP and the text nginx conf.

Changes

ddiff --git a/apps/gateway/public/dev.html b/apps/gateway/public/dev.html
iindex 4fa31e3..f13594e 100644
---- a/apps/gateway/public/dev.html
++++ b/apps/gateway/public/dev.html
@@@ -151,12 +151,12 @@ H=s=>s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').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.rem
function 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-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('');
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
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}
ddiff --git a/apps/gateway/public/index.html b/apps/gateway/public/index.html
iindex b32a29e..94b2a63 100644
---- a/apps/gateway/public/index.html
++++ b/apps/gateway/public/index.html
@@@ -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' },
ddiff --git a/apps/gateway/server/dev/config.js b/apps/gateway/server/dev/config.js
iindex bd8a75a..917894d 100644
---- a/apps/gateway/server/dev/config.js
++++ b/apps/gateway/server/dev/config.js
@@@ -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 };
ddiff --git a/apps/gateway/server/seo/catalog.js b/apps/gateway/server/seo/catalog.js
iindex df1cc5e..82e8440 100644
---- a/apps/gateway/server/seo/catalog.js
++++ b/apps/gateway/server/seo/catalog.js
@@@ -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',
ddiff --git a/apps/gateway/server/seo/render.js b/apps/gateway/server/seo/render.js
iindex 62d7a33..80f1a8b 100644
---- a/apps/gateway/server/seo/render.js
++++ b/apps/gateway/server/seo/render.js
@@@ -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 };
ddiff --git a/apps/text/deploy/nginx/text.openvibe.tools.conf b/apps/text/deploy/nginx/text.openvibe.tools.conf
iindex befd0ee..f5bb3a4 100644
---- a/apps/text/deploy/nginx/text.openvibe.tools.conf
++++ b/apps/text/deploy/nginx/text.openvibe.tools.conf
@@@ -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.tools
braille.openvibe.tools morse.openvibe.tools binary.openvibe.tools
case.openvibe.tools caps.openvibe.tools titlecase.openvibe.tools
++ reversetext.openvibe.tools mirror.openvibe.tools
clean.openvibe.tools strip.openvibe.tools
count.openvibe.tools lines.openvibe.tools sort.openvibe.tools
dedupe.openvibe.tools slug.openvibe.tools compare.openvibe.tools
@@@ -49,6 +56,7 @@ server {
gothic.openvibe.tools wide.openvibe.tools monospaced.openvibe.tools
braille.openvibe.tools morse.openvibe.tools binary.openvibe.tools
case.openvibe.tools caps.openvibe.tools titlecase.openvibe.tools
++ reversetext.openvibe.tools mirror.openvibe.tools
clean.openvibe.tools strip.openvibe.tools
count.openvibe.tools lines.openvibe.tools sort.openvibe.tools
dedupe.openvibe.tools slug.openvibe.tools compare.openvibe.tools
ddiff --git a/apps/text/public/index.html b/apps/text/public/index.html
iindex b88f065..287c520 100644
---- a/apps/text/public/index.html
++++ b/apps/text/public/index.html
@@@ -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>
ddiff --git a/apps/text/public/reverse.html b/apps/text/public/reverse.html
iindex 30e87fa..2fc09d0 100644
---- a/apps/text/public/reverse.html
++++ b/apps/text/public/reverse.html
@@@ -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">
ddiff --git a/apps/text/server/index.js b/apps/text/server/index.js
iindex e59cf2a..5d2ae1e 100644
---- a/apps/text/server/index.js
++++ b/apps/text/server/index.js
@@@ -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',