⏱️ Reading time: 16 min
bigwords.page has no database, no user registration, and doesn’t store a single byte on a server: every sign, timer, or wifi password it generates lives entirely inside the link you share. The key is the URL fragment, the part that comes after the hash sign (#), which the browser never sends to any server. This article explains step by step how that technique works and how you can use it to build your own backend-free tools, with no accounts, that are 100% shareable by link.
📑 En este artículo
TL;DR
- The browser never sends the URL fragment (everything after #) to any server, as defined by RFC 3986.
- bigwords.page encodes text, color, typography, and slide interval in a single URL, with no backend or database.
- window.location.hash and the History API let you read and update that state without reloading the page.
- A qr= parameter generates complete QR codes in the browser, without asking anything of an external server.
- Building your own serverless tool takes fewer than 40 lines of plain JavaScript.
What Is the URL Fragment?
The URL fragment is the portion of a link that starts with the hash sign (#) and that, according to RFC 3986, the browser processes entirely on the client side: it never travels in the HTTP request. bigwords.page takes advantage of that property to store the app’s entire state there: text, color, typography, and slide interval.
Originally the fragment was used to jump to a section within the same page, that old trick of #introduccion that takes you to an <h2 id='introduccion'>. The difference with the query string, the part that starts with ?, is key. The query string is sent to the server on every request, which is why it shows up in access logs; the fragment, on the other hand, only exists in the browser’s memory.
Why It Matters: Zero Backend, Zero Accounts
When an app’s entire state fits in a link, there’s no need for a server to store it. bigwords.page is a static file: HTML, CSS, and JavaScript that you can serve from any CDN without worrying about databases, backups, or migrations. Infrastructure cost approaches zero because there’s nothing to scale on the server side. Every visitor does all the work in their own browser.
There’s no need for accounts either. To store a café’s wifi password or a flight’s schedule, you don’t need the user to log in, because the link already contains the data. This simplifies the entire flow: creating, editing, and sharing are all the same action of copying a URL.
There’s an almost accidental privacy benefit. Since the fragment never leaves the browser, it’s never logged on any server or stored in any external database. That doesn’t mean the data is secret, since anyone with the link can read it just the same, but it does mean that no one other than the user’s browser ever processes that content.
This same logic explains why teams organizing events or temporary signage prefer this technique over building an admin panel. A sign for a one-day conference doesn’t justify paying for a backend that will be used for just a few hours, and a link that anyone can generate, edit, and reshare in seconds is enough.
💡 Tip: if you need data to be genuinely private, the URL fragment isn’t enough: anyone with the full link can access everything. That requires additional encryption, not just skipping the server.
How It Works Under the Hood
bigwords.page’s scheme reuses a syntax similar to the query string’s, but placed after the #. Everything after the hash sign falls inside the URL fragment, and the browser processes it without asking anything of any server. The first segment, before the first &, is the message; the rest are key=value pairs separated by &, just like in a classic HTML form.
Since the message can contain spaces, line breaks, or the hash sign itself, everything goes through percent-encoding first: a space is written as %20, a line break as %0A, and a literal #, to write a markdown heading, is written as %23. bigwords.page chose || as the slide separator because that character combination never appears in any standard percent-encoding, so it never accidentally collides with the message content.
The markdown the site supports is deliberately minimal. A function replaces every **text** with <strong>text</strong> using a regular expression, and every already-decoded %0A becomes a real line break inside the HTML. You don’t need a full markdown parser when the vocabulary you need to cover is this small.
The interval between slides (&interval=3) and the animations (&anim=pulse, &anim=scroll, &anim=rainbow) work on the same principle: they’re CSS classes that the script activates based on the value read from the fragment, with a setInterval() that rotates the slide array every N seconds. There’s nothing on the server deciding when to switch slides, the entire timer lives in the browser in front of you.
The most striking case is images. The img= parameter doesn’t point to a file on a server: it receives a data URI, meaning the entire image encoded in base64 inside the same link. That way, even a café’s logo for its wifi sign lives inside the URL, with no external file that could break or disappear.
flowchart TD
A["Full URL"] --> B["Before the #: sent to the server"]
A --> C["After the #: fragment, browser only"]
B --> D[("Server: serves static HTML and JS")]
C --> E["Local JavaScript decodes text, color, and typography"]
The complete sequence, end to end, never includes a second trip to the server to get the sign’s content:
sequenceDiagram
participant U as User
participant S as Server
participant N as Browser
U->>S: GET /index.html (no fragment)
S-->>N: Static HTML and JS
Note over N: The browser reads the fragment locally
N->>N: Decodes message, color, and typography
N-->>U: Renders the complete sign
Practical Examples
Before touching anything in bigwords.page, separate the two questions the technique solves: how to read the current fragment, and how to turn it into something renderable. The following code snippets show the full path, from the simplest to the closest to what runs in production.
console.log(window.location.hash);
// if the URL is https://ejemplo.com/#Hello%20World
// the console prints: "#Hello%20World"
Note that location.hash includes the # symbol. To separate the message from the parameters, you need to split the string at the first & and decode each part separately:
const raw = window.location.hash.slice(1); // removes the #
const [mensajeCrudo, ...resto] = raw.split('&');
const mensaje = decodeURIComponent(mensajeCrudo);
const params = new URLSearchParams(resto.join('&'));
console.log(mensaje); // "Hello World"
console.log(params.get('bg')); // "111111"
With mensaje and params already separated, building the slides is just a matter of splitting by ||:
const slides = mensaje.split('||');
console.log(slides);
// ["Coffee", "Tea", "Water"]
// for the URL #Coffee||Tea||Water&interval=2
With those pieces, separating the message from the parameters, decoding, and splitting by ||, you already have the complete logic bigwords.page uses to turn a plain URL into an interactive sign. The rest is style: typography, colors, and animations applied on top of that same already-decoded text.
Getting Started: Build Your Own Mini-bigwords
You don’t need to install anything to try this technique: an .html file and a browser are enough. If you also want to open the link from another device on your local network, to test it on a phone or a smart TV, install Node.js and run npx serve . from the project folder; the command is identical on Windows, macOS, and Linux.
Save this file as index.html:
<!DOCTYPE html>
<html lang='es'>
<body style='margin:0;height:100vh;display:flex;align-items:center;justify-content:center;'>
<div id='cartel' style='font-size:10vw;text-align:center;'></div>
<script>
function render() {
const raw = location.hash.slice(1);
const [textoCrudo, ...resto] = raw.split('&');
const params = new URLSearchParams(resto.join('&'));
const texto = decodeURIComponent(textoCrudo || 'Hello%20World').replace(/%0A/g, '\n');
document.body.style.background = '#' + (params.get('bg') || '111111');
const cartel = document.getElementById('cartel');
cartel.style.color = '#' + (params.get('fg') || 'ffd60a');
cartel.textContent = texto;
}
render();
window.addEventListener('hashchange', render);
</script>
</body>
</html>
Open the file in the browser and add #Hello%20World&bg=111111&fg=ffd60a to the end of the URL in the address bar. The expected result: an almost black background (#111111) with the text “Hello World” in yellow (#ffd60a), centered and filling the whole screen. Change the fragment without reloading the page, for example to #Another%20Message, and the hashchange listener updates the sign instantly.
Real-World Use Cases
The technique works best when the content is temporary and the destination is a large screen or a link shared only once. Some examples documented on the site itself:
- Welcome sign: a message with custom background and typography to greet someone at the airport, ready to display full screen from a phone.
- Exam or pitch timer: a countdown with
{countdown}and a different final message when it reaches zero, using&timer=. - Wifi password at a café: the network name and password in large text, with a QR code generated in the browser via
&qr=so customers just scan it. - Room sign or boarding gate: a static sign like “Gate 12: Boarding now” that anyone can put together without asking anything of a corporate signage system.
- QR code menu: a minimalist sign inviting people to scan to see the menu, pointing to another URL, with nothing new to print every time a price changes.
- Event day agenda: several slides separated by
||with the schedule, rotating every few seconds on a screen at the entrance.
Common Mistakes and Best Practices
The most common mistake is forgetting percent-encoding: writing a literal & or # inside the message breaks the parsing, because the browser and the script itself interpret them as separators instead of text. The simple rule is to always use encodeURIComponent() before building the link by hand.
Another problem comes up when sharing the link through URL shorteners: not all of them preserve the complete fragment through the redirect. Before sharing a shortened link with critical content, a wifi password for example, test it first in a new tab.
There’s also a practical length limit. There’s no fixed cap in the standard, but browsers and QR readers do impose real limits: a high-capacity QR code supports about 4,000 alphanumeric characters in its densest version, so a very long message with a base64 image can exceed that margin and stop being scannable.
A third problem comes up when copying and pasting a link by hand from a text document: some editors convert the space encoded as %20 into a + sign if they interpret it as part of a classic form, and bigwords.page doesn’t expect that format. Copy the complete link from the address bar, never type it from memory.
Test the link on more than one channel before relying on it for a live event: sharing it via SMS, WhatsApp, or a QR code imposes different length limits on each. If the final link exceeds a few thousand characters, for example because of a heavy base64 image, some of those channels may truncate or reject it before it reaches its destination.
⚠️ Heads up: the URL fragment is not private. Anyone who sees the full link, in the browser history, in a screenshot, or forwarded in a chat, reads exactly the same content you do.
Comparison with Alternatives
Storing state on the client isn’t a new idea: the real question is where each type of data should go, based on who needs to read it and for how long. The table compares the URL hash with the other three most common options in a web app.
| Option | When to use it | Advantage | Limitation |
|---|---|---|---|
| URL fragment (#) | Content to share by link, no accounts | Never reaches the server; zero backend | Not private and not indexable by search engines |
| Query string (?) | Parameters the server needs to read (filters, tracking) | The server can process and log it | Sent on every request; shows up in access logs |
| localStorage | Preferences for a single device, not shared | Persists between sessions without touching the URL | Doesn’t travel between devices and can’t be shared by link |
| Server + database | Data that must be private, editable by multiple users, or queryable | Full control over access and queries | Requires a backend, accounts, and ongoing maintenance |
Going Deeper
Updating the fragment without reloading the page or cluttering the browsing history has two approaches. Directly assigning location.hash = 'new value' fires the hashchange event and adds an entry to the history, the “back” button returns to the previous state. If instead you use history.replaceState(null, '', '#new value'), from the History API, the fragment changes without creating a new entry, useful when the user drags a color slider and you don’t want every movement recorded in the history.
There’s a limitation to know about before using this technique for anything that needs to show up in social media previews: the bots that generate those cards (Telegram, WhatsApp, Slack) request the URL from the server without executing JavaScript, so they never see the content living in the URL fragment. That’s why bigwords.page works perfectly as a personal tool or for sharing in a chat, but it doesn’t automatically generate a rich preview with the sign’s text.
To confirm in practice that the fragment never leaves the browser, open developer tools, the Network tab, and load any bigwords.page link: the GET request that shows up in the list ends in /, with no trace of what comes after the #. If you run your own local HTTP server and check its access log, you’ll see exactly the same thing: the fragment never shows up there.
The same idea of not sending the server what should stay in the browser shows up in other well-known tools. PrivateBin encrypts text on the client side and stores only the encrypted version on its server; the decryption key travels in the fragment, so not even the service operator can read the content without the full link. bigwords.page doesn’t encrypt anything, because the content isn’t meant to be secret, but it shares the same design principle: everything sensitive to the experience lives on the client side.
That same design has a cost for discoverability. A search engine that indexes the web sees bigwords.page’s root URL, but not each individual sign, because each one’s content lives in a fragment that varies by user and isn’t part of the document the server delivers. It’s the logical trade-off of having no backend: what you gain in infrastructure simplicity, you lose in every sign being, by design, invisible to a search engine.
flowchart LR
A["Message after the #"] --> B["%0A = line break"]
A --> C["%23 = literal hash sign"]
A --> D["double slash separates slides"]
B --> E["Parameters with &key=value"]
C --> E
D --> E
E --> F["Browser renders the sign"]
Your next step: copy the HTML from the “Getting Started” section, open it in the browser, and add your own &anim=rainbow or &timer=30s until you understand how each parameter changes the render.
Frequently Asked Questions
What’s the difference between the URL fragment and the query string?
The query string, what comes after ?, is sent to the server on every HTTP request and gets logged there. The fragment, what comes after #, is processed entirely on the client side by the browser and is never included in the request.
Is it safe to store a wifi password in the URL hash?
It’s private in the sense that no server logs it, but it’s not secret: anyone with the full link reads the password just like you do. It’s useful for keeping it out of a database, not for hiding it from whoever receives the link.
Does bigwords.page work without an internet connection?
Once the browser has downloaded the site’s HTML and JavaScript, rendering or editing the sign doesn’t need any additional request to the server, because the entire state is already in the URL and the parsing runs locally.
Is there a character limit for the message in the URL?
The standard doesn’t set a maximum, but browsers, and especially QR code readers, do impose practical limits: an image embedded in base64 via img= can push the link beyond what a QR code can encode legibly.
Can I use this same technique outside of bigwords.page?
Yes: any static app can read window.location.hash, parse parameters with URLSearchParams, and render accordingly. It’s the same idea behind several online editors that store source code in the URL hash so the link is self-contained.
References
- bigwords.page: original site and documentation of the URL parameters.
- MDN: Location.hash: reference for the API that exposes the URL fragment in JavaScript.
- RFC 3986: URI format specification, including the section on the fragment component.
- MDN: History.replaceState(): how to update the URL without adding an entry to the history.
- PrivateBin: a real example of another tool that puts sensitive information in the URL fragment instead of on the server.
📱 Like this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day.
Featured image: Foto de Markus Spiske en Unsplash
Did it work for you? Got a different error? Say so below: questions get answered and help the next reader.
Leave a comment
0 Comments