felix-berlin / astro-breadcrumbs

Well configurable breadcrumb component for Astro.js. Create breadcrumbs completely dynamically or specify exactly how they should look.
https://docs.astro-breadcrumbs.kasimir.dev
GNU General Public License v3.0
81 stars 12 forks source link

Resolving Trailing Slash Issue in Breadcrumbs Links #212

Open shah-iq opened 8 months ago

shah-iq commented 8 months ago

I'm encountering an issue with the using astro-breadcrumbs with Astrowind template on my website. I'm using the astro-breadcrumbs integration to display breadcrumbs. However, after running the build command pnpm run build, the links in the breadcrumbs are having trailing slashes appended to them, despite having set the trailing slash option to "never." Additionally, I've included trailingSlash={false} in the Breadcrumb component for added assurance. It seems the problem only pertains to the links generated by the breadcrumbs. Can you suggest any potential solutions?

felix-berlin commented 8 months ago

Hi @shah-iq thanks for creating this issue. Do you have a link to your repo?

shah-iq commented 8 months ago

Hi @shah-iq thanks for creating this issue. Do you have a link to your repo?

Actually this is a project for a client and is a private repo

felix-berlin commented 8 months ago

@shah-iq I will check this issue within this (hopefully) week. By the way the traillingSlash prop has been deprecated since V2. Please make sure you read the Migration Guide https://docs.astro-breadcrumbs.kasimir.dev/guides/migration-to-v2/

felix-berlin commented 8 months ago

Did you use the current release?

shah-iq commented 8 months ago

Did you use the current release?

Yes I am using the latest release and I used the traillingSlash prop only to just double check if by any chance resolved the issue. I had seen the migration guide that this prop had been deprecated.

felix-berlin commented 3 months ago

@shah-iq is the problem still there? Otherwise I would close the issue.

ccoyotedev commented 2 months ago

Hey @felix-berlin

My team is also encountering this issue. It work's fine in dev mode, but in the production build the links are appending a / even if there isn't one in the URL.

This is problematic for SEO

felix-berlin commented 2 months ago

Hi @ccoyotedev ,

can you maybe provide a working demo of your problem?

When this is not possible, can you tell me your current astro-breadcrumb version, astro version, used adapters, astro config and the astro-breadcrumbs usage?

ccoyotedev commented 2 months ago

@felix-berlin I cannot share as it's a private repository for the company I work for.

"astro": "^3.6.5",
"astro-breadcrumbs": "^3.0.1",

In production it is a static build, hosted with Cloudflare.

// astro.config.mjs

export default defineConfig({
  output: process.env.PUBLIC_APP_ENV === 'preview' ? 'server' : 'static',
  adapter: process.env.PUBLIC_APP_ENV === 'preview' ? node({ mode: 'standalone' }) : undefined,
  site: SITE_URL,
  trailingSlash: process.env.PUBLIC_APP_ENV === 'preview' ? 'ignore' : 'never',
  integrations: [
    react(),
    tailwind({
      config: {
        applyBaseStyles: false
      }
    }),
    // https://docs.astro.build/en/guides/integrations-guide/sitemap/
    sitemap({
      customPages: externalPages
    }),
    storyblok({
      ...
    })
  ]
})

Used like so:

<Breadcrumbs linkTextFormat="capitalized">
  <span slot="separator">/</span>
</Breadcrumbs>
ccoyotedev commented 2 months ago

FYI, I have got around this issue by extracting your generateCrumbs function and removing the addTrailingSlash logic. I then pass the returned value to the crumbs prop.

// Breadcrumbs.astro
---
import { Breadcrumbs as AstroBreadcrumbs } from 'astro-breadcrumbs'

import { generateCrumbs } from '@/helpers/breadcrumbs'

const paths = Astro.url.pathname.split('/').filter((crumb: any) => crumb)
const crumbs = generateCrumbs({ paths, indexText: 'Home', linkTextFormat: 'capitalized' })
---

<div class="container text-[#F6F7F4]/50">
  <AstroBreadcrumbs crumbs={crumbs}>
    <span slot="separator">/</span>
  </AstroBreadcrumbs>
</div>
// src/helpers/breadcrumbs.ts

interface BreadcrumbItem {
  text: string
  href: string
  'aria-current'?: string
}

type GenerateCrumbs = {
  paths: string[]
  indexText: string
  linkTextFormat: 'lower' | 'capitalized' | 'sentence'
}

export const generateCrumbs = ({ paths, indexText = 'Home', linkTextFormat }: GenerateCrumbs) => {
  const parts: Array<BreadcrumbItem> = []
  const baseUrl = import.meta.env.BASE_URL

  const basePartCount = baseUrl.split('/').filter((s) => s).length

  const hasBaseUrl = baseUrl !== '/'

  /**
   * Loop through the paths and create a breadcrumb item for each.
   */
  paths.forEach((text: string, index: number) => {
    /**
     * generateHref will create the href out of the paths array.
     * Example: ["path1", "path2"] => /path1/path2
     */
    const finalHref = `/${paths.slice(0, index + 1).join('/')}`

    // strip out any file extensions
    const matches = text.match(/^(.+?)(\.[a-z0-9]+)?\/?$/i)

    if (matches?.[2]) {
      text = matches[1]
    }

    parts.push({
      text: formatLinkText(text, linkTextFormat),
      href: finalHref
    })
  })

  /**
   * If there is NO base URL, the index item is missing.
   * Add it to the start of the array.
   */
  if (!hasBaseUrl) {
    parts.unshift({
      text: indexText!,
      href: baseUrl
    })
  }

  /**
   * If there more than one part in the base URL,
   * we have to remove all those extra parts at the start.
   */
  if (basePartCount > 1) {
    let toRemove = basePartCount - 1
    while (toRemove--) {
      parts.shift()
    }
  }

  /**
   * If there is a base URL, the index item is present.
   * Modify the first item to use the index page text.
   */
  parts[0] = {
    text: indexText!,
    href: parts[0]?.href
  }

  return parts
}

const findSeparator = (slug: string): string | undefined => {
  const separators = ['-', '_']
  for (const separator of separators) {
    if (slug.includes(separator)) {
      return separator
    }
  }
}

const unSlugTrimmed = (slug: string): string => {
  const separator = findSeparator(slug)
  if (separator) {
    return slug.split(separator).join(' ').trim()
  }
  return slug
}

const formatLinkText = (slug: string, format?: GenerateCrumbs['linkTextFormat']) => {
  const slugToFormat = unSlugTrimmed(slug)

  switch (format) {
    case 'lower':
      return slugToFormat.toLowerCase()

    case 'capitalized':
      return slugToFormat
        .split(' ')
        .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
        .join(' ')

    case 'sentence':
      return slugToFormat.charAt(0).toUpperCase() + slugToFormat.slice(1)

    default:
      return slug
  }
}
felix-berlin commented 2 months ago

@ccoyotedev Interesting, if I find time at the weekend I'll take a closer look at the problem and create a bugfix.

felix-berlin commented 2 months ago

I just tried to reproduce this behavior with the docs site, there I also use Cloudflare (Pages). But had no luck.

image

I checked whether hasTrailingSlash shows any abnormalities. This was not the case, at this point I am now a little perplexed.

If const hasTrailingSlash = Astro.url.pathname.endsWith("/"); returns a wrong result under certain circumstances, I could try to query this logic without the values on Astro.url, i.e. do the whole thing directly in generateCrumbs().