Building a Code Sharing Tool: URL Hash Storage and Base64 Encoding Techniques
Building a Code Sharing Tool: URL Hash Storage and Base64 Encoding Techniques#
Recently, I built a code sharing tool that requires a “no backend, permanent validity” sharing mechanism. After researching several approaches, I chose URL Hash + Base64 encoding - simple and reliable.
Why URL Hash Storage?#
Traditional code sharing tools use two approaches:
- Backend database storage - Store code in database, generate short links. Pros: short links. Cons: needs backend, storage cost, links can expire.
- Client-side storage - Use localStorage or IndexedDB. Pros: no backend. Cons: local-only, can’t share with others.
URL Hash is a third way: encode data into the URL itself. The link IS the data. When users open the link, code is decoded from the URL - no backend needed.
// Core approach
const shareData = {
code: base64Encode(codeContent),
title: 'My Code Snippet',
language: 'javascript'
}
const url = `${window.location.href}#${base64Encode(JSON.stringify(shareData))}`
// Example: https://jsokit.com/tools/code-share#eyJjb2RlIjoi...
Key advantages:
- Permanent - Data lives in the link, no server dependency
- Zero cost - No database, no storage, pure frontend
- Privacy - Data never touches the server
Base64 Encoding with Unicode Support#
Base64 encoding is simple, but non-Latin characters break it. Standard btoa() only supports Latin1:
btoa('Hello') // SGVsbG8=
btoa('你好') // Uncaught DOMException: Failed to execute 'btoa'
The fix uses encodeURIComponent + String.fromCharCode:
function safeBtoa(str: string): string {
// Convert UTF-8 chars to %XX, then to Latin1, then base64
return btoa(
encodeURIComponent(str).replace(
/%([0-9A-F]{2})/g,
(_, p1) => String.fromCharCode(parseInt(p1, 16))
)
)
}
function safeAtob(base64: string): string {
// Reverse: base64 → Latin1 → %XX → UTF-8
return decodeURIComponent(
atob(base64)
.split('')
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
)
}
// Test
safeBtoa('你好世界') // JUU0JUJEJUEwJUU1JUE1JUJEJUU0JUI4JTgx
safeAtob('JUU0JUJEJUEwJUU1JUE1JUJEJUU0JUI4JTgx') // 你好世界
The essence: UTF-8 → percent-encoding → Latin1 → Base64. Reverse for decoding.
URL Length Limits and Optimization#
The main constraint is URL length. While HTTP specs don’t define max URL length, browsers do:
- Chrome: ~2MB (tested)
- Firefox: ~65,536 chars
- Safari: ~80,000 chars
- IE: 2,083 chars (obsolete)
In practice, 10KB code encodes to ~13KB URL, well within limits. For larger files, compress first:
import { gzipSync, gunzipSync } from 'fflate'
function compressCode(code: string): string {
const compressed = gzipSync(new TextEncoder().encode(code))
return btoa(String.fromCharCode(...compressed))
}
function decompressCode(base64: string): string {
const compressed = Uint8Array.from(atob(base64), c => c.charCodeAt(0))
const decompressed = gunzipSync(compressed)
return new TextDecoder().decode(decompressed)
}
Gzip compression reduces size by 60-70%, supporting larger files.
Loading Data on Page Load#
When users open a shared link, extract data from URL Hash:
useEffect(() => {
const loadSharedCode = () => {
try {
// 1. Get hash (remove leading #)
const hash = window.location.hash.slice(1)
if (!hash) return
// 2. Base64 decode to JSON string
const jsonStr = safeAtob(hash)
const shareData = JSON.parse(jsonStr)
// 3. Decode code content (double-encoded)
const code = safeAtob(shareData.code)
// 4. Set state
setCode(code)
setTitle(shareData.title)
setLanguage(shareData.language)
setIsViewMode(true)
} catch (err) {
console.error('Failed to parse link:', err)
setError('Share link is corrupted or invalid')
}
}
loadSharedCode()
}, [])
Note the double encoding: safeBtoa(code) first, then safeBtoa(JSON.stringify(shareData)). This avoids special characters in JSON affecting Base64 encoding.
Code Formatting Implementation#
Code sharing tools need formatting for readability. Different languages need different strategies:
function formatCode(code: string, language: string): string {
switch (language) {
case 'json':
// JSON.stringify has built-in formatting
return JSON.stringify(JSON.parse(code), null, 2)
case 'javascript':
case 'typescript':
// Simple regex replacement
return code
.replace(/;/g, ';\n')
.replace(/{/g, ' {\n ')
.replace(/}/g, '\n}')
.replace(/,\s*/g, ', ')
case 'html':
return code
.replace(/>\s*</g, '>\n<') // Newline between tags
.replace(/\n\s*\n/g, '\n') // Remove extra blank lines
case 'sql':
const keywords = ['SELECT', 'FROM', 'WHERE', 'ORDER BY']
let formatted = code
keywords.forEach((keyword, index) => {
if (index > 0) {
formatted = formatted.replace(
new RegExp(`\\b${keyword}\\b`, 'gi'),
`\n${keyword}`
)
}
})
return formatted
default:
// Generic formatting
return code.split('\n').map(line => line.trim()).join('\n')
}
}
For production, use professional libraries:
- JavaScript/TypeScript:
prettier - Python:
blackorautopep8 - Go:
gofmt - Rust:
rustfmt
Theme Rendering and Styling#
The tool supports multiple themes (Monokai, Dracula, Nord). Implementation is straightforward:
const THEMES = [
{
value: 'monokai',
bg: 'bg-[#272822]',
text: 'text-[#f8f8f2]'
},
{
value: 'dracula',
bg: 'bg-[#282a36]',
text: 'text-[#f8f8f2]'
}
]
function CodePreview({ code, theme }: Props) {
const current = THEMES.find(t => t.value === theme)
return (
<div className={`${current.bg} ${current.text}`}>
<pre className="font-mono text-sm">
{code.split('\n').map((line, i) => (
<div key={i}>
<span className="opacity-30">{i + 1}</span>
<span>{line}</span>
</div>
))}
</pre>
</div>
)
}
Line numbers are more elegant with CSS counter-increment:
.line-numbers {
counter-reset: line;
}
.line-numbers > div::before {
counter-increment: line;
content: counter(line);
display: inline-block;
width: 3em;
text-align: right;
margin-right: 1em;
color: rgba(255, 255, 255, 0.3);
}
Edge Cases in Practice#
While building JsonKit Code Share, I encountered several edge cases worth noting:
1. Special Characters in URL#
Base64 strings may contain +, /, = which have special meaning in URLs. Though the hash fragment (# onwards) isn’t parsed by browsers, use URL-safe Base64 to be safe:
function base64ToUrlSafe(base64: string): string {
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
}
function urlSafeToBase64(urlSafe: string): string {
let base64 = urlSafe.replace(/-/g, '+').replace(/_/g, '/')
// Restore padding
while (base64.length % 4) base64 += '='
return base64
}
2. Large File Handling#
Though URLs support 2MB, long links cause issues:
- WeChat/DingTalk truncates
- Email clients may truncate
- QR code generators have limits
Add a size check:
function generateShareLink(code: string): string {
const encoded = safeBtoa(code)
const url = `${baseUrl}#${encoded}`
if (url.length > 100 * 1024) { // 100KB
alert('Code too long. Consider compression or use Gist')
return ''
}
return url
}
3. Clipboard API Compatibility#
navigator.clipboard.writeText() requires HTTPS or user interaction in some browsers:
async function copyToClipboard(text: string) {
try {
await navigator.clipboard.writeText(text)
} catch {
// Fallback: create temporary textarea
const textarea = document.createElement('textarea')
textarea.value = text
textarea.style.position = 'fixed'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
}
}
Summary#
The URL Hash + Base64 code sharing approach offers “no backend, permanent, zero cost”. Key technical points:
- Unicode-safe Base64 encoding (
encodeURIComponent+String.fromCharCode) - Double encoding to avoid JSON special character conflicts
- URL length limits and compression optimization
- Multi-theme rendering with line numbers
This approach works well for small-to-medium code snippets. For large projects, consider Gist or GitLab Snippets.
Related: Code Formatter | Code Minifier