Skip to content

Commit

Permalink
feat(content): add customFilename param to injectContent (#597)
Browse files Browse the repository at this point in the history
  • Loading branch information
choyiny committed Aug 8, 2023
1 parent d903fd4 commit 4f3dd68
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 42 deletions.
12 changes: 12 additions & 0 deletions apps/docs-app/docs/features/routing/content.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,15 @@ export default class ProjectComponent {
});
}
```

## Loading Custom Content

By default, Analog uses the route params to build the filename for retrieving a content file from the `src/content` folder. Analog also supports using a custom filename for retrieving content from the `src/content` folder. This can be useful if, for instance, you have a custom markdown file that you want to load on a page.

The `injectContent()` function can be used by passing an object that contains the `customFilename` property.

```ts
readonly post$ = injectContent<ProjectAttributes>({
customFilename: 'path/to/custom/file',
});
```
47 changes: 45 additions & 2 deletions packages/content/src/lib/content.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing';
import {
fakeAsync,
flushMicrotasks,
flush,
TestBed,
} from '@angular/core/testing';
import { ActivatedRoute, convertToParamMap } from '@angular/router';
import { expect } from 'vitest';
import { Observable, of } from 'rxjs';
Expand All @@ -22,6 +27,7 @@ describe('injectContent', () => {
expect(c.filename).toEqual('/src/content/test.md');
});
flushMicrotasks();
flush();
}));

it("should return ContentFile object with empty filename, empty attributes, and the custom fallback 'Custom Fallback' as content when no match between slug and files and custom fallback 'Custom Fallback' provided", fakeAsync(() => {
Expand All @@ -38,6 +44,7 @@ describe('injectContent', () => {
expect(c.filename).toEqual('/src/content/test.md');
});
flushMicrotasks();
flush();
}));

it('should return ContentFile object with correct filename, correct attributes, and the correct content of the file when match between slug and files', fakeAsync(() => {
Expand Down Expand Up @@ -65,6 +72,7 @@ Test Content`),
expect(c.slug).toEqual('test');
});
flushMicrotasks();
flush();
}));

it('should return ContentFile object with correct filename, correct attributes, and the correct content of the file when match between custom param and files', fakeAsync(() => {
Expand Down Expand Up @@ -94,6 +102,7 @@ Test Content`),
expect(c.slug).toEqual('custom-slug-test');
});
flushMicrotasks();
flush();
}));

it('should return ContentFile object when a custom param with prefix is provided', fakeAsync(() => {
Expand Down Expand Up @@ -125,11 +134,45 @@ Test Content`),
expect(c.slug).toEqual('custom-prefix-slug-test');
});
flushMicrotasks();
flush();
}));

it('should return ContentFile object when a custom filename is provided', fakeAsync(() => {
const customParam = { customFilename: 'custom-filename-test' };
const routeParams = {};
const contentFiles = {
'/src/content/dont-match.md': () =>
Promise.resolve(`---
slug: 'dont-match'
---
Dont Match'`),
'/src/content/custom-filename-test.md': () =>
Promise.resolve(`---
slug: 'custom-filename-test-slug'
---
Test Content`),
};
const { injectContent } = setup({
customParam,
routeParams,
contentFiles,
});
injectContent().subscribe((c) => {
expect(c.content).toMatch('Test Content');
expect(c.attributes).toEqual({ slug: 'custom-filename-test-slug' });
expect(c.filename).toEqual('/src/content/custom-filename-test.md');
expect(c.slug).toEqual('custom-filename-test');
});
flushMicrotasks();
flush();
}));

function setup(
args: Partial<{
customParam: string | { subdirectory: string; param: string };
customParam:
| string
| { subdirectory: string; param: string }
| { customFilename: string };
customFallback: string;
routeParams: { [key: string]: any };
contentFiles: Record<string, () => Promise<string>>;
Expand Down
118 changes: 78 additions & 40 deletions packages/content/src/lib/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,52 @@ import { CONTENT_FILES_TOKEN } from './content-files-token';
import { parseRawContentFile } from './parse-raw-content-file';
import { waitFor } from './utils/zone-wait-for';

function getContentFile<
Attributes extends Record<string, any> = Record<string, any>
>(
contentFiles: Record<string, () => Promise<string>>,
prefix: string,
slug: string,
fallback: string
): Observable<ContentFile<Attributes | Record<string, never>>> {
const filePath = `/src/content/${prefix}${slug}.md`;
const contentFile = contentFiles[filePath];
if (!contentFile) {
return of({
filename: filePath,
attributes: {},
slug: '',
content: fallback,
});
}

return new Observable<string>((observer) => {
const contentResolver = contentFile();

if (import.meta.env.SSR === true) {
waitFor(contentResolver).then((content) => {
observer.next(content);
});
} else {
contentResolver.then((content) => {
observer.next(content);
});
}
}).pipe(
map((rawContentFile) => {
const { content, attributes } =
parseRawContentFile<Attributes>(rawContentFile);

return {
filename: filePath,
slug,
attributes,
content,
};
})
);
}

/**
* Retrieves the static content using the provided param and/or prefix.
*
Expand All @@ -24,52 +70,44 @@ export function injectContent<
| {
param: string;
subdirectory: string;
}
| {
customFilename: string;
} = 'slug',
fallback = 'No Content Found'
): Observable<ContentFile<Attributes | Record<string, never>>> {
const route = inject(ActivatedRoute);
const contentFiles = inject(CONTENT_FILES_TOKEN);
const prefix = typeof param === 'string' ? '' : `${param.subdirectory}/`;

const paramKey = typeof param === 'string' ? param : param.param;
return route.paramMap.pipe(
map((params) => params.get(paramKey)),
switchMap((slug) => {
const filename = `/src/content/${prefix}${slug}.md`;
const contentFile = contentFiles[filename];

if (!contentFile) {
return of({
attributes: {},
filename: filename,
slug: slug || '',
content: fallback,
});
}

return new Promise<string>((resolve) => {
const contentResolver = contentFile();

if (import.meta.env.SSR === true) {
waitFor(contentResolver).then((content) => {
resolve(content);
});
if (typeof param === 'string' || 'param' in param) {
const prefix = typeof param === 'string' ? '' : `${param.subdirectory}/`;
const route = inject(ActivatedRoute);
const paramKey = typeof param === 'string' ? param : param.param;
return route.paramMap.pipe(
map((params) => params.get(paramKey)),
switchMap((slug) => {
if (slug) {
return getContentFile<Attributes>(
contentFiles,
prefix,
slug,
fallback
);
} else {
contentResolver.then((content) => {
resolve(content);
return of({
filename: '',
slug: '',
attributes: {},
content: fallback,
});
}
}).then((rawContentFile) => {
const { content, attributes } =
parseRawContentFile<Attributes>(rawContentFile);

return {
filename,
slug: slug || '',
attributes,
content,
};
});
})
);
})
);
} else {
return getContentFile<Attributes>(
contentFiles,
'',
param.customFilename,
fallback
);
}
}

0 comments on commit 4f3dd68

Please sign in to comment.