lumeland / lume

🔥 Static site generator for Deno 🦕
https://lume.land
MIT License
1.85k stars 85 forks source link

Support for removal of trailing slash in URL #574

Closed pmuens closed 7 months ago

pmuens commented 7 months ago

Enter your suggestions in details:

First of all: Thanks a lot for working in Lume. I really enjoy it thus far.

While working on my new Blog setup which I'd love to power via Lume I ran into the problem of configuring everything so that the trailing slash in URLs is removed.

Specifically I'd love to have "clean URLs" without trailing slashes for the RSS Feed as well as Sitemap files I generate via their respective plugins. Not redirecting to a trailing-slash version would also be great.

I tried various configurations including the url function in which I returned a trailing-slash-free URL no avail.

Is there maybe already support for this and I'm missing it? Thanks a lot for looking into this!

oscarotero commented 7 months ago

Hi. Lume needs the .html extension in the filename to identify the page as HTML and run html-specific processors. But it's possible to remove the trailing slashes doing the following:

site.use(modifyUrls({
  fn(url) {
    return url.endsWith(".html")
      ? url.slice(0, -5)
      : url;
  }
});

This converts <a href="/example.html"> to <a href="/example">.

For feed and sitemaps plugin, there's no way to modify the urls (maybe it could be a good feature). You can process the files after created. For example:

site.process([".xml"], (pages) => {
    for (const page of pages) {
      if (page.data.url === "/sitemap.xml") {
        page.content = page.content.replaceAll(/href="([^"]+)\.html"/g, "href="$1");
      }
    }
});
pmuens commented 7 months ago

Thanks a lot for the prompt reply and your help on this.

I implemented your proposal and adapted it slightly (mostly using the afterBuild to also get access to the rss.xml file) and it works like a charm.

For anyone else looking, here's what I came up with (based on @oscarotero writeup above):

// _config.ts

import lume from "lume/mod.ts";
import feed from "lume/plugins/feed.ts";
import sitemap from "lume/plugins/sitemap.ts";
import modifyUrls from "lume/plugins/modify_urls.ts";

import { join } from "https://deno.land/std@0.215.0/path/mod.ts";

const RSS_FILE_NAME = "rss.xml";
const SITEMAP_FILE_NAME = "sitemap.xml";

const site = lume({
  prettyUrls: false
});

site.ignore("README.md")
  .use(sitemap())
  .use(feed(
    {
      output: [`/${RSS_FILE_NAME}`],
      query: "type=posts",
      info: {
        title: "=site.title",
        description: "=site.description",
      },
      items: {
        title: "=title",
        description: "=excerpt",
      },
    },
  ))
  .use(modifyUrls({
    fn: (url) => normalizeUrl(url),
  }))

site.addEventListener("afterBuild", (event) => {
  event.pages.forEach(async (page) => {
    if (
      page.data.url.includes(SITEMAP_FILE_NAME) ||
      page.data.url.includes(RSS_FILE_NAME)
    ) {
      const filePath = join("_site", page.data.url);
      let content = await Deno.readTextFile(filePath);

      if (page.data.url.includes(SITEMAP_FILE_NAME)) {
        content = content.replaceAll(
          /(<loc>)(.+)(\.html)(<\/loc>)/g,
          "$1$2$4",
        );
      } else if (page.data.url.includes(RSS_FILE_NAME)) {
        content = content.replaceAll(
          /(<link>)(.+)(\.html)(<\/link>)/g,
          "$1$2$4",
        );
        content = content.replaceAll(
          /(<guid.*>)(.+)(\.html)(<\/guid>)/g,
          "$1$2$4",
        );
      }

      await Deno.writeTextFile(filePath, content);
    }
  });
});

export function normalizeUrl(url: string) {
  let result = url;
  result = result.endsWith(".html") ? result.slice(0, -5) : result;
  result = result.endsWith("index") ? result.replace("index", "") : result;
  result = result.endsWith("/") ? result.slice(0, -1) : result;
  return result;
}

export default site;