diff --git a/client/html/post.tpl b/client/html/post.tpl
index ecb4ba8..af98422 100644
--- a/client/html/post.tpl
+++ b/client/html/post.tpl
@@ -3,11 +3,7 @@
         <nav class='buttons'>
             <article class='next-post'>
                 <% if (ctx.nextPostId) { %>
-                    <% if (ctx.searchQuery && ctx.searchQuery.text) { %>
-                        <a href='/post/<%- encodeURIComponent(ctx.nextPostId) %>/text=<%- encodeURIComponent(ctx.searchQuery.text) %>'>
-                    <% } else { %>
-                        <a href='/post/<%- encodeURIComponent(ctx.nextPostId) %>'>
-                    <% } %>
+                    <a href='<%= ctx.getPostUrl(ctx.nextPostId, ctx.parameters) %>'>
                 <% } else { %>
                     <a class='inactive'>
                 <% } %>
@@ -17,11 +13,7 @@
             </article>
             <article class='previous-post'>
                 <% if (ctx.prevPostId) { %>
-                    <% if (ctx.searchQuery && ctx.searchQuery.text) { %>
-                        <a href='/post/<%- encodeURIComponent(ctx.prevPostId) %>/text=<%- encodeURIComponent(ctx.searchQuery.text) %>'>
-                    <% } else { %>
-                        <a href='/post/<%- encodeURIComponent(ctx.prevPostId) %>'>
-                    <% } %>
+                    <a href='<%= ctx.getPostUrl(ctx.prevPostId, ctx.parameters) %>'>
                 <% } else { %>
                     <a class='inactive'>
                 <% } %>
