Skip to content

Commit

Permalink
feat: add initial toc
Browse files Browse the repository at this point in the history
  • Loading branch information
fbuireu committed Sep 26, 2024
1 parent bd9603b commit 8b21ee4
Show file tree
Hide file tree
Showing 11 changed files with 168 additions and 84 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"@fontsource/baskervville": "^5.1.0",
"@hookform/resolvers": "^3.9.0",
"@million/lint": "1.0.0-rc.84",
"algoliasearch": "^5.6.0",
"algoliasearch": "^5.6.1",
"astro": "^4.15.9",
"clsx": "^2.1.1",
"contentful": "^11.0.3",
Expand Down Expand Up @@ -89,7 +89,7 @@
"@testing-library/react-hooks": "^8.0.1",
"@types/add": "^2.0.3",
"@types/markdown-it": "^14.1.2",
"@types/node": "^22.7.0",
"@types/node": "^22.7.2",
"@types/react": "^18.3.9",
"@types/react-dom": "^18.3.0",
"@types/three": "^0.168.0",
Expand Down
9 changes: 6 additions & 3 deletions src/application/dto/article/articleDTO.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ArticleDTO, RawArticle } from "@application/dto/article/types";
import { ArticleType } from "@application/dto/article/types";
import { createRelatedArticles } from "@application/dto/article/utils/createRelatedArticles";
import { generateTableOfContents } from "@application/dto/article/utils/generateTableOfContents";
import { getRelatedArticles } from "@application/dto/article/utils/getRelatedArticles/getRelatedArticles";
import { DEFAULT_DATE_FORMAT } from "@const/index";
import { documentToHtmlString } from "@contentful/rich-text-html-renderer";
Expand All @@ -19,14 +20,15 @@ const PARSER: MarkdownIt = new MarkdownIt();
export const articleDTO: BaseDTO<RawArticle[], ArticleDTO[]> = {
create: (raw) => {
return raw.map((rawArticle) => {
const contentHtml = documentToHtmlString(rawArticle.fields.content as unknown as Document);

const description =
rawArticle.fields.description ??
generateExcerpt({
parser: PARSER,
content: documentToHtmlString(rawArticle.fields.content as unknown as Document),
content: contentHtml,
});

const tags = createTags(rawArticle.fields.tags);
const relatedArticles = rawArticle.fields.relatedArticles
? createRelatedArticles(rawArticle.fields.relatedArticles)
: getRelatedArticles({ rawArticle, allRawArticles: raw });
Expand All @@ -44,8 +46,9 @@ export const articleDTO: BaseDTO<RawArticle[], ArticleDTO[]> = {
content,
isFeaturedArticle: rawArticle.fields.featuredArticle,
readingTime: getReadingTime(content),
tags,
tag: createTags(rawArticle.fields.tags),
relatedArticles,
tableOfContents: generateTableOfContents(contentHtml),
} as unknown as ArticleDTO;
});
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { CollectionEntry } from "astro:content";
import { slugify } from "@modules/core/utils/slugify";

type TableOfContentsReturnType = CollectionEntry<"articles">["data"]["tableOfContents"];

const HEADINGS_REGEX = /<h([1-6])>(.*?)<\/h\1>/g;

export function generateTableOfContents(html: string): TableOfContentsReturnType {
const items: TableOfContentsReturnType = [];
const matches = html.matchAll(HEADINGS_REGEX);

for (const match of matches) {
const level = Number.parseInt(match[1]);
const heading = match[2];
const id = slugify(heading);

items.push({ id, heading, level });
}

return items;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./generateTableOfContents";
1 change: 1 addition & 0 deletions src/application/entities/articles/articles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const articles = defineCollection({
});

const articles = articleDTO.create(rawArticles as unknown as RawArticle[]);

return articles.map((article) => ({
id: article.slug,
...article,
Expand Down
10 changes: 10 additions & 0 deletions src/application/entities/articles/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,14 @@ export const articleSchema = z.object({
readingTime: z.number(),
tags: z.array(tagSchema).optional(),
relatedArticles: z.array(reference("articles")).default([]),
tableOfContents: z
.array(
z.object({
id: z.string(),
heading: z.string(),
level: z.number(),
}),
)
.optional()
.default([]),
});
2 changes: 2 additions & 0 deletions src/pages/articles/[...slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SITE_URL } from "astro:env/client";
import { PAGES_ROUTES } from "@const/index";
import type { SeoMetadata } from "@const/types";
import ArticleDetails from "@modules/article/components/articleDetails/ArticleDetails.astro";
import TableOfContents from "@modules/article/components/tableOfContents/TableOfContents.astro";
import ReadingProgress from "@modules/articles/components/readingProgress/ReadingProgress.astro";
import BaseLayout from "@modules/core/components/baseLayout/BaseLayout.astro";
import Breadcrumbs from "@modules/core/components/breadcrumbs/Breadcrumbs.astro";
Expand Down Expand Up @@ -56,6 +57,7 @@ const metadata: Partial<SeoMetadata> = {
<Breadcrumbs />
<ReadingProgress />
<ArticleDetails article={article} />
<TableOfContents tableOfContents={article.data.tableOfContents} />
<article class="article__wrapper" set:html={article.data.content} />
<script is:inline type="application/ld+json" set:html={JSON.stringify({
'@context': 'https://schema.org',
Expand Down
2 changes: 1 addition & 1 deletion src/pages/articles/_article.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
gap: 0 1rem;
grid: ". Article-Content ." / 1fr min(var(--grid-article), 100%) 1fr;
line-height: var(--base-line-height);
margin-block: 4rem;
margin-block: 2rem 4rem;

@supports (initial-letter: 3 2) {
& > p:first-of-type::first-letter {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
import type { CollectionEntry } from "astro:content";
import "./table-of-contents.css";
interface TableOfContentsProps {
tableOfContents: CollectionEntry<"articles">["data"]["tableOfContents"];
}
const { tableOfContents } = Astro.props as TableOfContentsProps;
---
<aside class="article__table-of-contents__wrapper" role="complementary">
<nav class="article__table-of-contents__nav" aria-label="Table of contents">
<ul class="article__table-of-contents__list flex column-nowrap">
{tableOfContents.map(({ id, heading, level}) => (
<li class="article__table-of-contents__item --underline-on-hover --is-clickable" style={`--inline-level: ${level};`}>
<a class="article__table-of-contents__item__link" href={`#${id}`}>{heading}</a>
</li>
))}
</ul>
</nav>
</aside>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.article__table-of-contents__wrapper {
position: sticky;
top: calc(var(--header-height) + 2rem);
}

.article__table-of-contents__nav {
position: absolute;
right: 1rem;
}
.article__table-of-contents__list {
gap: 0.25rem 0;
}
.article__table-of-contents__item {
margin-inline-start: calc(0.25rem * var(--inline-level));
width: fit-content;

&:hover {
color: var(--primary-main);
}
}

.article__table-of-contents__item__link {
position: relative;
z-index: 1;
}
Loading

0 comments on commit 8b21ee4

Please sign in to comment.