Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: handle collections in root of site (#18) #19

Merged
merged 15 commits into from
Apr 11, 2024
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