@@ -37,11 +29,7 @@
                     </a>
                 <% } else { %>
                     <% if (ctx.canEditPosts) { %>
-                        <% if (ctx.searchQuery && ctx.searchQuery.text) { %>
-                            <a href='/post/<%- encodeURIComponent(ctx.post.id) %>/edit/text=<%- encodeURIComponent(ctx.searchQuery.text) %>'>
-                        <% } else { %>
-                            <a href='/post/<%- encodeURIComponent(ctx.post.id) %>/edit'>
-                        <% } %>
+                        <a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'>
                     <% } else { %>
                         <a class='inactive'>
                     <% } %>
diff --git a/client/html/post_readonly_sidebar.tpl b/client/html/post_readonly_sidebar.tpl
index 5272731..351d93d 100644
--- a/client/html/post_readonly_sidebar.tpl
+++ b/client/html/post_readonly_sidebar.tpl
@@ -51,11 +51,7 @@
             <ul><!--
                 --><% for (let post of ctx.post.relations) { %><!--
                     --><li><!--
-                        --><% if (ctx.searchQuery && ctx.searchQuery.text) { %><!--
-                            --><a href='/post/<%- encodeURIComponent(post.id) %>/text=<%- encodeURIComponent(ctx.searchQuery.text) %>'><!--
-                        --><% } else { %><!--
-                            --><a href='/post/<%- encodeURIComponent(post.id) %>'><!--
-                        --><% } %><!--
+                        --><a href='<%= ctx.getPostUrl(post.id, ctx.parameters) %>'><!--
                             --><%= ctx.makeThumbnail(post.thumbnailUrl) %><!--
                         --></a><!--
                     --></li><!--
@@ -77,7 +73,7 @@
                         --></a><!--
                     --><% } %><!--
                     --><% if (ctx.canListPosts) { %><!--
-                        --><a href='/posts/text=<%- encodeURIComponent(tag) %>' class='<%= ctx.makeCssName(ctx.getTagCategory(tag), 'tag') %>'><!--
+                        --><a href='/posts/query=<%- encodeURIComponent(tag) %>' class='<%= ctx.makeCssName(ctx.getTagCategory(tag), 'tag') %>'><!--
                     --><% } %><!--
                         --><%- tag %><!--
                     --><% if (ctx.canListPosts) { %><!--
diff --git a/client/html/posts_header.tpl b/client/html/posts_header.tpl
index f92cff7..a1af737 100644
--- a/client/html/posts_header.tpl
+++ b/client/html/posts_header.tpl
@@ -1,6 +1,6 @@
 <div class='post-list-header'>
     <form class='horizontal search'>
-        <%= ctx.makeTextInput({text: 'Search query', id: 'search-text', name: 'search-text', value: ctx.searchQuery.text}) %>
+        <%= ctx.makeTextInput({text: 'Search query', id: 'search-text', name: 'search-text', value: ctx.parameters.query}) %>
         <input class='mousetrap' type='submit' value='Search'/>
         <input data-safety=safe type='button' class='mousetrap safety safety-safe <%- ctx.settings.listPosts.safe ? '' : 'disabled' %>'/>
         <input data-safety=sketchy type='button' class='mousetrap safety safety-sketchy <%- ctx.settings.listPosts.sketchy ? '' : 'disabled' %>'/>
@@ -9,12 +9,12 @@
     </form>
     <% if (ctx.canMassTag) { %>
         <form class='masstag horizontal'>
-            <% if (ctx.searchQuery.tag) { %>
+            <% if (ctx.parameters.tag) { %>
                 <span class='append'>Tagging with:</span>
             <% } else { %>
                 <a class='mousetrap button append open-masstag' href='#'>Mass tag</a>
             <% } %>
-            <%= ctx.makeTextInput({name: 'masstag', value: ctx.searchQuery.tag}) %>
+            <%= ctx.makeTextInput({name: 'masstag', value: ctx.parameters.tag}) %>
             <input class='mousetrap start-tagging' type='submit' value='Start tagging'/>
             <a class='mousetrap button append stop-tagging' href='#'>Stop tagging</a>
         </form>
diff --git a/client/html/posts_page.tpl b/client/html/posts_page.tpl
index 89cb94e..bb4ffe6 100644
--- a/client/html/posts_page.tpl
+++ b/client/html/posts_page.tpl
@@ -4,11 +4,7 @@
             <% for (let post of ctx.results) { %>
                 <li>
                     <% if (ctx.canViewPosts) { %>
-                        <% if (ctx.searchQuery && ctx.searchQuery.text) { %>
-                            <a class='thumbnail-wrapper' href='/post/<%- encodeURIComponent(post.id) %>/text=<%- encodeURIComponent(ctx.searchQuery.text) %>' title='@<%- post.id %> (<%- post.type %>)&#10;&#10;Tags: <%- post.tags.map(tag => '#' + tag).join(' ') %>'>
-                        <% } else { %>
-                            <a class='thumbnail-wrapper' href='/post/<%- encodeURIComponent(post.id) %>' title='@<%- post.id %> (<%- post.type %>)&#10;&#10;Tags: <%- post.tags.map(tag => '#' + tag).join(' ') %>'>
-                        <% } %>
+                        <a class='thumbnail-wrapper' href='<%= ctx.getPostUrl(post.id, ctx.parameters) %>' title='@<%- post.id %> (<%- post.type %>)&#10;&#10;Tags: <%- post.tags.map(tag => '#' + tag).join(' ') %>'>
                     <% } else { %>
                         <a class='thumbnail-wrapper'>
                     <% } %>
@@ -39,7 +35,7 @@
                             </span>
                         <% } %>
                     </a>
-                    <% if (ctx.searchQuery && ctx.searchQuery.tag) { %>
+                    <% if (ctx.parameters && ctx.parameters.tag) { %>
                         <a data-post-id='<%= post.id %>' class='masstag'>
                         </a>
                     <% } %>
diff --git a/client/html/tag_category_row.tpl b/client/html/tag_category_row.tpl
index 18897ea..c633464 100644
--- a/client/html/tag_category_row.tpl
+++ b/client/html/tag_category_row.tpl
@@ -17,7 +17,7 @@
     </td>
     <td class='usages'>
         <% if (ctx.tagCategory.name) { %>
-            <a href='/tags/text=category:<%- encodeURIComponent(ctx.tagCategory.name) %>'>
+            <a href='/tags/query=category:<%- encodeURIComponent(ctx.tagCategory.name) %>'>
                 <%- ctx.tagCategory.tagCount %>
             </a>
         <% } else { %>
diff --git a/client/html/tag_delete.tpl b/client/html/tag_delete.tpl
index 6f53c23..a406b33 100644
--- a/client/html/tag_delete.tpl
+++ b/client/html/tag_delete.tpl
@@ -2,7 +2,7 @@
     <form>
         <% if (ctx.tag.postCount) { %>
             <p>For extra <s>paranoia</s> safety, only tags that are unused can be deleted.</p>
-            <p>Check <a href='/posts/text=<%- encodeURIComponent(ctx.tag.names[0]) %>'>which posts</a> are tagged with <%- ctx.tag.names[0] %>.</p>
+            <p>Check <a href='/posts/query=<%- encodeURIComponent(ctx.tag.names[0]) %>'>which posts</a> are tagged with <%- ctx.tag.names[0] %>.</p>
         <% } else { %>
             <div class='input'>
                 <ul>
diff --git a/client/html/tag_summary.tpl b/client/html/tag_summary.tpl
index c25b39c..38bb238 100644
--- a/client/html/tag_summary.tpl
+++ b/client/html/tag_summary.tpl
@@ -36,6 +36,6 @@
     <section class='description'>
         <hr/>
         <%= ctx.makeMarkdown(ctx.tag.description || 'This tag has no description yet.') %>
-        <p>This tag has <a href='/posts/text=<%- encodeURIComponent(ctx.tag.names[0]) %>'><%- ctx.tag.postCount %> usages</a>.</p>
+        <p>This tag has <a href='/posts/query=<%- encodeURIComponent(ctx.tag.names[0]) %>'><%- ctx.tag.postCount %> usages</a>.</p>
     </section>
 </div>
diff --git a/client/html/tags_header.tpl b/client/html/tags_header.tpl
index 6940155..a3d4641 100644
--- a/client/html/tags_header.tpl
+++ b/client/html/tags_header.tpl
@@ -3,7 +3,7 @@
         <div class='input'>
             <ul>
                 <li>
-                    <%= ctx.makeTextInput({text: 'Search query', id: 'search-text', name: 'search-text', value: ctx.searchQuery.text}) %>
+                    <%= ctx.makeTextInput({text: 'Search query', id: 'search-text', name: 'search-text', value: ctx.parameters.query}) %>
                 </li>
             </ul>
         </div>
diff --git a/client/html/tags_page.tpl b/client/html/tags_page.tpl
index fafc1bb..c819c92 100644
--- a/client/html/tags_page.tpl
+++ b/client/html/tags_page.tpl
@@ -4,37 +4,37 @@
             <thead>
                 <th class='names'>
                     <% if (ctx.query == 'sort:name' || !ctx.query) { %>
-                        <a href='/tags/text=-sort:name'>Tag name(s)</a>
+                        <a href='/tags/query=-sort:name'>Tag name(s)</a>
                     <% } else { %>
-                        <a href='/tags/text=sort:name'>Tag name(s)</a>
+                        <a href='/tags/query=sort:name'>Tag name(s)</a>
                     <% } %>
                 </th>
                 <th class='implications'>
                     <% if (ctx.query == 'sort:implication-count') { %>
-                        <a href='/tags/text=-sort:implication-count'>Implications</a>
+                        <a href='/tags/query=-sort:implication-count'>Implications</a>
                     <% } else { %>
-                        <a href='/tags/text=sort:implication-count'>Implications</a>
+                        <a href='/tags/query=sort:implication-count'>Implications</a>
                     <% } %>
                 </th>
                 <th class='suggestions'>
                     <% if (ctx.query == 'sort:suggestion-count') { %>
-                        <a href='/tags/text=-sort:suggestion-count'>Suggestions</a>
+                        <a href='/tags/query=-sort:suggestion-count'>Suggestions</a>
                     <% } else { %>
-                        <a href='/tags/text=sort:suggestion-count'>Suggestions</a>
+                        <a href='/tags/query=sort:suggestion-count'>Suggestions</a>
                     <% } %>
                 </th>
                 <th class='usages'>
                     <% if (ctx.query == 'sort:usages') { %>
-                        <a href='/tags/text=-sort:usages'>Usages</a>
+                        <a href='/tags/query=-sort:usages'>Usages</a>
                     <% } else { %>
-                        <a href='/tags/text=sort:usages'>Usages</a>
+                        <a href='/tags/query=sort:usages'>Usages</a>
                     <% } %>
                 </th>
                 <th class='edit-time'>
                     <% if (ctx.query == 'sort:last-edit-time') { %>
-                        <a href='/tags/text=-sort:last-edit-time'>Edit time</a>
+                        <a href='/tags/query=-sort:last-edit-time'>Edit time</a>
                     <% } else { %>
-                        <a href='/tags/text=sort:last-edit-time'>Edit time</a>
+                        <a href='/tags/query=sort:last-edit-time'>Edit time</a>
                     <% } %>
                 </th>
             </thead>
diff --git a/client/html/user_summary.tpl b/client/html/user_summary.tpl
index 314db77..b2d40ac 100644
--- a/client/html/user_summary.tpl
+++ b/client/html/user_summary.tpl
@@ -10,9 +10,9 @@
         <nav>
             <p><strong>Quick links</strong></p>
             <ul>
-                <li><a href='/posts/text=submit:<%- encodeURIComponent(ctx.user.name) %>'><%- ctx.user.uploadedPostCount %> uploads</a></li>
-                <li><a href='/posts/text=fav:<%- encodeURIComponent(ctx.user.name) %>'><%- ctx.user.favoritePostCount %> favorites</a></li>
-                <li><a href='/posts/text=comment:<%- encodeURIComponent(ctx.user.name) %>'><%- ctx.user.commentCount %> comments</a></li>
+                <li><a href='/posts/query=submit:<%- encodeURIComponent(ctx.user.name) %>'><%- ctx.user.uploadedPostCount %> uploads</a></li>
+                <li><a href='/posts/query=fav:<%- encodeURIComponent(ctx.user.name) %>'><%- ctx.user.favoritePostCount %> favorites</a></li>
+                <li><a href='/posts/query=comment:<%- encodeURIComponent(ctx.user.name) %>'><%- ctx.user.commentCount %> comments</a></li>
             </ul>
         </nav>
 
@@ -20,8 +20,8 @@
             <nav>
                 <p><strong>Only visible to you</strong></p>
                 <ul>
-                    <li><a href='/posts/text=special:liked'><%- ctx.user.likedPostCount %> liked posts</a></li>
-                    <li><a href='/posts/text=special:disliked'><%- ctx.user.dislikedPostCount %> disliked posts</a></li>
+                    <li><a href='/posts/query=special:liked'><%- ctx.user.likedPostCount %> liked posts</a></li>
+                    <li><a href='/posts/query=special:disliked'><%- ctx.user.dislikedPostCount %> disliked posts</a></li>
                 </ul>
             </nav>
         <% } %>
diff --git a/client/html/users_header.tpl b/client/html/users_header.tpl
index 87d1091..5b7d38b 100644
--- a/client/html/users_header.tpl
+++ b/client/html/users_header.tpl
@@ -3,7 +3,7 @@
         <div class='input'>
             <ul>
                 <li>
-                    <%= ctx.makeTextInput({text: 'Search query', id: 'search-text', name: 'search-text', value: ctx.searchQuery.text}) %>
+                    <%= ctx.makeTextInput({text: 'Search query', id: 'search-text', name: 'search-text', value: ctx.parameters.query}) %>
                 </li>
             </ul>
         </div>
diff --git a/client/js/controllers/comments_controller.js b/client/js/controllers/comments_controller.js
index 0f2684f..7fa490e 100644
--- a/client/js/controllers/comments_controller.js
+++ b/client/js/controllers/comments_controller.js
@@ -14,11 +14,11 @@ class CommentsController {
         topNavigation.activate('comments');
 
         this._pageController = new PageController({
-            searchQuery: ctx.searchQuery,
+            parameters: ctx.parameters,
             getClientUrlForPage: page => {
-                const searchQuery = Object.assign(
-                    {}, ctx.searchQuery, {page: page});
-                return '/comments/' + misc.formatSearchQuery(searchQuery);
+                const parameters = Object.assign(
+                    {}, ctx.parameters, {page: page});
+                return '/comments/' + misc.formatUrlParameters(parameters);
             },
             requestPage: page => {
                 return PostList.search(
@@ -63,7 +63,7 @@ class CommentsController {
 };
 
 module.exports = router => {
-    router.enter('/comments/:query?',
-        (ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
+    router.enter('/comments/:parameters?',
+        (ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
         (ctx, next) => { new CommentsController(ctx); });
 };
diff --git a/client/js/controllers/help_controller.js b/client/js/controllers/help_controller.js
index f7dcafa..4c42d0b 100644
--- a/client/js/controllers/help_controller.js
+++ b/client/js/controllers/help_controller.js
@@ -15,9 +15,9 @@ module.exports = router => {
         new HelpController();
     });
     router.enter('/help/:section', (ctx, next) => {
-        new HelpController(ctx.params.section);
+        new HelpController(ctx.parameters.section);
     });
     router.enter('/help/:section/:subsection', (ctx, next) => {
-        new HelpController(ctx.params.section, ctx.params.subsection);
+        new HelpController(ctx.parameters.section, ctx.parameters.subsection);
     });
 };
diff --git a/client/js/controllers/page_controller.js b/client/js/controllers/page_controller.js
index a7154ee..6a490ee 100644
--- a/client/js/controllers/page_controller.js
+++ b/client/js/controllers/page_controller.js
@@ -8,7 +8,7 @@ class PageController {
     constructor(ctx) {
         const extendedContext = {
             getClientUrlForPage: ctx.getClientUrlForPage,
-            searchQuery: ctx.searchQuery,
+            parameters: ctx.parameters,
         };
 
         ctx.headerContext = Object.assign({}, extendedContext);
diff --git a/client/js/controllers/password_reset_controller.js b/client/js/controllers/password_reset_controller.js
index 00a288c..f291bd4 100644
--- a/client/js/controllers/password_reset_controller.js
+++ b/client/js/controllers/password_reset_controller.js
@@ -58,6 +58,6 @@ module.exports = router => {
     });
     router.enter(/\/password-reset\/([^:]+):([^:]+)$/, (ctx, next) => {
         ctx.controller = new PasswordResetFinishController(
-            ctx.params[0], ctx.params[1]);
+            ctx.parameters[0], ctx.parameters[1]);
     });
 };
diff --git a/client/js/controllers/post_controller.js b/client/js/controllers/post_controller.js
index c00e87b..a9c6dd2 100644
--- a/client/js/controllers/post_controller.js
+++ b/client/js/controllers/post_controller.js
@@ -11,14 +11,14 @@ const PostView = require('../views/post_view.js');
 const EmptyView = require('../views/empty_view.js');
 
 class PostController {
-    constructor(id, editMode, searchQuery) {
+    constructor(id, editMode, parameters) {
         topNavigation.activate('posts');
 
         Promise.all([
                 Post.get(id),
                 PostList.getAround(
                     id, this._decorateSearchQuery(
-                        searchQuery ? searchQuery.text : '')),
+                        parameters ? parameters.query : '')),
         ]).then(responses => {
             const [post, aroundResponse] = responses;
             this._post = post;
@@ -30,7 +30,7 @@ class PostController {
                 canEditPosts: api.hasPrivilege('posts:edit'),
                 canListComments: api.hasPrivilege('comments:list'),
                 canCreateComments: api.hasPrivilege('comments:create'),
-                searchQuery: searchQuery,
+                parameters: parameters,
             });
             if (this._view.sidebarControl) {
                 this._view.sidebarControl.addEventListener(
@@ -149,17 +149,17 @@ class PostController {
 }
 
 module.exports = router => {
-    router.enter('/post/:id/edit/:query?',
-        (ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
+    router.enter('/post/:id/edit/:parameters?',
+        (ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
         (ctx, next) => {
             ctx.controller = new PostController(
-                ctx.params.id, true, ctx.searchQuery);
+                ctx.parameters.id, true, ctx.parameters);
         });
     router.enter(
-        '/post/:id/:query?',
-        (ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
+        '/post/:id/:parameters?',
+        (ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
         (ctx, next) => {
             ctx.controller = new PostController(
-                ctx.params.id, false, ctx.searchQuery);
+                ctx.parameters.id, false, ctx.parameters);
         });
 };
diff --git a/client/js/controllers/post_list_controller.js b/client/js/controllers/post_list_controller.js
index 16f3765..8219520 100644
--- a/client/js/controllers/post_list_controller.js
+++ b/client/js/controllers/post_list_controller.js
@@ -19,15 +19,15 @@ class PostListController {
 
         this._ctx = ctx;
         this._pageController = new PageController({
-            searchQuery: ctx.searchQuery,
+            parameters: ctx.parameters,
             getClientUrlForPage: page => {
-                const searchQuery = Object.assign(
-                    {}, ctx.searchQuery, {page: page});
-                return '/posts/' + misc.formatSearchQuery(searchQuery);
+                const parameters = Object.assign(
+                    {}, ctx.parameters, {page: page});
+                return '/posts/' + misc.formatUrlParameters(parameters);
             },
             requestPage: page => {
                 return PostList.search(
-                    this._decorateSearchQuery(ctx.searchQuery.text),
+                    this._decorateSearchQuery(ctx.parameters.query),
                     page, 40, fields);
             },
             headerRenderer: headerCtx => {
@@ -51,7 +51,7 @@ class PostListController {
     }
 
     get _massTagTags() {
-        return (this._ctx.searchQuery.tag || '').split(/\s+/).filter(s => s);
+        return (this._ctx.parameters.tag || '').split(/\s+/).filter(s => s);
     }
 
     _evtTag(e) {
@@ -91,7 +91,7 @@ class PostListController {
 
 module.exports = router => {
     router.enter(
-        '/posts/:query?',
-        (ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
+        '/posts/:parameters?',
+        (ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
         (ctx, next) => { ctx.controller = new PostListController(ctx); });
 };
diff --git a/client/js/controllers/tag_categories_controller.js b/client/js/controllers/tag_categories_controller.js
index f33438d..f676eb4 100644
--- a/client/js/controllers/tag_categories_controller.js
+++ b/client/js/controllers/tag_categories_controller.js
@@ -2,7 +2,6 @@
 
 const api = require('../api.js');
 const tags = require('../tags.js');
-const misc = require('../util/misc.js');
 const TagCategoryList = require('../models/tag_category_list.js');
 const topNavigation = require('../models/top_navigation.js');
 const TagCategoriesView = require('../views/tag_categories_view.js');
diff --git a/client/js/controllers/tag_controller.js b/client/js/controllers/tag_controller.js
index 08a3c09..9d46744 100644
--- a/client/js/controllers/tag_controller.js
+++ b/client/js/controllers/tag_controller.js
@@ -10,10 +10,10 @@ const EmptyView = require('../views/empty_view.js');
 
 class TagController {
     constructor(ctx, section) {
-        Tag.get(ctx.params.name).then(tag => {
+        Tag.get(ctx.parameters.name).then(tag => {
             topNavigation.activate('tags');
 
-            this._name = ctx.params.name;
+            this._name = ctx.parameters.name;
             tag.addEventListener('change', e => this._evtSaved(e));
 
             const categories = {};
diff --git a/client/js/controllers/tag_list_controller.js b/client/js/controllers/tag_list_controller.js
index 7492398..f059771 100644
--- a/client/js/controllers/tag_list_controller.js
+++ b/client/js/controllers/tag_list_controller.js
@@ -16,14 +16,14 @@ class TagListController {
         topNavigation.activate('tags');
 
         this._pageController = new PageController({
-            searchQuery: ctx.searchQuery,
+            parameters: ctx.parameters,
             getClientUrlForPage: page => {
-                const searchQuery = Object.assign(
-                    {}, ctx.searchQuery, {page: page});
-                return '/tags/' + misc.formatSearchQuery(searchQuery);
+                const parameters = Object.assign(
+                    {}, ctx.parameters, {page: page});
+                return '/tags/' + misc.formatUrlParameters(parameters);
             },
             requestPage: page => {
-                return TagList.search(ctx.searchQuery.text, page, 50, fields);
+                return TagList.search(ctx.parameters.query, page, 50, fields);
             },
             headerRenderer: headerCtx => {
                 Object.assign(headerCtx, {
@@ -49,7 +49,7 @@ class TagListController {
 
 module.exports = router => {
     router.enter(
-        '/tags/:query?',
-        (ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
+        '/tags/:parameters?',
+        (ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
         (ctx, next) => { ctx.controller = new TagListController(ctx); });
 };
diff --git a/client/js/controllers/user_controller.js b/client/js/controllers/user_controller.js
index 31a562b..e068bfd 100644
--- a/client/js/controllers/user_controller.js
+++ b/client/js/controllers/user_controller.js
@@ -11,11 +11,11 @@ const EmptyView = require('../views/empty_view.js');
 
 class UserController {
     constructor(ctx, section) {
-        User.get(ctx.params.name).then(user => {
+        User.get(ctx.parameters.name).then(user => {
             const isLoggedIn = api.isLoggedIn(user);
             const infix = isLoggedIn ? 'self' : 'any';
 
-            this._name = ctx.params.name;
+            this._name = ctx.parameters.name;
             user.addEventListener('change', e => this._evtSaved(e));
 
             const myRankIndex = api.user ?
diff --git a/client/js/controllers/user_list_controller.js b/client/js/controllers/user_list_controller.js
index e9f0894..31d0e72 100644
--- a/client/js/controllers/user_list_controller.js
+++ b/client/js/controllers/user_list_controller.js
@@ -13,14 +13,14 @@ class UserListController {
         topNavigation.activate('users');
 
         this._pageController = new PageController({
-            searchQuery: ctx.searchQuery,
+            parameters: ctx.parameters,
             getClientUrlForPage: page => {
-                const searchQuery = Object.assign(
-                    {}, ctx.searchQuery, {page: page});
-                return '/users/' + misc.formatSearchQuery(searchQuery);
+                const parameters = Object.assign(
+                    {}, ctx.parameters, {page: page});
+                return '/users/' + misc.formatUrlParameters(parameters);
             },
             requestPage: page => {
-                return UserList.search(ctx.searchQuery.text, page);
+                return UserList.search(ctx.parameters.query, page);
             },
             headerRenderer: headerCtx => {
                 return new UsersHeaderView(headerCtx);
@@ -41,7 +41,7 @@ class UserListController {
 
 module.exports = router => {
     router.enter(
-        '/users/:query?',
-        (ctx, next) => { misc.parseSearchQueryRoute(ctx, next); },
+        '/users/:parameters?',
+        (ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
         (ctx, next) => { ctx.controller = new UserListController(ctx); });
 };
diff --git a/client/js/router.js b/client/js/router.js
index 426d6d4..490b804 100644
--- a/client/js/router.js
+++ b/client/js/router.js
@@ -39,7 +39,7 @@ class Context {
         this.title = document.title;
         this.state = state || {};
         this.state.path = path;
-        this.params = {};
+        this.parameters = {};
     }
 
     pushState() {
@@ -61,14 +61,14 @@ class Route {
 
     middleware(fn) {
         return (ctx, next) => {
-            if (this.match(ctx.path, ctx.params)) {
+            if (this.match(ctx.path, ctx.parameters)) {
                 return fn(ctx, next);
             }
             next();
         };
     }
 
-    match(path, params) {
+    match(path, parameters) {
         const keys = this.keys;
         const qsIndex = path.indexOf('?');
         const pathname = ~qsIndex ? path.slice(0, qsIndex) : path;
@@ -81,8 +81,9 @@ class Route {
         for (let i = 1, len = m.length; i < len; ++i) {
             const key = keys[i - 1];
             const val = _decodeURLEncodedURIComponent(m[i]);
-            if (val !== undefined || !(hasOwnProperty.call(params, key.name))) {
-                params[key.name] = val;
+            if (val !== undefined ||
+                    !(hasOwnProperty.call(parameters, key.name))) {
+                parameters[key.name] = val;
             }
         }
 
diff --git a/client/js/util/misc.js b/client/js/util/misc.js
index 5e2297a..a3efede 100644
--- a/client/js/util/misc.js
+++ b/client/js/util/misc.js
@@ -113,7 +113,7 @@ function formatMarkdown(text) {
             '$1[$2]($2)');
         text = text.replace(/\]\(@(\d+)\)/g, '](/post/$1)');
         text = text.replace(/\]\(\+([a-zA-Z0-9_-]+)\)/g, '](/user/$1)');
-        text = text.replace(/\]\(#([a-zA-Z0-9_-]+)\)/g, '](/posts/text=$1)');
+        text = text.replace(/\]\(#([a-zA-Z0-9_-]+)\)/g, '](/posts/query=$1)');
         return text;
     };
 
@@ -131,7 +131,7 @@ function formatMarkdown(text) {
         //search permalinks
         text = text.replace(
             /\[search\]((?:[^\[]|\[(?!\/?search\]))+)\[\/search\]/ig,
-            '<a href="/posts/text=$1"><code>$1</code></a>');
+            '<a href="/posts/query=$1"><code>$1</code></a>');
         //spoilers
         text = text.replace(
             /\[spoiler\]((?:[^\[]|\[(?!\/?spoiler\]))+)\[\/spoiler\]/ig,
@@ -149,10 +149,13 @@ function formatMarkdown(text) {
     return postDecorator(marked(preDecorator(text), options));
 }
 
-function formatSearchQuery(dict) {
+function formatUrlParameters(dict) {
     let result = [];
     for (let key of Object.keys(dict)) {
         const value = dict[key];
+        if (key === 'parameters') {
+            continue;
+        }
         if (value) {
             result.push(`${key}=${value}`);
         }
@@ -160,19 +163,23 @@ function formatSearchQuery(dict) {
     return result.join(';');
 }
 
-function parseSearchQuery(query) {
+function parseUrlParameters(query) {
     let result = {};
     for (let word of (query || '').split(/;/)) {
         const [key, value] = word.split(/=/, 2);
         result[key] = value;
     }
-    result.text = result.text || '';
+    result.query = result.query || '';
     result.page = parseInt(result.page || '1');
     return result;
 }
 
-function parseSearchQueryRoute(ctx, next) {
-    ctx.searchQuery = parseSearchQuery(ctx.params.query || '');
+function parseUrlParametersRoute(ctx, next) {
+    // ctx.parameters = {"user":...,"action":...} from /users/:user/:action
+    // ctx.parameters.parameters = value of :parameters as per /url/:parameters
+    Object.assign(
+        ctx.parameters,
+        parseUrlParameters(ctx.parameters.parameters));
     next();
 }
 
@@ -235,9 +242,9 @@ function escapeHtml(unsafe) {
 
 module.exports = {
     range: range,
-    formatSearchQuery: formatSearchQuery,
-    parseSearchQuery: parseSearchQuery,
-    parseSearchQueryRoute: parseSearchQueryRoute,
+    formatUrlParameters: formatUrlParameters,
+    parseUrlParameters: parseUrlParameters,
+    parseUrlParametersRoute: parseUrlParametersRoute,
     formatRelativeTime: formatRelativeTime,
     formatFileSize: formatFileSize,
     formatMarkdown: formatMarkdown,
diff --git a/client/js/util/views.js b/client/js/util/views.js
index a8af438..9561a27 100644
--- a/client/js/util/views.js
+++ b/client/js/util/views.js
@@ -146,6 +146,22 @@ function makeColorInput(options) {
         'label', {class: 'color'}, colorInput + textInput);
 }
 
+function getPostUrl(id, parameters) {
+    let url = '/post/' + encodeURIComponent(id);
+    if (parameters && parameters.query) {
+        url += '/query=' + encodeURIComponent(parameters.query);
+    }
+    return url;
+}
+
+function getPostEditUrl(id, parameters) {
+    let url = '/post/' + encodeURIComponent(id) + '/edit';
+    if (parameters && parameters.query) {
+        url += '/query=' + encodeURIComponent(parameters.query);
+    }
+    return url;
+}
+
 function makePostLink(id) {
     const text = '@' + id;
     return api.hasPrivilege('posts:view') ?
@@ -303,6 +319,8 @@ function getTemplate(templatePath) {
             ctx = {};
         }
         Object.assign(ctx, {
+            getPostUrl: getPostUrl,
+            getPostEditUrl: getPostEditUrl,
             makeRelativeTime: makeRelativeTime,
             makeFileSize: makeFileSize,
             makeMarkdown: makeMarkdown,
diff --git a/client/js/views/endless_page_view.js b/client/js/views/endless_page_view.js
index 5965642..142a6dc 100644
--- a/client/js/views/endless_page_view.js
+++ b/client/js/views/endless_page_view.js
@@ -30,7 +30,7 @@ class EndlessPageView {
             ctx.headerRenderer(ctx.headerContext);
         }
 
-        this._loadPage(ctx, ctx.searchQuery.page, true);
+        this._loadPage(ctx, ctx.parameters.page, true);
         this._probePageLoad(ctx);
     }
 
diff --git a/client/js/views/home_view.js b/client/js/views/home_view.js
index a8d151b..3278972 100644
--- a/client/js/views/home_view.js
+++ b/client/js/views/home_view.js
@@ -87,8 +87,8 @@ class HomeView {
     _evtFormSubmit(e) {
         e.preventDefault();
         this._searchInputNode.blur();
-        router.show('/posts/' + misc.formatSearchQuery({
-            text: this._searchInputNode.value}));
+        router.show('/posts/' + misc.formatUrlParameters({
+            query: this._searchInputNode.value}));
     }
 }
 
diff --git a/client/js/views/manual_page_view.js b/client/js/views/manual_page_view.js
index 4d896e9..1ceac67 100644
--- a/client/js/views/manual_page_view.js
+++ b/client/js/views/manual_page_view.js
@@ -63,7 +63,7 @@ class ManualPageView {
         const pageHeaderHolderNode
             = sourceNode.querySelector('.page-header-holder');
         const pageNavNode = sourceNode.querySelector('.page-nav');
-        const currentPage = ctx.searchQuery.page;
+        const currentPage = ctx.parameters.page;
 
         ctx.headerContext.hostNode = pageHeaderHolderNode;
         if (ctx.headerRenderer) {
diff --git a/client/js/views/posts_header_view.js b/client/js/views/posts_header_view.js
index 5958e2a..6824b87 100644
--- a/client/js/views/posts_header_view.js
+++ b/client/js/views/posts_header_view.js
@@ -53,7 +53,7 @@ class PostsHeaderView {
                 'click', e => this._evtStopTaggingClick(e));
             this._massTagFormNode.addEventListener(
                 'submit', e => this._evtMassTagFormSubmit(e));
-            this._toggleMassTagVisibility(!!ctx.searchQuery.tag);
+            this._toggleMassTagVisibility(!!ctx.parameters.tag);
         }
     }
 
@@ -96,9 +96,9 @@ class PostsHeaderView {
 
     _evtStopTaggingClick(e) {
         e.preventDefault();
-        router.show('/posts/' + misc.formatSearchQuery({
-            text: this._ctx.searchQuery.text,
-            page: this._ctx.searchQuery.page,
+        router.show('/posts/' + misc.formatUrlParameters({
+            query: this._ctx.parameters.query,
+            page: this._ctx.parameters.page,
         }));
     }
 
@@ -117,7 +117,7 @@ class PostsHeaderView {
         e.preventDefault();
         const text = this._queryInputNode.value;
         this._queryInputNode.blur();
-        router.show('/posts/' + misc.formatSearchQuery({text: text}));
+        router.show('/posts/' + misc.formatUrlParameters({query: text}));
     }
 
     _evtMassTagFormSubmit(e) {
@@ -125,10 +125,10 @@ class PostsHeaderView {
         const text = this._queryInputNode.value;
         const tag = this._massTagInputNode.value;
         this._massTagInputNode.blur();
-        router.show('/posts/' + misc.formatSearchQuery({
-            text: text,
+        router.show('/posts/' + misc.formatUrlParameters({
+            query: text,
             tag: tag,
-            page: this._ctx.searchQuery.page,
+            page: this._ctx.parameters.page,
         }));
     }
 }
diff --git a/client/js/views/tags_header_view.js b/client/js/views/tags_header_view.js
index 61ea8d2..a9babc2 100644
--- a/client/js/views/tags_header_view.js
+++ b/client/js/views/tags_header_view.js
@@ -37,8 +37,8 @@ class TagsHeaderView {
         e.preventDefault();
         this._queryInputNode.blur();
         router.show(
-            '/tags/' + misc.formatSearchQuery({
-                text: this._queryInputNode.value,
+            '/tags/' + misc.formatUrlParameters({
+                query: this._queryInputNode.value,
             }));
     }
 }
diff --git a/client/js/views/users_header_view.js b/client/js/views/users_header_view.js
index 2284ba7..8678025 100644
--- a/client/js/views/users_header_view.js
+++ b/client/js/views/users_header_view.js
@@ -31,8 +31,8 @@ class UsersHeaderView {
         e.preventDefault();
         this._queryInputNode.blur();
         router.show(
-            '/users/' + misc.formatSearchQuery({
-                text: this._queryInputNode.value,
+            '/users/' + misc.formatUrlParameters({
+                query: this._queryInputNode.value,
             }));
     }
 }