Skip to content

Commit

Permalink
refactor(app): better contents management
Browse files Browse the repository at this point in the history
  • Loading branch information
nfroidure committed Dec 22, 2023
1 parent 6996e2e commit c767aa9
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 71 deletions.
16 changes: 8 additions & 8 deletions src/app/conferences/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
import { join as pathJoin } from 'path';
import { entriesToBaseProps } from '../page';
import { readEntries } from '../../../utils/frontmatter';
import ContentBlock from '../../../components/contentBlock';
import Paragraph from '../../../components/p';
import Anchor from '../../../components/a';
import { fixText } from '../../../utils/text';
import { renderMarkdown } from '../../../utils/markdown';
import buildMetadata from '../../../utils/metadata';
import { entriesToBaseListingMetadata } from '../../../utils/conference';
import styles from './conference.module.scss';
import type { Metadata } from '../page';
import type { Entry } from '../page';

type Params = { id: string };

export async function generateMetadata({ params }: { params: Params }) {
const baseProps = entriesToBaseProps(
const baseListingMetadata = entriesToBaseListingMetadata(
await readEntries<Metadata>(pathJoin('.', 'contents', 'conferences'))
);
const entry = baseProps.entries.find(
const entry = baseListingMetadata.entries.find(
({ id }) => id === (params || {}).id
) as Entry;

return buildMetadata({
title: fixText(entry.title),
description: fixText(entry.description),
image: entry.illustration?.url,
pathname: `/conference/${params.id}`,
pathname: `/conferences/${params.id}`,
});
}

export default async function BlogPost({ params }: { params: Params }) {
const baseProps = entriesToBaseProps(
const baseListingMetadata = entriesToBaseListingMetadata(
await readEntries<Metadata>(pathJoin('.', 'contents', 'conferences'))
);
const entry = baseProps.entries.find(
const entry = baseListingMetadata.entries.find(
({ id }) => id === (params || {}).id
) as Entry;

Expand All @@ -59,10 +59,10 @@ export default async function BlogPost({ params }: { params: Params }) {
}

export async function generateStaticParams() {
const baseProps = entriesToBaseProps(
const baseListingMetadata = entriesToBaseListingMetadata(
await readEntries<Metadata>(pathJoin('.', 'contents', 'conferences'))
);
const paths = baseProps.entries.map((entry) => ({
const paths = baseListingMetadata.entries.map((entry) => ({
id: entry.id,
}));

Expand Down
77 changes: 31 additions & 46 deletions src/app/conferences/page.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,44 @@
import { join as pathJoin } from 'path';
import { readEntries } from '../../utils/frontmatter';
import { toASCIIString } from '../../utils/ascii';
import { readParams } from '../../utils/params';
import { parseMarkdown } from '../../utils/markdown';
import { buildSearchIndex } from '../../utils/search';
import { Entries } from './entries';
import buildMetadata from '../../utils/metadata';
import type { FrontMatterResult } from 'front-matter';
import { entriesToBaseListingMetadata } from '../../utils/conference';
import type { MarkdownRootNode } from '../../utils/markdown';
import type { BuildQueryParamsType } from '../../utils/params';
import { slicePage } from '../../utils/pagination';
import { POSTS_PER_PAGE } from '../../utils/conference';

export async function generateMetadata({
params,
}: {
params: { page: string };
params?: { page: string };
}) {
const page = params?.page || 1;
const title = `Conférences${page && page !== 1 ? ` - page ${page}` : ''}`;
const description = 'Découvrez le résumé de nos rencontres précédentes.';

return buildMetadata({
pathname: '/',
const metadata = await buildMetadata({
pathname: `/conferences${page && page !== 1 ? `/pages/${page}` : ''}`,
title,
description,
});

return {
...metadata,
alternates: {
...(metadata.alternates || {}),
types: {
...(metadata.alternates?.types || {}),
'application/rss+xml': [
{ url: '/conferences.rss', title: `${title} (RSS)` },
],
'application/atom+xml': [
{ url: '/conferences.atom', title: `${title} (Atom)` },
],
},
},
};
}

export type Metadata = {
Expand Down Expand Up @@ -65,53 +80,23 @@ const PARAMS_DEFINITIONS = {

type Params = BuildQueryParamsType<typeof PARAMS_DEFINITIONS>;

const POSTS_PER_PAGE = 10;

export default async function BlogEntries({
params,
}: {
params: { page: string };
}) {
const castedParams = readParams(PARAMS_DEFINITIONS, params || {}) as Params;
const page = castedParams?.page || 1;
const baseProps = entriesToBaseProps(
const baseListingMetadata = entriesToBaseListingMetadata(
await readEntries<Metadata>(pathJoin('.', 'contents', 'conferences'))
);
const entries = baseProps.entries.slice(
(page - 1) * POSTS_PER_PAGE,
(page - 1) * POSTS_PER_PAGE + POSTS_PER_PAGE
);
const pagesCount = Math.ceil(baseProps.entries.length / POSTS_PER_PAGE);
const entries = slicePage(baseListingMetadata.entries, page, POSTS_PER_PAGE);

// WARNING: This is not a nice way to generate the search index
// but having scripts run in the NextJS build context is a real
// pain
await buildSearchIndex(baseProps);

return <Entries entries={entries} pagesCount={pagesCount} page={page} />;
return (
<Entries
entries={entries}
pagesCount={baseListingMetadata.pagesCount}
page={page}
/>
);
}

export const entriesToBaseProps = (
baseEntries: FrontMatterResult<Metadata>[]
): BaseProps => {
const title = `Conférences`;
const description = 'Découvrez le résumé de nos rencontres précédentes.';
const entries = baseEntries
.map<Entry>((entry) => ({
...entry.attributes,
id: entry.attributes.leafname || toASCIIString(entry.attributes.title),
content: parseMarkdown(entry.body) as MarkdownRootNode,
}))
.filter((entry) => !entry.draft || process.env.NODE_ENV === 'development')
.sort(
({ date: dateA }: { date: string }, { date: dateB }: { date: string }) =>
Date.parse(dateA) > Date.parse(dateB) ? -1 : 1
);

return {
title,
description,
entries,
pagesCount: Math.ceil(entries.length / POSTS_PER_PAGE),
};
};
27 changes: 23 additions & 4 deletions src/app/conferences/pages/[page]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,42 @@
import { join as pathJoin } from 'path';
import BlogEntries, { entriesToBaseProps, generateMetadata } from '../../page';
import { entriesToBaseListingMetadata } from '../../../../utils/conference';
import BlogEntries, { generateMetadata } from '../../page';
import { readEntries } from '../../../../utils/frontmatter';
import { buildAssets } from '../../../../utils/build';
import { buildSearchIndex } from '../../../../utils/search';
import type { Metadata } from '../../page';

export { generateMetadata };
export default BlogEntries;

export async function generateStaticParams() {
const baseProps = entriesToBaseProps(
const baseListingMetadata = entriesToBaseListingMetadata(
await readEntries<Metadata>(pathJoin('.', 'contents', 'conferences'))
);
const { title, description } = await generateMetadata({});

// WARNING: This is not a nice way to generate the news feeds
// but having scripts run in the NextJS build context is a real
// pain
await buildAssets(baseProps);
await buildAssets(
{
...baseListingMetadata,
title: title as string,
description: description as string,
},
'/conferences'
);

// WARNING: This is not a nice way to generate the search index
// but having scripts run in the NextJS build context is a real
// pain
await buildSearchIndex({
...baseListingMetadata,
title: title as string,
description: description as string,
});

const paths = new Array(baseProps.pagesCount)
const paths = new Array(baseListingMetadata.pagesCount)
.fill('')
.map((_, index) => index + 1)
.filter((page) => page !== 1)
Expand Down
35 changes: 23 additions & 12 deletions src/utils/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,31 @@ import { generateAtomFeed, generateRSSFeed } from './feeds';
import { publicRuntimeConfig } from './config';
import { ORGANISATION_NAME } from './constants';
import type { FeedDescription, FeedItem } from './feeds';
import type { BaseProps } from '../app/conferences/page';
import type {
BaseListingPageMetadata,
BaseContentPageMetadata,
} from './contents';

const doWriteFile = promisify(writeFile);

const PROJECT_DIR = joinPath('.');
const baseURL = publicRuntimeConfig.baseURL;
const builtAt = new Date().toISOString();

export async function buildAssets(props: BaseProps) {
export async function buildAssets<T extends BaseContentPageMetadata>(
props: BaseListingPageMetadata<T> & {
title: string;
description: string;
},
path: string
) {
await Promise.all([
(async () => {
const { title, description, entries } = props;
const feedItems = entries.map((entry) => ({
title: entry.title,
description: entry.description,
url: baseURL + '/conferences/' + entry.id,
url: `${baseURL}${path}/${entry.id}`,
updatedAt: entry.date,
publishedAt: entry.date,
author: {
Expand All @@ -29,7 +38,7 @@ export async function buildAssets(props: BaseProps) {
}));
const commonDescription: Omit<FeedDescription, 'url'> = {
title: `${title} - ${ORGANISATION_NAME}`,
sourceURL: baseURL + '/conferences',
sourceURL: `${baseURL}${path}`,
description,
updatedAt: new Date(
entries.reduce(
Expand All @@ -42,45 +51,47 @@ export async function buildAssets(props: BaseProps) {
};

await Promise.all([
buildAtomFeed(commonDescription, feedItems),
buildRSSFeed(commonDescription, feedItems),
buildAtomFeed(commonDescription, feedItems, path),
buildRSSFeed(commonDescription, feedItems, path),
]);
})(),
]);
}

async function buildAtomFeed(
commonDescription: Omit<FeedDescription, 'url'>,
feedItems: FeedItem[]
feedItems: FeedItem[],
path: string
) {
const content = await generateAtomFeed(
{
...commonDescription,
url: baseURL + '/conferences.atom',
url: `${baseURL}${path}.atom`,
},
feedItems
);

await doWriteFile(
joinPath(PROJECT_DIR, 'public', 'conferences.atom'),
joinPath(PROJECT_DIR, 'public', `${path.slice(1)}.atom`),
content
);
}

async function buildRSSFeed(
commonDescription: Omit<FeedDescription, 'url'>,
feedItems: FeedItem[]
feedItems: FeedItem[],
path: string
) {
const content = await generateRSSFeed(
{
...commonDescription,
url: baseURL + '/conferences.rss',
url: `${baseURL}${path}.rss`,
},
feedItems
);

await doWriteFile(
joinPath(PROJECT_DIR, 'public', 'conferences.rss'),
joinPath(PROJECT_DIR, 'public', `${path.slice(1)}.rss`),
content
);
}
58 changes: 58 additions & 0 deletions src/utils/conference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { toASCIIString } from './ascii';
import { parseMarkdown, qualifyPath } from './markdown';
import { datedPagesSorter } from './contents';
import type { FrontMatterResult } from 'front-matter';
import type {
BaseContentPageMetadata,
BaseListingPageMetadata,
} from './contents';
import type { MarkdownRootNode } from './markdown';

export type ConferenceFrontmatterMetadata = {
leafname?: string;
title: string;
description: string;
date: string;
draft: boolean;
tags: string[];
categories: string[];
illustration?: {
url: string;
alt: string;
};
lang: string;
location: string;
};

export type Conference = {
id: string;
content: MarkdownRootNode;
} & ConferenceFrontmatterMetadata &
BaseContentPageMetadata;

export const POSTS_PER_PAGE = 10;
export const entriesToBaseListingMetadata = (
baseEntries: FrontMatterResult<ConferenceFrontmatterMetadata>[]
): BaseListingPageMetadata<Conference> => {
const entries = baseEntries
.map<Conference>((entry) => ({
...entry.attributes,
...(entry.attributes.illustration
? {
illustration: {
...entry.attributes.illustration,
url: qualifyPath(entry.attributes.illustration.url),
},
}
: {}),
id: entry.attributes.leafname || toASCIIString(entry.attributes.title),
content: parseMarkdown(entry.body) as MarkdownRootNode,
}))
.filter((entry) => !entry.draft || process.env.NODE_ENV === 'development')
.sort(datedPagesSorter);

return {
entries,
pagesCount: Math.ceil(entries.length / POSTS_PER_PAGE),
};
};
Loading

0 comments on commit c767aa9

Please sign in to comment.