Skip to content

Commit

Permalink
fix: handle collections in root of site (#19) - fixes #18
Browse files Browse the repository at this point in the history
introduce `collectionPathMode` to handle sites where content collections are in the root of the site

---------

Co-authored-by: Alex Vernacchia <[email protected]>
  • Loading branch information
techfg and vernak2539 authored Apr 11, 2024
1 parent 3ce0276 commit fc33147
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 8 deletions.
1 change: 1 addition & 0 deletions src/fixtures/dir-test/dir-test-child.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Test
24 changes: 24 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,28 @@
export type CollectionPathMode = 'subdirectory' | `root`

export interface Options {
contentPath?: string; // where you store your content relative to the root directory
/**
* @name collectionPathMode
* @type {CollectionPathMode}
* @default `'subdirectory'`
* @description
*
* Where you store your collections:
* - `'subdirectory'` - Subdirectories under `contentPath` (ex: `src/content/docs/index.md` where `docs` is the content collection subdirectory of the contentPath `src/content`)
* - `'root'` - Directly inside `contentPath` (ex: `src/content/docs/index.md` where `src/content/docs` is the `contentPath`)
*
* Use the `root` configuration option when you are explicitly setting the {@link contentPath} property to something other than `src/content` and you want the directory you specify
* for {@link contentPath} to be treated a single content collection as if it where located in the site root. In most scenarios, you should set this value to `subdirectory` or not
* set this value and the default of `subdirectory` will be used.
* @example
* ```js
* {
* // Use `subdirectory` mode
* collectionPathMode: `subdirectory`
* }
* ```
*/
collectionPathMode?: CollectionPathMode;
basePath?: string; // https://docs.astro.build/en/reference/configuration-reference/#base
}
31 changes: 23 additions & 8 deletions src/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const debug = debugFn("astro-rehype-relative-markdown-links");
// This is very specific to Astro
const defaultContentPath = ["src", "content"].join(path.sep);

/** @type {import("./index").CollectionPathMode} */
const defaultCollectionPathMode = 'subdirectory';

/** @param {import('./index').Options} options */
function astroRehypeRelativeMarkdownLinks(options = {}) {
return (tree, file) => {
Expand Down Expand Up @@ -54,6 +57,7 @@ function astroRehypeRelativeMarkdownLinks(options = {}) {
const { data: frontmatter } = matter(relativeFileContent);
const frontmatterSlug = frontmatter.slug;
const contentDir = options.contentPath || defaultContentPath;
const collectionPathMode = options.collectionPathMode || defaultCollectionPathMode;

/*
By default, Astro assumes content collections are subdirectories of a content path which by default is src/content.
Expand All @@ -68,6 +72,11 @@ function astroRehypeRelativeMarkdownLinks(options = {}) {
name followed by the slug of the content collection page. We do this by following Astro's own assumptions on the directory
structure of content collections - they are subdirectories of content path.
We make an expection to the above approach when collectionPathMode is `root` and instead, treat the content collection
as the site root so we do not include the content collection physical directory name in the transformed url. For example,
with a content collection page of of src/content/docs/page-1.md, if the collectionPathMode is `root`, the url would be
`/page-1` whereas with collectionPathMode of `subdirectory`, it would be `/docs/page-1`.
KNOWN LIMITATIONS/ISSUES
- Astro allows pages within a content collection to be excluded (see https://docs.astro.build/en/guides/routing/#excluding-pages).
We currently do not adhere to this logic (See https://docs.astro.build/en/guides/routing/#excluding-pages).
Expand All @@ -79,8 +88,12 @@ function astroRehypeRelativeMarkdownLinks(options = {}) {

// determine the path of the target file relative to the content path
const relativeToContentPath = path.relative(contentDir, relativeFile);
// determine the collection name using Astros default assumption that content collections are subdirs of content path
const collectionName = path.dirname(relativeToContentPath).split(path.posix.sep)[0];
// When collectionPathMode is:
// - `root`: We assume the content collection is located in the root of the site so there is no collection name in the page path,
// the collection path is equivalent to the site root path
// - `subdirectory` - Determine the collection name using Astros default assumption that content collections are subdirs of content path
// when the collectionPathMode is `subdirectory` or
const collectionName = collectionPathMode === 'root' ? "" : path.dirname(relativeToContentPath).split(path.posix.sep)[0];
// determine the path of the target file relative to the collection
// since the slug for content collection pages is always relative to collection root
const relativeToCollectionPath = path.relative(collectionName, relativeToContentPath);
Expand All @@ -93,13 +106,14 @@ function astroRehypeRelativeMarkdownLinks(options = {}) {
// if we have a custom slug, use it, else use the default
const resolvedSlug = resolveSlug(generatedSlug, frontmatterSlug);

// content collection slugs are relative to content collection root
// so build url including the content collection name and the pages slug
// NOTE - This only handles situations where the name of the content collection is directly
// mapped to the site page serving the content collection page (see details above)
let webPathFinal = path.posix.sep +
// content collection slugs are relative to content collection root (or site root if collectionPathMode is `root`)
// so build url including the content collection name (if applicable) and the pages slug
// NOTE - When there is a content collection name being applied, this only handles situations where the physical
// directory name of the content collection maps 1:1 to the site page path serviing the content collection
// page (see details above)
let webPathFinal =
[
collectionName,
collectionName === '' ? '' : (path.posix.sep + collectionName),
resolvedSlug,
].join(path.posix.sep);

Expand All @@ -111,6 +125,7 @@ function astroRehypeRelativeMarkdownLinks(options = {}) {

// Debugging
debug("--------------------------------");
debug("CollectionPathMode : %s", collectionPathMode);
debug("md/mdx AST Current File : %s", currentFile);
debug("md/mdx AST Current File Dir : %s", currentFileDirectory);
debug("md/mdx AST href full : %s", nodeHref);
Expand Down
112 changes: 112 additions & 0 deletions src/index.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,118 @@ test("astroRehypeRelativeMarkdownLinks", async (t) => {
const expected =
'<html><head></head><body><a href="./fixtures/dir-does-not-exist.mdx/">foo</a></body></html>';

assert.equal(actual, expected);
},
);

await t.test(
"should transform and contain index for root index.md when content path same as collection path",
async () => {
const input = '<a href="./fixtures/index.md">foo</a>';
const { value: actual } = await rehype()
.use(testSetupRehype)
.use(astroRehypeRelativeMarkdownLinks, { contentPath: "src/fixtures", collectionPathMode: 'root' })
.process(input);

const expected =
'<html><head></head><body><a href="/index">foo</a></body></html>';

assert.equal(actual, expected);
},
);

await t.test(
"should transform root index.md with empty string custom slug when content path same as collection path",
async () => {
const input = '<a href="./fixtures/dir-test-custom-slug/index.md">foo</a>';
const { value: actual } = await rehype()
.use(testSetupRehype)
.use(astroRehypeRelativeMarkdownLinks, { contentPath: "src/fixtures/dir-test-custom-slug", collectionPathMode: 'root' })
.process(input);

const expected =
'<html><head></head><body><a href="/">foo</a></body></html>';

assert.equal(actual, expected);
},
);

await t.test(
"should transform root path when content path same as collection path",
async () => {
const input = '<a href="./fixtures/test.md">foo</a>';
const { value: actual } = await rehype()
.use(testSetupRehype)
.use(astroRehypeRelativeMarkdownLinks, { contentPath: "src/fixtures", collectionPathMode: 'root' })
.process(input);

const expected =
'<html><head></head><body><a href="/test">foo</a></body></html>';

assert.equal(actual, expected);
},
);

await t.test(
"should transform root path custom slug when content path same as collection path",
async () => {
const input = '<a href="./fixtures/test-custom-slug.md">foo</a>';
const { value: actual } = await rehype()
.use(testSetupRehype)
.use(astroRehypeRelativeMarkdownLinks, { contentPath: "src/fixtures", collectionPathMode: 'root' })
.process(input);

const expected =
'<html><head></head><body><a href="/test.custom.slug-custom">foo</a></body></html>';

assert.equal(actual, expected);
},
);

await t.test(
"should transform subdir index.md when content path same as collection path",
async () => {
const input = '<a href="./fixtures/dir-test/index.md">foo</a>';
const { value: actual } = await rehype()
.use(testSetupRehype)
.use(astroRehypeRelativeMarkdownLinks, { contentPath: "src/fixtures", collectionPathMode: 'root' })
.process(input);

const expected =
'<html><head></head><body><a href="/dir-test">foo</a></body></html>';

assert.equal(actual, expected);
},
);

await t.test(
"should transform subdir path when content path same as collection path",
async () => {
const input = '<a href="./fixtures/dir-test/dir-test-child.md">foo</a>';
const { value: actual } = await rehype()
.use(testSetupRehype)
.use(astroRehypeRelativeMarkdownLinks, { contentPath: "src/fixtures", collectionPathMode: 'root' })
.process(input);

const expected =
'<html><head></head><body><a href="/dir-test/dir-test-child">foo</a></body></html>';

assert.equal(actual, expected);
},
);

await t.test(
"should transform subdir path custom slug when content path same as collection path",
async () => {
const input = '<a href="./fixtures/dir-test-custom-slug.md/test-custom-slug-in-dot-dir.md">foo</a>';
const { value: actual } = await rehype()
.use(testSetupRehype)
.use(astroRehypeRelativeMarkdownLinks, { contentPath: "src/fixtures", collectionPathMode: 'root' })
.process(input);

const expected =
'<html><head></head><body><a href="/dir-test-custom-slug.md/test.custom.slug.in.dot.dir">foo</a></body></html>';

assert.equal(actual, expected);
},
);
Expand Down

0 comments on commit fc33147

Please sign in to comment.