Build a Serverless Text Editor with URL Storage – textarea.my
The text editor uses contenteditable elements, the Compression Streams API, and debounced auto-save to create a functional note-taking experience. It’s great for developers building shareable text tools, browser-based documentation, or offline-first applications.
1. Create a contenteditable element in your HTML. The plaintext-only attribute limits formatting to plain text.
<!-- The main editor element --> <article > 2. The core JavaScript logic handles compression, state management, and event listeners.// Select the contenteditable element const article = document.querySelector('article') // Listen for user input and trigger debounced save article.addEventListener('input', debounce(500, save)) // Load content when the page loads addEventListener('DOMContentLoaded', load) // Load content when the URL hash changes addEventListener('hashchange', load) // Load function: retrieves content from URL hash or localStorage async function load() { try { // Check if URL has a hash (shared content) if (location.hash !== '') { await set(location.hash) } else { // Fall back to localStorage await set(localStorage.getItem('hash') ?? '') // If content exists, update URL without creating history entry if (article.textContent) { history.replaceState({}, '', await get()) } // Focus the editor for immediate typing article.focus() } } catch (e) { // Handle decompression errors by resetting content article.textContent = '' article.removeAttribute('style') } // Update page title based on content updateTitle() } // Save function: compresses content and updates URL + localStorage async function save() { const hash = await get() // Update URL only if hash has changed if (location.hash !== hash) { history.replaceState({}, '', hash) } // Attempt to save to localStorage (may fail if quota exceeded) try { localStorage.setItem('hash', hash) } catch (e) { // Silent fail - URL storage remains functional } // Update page title after save updateTitle() } // Set function: decompresses hash and populates editor async function set(hash) { // Decompress the base64-encoded hash const [content, style] = (await decompress(hash.slice(1))).split('x00') // Set the text content article.textContent = content // Apply saved styles if they exist if (style) { article.setAttribute('style', style) } } // Get function: compresses current content into hash format async function get() { const style = article.getAttribute('style') // Combine content and style with null separator const content = article.textContent + (style !== null ? 'x00' + style : '') // Return compressed hash with # prefix return '#' + await compress(content) } // Update page title from markdown-style header function updateTitle() { // Match first line starting with # (markdown header) const match = article.textContent.match(/^n*#(.+)n/) // Set title to header text or default document.title = match?.[1] ?? 'Textarea' } // Compress function: converts string to base64-encoded deflate data async function compress(string) { // Encode string to byte array const byteArray = new TextEncoder().encode(string) // Create deflate-raw compression stream const stream = new CompressionStream('deflate-raw') const writer = stream.writable.getWriter() // Write data and close stream writer.write(byteArray) writer.close() // Read compressed output const buffer = await new Response(stream.readable).arrayBuffer() // Convert to URL-safe base64 (replace + and / characters) return new Uint8Array(buffer) .toBase64() .replace(/+/g, "-") .replace(///g, "_") } // Decompress function: converts base64 to original string async function decompress(b64) { // Convert URL-safe base64 back to standard format const byteArray = Uint8Array.fromBase64( b64.replace(/-/g, "+").replace(/_/g, "/") ) // Create decompression stream const stream = new DecompressionStream('deflate-raw') const writer = stream.writable.getWriter() // Write compressed data and close writer.write(byteArray) writer.close() // Read decompressed output const buffer = await new Response(stream.readable).arrayBuffer() // Decode byte array back to string return new TextDecoder().decode(buffer) } // Debounce function: delays execution until user stops typing function debounce(ms, fn) { let timer return (...args) => { clearTimeout(timer) timer = setTimeout(() => fn(...args), ms) } }3. You can set a custom page title by starting your document with a markdown-style header. The editor automatically extracts the first line after `#` and uses it as the page title.
# CSSScript.com Content goes here4. To add custom styling that persists in the URL, open DevTools and add inline styles to the article element:
document.querySelector('article').setAttribute('style', 'font-family: monospace; color: #00ff00; background: #000;' )FAQs:
Q: What happens when the compressed URL exceeds browser length limits?
A: Most browsers support URLs up to 2,000 characters. The deflate compression typically achieves 40-60% size reduction. This means you can store roughly 3,000-5,000 characters of plain text before hitting limits.Q: Why does localStorage fail sometimes?
A: Browsers enforce storage quotas, typically 5-10MB per origin. If other scripts have filled localStorage, the save attempt throws an exception.Q: Can I encrypt the content before compression?
A: Yes, but you’ll need to add encryption logic before the compression step. The Web Crypto API provides AES-GCM encryption. You’d encrypt the plaintext, then compress the encrypted bytes, then base64 encode. The recipient would need the decryption key to read the content.The post Build a Serverless Text Editor with URL Storage – textarea.my appeared first on CSS Script.
You may recognize Jonathan Djob Nkondo's work from animated projects like the surreal sci-fi series…
A new weekend has arrived, and today, you can save big on LEGO Star Wars,…
The Michigan synagogue that came under attack this week when an armed man drove his car into…
They look like your average open earbuds, but with optional RGB LED effects. | Photo…
200 Years Ago By virtue of a warrant from the selectmen of the town of…
Ally Connor, back, and Eva Dentremont, bottom, lounge with Lincoln on their porch as the…
This website uses cookies.