diff --git a/src/promnesia/cannon.py b/src/promnesia/cannon.py
index a3a303f..b76cb9f 100755
--- a/src/promnesia/cannon.py
+++ b/src/promnesia/cannon.py
@@ -105,11 +105,13 @@ default_qkeep = [
 
 # TODO perhaps, decide if fragment is meaningful (e.g. wiki) or random sequence of letters?
 class Spec(NamedTuple):
-    qkeep  : Optional[Collection[str]] = None
+    qkeep  : Optional[Union[Collection[str], bool]] = None
     qremove: Optional[Set[str]] = None
     fkeep  : bool = False
 
     def keep_query(self, q: str) -> Optional[int]: # returns order
+        if self.qkeep is True:
+            return 1
         qkeep = {
             q: i for i, q in enumerate(chain(default_qkeep, self.qkeep or []))
         }
@@ -183,6 +185,7 @@ specs: Dict[str, Spec] = {
     'ycombinator.com'    : S(qkeep={'id'}), # todo just keep id by default?
     'play.google.com'    : S(qkeep={'id'}),
     'answers.yahoo.com'  : S(qkeep={'qid'}),
+    'isfdb.org': S(qkeep=True),
 }
 
 _def_spec = S()
@@ -271,7 +274,7 @@ def transform_split(split: SplitResult):
     netloc = canonify_domain(split.netloc)
 
     path     = split.path
-    qparts   = parse_qsl(split.query)
+    qparts   = parse_qsl(split.query, keep_blank_values=True)
 
     fragment = split.fragment
 
@@ -319,7 +322,7 @@ def transform_split(split: SplitResult):
             to = to + ('', )
 
         (netloc, path, qq) = [t.format(**gd) for t in to]
-        qparts.extend(parse_qsl(qq)) # TODO hacky..
+        qparts.extend(parse_qsl(qq, keep_blank_values=True)) # TODO hacky..
         # TODO eh, qparts should really be a map or something...
         break
 

diff --git a/tests/cannon.py b/tests/cannon.py
index d76693b..5cd6ef5 100644
--- a/tests/cannon.py
+++ b/tests/cannon.py
@@ -294,3 +294,19 @@ def test_error():
     with pytest.raises(CanonifyException):
         # borrowed from https://bugs.mageia.org/show_bug.cgi?id=24640#c7
         canonify('https://example.com\uFF03@bing.com')
+
+@pytest.mark.parametrize("url,expected", [
+    ('https://news.ycombinator.com/item?id=', 'news.ycombinator.com/item?id='),
+    ('https://www.youtube.com/watch?v=hvoQiF0kBI8&list&index=2',
+     'youtube.com/watch?v=hvoQiF0kBI8&list='),
+])
+def test_empty_query_parameter(url, expected):
+    assert canonify(url) == expected
+
+@pytest.mark.parametrize("url,expected", [
+    ('http://www.isfdb.org/cgi-bin/title.cgi?2172', 'isfdb.org/cgi-bin/title.cgi?2172='),
+    ('http://www.isfdb.org/cgi-bin/title.cgi?2172+1', 'isfdb.org/cgi-bin/title.cgi?2172%201='),
+    ('http://www.isfdb.org/cgi-bin/title.cgi?2172&foo=bar&baz&quux', 'isfdb.org/cgi-bin/title.cgi?2172=&baz=&foo=bar&quux='),
+])
+def test_qkeep_true(url, expected):
+    assert canonify(url) == expected

Handle query parameters without values
Right now, pages on isfdb like `http://www.isfdb.org/cgi-bin/title.cgi?2172` are canonicalized to `http://www.isfdb.org/cgi-bin/title.cgi`, which removes important info from the URL.

I made a somewhat hacky solution, but there is perhaps a more elegant way. The solution is in two parts:

First, modify `Spec` to accept `qkeep=True`, which is taken to mean "retain all query parameters", and have isfdb.org use that option.

```
@@ -105,11 +105,13 @@ default_qkeep = [
 
 # TODO perhaps, decide if fragment is meaningful (e.g. wiki) or random sequence of letters?
 class Spec(NamedTuple):
-    qkeep  : Optional[Collection[str]] = None
+    qkeep  : Optional[Union[Collection[str], bool]] = None
     qremove: Optional[Set[str]] = None
     fkeep  : bool = False
 
     def keep_query(self, q: str) -> Optional[int]: # returns order
+        if self.qkeep is True:
+            return 1
         qkeep = {
             q: i for i, q in enumerate(chain(default_qkeep, self.qkeep or []))
         }
@@ -183,6 +185,7 @@ specs: Dict[str, Spec] = {
     'ycombinator.com'    : S(qkeep={'id'}), # todo just keep id by default?
     'play.google.com'    : S(qkeep={'id'}),
     'answers.yahoo.com'  : S(qkeep={'qid'}),
+    'isfdb.org': S(qkeep=True),
 }
 _def_spec = S()
```

Second, pass, `keep_blank_values=True` to `parse_qsl`.

```
@@ -271,7 +274,7 @@ def transform_split(split: SplitResult):
     netloc = canonify_domain(split.netloc)
 
     path     = split.path
-    qparts   = parse_qsl(split.query)
+    qparts   = parse_qsl(split.query, keep_blank_values=True)
 
     fragment = split.fragment
 
@@ -319,7 +322,7 @@ def transform_split(split: SplitResult):
             to = to + ('', )
 
         (netloc, path, qq) = [t.format(**gd) for t in to]
-        qparts.extend(parse_qsl(qq)) # TODO hacky..
+        qparts.extend(parse_qsl(qq, keep_blank_values=True)) # TODO hacky..
         # TODO eh, qparts should really be a map or something...
         break
```

This achieves the desired result, but it's unpleasantly hacky.

First problem: this will not guarantee the order of query parameters. But is that important? Why doesn't the code just always alphabetize them? There's a note for youtube that "order matters here", but it seems to me that youtube doesn't care what order the parameters are in.

Second problem: maybe you usually don't want empty parameters kept. It could complicate things to add logic around this, but maybe something like a `keep_blank_values` property on Spec would do the job.

Third problem: it's not great to have to dig around in the program code to fix this behavior. Maybe it could be patched in from my `config.py`, once the supporting code is in place, which something like:

```python
from promnesia.cannon import specs, S
specs['isfdb.org'] = S(qkeep=True)
```

Really, all of `cannon.py` is a bit intimidating to modify (and, incidentally, shouldn't it be `canon.py`?). I'm not sure what's the right direction to go.
tests/cannon.py::test_qkeep_true[http://www.isfdb.org/cgi-bin/title.cgi?2172-isfdb.org/cgi-bin/title.cgi?2172=] tests/cannon.py::test_empty_query_parameter[https://news.ycombinator.com/item?id=-news.ycombinator.com/item?id=] tests/cannon.py::test_qkeep_true[http://www.isfdb.org/cgi-bin/title.cgi?2172&foo=bar&baz&quux-isfdb.org/cgi-bin/title.cgi?2172=&baz=&foo=bar&quux=] tests/cannon.py::test_qkeep_true[http://www.isfdb.org/cgi-bin/title.cgi?2172+1-isfdb.org/cgi-bin/title.cgi?2172%201=] tests/cannon.py::test_empty_query_parameter[https://www.youtube.com/watch?v=hvoQiF0kBI8&list&index=2-youtube.com/watch?v=hvoQiF0kBI8&list=]
tests/cannon.py::test_youtube[youtube.com/watch?v=wHrCkyoe72U&feature=share&time_continue=6-youtube.com/watch?v=wHrCkyoe72U] tests/cannon.py::test[https://news.ycombinator.com/item?id=12172351-news.ycombinator.com/item?id=12172351] tests/cannon.py::test[https://www.youtube.com/watch?v=hvoQiF0kBI8&list=WL&index=2-youtube.com/watch?v=hvoQiF0kBI8&list=WL] tests/cannon.py::test_reddit[https://www.reddit.com/r/selfhosted/comments/8j8mo3/what_are_you_self_hosting/dz19gh9/?utm_content=permalink&utm_medium=user&utm_source=reddit&utm_name=u_karlicoss-reddit.com/r/selfhosted/comments/8j8mo3/what_are_you_self_hosting/dz19gh9] tests/cannon.py::test[https://ubuntuforums.org/showthread.php?t=1403470&s=0dd67bdb12559c22e73a220752db50c7&p=8806195#post8806195-ubuntuforums.org/showthread.php?t=1403470&p=8806195] tests/cannon.py::test[withouthspec.co.uk/rooms/16867952?guests=2&adults=2&location=Berlin%2C+Germany&check_in=2017-08-16&check_out=2017-08-20-withouthspec.co.uk/rooms/16867952] tests/cannon.py::test_youtube[m.youtube.com/watch?v=Zn6gV2sdl38-youtube.com/watch?v=Zn6gV2sdl38] tests/cannon.py::test_same_norm[urls0] tests/cannon.py::test_hackernews[https://news.ycombinator.com/from?site=jacopo.io-jacopo.io] tests/cannon.py::test_youtube[youtube.com/embed/nyc6RJEEe0U?feature=oembed-youtube.com/watch?v=nyc6RJEEe0U] tests/cannon.py::test[https://answers.yahoo.com/question/index?qid=20071101131442AAk9bGp-answers.yahoo.com/question/index?qid=20071101131442AAk9bGp] tests/cannon.py::test[zoopla.co.uk/to-rent/details/42756337#D0zlBWeD4X85odsR.97-zoopla.co.uk/to-rent/details/42756337] tests/cannon.py::test_error tests/cannon.py::test_hackernews[https://news.ycombinator.com/item?id=25099862-news.ycombinator.com/item?id=25099862] tests/cannon.py::test_reddit[https://www.reddit.com/r/firefox/comments/bbugc5/firefox_bans_free_speech_commenting_plugin/?ref=readnext-reddit.com/r/firefox/comments/bbugc5/firefox_bans_free_speech_commenting_plugin] tests/cannon.py::test[https://www.facebook.com/photo.php?fbid=24147689823424326&set=pcb.2414778905423667&type=3&theater-facebook.com/photo.php?fbid=24147689823424326] tests/cannon.py::test[https://play.google.com/store/apps/details?id=com.faultexception.reader&hl=en-play.google.com/store/apps/details?id=com.faultexception.reader] tests/cannon.py::test[flowingdata.com/2010/12/14/10-best-data-visualization-projects-of-the-year-%e2%80%93-2010-flowingdata.com/2010/12/14/10-best-data-visualization-projects-of-the-year-%E2%80%93-2010] tests/cannon.py::test[https://github.com/search?o=asc&q=track&s=stars&type=Repositories-github.com/search?q=track] tests/cannon.py::test_youtube[https://youtu.be/iCvmsMzlF7o?list=WL-youtube.com/watch?v=iCvmsMzlF7o&list=WL] tests/cannon.py::test_same_norm[urls1] tests/cannon.py::test[https://urbandictionary.com/define.php?term=Belgian%20Whistle-urbandictionary.com/define.php?term=Belgian%20Whistle] tests/cannon.py::test[https://en.wikipedia.org/wiki/Dinic%27s_algorithm-en.wikipedia.org/wiki/Dinic%27s_algorithm] tests/cannon.py::test_archiveorg[https://web.archive.org/web/20090902224414/http://reason.com/news/show/119237.html-reason.com/news/show/119237.html] tests/cannon.py::test[https://80000hours.org/career-decision/article/?utm_source=The+EA+Newsletter&utm_campaign=04ca3c2244-EMAIL_CAMPAIGN_2019_04_03_04_26&utm_medium=email&utm_term=0_51c1df13ac-04ca3c2244-318697649-80000hours.org/career-decision/article] tests/cannon.py::test[https://www.youtube.com/watch?list=WL&v=hvoQiF0kBI8&index=2-youtube.com/watch?v=hvoQiF0kBI8&list=WL] tests/cannon.py::test[flowingdata.com/2010/12/14/10-best-data-visualization-projects-of-the-year-\u2013-2010-flowingdata.com/2010/12/14/10-best-data-visualization-projects-of-the-year-%E2%80%93-2010] tests/cannon.py::test[https://bbs.archlinux.org/viewtopic.php?id=212740-bbs.archlinux.org/viewtopic.php?id=212740] tests/cannon.py::test[https://google.co.uk/amp/s/amp.reddit.com/r/androidapps/comments/757e2t/swiftkey_or_gboard-reddit.com/r/androidapps/comments/757e2t/swiftkey_or_gboard] tests/cannon.py::test_youtube[https://www.youtube.com/watch?t=491s&v=1NHbPN9pNPM&index=63&list=WL-youtube.com/watch?v=1NHbPN9pNPM&t=491s&list=WL] tests/cannon.py::test[amp.theguardian.com/technology/2017/oct/09/mark-zuckerberg-facebook-puerto-rico-virtual-reality-theguardian.com/technology/2017/oct/09/mark-zuckerberg-facebook-puerto-rico-virtual-reality] tests/cannon.py::test[https://arstechnica.com/?p=1371299-arstechnica.com/?p=1371299] tests/cannon.py::test[https://spoonuniversity.com/lifestyle/marmite-ways-to-eat-it&usg=AFQjCNH4s1SOEjlpENlfPV5nuvADZpSdow-spoonuniversity.com/lifestyle/marmite-ways-to-eat-it]