How to get query string parameters as an object – JavaScript

If you want to access the location.search parameters with JavaScript and have a key / value object returned, here's a snippet for you:

/**
 * Avoid tag injections into URL params
 * @param {string} html
 * @param {HTMLElement} [tag]
 * @return {string}
 */
function getEscapedURLString(html, tag = document.createElement(`textarea`)) {
  tag.textContent = html

  return tag.innerHTML
}

/**
 * Returns a key value object of the given locationSearch expecting it to
 * be window.location.search
 *
 * E.g. ('?a=1&b=2') => {a: 1, b: 2}
 *
 * @param {string} [locationSearch] - normally window.location.search
 * @return {object} key value presentation of search params
 */
function getLocationSearchParameters(locationSearch = window.location.search) {
  const queryParameters = {}
  let nameValue

  if (!locationSearch || locationSearch === `?`) {
    return queryParameters
  }
  // remove '?' symbol at the beginning
  locationSearch.substr(1).split(`&`).forEach((searchPart) => {
    nameValue = searchPart.split(`=`)

    queryParameters[nameValue[0].toLowerCase()] = getEscapedURLString(nameValue[1])
  })

  return queryParameters
}Code language: JavaScript (javascript)

How to use it

console.log(
	getLocationSearchParameters('?first=test&second=hello&third=world')
);Code language: JavaScript (javascript)

Output

Object {
  first: "test",
  second: "hello",
  third: "world"
}Code language: CSS (css)

Jest Unit Tests

I even have some unit tests for you, using Jest

describe(`getLocationSearchParameters`, () => {
  test(`should return the correct array given '?test1=a&test2=b'`, () => {
    const result = getLocationSearchParameters(`?test1=a&test2=b`)
    const expected = {
      test1: `a`,
      test2: `b`
    }

    expect(result).toEqual(expected)
  })

  test(`should return empty object`, () => {
    const result = getLocationSearchParameters(`?`)

    expect(result).toEqual(expect.objectContaining({}))
  })
})Code language: JavaScript (javascript)

