Terminating slash is being added to my urls that contain anchor ids (#)

I am migrating to Ghost from Medium over time. Basically, I am leaving my posts for the most part on Medium for the time being, but setting up subscriptions on Ghost, as well as table of contents navigation, a glossary, footnotes, and an About. New posts will be published on Ghost. So my readers will be navigating my Medium articles through Ghost, and referencing stuff directly on Ghost as well. As part of this (glossary and footnotes), I merged a number of pages into one page, using anchors in the page, to access what were separate pages on Medium, which was not possible on Medium, ex: id=“canosis”>Canosis:

I have spent a few days in Cloudflare, my dns provider for the domain I am using with Ghost, writing both a Cloudflare ‘worker’ in javascript to rewrite the incoming old urls, and also trying a different route, using a Cloudflare “rule” to do the same thing. In my browser, what I see on the 404 page that I get is the url that I want, with a terminating slash, which stops the recognition of the anchor label. I have debugged the javascript on Cloudflare and assured myself that the rewritten url does not have a terminating slash when I return it, but still it ends up with a slash in my browser. I have tested this with Firefox, Safari, and Orion and I get the same results, so I was wondering if Ghost does some url manipulation as well? (I have asked Cloudflare this same question to see if they do something on the way out to protect against malformed urls. no response yet.)

Example: I want to rewrite a clicked link (on Medium) in the form: https://stilljustjames.com/toc/canosis to be rewritten in the new path format on Ghost: /glossary#canosis but what I get when the rewritten url is parsed by the browser is a 404 error showing the url path as /glossary#canosis/

Any insights?

Numbering for simplicity:

  1. Ghost requires all pages to have a trailing slash, so for your redirects, you’ll want to include the trailing slash (e.g. /glossary/#canosis). This behavior can’t be changed.
  2. Ghost has built-in support for redirects, so you don’t have to use CF to do the work for you. But if you already have it working on CF, I guess it doesn’t matter :grin:

I appreciate your reply @vikaspotluri123, and the reference to the guide on how to do redirects on Ghost. What I don’t understand though, after reading it, is that if I enter the url in my browser without an ending slash (and without any manipulation by Cloudflare):
https://stilljustjames.com/glossary/#naturing
it works perfectly, going directly to the specific id “naturing” in the page, and the url displayed in the browser header is without an ending slash. It also works perfectly if I leave off the slash just before the number sign, although the slash gets added there, perhaps by Ghost. But what doesn’t work is:
https://stilljustjames.com/glossary/#naturing/
That does go to the correct page, but ignores the id, and so it does not position itself correctly. These are very interactive pages, with entries cross-referencing each other. It’s a great tool, and one that is not as effective if each entry is a separate page.

Is there no way to support HTML ids in Ghost? I don’t want this work to have been a waste of time.

I tried setting up the yaml file, following the instructions, but I keep getting an error on upload. I have no knowledge of yaml though, so perhaps you can tell me what I am doing wrong. The error is either “The following definition “301” is invalid: A trailing slash is required.” or “, cannot fetch settings” with that initial comma. Any ideas? The file (downloaded first, then added to):

routes:
  301: ^\/toc\/([a-z0-9-]+)$: /glossary/#$1

collections:
  /:
    permalink: /{slug}/
    filter: featured:false
    template: index
  /featured/:
    permalink: /featured/{slug}/
    filter: featured:true
    template: index

taxonomies:
  tag: /tag/{slug}/
  author: /author/{slug}/

OK, so there are two separate YAML files, with separate formats. The 301 notation goes into the redirects.yaml file. What you have looks like mostly a routes.yaml file.

You can see examples of a redirects.yaml here:

And you can see examples of routes.yaml here:

I’m not sure if you were trying to upload your “yaml file” as a routes.yaml or a redirects.yaml, but it isn’t going to be valid for either! :slight_smile: They’re separate files and separate uploads.

1 Like

Thank you for your reply @Cathy_Sarisky. I had been confused when I downloaded the redirects.yaml file via the admin.labs function and received an empty “.json”, because I had read that the json version was replaced by .yaml version.

In relation to the mandatory ending slash, I wondered how that could be the whole story when I get a correct responses from ghost to a url without an ending slash, just a fragment identifier. I researched it and found out how to do the redirect to a page and fragment identifier:

If the Location value provided in a [3xx (Redirection)] response does not have a fragment component, a user agent MUST process the redirection as if the value inherits the fragment component of the URI reference used to generate the target URI (i.e., the redirection inherits the original reference’s fragment, if any).

For example, a GET request generated for the URI reference “http://www.example.org/~tim” might result in a 303 (See Other) response containing the header field:

Location: /People.html#Tim

which suggests that the user agent redirect to “http://www.example.org/People.html#tim”

In other words, the fragment id, if present, is handled by the browser, unless a redirect is returned to the DNS lookup which has a value in the url.hash field (with a new fragment identifier. I’ll have to do that at Cloudflare.

I thank you both for your help. Hopefully, this will succeed and I won’t have to manually edit 1k links in production.

In case anyone else might need this, it worked as a cloudflare ‘worker’ and it consisted, in my case of replacing “/toc/” with “/glossary/” in the 302 redirect, and with the fragment identifier assigned to the url.hash field:

const redirectHttpCode = 302

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

/**
 * Respond to the request
 * @param {Request} request
 */
async function handleRequest(request) {
  const url = new URL(request.url)
  const { pathname } = url
  const pathParts = pathname.split('/')

  if (pathParts[1] === 'toc') {  
    url.pathname = `/glossary/`
    url.hash = '#' + pathParts [2]
    return Response.redirect(url.toString(), redirectHttpCode)
  }   

  return fetch(request) // by default proxy request as usual
}
1 Like