Repository: sindresorhus/query-string
Pull request #107 (2017-09-13 10:24:41)

Add `sort` option
Hi, I've added custom option to `stringify`- `compareFunction`. With this we can override default behaviour of `Array.prototype.sort`. It can also be used to disable sorting #102 in the following way

```js
stringify(urlObject, { compareFunction: () => {} });
```

What do you think?

SamVerschueren: Same question as in #11. Why? Can you provide a use case? I can't come up with a reason why order is important.
msn0: The reason was not even obvious for me so I decided to ask SEO team in my company.

In general, from SEO point of view, url’s with different order of query params are two different url’’s. Some query params (not all of them) are used to construct canonical url. If two content-equivalent url’s got indexed, then Google will penalize us because of duplicate content. If multiple versions of the same site will get crawled and indexed, then (besides duplicate content) we’ll run out of crawling budget. So query params order needs to be preserved for content-equivalent sites. Unfortunately they’re not always in alphabetical order because of UX point of view - some of them are more important for the user and need to be presented first, some of them are not important at all, and they’re in the end of the url. Unfortunately intersection of ux-query-params and seo-query-params is non empty.

Here’s the working example - https://allegro.pl/kategoria/twarz-podklady-45668?marka=Revlon&string=colorstay&wlasciwosci=nawilżające&produkt-wodoodporny=tak&faktor-spf=11-20&konsystencja=kremowa&dostawa-z-polski=Tak&order=m&bmatch=ss-base-relevance-floki-5-nga-hc-fas-1-2-0905

Compare query-params in url and canonical-url and note the order in the latter. Query-params are splitted into groups and only some of them are in alphabetical order.

We have our own canonical-url-builder solution which is not as efficient as we expect. Right now we try to build a new one, lighter and faster.

sindresorhus: I'm ok with this, but the option should be named `sort` and also support `false` as a value to completely turn off sorting.
msn0: Great, I'll fix it and let you know.
ajhsu: The order of query string does matter to me because of the URL readability.

While some of query params were required, some were optional; Those required fields should be placed in higher order, like the following example: 

`/resource?query=value&limit=10`

That `query` is required field but `limit` is totally optional. But it will be stringified into following order: 

`/resource?limit=10&query=value`

Which is loses its readability (to separate from required and optional fields) and modifiability.
sindresorhus: Ping @msn0 :)
msn0: @sindresorhus, @kevva, @SamVerschueren please proceed with the review.
msn0: @sindresorhus any chance for this change? 
sindresorhus: @neuhaus In V8 the order is predictable for alphanumeric keys, so it's fine.

Patch:
diff --git a/index.js b/index.js
index 7da4fea..0517260 100644
--- a/index.js
+++ b/index.js
@@ -176,9 +176,13 @@ exports.stringify = function (obj, opts) {
 
 	opts = objectAssign(defaults, opts);
 
+	if (opts.sort === false) {
+		opts.sort = function () {};
+	}
+
 	var formatter = encoderForArrayFormat(opts);
 
-	return obj ? Object.keys(obj).sort().map(function (key) {
+	return obj ? Object.keys(obj).sort(opts.sort).map(function (key) {
 		var val = obj[key];
 
 		if (val === undefined) {
diff --git a/readme.md b/readme.md
index 8d2148c..4e0d2db 100644
--- a/readme.md
+++ b/readme.md
@@ -133,6 +133,31 @@ queryString.stringify({foo: [1,2,3]});
 // => foo=1&foo=2&foo=3
 ```
 
+#### sort
+
+Type: `Function` `boolean`
+
+Supports both `Function` as a custom sorting function or `false` for disabling sorting.
+
+- `Function`:
+
+```js
+const order = ['c', 'a', 'b'];
+queryString.stringify({ a: 1, b: 2, c: 3}, {
+	sort: (m, n) => order.indexOf(m) >= order.indexOf(n)
+});
+// => 'c=3&a=1&b=2'
+```
+
+- `false`:
+
+```js
+queryString.stringify({ b: 1, c: 2, a: 3}, {sort: false});
+// => 'c=3&a=1&b=2'
+```
+
+If omitted, keys are sorted using `Array#sort`, which means, converting them to strings and comparing strings in Unicode code point order.
+
 ### .extract(*string*)
 
 Extract a query string from a URL that can be passed into `.parse()`.


Test patch:
diff --git a/test/stringify.js b/test/stringify.js
index 5f8234e..3807007 100644
--- a/test/stringify.js
+++ b/test/stringify.js
@@ -122,3 +122,20 @@ test('array stringify representation with array indexes and sparse array', t =>
 	a[10] = 'three';
 	t.is(fn.stringify({bar: a}, {arrayFormat: 'index'}), 'bar[0]=one&bar[1]=two&bar[2]=three');
 });
+
+test('should sort keys in given order', t => {
+	const order = ['c', 'a', 'b'];
+	const sort = (key1, key2) => order.indexOf(key1) >= order.indexOf(key2);
+
+	t.is(fn.stringify({a: 'foo', b: 'bar', c: 'baz'}, {sort}), 'c=baz&a=foo&b=bar');
+});
+
+test('should disable sorting', t => {
+	t.is(fn.stringify({
+		c: 'foo',
+		b: 'bar',
+		a: 'baz'
+	}, {
+		sort: false
+	}), 'c=foo&b=bar&a=baz');
+});