diff --git a/src/app/conferences/[id]/page.tsx b/src/app/conferences/[id]/page.tsx index 5cbf74af..cb8271f0 100644 --- a/src/app/conferences/[id]/page.tsx +++ b/src/app/conferences/[id]/page.tsx @@ -1,5 +1,4 @@ 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'; @@ -7,6 +6,7 @@ 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'; @@ -14,10 +14,10 @@ import type { Entry } from '../page'; type Params = { id: string }; export async function generateMetadata({ params }: { params: Params }) { - const baseProps = entriesToBaseProps( + const baseListingMetadata = entriesToBaseListingMetadata( await readEntries(pathJoin('.', 'contents', 'conferences')) ); - const entry = baseProps.entries.find( + const entry = baseListingMetadata.entries.find( ({ id }) => id === (params || {}).id ) as Entry; @@ -25,15 +25,15 @@ export async function generateMetadata({ params }: { params: Params }) { 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(pathJoin('.', 'contents', 'conferences')) ); - const entry = baseProps.entries.find( + const entry = baseListingMetadata.entries.find( ({ id }) => id === (params || {}).id ) as Entry; @@ -59,10 +59,10 @@ export default async function BlogPost({ params }: { params: Params }) { } export async function generateStaticParams() { - const baseProps = entriesToBaseProps( + const baseListingMetadata = entriesToBaseListingMetadata( await readEntries(pathJoin('.', 'contents', 'conferences')) ); - const paths = baseProps.entries.map((entry) => ({ + const paths = baseListingMetadata.entries.map((entry) => ({ id: entry.id, })); diff --git a/src/app/conferences/page.tsx b/src/app/conferences/page.tsx index 4b0dc75a..14660b2b 100644 --- a/src/app/conferences/page.tsx +++ b/src/app/conferences/page.tsx @@ -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 = { @@ -65,8 +80,6 @@ const PARAMS_DEFINITIONS = { type Params = BuildQueryParamsType; -const POSTS_PER_PAGE = 10; - export default async function BlogEntries({ params, }: { @@ -74,44 +87,16 @@ export default async function BlogEntries({ }) { const castedParams = readParams(PARAMS_DEFINITIONS, params || {}) as Params; const page = castedParams?.page || 1; - const baseProps = entriesToBaseProps( + const baseListingMetadata = entriesToBaseListingMetadata( await readEntries(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 ; + return ( + + ); } - -export const entriesToBaseProps = ( - baseEntries: FrontMatterResult[] -): BaseProps => { - const title = `Conférences`; - const description = 'Découvrez le résumé de nos rencontres précédentes.'; - const entries = baseEntries - .map((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), - }; -}; diff --git a/src/app/conferences/pages/[page]/page.tsx b/src/app/conferences/pages/[page]/page.tsx index 21199c69..a4308fb2 100644 --- a/src/app/conferences/pages/[page]/page.tsx +++ b/src/app/conferences/pages/[page]/page.tsx @@ -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(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) diff --git a/src/utils/build.ts b/src/utils/build.ts index ee526fa2..94f7a5ce 100644 --- a/src/utils/build.ts +++ b/src/utils/build.ts @@ -5,7 +5,10 @@ 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); @@ -13,14 +16,20 @@ const PROJECT_DIR = joinPath('.'); const baseURL = publicRuntimeConfig.baseURL; const builtAt = new Date().toISOString(); -export async function buildAssets(props: BaseProps) { +export async function buildAssets( + props: BaseListingPageMetadata & { + 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: { @@ -29,7 +38,7 @@ export async function buildAssets(props: BaseProps) { })); const commonDescription: Omit = { title: `${title} - ${ORGANISATION_NAME}`, - sourceURL: baseURL + '/conferences', + sourceURL: `${baseURL}${path}`, description, updatedAt: new Date( entries.reduce( @@ -42,8 +51,8 @@ export async function buildAssets(props: BaseProps) { }; await Promise.all([ - buildAtomFeed(commonDescription, feedItems), - buildRSSFeed(commonDescription, feedItems), + buildAtomFeed(commonDescription, feedItems, path), + buildRSSFeed(commonDescription, feedItems, path), ]); })(), ]); @@ -51,36 +60,38 @@ export async function buildAssets(props: BaseProps) { async function buildAtomFeed( commonDescription: Omit, - 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, - 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 ); } diff --git a/src/utils/conference.ts b/src/utils/conference.ts new file mode 100644 index 00000000..eea62b81 --- /dev/null +++ b/src/utils/conference.ts @@ -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[] +): BaseListingPageMetadata => { + const entries = baseEntries + .map((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), + }; +}; diff --git a/src/utils/contents.ts b/src/utils/contents.ts new file mode 100644 index 00000000..4831255a --- /dev/null +++ b/src/utils/contents.ts @@ -0,0 +1,40 @@ +export type BaseContentPageMetadata = { + id: string; + leafname?: string; + title: string; + description: string; + summary?: string; + date: string; + draft?: boolean; + illustration?: { + url: string; + alt: string; + }; +}; + +export type BaseListingPageMetadata = { + entries: T[]; + pagesCount: number; +}; +export type BasePagingPageMetadata = + BaseListingPageMetadata & { + page: number; + }; + +export function datedPagesSorter( + { date: dateA }: T, + { date: dateB }: T +): number { + return Date.parse(dateA) === Date.parse(dateB) + ? 0 + : Date.parse(dateA) > Date.parse(dateB) + ? -1 + : 1; +} + +export function titledPagesSorter( + { title: titleA }: T, + { title: titleB }: T +): number { + return titleA.localeCompare(titleB); +} diff --git a/src/utils/markdown.tsx b/src/utils/markdown.tsx index 502c9fc1..e0e65e9a 100644 --- a/src/utils/markdown.tsx +++ b/src/utils/markdown.tsx @@ -461,3 +461,16 @@ function eventuallyConvertHTMLNodes(rootNode: MarkdownRootNode): MarkdownNode { return rootNode; } + +// Change VSCode autocompleted paths to URLs +export function qualifyPath(path: string): string { + if (/^https?:\/\//.test(path)) { + return path; + } + if (path.startsWith('/public/')) { + return ( + (publicRuntimeConfig?.basePath || '') + path.replace('/public/', '/') + ); + } + return path; +} diff --git a/src/utils/pagination.ts b/src/utils/pagination.ts new file mode 100644 index 00000000..432b4097 --- /dev/null +++ b/src/utils/pagination.ts @@ -0,0 +1,6 @@ +export function slicePage(entries: T[], page: number, postPerPage: number) { + return entries.slice( + (page - 1) * postPerPage, + (page - 1) * postPerPage + postPerPage + ); +} diff --git a/tsconfig.json b/tsconfig.json index fa56f4fe..c4ca6192 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,7 +28,8 @@ "next-env.d.ts", "**/*.ts", "**/*.tsx", - "out/types/**/*.ts" + "out/types/**/*.ts", + ".next/types/**/*.ts" ], "exclude": [ "node_modules"