URLs can only carry a limited set of characters directly. Anything else, such as spaces, non-ASCII letters or characters with special meaning, must be encoded. Getting this right prevents broken links, double-encoded parameters and security bugs. This guide explains the rules and how they apply to query strings and human-readable slugs.
Reserved and unreserved characters
The URI standard (RFC 3986) defines unreserved characters that never need encoding: letters A–Z and a–z, digits 0–9, and - . _ ~. Reserved characters have special meaning as delimiters, for example : / ? # [ ] @ and ! $ & ' ( ) * + , ; =. When a reserved character is meant as data rather than as a delimiter, it must be encoded.
How percent-encoding works
Percent-encoding replaces a byte with a percent sign followed by two hexadecimal digits. A space becomes %20, and a slash used as data becomes %2F. Non-ASCII characters are first encoded as UTF-8 bytes, and each byte is then percent-encoded. For example, "é" is the UTF-8 bytes C3 A9, so it becomes %C3%A9.
Plus or %20?
HTML forms submitted with the application/x-www-form-urlencoded format encode spaces as +. In the path part of a URL, however, a + is a literal plus sign. This difference causes many bugs: a literal plus sign in a query value must be encoded as %2B, or it will be read back as a space. Use your platform's form-encoding function for query strings and its path-encoding function for paths.
Encode each component separately
Build URLs from parts and encode each part on its own. In JavaScript, encodeURIComponent() encodes a single component such as a query value, while encodeURI() leaves delimiters alone and is meant for a whole URL. Encoding a full URL with the component function breaks it; encoding a value with the whole-URL function leaves characters like & unencoded, which can split one parameter into two.
Most languages also provide URL builder objects, such as URL and URLSearchParams in JavaScript or urllib.parse.urlencode in Python, which handle encoding for you.
Avoid double encoding
If %20 appears in a URL as %2520, the text was encoded twice: the percent sign itself was encoded as %25. This usually happens when an already encoded value is passed through another encoding step. Keep values decoded inside your program and encode exactly once, when building the URL.
Internationalized URLs
Browsers display non-ASCII paths in a readable form but send them percent-encoded as UTF-8. Domain names with non-ASCII characters use a different mechanism, Punycode, which converts them into ASCII labels beginning with xn--. Percent-encoding does not apply to the host name.
Generating good slugs
A slug is the readable part of a URL that identifies a page, such as /blog/url-encoding-guide. A typical slug generator:
- normalizes Unicode and, if desired, transliterates or removes accents ("Café" to "cafe");
- lowercases the text;
- replaces spaces and punctuation with hyphens;
- collapses repeated hyphens and trims them from the ends;
- optionally removes very common words and limits length.
Transliteration is language-specific, and removing non-Latin characters entirely can make slugs empty for some languages. Many sites keep native characters in slugs and let the browser handle percent-encoding.
Slug stability
Once a URL is published, changing its slug breaks links and bookmarks. If you must change one, set up a permanent redirect from the old address. Ensure slugs are unique, for example by appending a number when a duplicate exists.
Key takeaways
- Only unreserved characters are always safe; encode everything else as UTF-8 percent-encoding.
- Use component-level encoding functions and URL builder objects.
- Remember that
+means space only in form-encoded query strings. - Generate slugs consistently and keep them stable once published.