// utility function written by: Michał Perłakowski
// (https://stackoverflow.com/users/3853934/)
// taken from:
// https://stackoverflow.com/a/39977764/82548
// used because the behaviour of Object.assign() doesn't
// work well in merging objects with unspecifed values/keys:
const assign = (target, ...sources) =>
  Object.assign(target, ...sources.map(x =>
    Object.entries(x)
    .filter(([key, value]) => value !== undefined)
    .reduce((obj, [key, value]) => (obj[key] = value, obj), {})
  ));
// new named function, set up in the same way as above,
// albeit with new arguments:
function insertTextIntoAttribute(opts = {}) {
  let defaults = {
      attribute: 'id',
      elements: 'a',
      // Boolean, do you wish to insert the new String
      // at the end of the current value?
      endWith: false,
      // String, the string you wish to insert:
      insert: 'user-content-',
      // Boolean, do you wish to insert the new String
      // at the start of the current value?
      startWith: true,
    },
    settings = assign({}, defaults, opts);
  const {
    elements,
    attribute,
    insert,
    startWith,
    endWith
  } = settings,
  // using a template literal to create a simple selector
  // to find the elements that match your requirements:
  selectorString = `${elements}`;
  // using document.querySelectorAll() to retrieve a 
  // NodeList of elements that match the selector
  // passed to the function:
  const haystack = document.querySelectorAll(
    selectorString
  );
  // NodeList.prototype.forEach() to iterate over the
  // returned NodeList:
  haystack.forEach(
    (el) => {
      // we retrieve the current attribute-value of the
      // relevant element:
      let currentValue = el.getAttribute(attribute),
        // because a hash requires some special consideration
        // (the '#' character has to be at the beginning) we
        // initialise this variable to false:
        isHash = false;
      // we use Element.matches to see if the current element
      // of the NodeList is an <a> element (we could have instead
      // used el.tagName === 'A') but Element.matches is
      // more concise, easier to read and doesn't require a
      // comparison), it is we then check if the current attribute-
      // value matches the <a> element's hash:
      if (el.matches('a') && currentValue === el.hash) {
        // if it does we then update the isHash variable to true:
        isHash = true;
      }
      // here we use Element.setAttribute() to update the named
      // attribute (first argument) to a new value:
      el.setAttribute(attribute,
        // this is perhaps a little confusing to read, as we're
        // taking advantage of Template strings' ability to
        // interpolate a variable into the String, and we're
        // using conditional operators to do so. In order:
        // 1. ${isHash ? '#' : ''}
        // we test isHash; if true/truthy
        // the expression returns the '#' character, if false/falsey
        // the expression returns the empty String ''.
        // 2. ${startWith ? insert : ''}
        // we test startWith; if true/truthy the expression returns
        // the 'insert' variable's value, otherwise if startWith is
        // false/falsey the expression returns the empty-string.
        // 3. ${isHash ? currentValue.replace('#','')
        // here we again test the isHash variable, and if true/truthy
        // the expression returns the result of calling
        // String.prototype.replace() on the current attribute-value
        // of the element; if isHash is false/falsey then it simply
        // returns the current attribute-value.
        // 4. ${endWith ? insert : ''}
        // this is exactly the same as the earlier assessment for
        // the startWith variable, if true/truthy we return the
        // content of the insert variable, otherwise if false/falsey
        // we return an empty String:
        `${isHash ? '#' : ''}${startWith ? insert : ''}${isHash ? currentValue.replace('#','') : currentValue}${endWith ? insert : ''}`
      );
    });
}
// here we call the function, specifying our
// options:
insertTextIntoAttribute({
  // we wish to modify the 'href' attribute:
  attribute: 'href',
  // and we're selecing the <a> elements inside of <li> elements
  // inside of the <nav> element:
  elements: 'nav li a',
});
:root {
  --color: #000f;
  --backgroundColor: #fff;
}
*,
::before,
::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  font-family: sans-serif;
  line-height: 1.5;
}
nav ul {
  display: flex;
  justify-content: space-around;
  list-style-type: none;
}
nav a:is(:link, :visited) {
  color: var(--color);
  background-color: var(--backgroundColor);
}
nav a:is(:hover, :active, :focus) {
  color: var(--backgroundColor);
  background-color: var(--color);
}
h2 {
  margin-block: 3em;
}
h2 a:is(:link, :visited) {
  background: linear-gradient(90deg, lime, #ffff);
  display: block;
  color: var(--color);
  text-decoration: none;
}
h2 a:is(:hover, :active, :focus) {
  text-decoration: underline;
  text-decoration-thickness: 3px;
}
h2 a:target {
  background: linear-gradient(90deg, #f90, #ffff);
}
h2 a::after {
  content: ' (#' attr(id) ').';
}
<nav id="toc">
  <ul>
    <li><a href="#test1">Link to "test1"</a></li>
    <li><a href="#best2">Link to "best2"</a></li>
    <li><a href="#nest3">Link to "nest3"</a></li>
    <li><a href="#rest4">Link to "rest4"</a></li>
  </ul>
</nav>
<h2>
  <a id="user-content-test1" href="https://www.example.com">
    Anything
  </a>
</h2>
<h2>
  <a id="user-content-best2" href="https://www.example.com">
    Anything
  </a>
</h2>
<h2>
  <a id="user-content-nest3" href="https://www.example.com">
    Anything
  </a>
</h2>
<h2>
  <a id="user-content-rest4" href="https://www.example.com">
    Anything
  </a>
</h2>