Comments

  1. You know I’m a huge WordPress fan and I rely on it whenever I need a CMS. At this point pretty much everything is possible, don’t want to mess with PHP? Sure, use it headless with GraphQL and Next.js or the provided out of the REST API.

    Today I won’t be using any of the edge use cases listed above but the regular WordPress capabilities on a simply hand-crafted theme, using the Share Target API to create a native-like UX experience to share links in my Android device to social media.

    If you know Pinboard, this will sound a little familiar.

    What’s the plan for today?

    I want to create a database for all the links I share. When I want to share something the current old flow looks like this:

    Grab the URL of the post/site/article.

    Grab the title.

    Open Mastodon and paste both.

    An IFTT recipe will take my Mastodon feed and post it to my Twitter account.

    Simple, sure, but we engineers like to automate all the things. That’s the plan. Automate to reduce steps and increase speed.

    Before this side-project I did not have a central location to store those links. Links that I usually go back to. I could use the old good browser bookmarks but that gets old fast, as soon as you reach a high number… that’s unmaintainable. But even more important I need it to be searchable, you can’t really search in your Twitter timeline.

    Above all, I would feel very bad to spend hours curating a Twitter feed of links to suddenly get my account blocked, banned, deleted… who knows! Twitter would always have the power to mess with my content. We can’t have that.

    So, let’s use WordPress served from my VPS to:

    Create a Custom Post Type (to not use posts or pages, but an actual “post type link”). Yes, I know WordPress had links in the past as a Dashboard section, but those are deprecated. No longer shown by default. A new Custom Post Type is the way to go to be future proof and to make it easier to extend.

    These items will be allowed to have Tags (taxonomy). Hashtags for the win.

    We’ll create a custom RSS Feed to customize the output, so it looks good when the links are automatically shared on social media.

    We’ll make the site a PWA to be installable on Chrome for Android and make use of the Share Target API so we can share a link from the native Android’s share context menu. The PWA will show up like Signal, WhatsApp or Instagram do in the share menu.

    We’ll create a JavaScript browser bookmark to easily share any site we’re browsing to the CMS.

    Finally, we’ll create an IFTT recipe that will get the site’s feed and post it to Mastodon (remember I already had another recipe to repost from Mastodon to Twitter?).

    Starter Theme

    I always rely on Underscores to start fresh. The last update was back in September so I’d say we’re pretty good. Do you know if there’s another service to generate a starting theme for a given name?

    Hack the Dashboard for better UI

    The WordPress dashboard is good but on mobile, it takes a lot of space (personal opinion). Thankfully WordPress is so extendable that we can load a custom CSS just for the Dashboard. Specifically for our Custom Post Type. We’ll hide unnecessary elements to that when I share a link on mobile I see all the fields on the viewport, without having to scroll to publish or add a hashtag.

    For instance removing the (unnecessary for our use case today) media buttons on the editor:

    .wp-block-code {
    border: 0;
    padding: 0;
    }

    .wp-block-code > div {
    overflow: auto;
    }

    .shcb-language {
    border: 0;
    clip: rect(1px, 1px, 1px, 1px);
    -webkit-clip-path: inset(50%);
    clip-path: inset(50%);
    height: 1px;
    margin: -1px;
    overflow: hidden;
    padding: 0;
    position: absolute;
    width: 1px;
    word-wrap: normal;
    word-break: normal;
    }

    .hljs {
    box-sizing: border-box;
    }

    .hljs.shcb-code-table {
    display: table;
    width: 100%;
    }

    .hljs.shcb-code-table > .shcb-loc {
    color: inherit;
    display: table-row;
    width: 100%;
    }

    .hljs.shcb-code-table .shcb-loc > span {
    display: table-cell;
    }

    .wp-block-code code.hljs:not(.shcb-wrap-lines) {
    white-space: pre;
    }

    .wp-block-code code.hljs.shcb-wrap-lines {
    white-space: pre-wrap;
    }

    .hljs.shcb-line-numbers {
    border-spacing: 0;
    counter-reset: line;
    }

    .hljs.shcb-line-numbers > .shcb-loc {
    counter-increment: line;
    }

    .hljs.shcb-line-numbers .shcb-loc > span {
    padding-left: 0.75em;
    }

    .hljs.shcb-line-numbers .shcb-loc::before {
    border-right: 1px solid #ddd;
    content: counter(line);
    display: table-cell;
    padding: 0 0.75em;
    text-align: right;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    white-space: nowrap;
    width: 1%;
    }

    function RemoveAddMediaButtonsForNonAdmins(){
      remove_action( 'media_buttons', 'media_buttons' );
    }
    add_action('admin_head', 'RemoveAddMediaButtonsForNonAdmins');Code language: JavaScript (javascript)

    Code language: JavaScript (javascript)

    I’ve also added a custom JS file to delete more stuff upon page load, such as:

      if (_$.minorPublishing) {
          _$.minorPublishing.setAttribute('hidden', '');
        }
        if (_$.screenMetaLinks) {
          _$.screenMetaLinks.setAttribute('hidden', '');
        }
        if (_$.slugBox) {
          _$.slugBox.setAttribute('hidden', '');
        }
        if (_$.editorToolbar) {
          _$.editorToolbar.setAttribute('hidden', '');
        }
        if (_$.editorPostStatus) {
          _$.editorPostStatus.setAttribute('hidden', '');
        }Code language: JavaScript (javascript)

    Code language: JavaScript (javascript)

    Full admin.js file here.

    Web Share API

    It’s good but the documentation does not seem to match the current implementation as of today. It seems you don’t get the description (???) in the query parameters.

    The documentation says you can do this, but

    text

    didn’t work for me.

    "share_target": {
      "action": "/share-target/",
      "method": "GET",
      "params": {
        "title": "title",
        "text": "text",
        "url": "url"
      }
    }Code language: JavaScript (javascript)

    Code language: JavaScript (javascript)

    Also, I couldn’t send a query parameter of mine from the manifest file. Why not? Not sure. Here’s how I ended up setting up the

    share_target
    {
        ....
        "start_url": "/links/",
        "scope": "/links/",
        "share_target": {
            "action": "/links/wp-admin/post-new.php?post_type=link",
            "method": "GET",
            "params": {
                "title": "title",
                "text": "url",
                "url": "url"
            }
        }
    }Code language: JavaScript (javascript)

    Code language: JavaScript (javascript)

    Check out the complete site.manifest

    Here’s how it looks on my Android device when I hit share from Firefox:

    Link Manager shows like any other native Android app

    Now we need to get these query params and put them into the WordPress dashboard fields (title, custom meta, description…). I’ve used the getLocationSearchParameters to get all the query params and then we simply add those to the form fields:

    ...
     _$.postTitle.value = _.queryParameters.title;
    ...Code language: JavaScript (javascript)

    Code language: JavaScript (javascript)

    Again, I’d recomend checking the full admin.js file for all the goodies.

    ServiceWorker

    Chrome is smart but no so much. It needs a ServiceWorker so it can install the fake PWA. The ServiceWorker does nothing, we’re just listening to the requests doing nothing with them. With this simple trick, Chrome will allow you to install the app without having to worry about actual offline support, requests caching, etc.

    The ServiceWorker should be placed at the root of the site. I noticed that the manifest

    scope

    property was complaining if the ServiceWorker wasn’t there.

    self.addEventListener('install', (event) => {
      console.log('👷', 'install', event);
      self.skipWaiting();
    });
    
    self.addEventListener('activate', (event) => {
      console.log('👷', 'activate', event);
      return self.clients.claim();
    });
    
    self.addEventListener('fetch', (event) => {
      console.log('👷', 'fetch', event);
    });Code language: PHP (php)

    Code language: PHP (php)

    Custom RSS

    As I mentioned I created a custom RSS with custom output, so the IFTT recipe can simply take the item content and push it to Mastodon.

    Here’s how to add a new

    ?feed=links

    RSS feed:

    function linkmanger_add_to_custom_feed($qv)
    {
      if (isset($qv["feed"]) && !isset($qv["post_type"])) {
        $qv["post_type"] = ["link"];
      }
      return $qv;
    }
    add_filter("request", "linkmanger_add_to_custom_feed");
    
    function linkmanager_feed_content()
    {
      require get_template_directory() . "/template-parts/rss.php";
    }
    
    function linkmanager_add_feed()
    {
      add_feed("links", "linkmanager_feed_content");
    }
    add_action("init", "linkmanager_add_feed");
    
    function encode_content_for_feed($content)
    {
      $content = str_replace("]]>", "]]>", $content);
    
      return apply_filters("the_content_feed", $content, "rss2");
    }Code language: PHP (php)

    Code language: PHP (php)

    Now here’s just a small part, so you get the idea on how I created the custom output:

    ...
    
     <?php
        $content = get_the_content();
    
        $description = "";
        if ($content !== "") {
          $description .= $content;
          $description .= "n";
          $description .= "n";
        }
        $description .= '"' . html_entity_decode(get_the_title_rss()) . '"';
        $description .= "n";
        $description .= "n";
        $description .= linkmanager_arrow_emoji() . " " . getLinkURL(get_the_ID());
        $description .= "n";
        $description .= "n";
        $description .= getLinkTagsRSS(get_the_ID(), true);
        ?>
        <description><![CDATA[<?php echo $content; ?>]]></description>
        <content:encoded><![CDATA[<?php echo $description; ?>]]></content:encoded>
    
    ....Code language: HTML, XML (xml)

    Code language: HTML, XML (xml)

    Check the full RSS template.

    Magic bookmark

    With this JavaScript browser bookmark it will take you to the WordPress Dashboard passing the URL, meta title, and meta description to the creation screen.

    With this it only takes 2 clicks to share a link:

    1 on the bookmark.

    1 on the publish button.

    javascript:window.location=`YOUR_SITE/wp-admin/post-new.php?post_type=link&title=${encodeURIComponent(document.title)}&url=${encodeURIComponent(window.location.href)}&description=${document.querySelector('meta[name="description"]')?.content ? document.querySelector('meta[name="description"]')?.content : ''}`Code language: JavaScript (javascript)

    Code language: JavaScript (javascript)

    Fork me

    Of course, all the code is open source and for you to grab, fork, and tweak to fit your needs.

    https://github.com/quicoto/link-manager

    Closing words

    A weekend project I’m really happy about. It is now for me super convenient to share links. No manual copy and pasting thanks to the bookmark and the Android share integration. Plus the links are stored on my server, as long as I want. I can search and tag them for easier access later on.

    Finally, here, check out all my shared links!

    PD: Own your content.

Leave a Reply

Your email address will not be published. Required fields are marked *