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:

  1. Backend database storage - Store code in database, generate short links. Pros: short links. Cons: needs backend, storage cost, links can expire.
  2. 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: black or autopep8
  • 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