Skip to content

Commit

Permalink
feat: Add childPages to context of index pages
Browse files Browse the repository at this point in the history
This property now gets (on-demand) the immediate children of an index page.

- Types (including arguments for generic types) created for the new property
- Pages getter can now take an array of globs instead of just a glob string
- New `contentPath` property on `Location` to make it easier to work with content directory-relative paths
  • Loading branch information
ericselin committed Dec 4, 2021
1 parent 61ad567 commit bee744e
Show file tree
Hide file tree
Showing 14 changed files with 203 additions and 33 deletions.
18 changes: 11 additions & 7 deletions core/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,18 @@ const createPagesGetter = (
const processWalkEntry = getWalkEntryProcessor(options);
const { log } = options;
return async (glob) => {
const contentGlob = path.join(options.contentDir, glob);
log?.debug(`Getting pages with glob "${contentGlob}"`);
let globArray = glob;
if (typeof globArray === "string") globArray = [globArray];
const pages: Page[] = [];
for await (const walkEntry of expandGlob(contentGlob)) {
const content = await getContent(processWalkEntry(walkEntry));
if (content?.type === ContentType.Page) {
log?.debug(`Found page ${content.location.inputPath}`);
pages.push(content);
for (const glob of globArray) {
const contentGlob = path.join(options.contentDir, glob);
log?.debug(`Getting pages with glob "${contentGlob}"`);
for await (const walkEntry of expandGlob(contentGlob, { extended: true })) {
const content = await getContent(processWalkEntry(walkEntry));
if (content?.type === ContentType.Page) {
log?.debug(`Found page ${content.location.inputPath}`);
pages.push(content);
}
}
}
return pages;
Expand Down
3 changes: 3 additions & 0 deletions core/dirty-checkers/file-mod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Deno.test("returns true if content newer than output", async () => {
type: ContentType.Unknown,
inputPath: path.join(dir, "content", "3.md"),
outputPath: path.join(dir, "public", "2.html"),
contentPath: "",
url: new URL('https://test.com'),
}),
true,
Expand All @@ -58,6 +59,7 @@ Deno.test("returns false if content older than output", async () => {
type: ContentType.Unknown,
inputPath: path.join(dir, "content", "1.md"),
outputPath: path.join(dir, "public", "2.html"),
contentPath: "",
url: new URL('https://test.com'),
}),
false,
Expand All @@ -70,6 +72,7 @@ Deno.test("returns true if output not found", async () => {
type: ContentType.Unknown,
inputPath: path.join(dir, "content", "1.md"),
outputPath: "not-found.html",
contentPath: "",
url: new URL('https://test.com'),
}),
true,
Expand Down
84 changes: 76 additions & 8 deletions core/jsx.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/** @jsx h */

import { assertEquals } from "../deps.ts";
import { h, createRenderer } from "./jsx.ts";
import type { BuildOptions, Component } from "../domain.ts";
import { createRenderer, h } from "./jsx.ts";
import { ContentType } from "../domain.ts";
import type { BuildOptions, Component, Page } from "../domain.ts";

const render = createRenderer({} as BuildOptions)();

Expand Down Expand Up @@ -47,12 +48,11 @@ Deno.test("rendering jsx with spread props works", async () => {
{children}
</div>
);
const Sub: Component<{who: string}> = (props) =>
(
<Base {...props}>
<p>Hello {props.who}</p>
</Base>
);
const Sub: Component<{ who: string }> = (props) => (
<Base {...props}>
<p>Hello {props.who}</p>
</Base>
);

assertEquals(
await render(<Sub who="world" />),
Expand Down Expand Up @@ -84,3 +84,71 @@ Deno.test("rendering nested custom jsx elements works", async () => {
"<p>Hello world</p>",
);
});

Deno.test("childPages pages getter run with correct glob", async () => {
const getPages = (glob: string | string[]): Promise<Page[]> =>
//@ts-ignore this returns string just for the test
Promise.resolve(glob);

const render = createRenderer({
contentDir: "../content",
layoutDir: "",
publicDir: "",
}, getPages)({
type: ContentType.Page,
//@ts-ignore only content path needed
location: {
contentPath: "page/index.md",
},
frontmatter: {},
content: "",
});

const Children: Component<
Record<string, unknown>,
undefined,
undefined,
unknown
> = async (_, { childPages }) => (
<p>{childPages && (await childPages)?.join(",")}</p>
);

assertEquals(
await render(<Children />),
"<p>page/!(index).md,page/*/index.md</p>",
);
});

Deno.test("childPages calls getPage only for index.md files", async () => {
const getPages = (_glob: string | string[]): Promise<Page[]> => {
throw new Error("This should not be called");
}

const render = createRenderer({
contentDir: "../content",
layoutDir: "",
publicDir: "",
}, getPages)({
type: ContentType.Page,
//@ts-ignore only content path needed
location: {
contentPath: "page/page.md",
},
frontmatter: {},
content: "",
});

const Children: Component<
Record<string, unknown>,
undefined,
undefined,
unknown
> = async (_, { childPages }) => (
<p>{childPages && (await childPages)?.join(",")}</p>
);

assertEquals(
await render(<Children />),
"<p></p>",
);
});
17 changes: 16 additions & 1 deletion core/jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ or email [email protected] <mailto:[email protected]>
*/

import type {
Page,
Context,
Element,
ElementCreator,
ElementRenderer,
ElementRendererCreator,
Location,
Page,
Props,
} from "../domain.ts";
import { path } from "../deps.ts";
Expand All @@ -48,6 +49,14 @@ const renderProps = (props?: Props): string => {
);
};

const _shouldHaveChildPages = ({ contentPath }: Location) =>
contentPath.split(path.sep).pop() === "index.md";

const _getChildPagesGlobs = ({ contentPath }: Location): string[] => {
const contentDir = path.dirname(contentPath);
return [`${contentDir}/!(index).md`, `${contentDir}/*/index.md`];
};

export const createRenderer: ElementRendererCreator = (options, getPages) =>
(contentPage) => {
const renderContext = {
Expand Down Expand Up @@ -83,6 +92,12 @@ export const createRenderer: ElementRendererCreator = (options, getPages) =>
const context: Context = {
page: contentPage as Page,
needsCss: renderContext.needsCss,
get childPages() {
if (getPages && _shouldHaveChildPages(this.page.location)) {
return getPages(_getChildPagesGlobs(this.page.location));
}
return undefined;
},
};
if (component.wantsPages) {
context.wantedPages = getPages &&
Expand Down
1 change: 1 addition & 0 deletions core/layout-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ const loadLayout = (
layout.module.default as Component<
DefaultProps,
unknown,
unknown,
unknown
>,
),
Expand Down
1 change: 1 addition & 0 deletions core/walk-entry-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const processWalkEntry: WalkEntryToLocationConverter = (
type,
inputPath: path.join(contentDir, inputPath),
outputPath: path.join(publicDir, outputPath),
contentPath: inputPath,
url: createURL(outputPath),
};
};
Expand Down
4 changes: 4 additions & 0 deletions docs/content/docs/list-pages/content/index.md
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
---
title: Index page
---

# Welcome to my page
5 changes: 5 additions & 0 deletions docs/content/docs/list-pages/content/list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: List page
---

# Welcome to my page
2 changes: 1 addition & 1 deletion docs/content/docs/list-pages/expected/index.html
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
<body><h1 id="welcome-to-my-page">Welcome to my page</h1>
<main><article><h2>This is the first page</h2>I have some content here. Since this text is not very long, all of it will be used as a summary. Normally, the summary is capped at 500 characters.</article><article><h2>This is the second page</h2>The second page is not very interesting, move on please.</article><article><h2>This is the third page</h2>I know you&#39;re out there. I can feel you now. I know that you&#39;re afraid... you&#39;re afraid of us. You&#39;re afraid of change. I don&#39;t know the future. I didn&#39;t come here to tell you how this is going to end. I came here to tell you how it&#39;s going to begin. I&#39;m going to hang up this phone, and then I&#39;m going to show these people what you don&#39;t want them to see. I&#39;m going to show them a world without you.</article></main></body>
<main><article><h2>List page</h2>Welcome to my page</article><article><h2>This is the first page</h2>I have some content here. Since this text is not very long, all of it will be used as a summary. Normally, the summary is capped at 500 characters.</article><article><h2>This is the second page</h2>The second page is not very interesting, move on please.</article><article><h2>This is the third page</h2>I know you&#39;re out there. I can feel you now. I know that you&#39;re afraid... you&#39;re afraid of us. You&#39;re afraid of change. I don&#39;t know the future. I didn&#39;t come here to tell you how this is going to end. I came here to tell you how it&#39;s going to begin. I&#39;m going to hang up this phone, and then I&#39;m going to show these people what you don&#39;t want them to see. I&#39;m going to show them a world without you.</article></main></body>
2 changes: 2 additions & 0 deletions docs/content/docs/list-pages/expected/list/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<body><h1 id="welcome-to-my-page">Welcome to my page</h1>
<main><article><h2>This is the first page</h2>I have some content here. Since this text is not very long, all of it will be used as a summary. Normally, the summary is capped at 500 characters.</article><article><h2>This is the second page</h2>The second page is not very interesting, move on please.</article><article><h2>This is the third page</h2>I know you&#39;re out there. I can feel you now. I know that you&#39;re afraid... you&#39;re afraid of us. You&#39;re afraid of change. I don&#39;t know the future. I didn&#39;t come here to tell you how this is going to end. I came here to tell you how it&#39;s going to begin. I&#39;m going to hang up this phone, and then I&#39;m going to show these people what you don&#39;t want them to see. I&#39;m going to show them a world without you.</article></main></body>
42 changes: 35 additions & 7 deletions docs/content/docs/list-pages/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,65 @@
title: List pages
---

Most websites have list pages of some sort - lists of blog posts on a marketing site or lists products on an ecommerce site. In order to create these list pages, your layout needs to know about not only the list page itself, but also the pages that should be rendered into the list. In `bob` this can be solved by providing a glob of content files to use in the layout. This is done by setting the `wantsPages` parameter on the layout `Component`.
Most websites have list pages of some sort - lists of blog posts on a marketing site or lists products on an ecommerce site. In order to create these list pages, your layout needs to know about not only the list page itself, but also the pages that should be rendered into the list. In `bob` you have a couple of options to achive this:

- Use the `childPages` property on `Context`

If the content page that is rendered is an index page (i.e. `index.md`), the page context will include the `childPages` property. This returns a promise of a page array (`Promise<Page[]>`). The pages returned are the immediate child pages of the index page in question.

- Use the `wantedPages` property on the layout page (`Component`)

If a layout page will always need the same content pages, you can specify any glob of content pages on the layout page. This is done by setting the `wantsPages` parameter on the layout `Component`.

> Remember to set the correct type for your layout component. The signature of the (generic) type is `Component<Props, ContentPage, WantedPages, ChildPages>`.

## Example project structure

## Example

Given the below folder structure, we want to get all pages to show in a listing on the index / home page.

```
site root
└-- content
| └-- index.md
| └-- list.md
| └-- page-1.md
| └-- page-2.md
| └-- page-3.md
└-- layouts
└-- index.tsx
└-- list.tsx
```

In the index layout, we specify the glob `*.md`. This glob is relative to the `content` folder, and thus means "all markdown files in the content root".
## Example using `childPages`

The pages that match this glob are returned to the layout (or any component, really) in the `context` argument. These pages receive the complete `Page` objects for the wanted pages. This means you can use properties like `title`, `summary` and `location.url`. The context is the second argument to the `Component` (the first being `Props`).
The `index.md` page is an "index page". That means it will have access to the `childPages` context property. In this case this will resolve to all of the other pages.

code:layouts/index.tsx

### Result

iframe:expected/index.html

## Example using `wantedPages`

The `list.tsx` layout is only used for the `list.md` page. This means that for this page we can specify the pages to list in the layout itself. In the index layout, we specify the glob `!(index).md`. This glob is relative to the `content` folder, and thus means "all markdown files in the content root, excluding the index.md file".

The pages that match this glob are returned to the layout (or any component, really) in the `context` argument. These pages receive the complete `Page` objects for the wanted pages. This means you can use properties like `title`, `summary` and `location.url`. The context is the second argument to the `Component` (the first being `Props`).

code:layouts/list.tsx

> Beware: The `wantedPages` array currently does not include the page itself. This is by design (as you can guess from the example), but might change in the future.
## Result
### Result

iframe:expected/index.html
iframe:expected/list/index.html

### Content
## Content

code:content/index.md

code:content/list.md

code:content/page-1.md
8 changes: 3 additions & 5 deletions docs/content/docs/list-pages/layouts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { Component, h } from "../../../../../mod.ts";

type EmptyObject = Record<string, never>;

const Index: Component<EmptyObject, unknown, EmptyObject> = (
const Index: Component<EmptyObject, unknown, undefined, EmptyObject> = async (
_props,
{ page: { content }, wantedPages },
{ page: { content }, childPages },
) => (
<body>
{content}
<main>
{wantedPages?.map((page) => (
{(await childPages)?.map((page) => (
<article>
<h2>{page.title}</h2>
{page.summary}
Expand All @@ -21,6 +21,4 @@ const Index: Component<EmptyObject, unknown, EmptyObject> = (
</body>
);

Index.wantsPages = "*.md";

export default Index;
26 changes: 26 additions & 0 deletions docs/content/docs/list-pages/layouts/list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/** @jsx h */

import { Component, h } from "../../../../../mod.ts";

type EmptyObject = Record<string, never>;

const Index: Component<EmptyObject, unknown, EmptyObject> = (
_props,
{ page: { content }, wantedPages },
) => (
<body>
{content}
<main>
{wantedPages?.map((page) => (
<article>
<h2>{page.title}</h2>
{page.summary}
</article>
))}
</main>
</body>
);

Index.wantsPages = "!(index).md";

export default Index;
Loading

0 comments on commit bee744e

Please sign in to comment.