aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--next.config.js3
-rw-r--r--package.json5
-rw-r--r--src/components/atoms/links/social-link/social-link.test.tsx2
-rw-r--r--src/components/organisms/forms/search-form/search-form.test.tsx2
-rw-r--r--src/components/templates/page/page-layout.tsx19
-rw-r--r--src/pages/blog/page/[number].tsx14
-rw-r--r--src/services/graphql/articles.ts35
-rw-r--r--src/services/graphql/thematics.ts40
-rw-r--r--src/services/graphql/topics.ts36
-rw-r--r--src/utils/helpers/index.ts1
-rw-r--r--src/utils/helpers/pages.tsx8
-rw-r--r--src/utils/helpers/rehype.ts23
-rw-r--r--src/utils/helpers/rss.ts4
-rw-r--r--src/utils/hooks/use-article.ts18
-rw-r--r--src/utils/hooks/use-headings-tree/use-headings-tree.test.ts47
-rw-r--r--src/utils/hooks/use-headings-tree/use-headings-tree.ts36
-rw-r--r--src/utils/hooks/use-posts-list/use-posts-list.ts11
-rw-r--r--yarn.lock181
18 files changed, 365 insertions, 120 deletions
diff --git a/next.config.js b/next.config.js
index d620718..8227603 100644
--- a/next.config.js
+++ b/next.config.js
@@ -2,6 +2,7 @@ import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import bundleAnalyzer from '@next/bundle-analyzer';
import nextMDX from '@next/mdx';
+import rehypeSlug from 'rehype-slug';
const currentDir = dirname(fileURLToPath(import.meta.url));
@@ -164,7 +165,7 @@ const withMDX = nextMDX({
extension: /\.mdx?$/,
options: {
remarkPlugins: [],
- rehypePlugins: [],
+ rehypePlugins: [rehypeSlug],
},
});
diff --git a/package.json b/package.json
index baf8fc3..c595c9f 100644
--- a/package.json
+++ b/package.json
@@ -57,9 +57,14 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-intl": "^6.5.5",
+ "rehype-parse": "^9.0.0",
+ "rehype-sanitize": "^6.0.0",
+ "rehype-slug": "^6.0.0",
+ "rehype-stringify": "^10.0.0",
"schema-dts": "^1.1.2",
"sharp": "^0.32.6",
"swr": "^2.2.4",
+ "unified": "^11.0.4",
"use-ackee": "^3.0.1"
},
"devDependencies": {
diff --git a/src/components/atoms/links/social-link/social-link.test.tsx b/src/components/atoms/links/social-link/social-link.test.tsx
index 9129c27..041e150 100644
--- a/src/components/atoms/links/social-link/social-link.test.tsx
+++ b/src/components/atoms/links/social-link/social-link.test.tsx
@@ -1,4 +1,4 @@
-import { describe, expect, it } from '@jest/globals';
+import { describe, expect, it, jest } from '@jest/globals';
import { render, screen as rtlScreen } from '@testing-library/react';
import { SocialLink } from './social-link';
diff --git a/src/components/organisms/forms/search-form/search-form.test.tsx b/src/components/organisms/forms/search-form/search-form.test.tsx
index 56ba0d7..d1fdfa9 100644
--- a/src/components/organisms/forms/search-form/search-form.test.tsx
+++ b/src/components/organisms/forms/search-form/search-form.test.tsx
@@ -1,4 +1,4 @@
-import { describe, expect, it } from '@jest/globals';
+import { describe, expect, it, jest } from '@jest/globals';
import { userEvent } from '@testing-library/user-event';
import { render, screen as rtlScreen } from '../../../../../tests/utils';
import { SearchForm } from './search-form';
diff --git a/src/components/templates/page/page-layout.tsx b/src/components/templates/page/page-layout.tsx
index 8ea0087..db71e07 100644
--- a/src/components/templates/page/page-layout.tsx
+++ b/src/components/templates/page/page-layout.tsx
@@ -4,14 +4,13 @@ import {
type FC,
type HTMLAttributes,
type ReactNode,
- useRef,
useCallback,
} from 'react';
import { useIntl } from 'react-intl';
import type { BreadcrumbList } from 'schema-dts';
import { sendComment } from '../../../services/graphql';
import type { SendCommentInput } from '../../../types';
-import { useHeadingsTree, useIsMounted } from '../../../utils/hooks';
+import { useHeadingsTree } from '../../../utils/hooks';
import { Heading, Sidebar } from '../../atoms';
import {
PageFooter,
@@ -137,9 +136,9 @@ export const PageLayout: FC<PageLayoutProps> = ({
id: 'eys2uX',
});
- const bodyRef = useRef<HTMLDivElement>(null);
- const isMounted = useIsMounted(bodyRef);
- const headingsTree = useHeadingsTree(bodyRef, { fromLevel: 2 });
+ const { ref: bodyRef, tree: headingsTree } = useHeadingsTree<HTMLDivElement>({
+ fromLevel: 2,
+ });
const saveComment: CommentFormSubmit = useCallback(
async (data) => {
@@ -223,12 +222,10 @@ export const PageLayout: FC<PageLayoutProps> = ({
})}
className={`${styles.sidebar} ${styles['sidebar--first']}`}
>
- {isMounted && bodyRef.current ? (
- <TocWidget
- heading={<Heading level={3}>{tocTitle}</Heading>}
- tree={headingsTree}
- />
- ) : null}
+ <TocWidget
+ heading={<Heading level={3}>{tocTitle}</Heading>}
+ tree={headingsTree}
+ />
</Sidebar>
) : null}
{typeof children === 'string' ? (
diff --git a/src/pages/blog/page/[number].tsx b/src/pages/blog/page/[number].tsx
index 49b5eb4..03b641b 100644
--- a/src/pages/blog/page/[number].tsx
+++ b/src/pages/blog/page/[number].tsx
@@ -39,12 +39,15 @@ import {
getBlogSchema,
getLinksItemData,
getPageLinkFromRawData,
- getPostsList,
getSchemaJson,
getWebPageSchema,
} from '../../../utils/helpers';
import { loadTranslation, type Messages } from '../../../utils/helpers/server';
-import { useBreadcrumb, useRedirection } from '../../../utils/hooks';
+import {
+ useBreadcrumb,
+ usePostsList,
+ useRedirection,
+} from '../../../utils/hooks';
type BlogPageProps = {
articles: EdgesResponse<RawArticle>;
@@ -70,6 +73,11 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({
redirectTo: ROUTES.BLOG,
});
+ const { posts } = usePostsList({
+ fallback: [articles],
+ fetcher: getArticles,
+ perPage: CONFIG.postsPerPage,
+ });
const intl = useIntl();
const title = intl.formatMessage({
defaultMessage: 'Blog',
@@ -260,7 +268,7 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({
/>,
]}
>
- <PostsList posts={getPostsList([articles])} sortByYear />
+ <PostsList posts={posts ?? []} sortByYear />
<Pagination
aria-label={paginationAriaLabel}
current={pageNumber}
diff --git a/src/services/graphql/articles.ts b/src/services/graphql/articles.ts
index 789ef2b..82bde41 100644
--- a/src/services/graphql/articles.ts
+++ b/src/services/graphql/articles.ts
@@ -1,19 +1,20 @@
-import {
- type Article,
- type ArticleCard,
- type EdgesResponse,
- type EndCursorResponse,
- type GraphQLEdgesInput,
- type GraphQLPageInfo,
- type RawArticle,
- type RawArticlePreview,
- type Slug,
- type TotalItems,
+import type {
+ Article,
+ ArticleCard,
+ EdgesResponse,
+ EndCursorResponse,
+ GraphQLEdgesInput,
+ GraphQLPageInfo,
+ RawArticle,
+ RawArticlePreview,
+ Slug,
+ TotalItems,
} from '../../types';
import {
getAuthorFromRawData,
getImageFromRawData,
getPageLinkFromRawData,
+ updateContentTree,
} from '../../utils/helpers';
import { fetchAPI } from './api';
import {
@@ -50,7 +51,9 @@ export type GetArticlesReturn = {
* @param {RawArticle} data - The page raw data.
* @returns {Article} The page data.
*/
-export const getArticleFromRawData = (data: RawArticle): Article => {
+export const getArticleFromRawData = async (
+ data: RawArticle
+): Promise<Article> => {
const {
acfPosts,
author,
@@ -67,19 +70,19 @@ export const getArticleFromRawData = (data: RawArticle): Article => {
} = data;
return {
- content: contentParts.afterMore,
+ content: await updateContentTree(contentParts.afterMore),
id: databaseId,
intro: contentParts.beforeMore,
meta: {
author: author && getAuthorFromRawData(author.node, 'page'),
- commentsCount: commentCount || 0,
+ commentsCount: commentCount ?? 0,
cover: featuredImage?.node
? getImageFromRawData(featuredImage.node)
: undefined,
dates: { publication: date, update: modified },
seo: {
- description: seo?.metaDesc || '',
- title: seo?.title || '',
+ description: seo?.metaDesc ?? '',
+ title: seo?.title ?? '',
},
thematics: acfPosts.postsInThematic?.map((thematic) =>
getPageLinkFromRawData(thematic, 'thematic')
diff --git a/src/services/graphql/thematics.ts b/src/services/graphql/thematics.ts
index 7a57824..c02a42c 100644
--- a/src/services/graphql/thematics.ts
+++ b/src/services/graphql/thematics.ts
@@ -1,13 +1,13 @@
-import {
- type EdgesResponse,
- type GraphQLEdgesInput,
- type PageLink,
- type RawArticle,
- type RawThematic,
- type RawThematicPreview,
- type Slug,
- type Thematic,
- type TotalItems,
+import type {
+ EdgesResponse,
+ GraphQLEdgesInput,
+ PageLink,
+ RawArticle,
+ RawThematic,
+ RawThematicPreview,
+ Slug,
+ Thematic,
+ TotalItems,
} from '../../types';
import {
getImageFromRawData,
@@ -59,7 +59,9 @@ export const getThematicsPreview = async (
* @param {RawThematic} data - The page raw data.
* @returns {Thematic} The page data.
*/
-export const getThematicFromRawData = (data: RawThematic): Thematic => {
+export const getThematicFromRawData = async (
+ data: RawThematic
+): Promise<Thematic> => {
const {
acfThematics,
contentParts,
@@ -84,9 +86,9 @@ export const getThematicFromRawData = (data: RawThematic): Thematic => {
posts.forEach((post) => {
if (post.acfPosts.postsInTopic) {
- post.acfPosts.postsInTopic.forEach((topic) =>
- topics.push(getPageLinkFromRawData(topic, 'topic'))
- );
+ for (const topic of post.acfPosts.postsInTopic) {
+ topics.push(getPageLinkFromRawData(topic, 'topic'));
+ }
}
});
@@ -103,16 +105,18 @@ export const getThematicFromRawData = (data: RawThematic): Thematic => {
id: databaseId,
intro: contentParts.beforeMore,
meta: {
- articles: acfThematics.postsInThematic.map((post) =>
- getArticleFromRawData(post)
+ articles: await Promise.all(
+ acfThematics.postsInThematic.map(async (post) =>
+ getArticleFromRawData(post)
+ )
),
cover: featuredImage?.node
? getImageFromRawData(featuredImage.node)
: undefined,
dates: { publication: date, update: modified },
seo: {
- description: seo?.metaDesc || '',
- title: seo?.title || '',
+ description: seo?.metaDesc ?? '',
+ title: seo?.title ?? '',
},
topics: getRelatedTopics(acfThematics.postsInThematic),
wordsCount: info.wordsCount,
diff --git a/src/services/graphql/topics.ts b/src/services/graphql/topics.ts
index 921b10d..d8a9b6a 100644
--- a/src/services/graphql/topics.ts
+++ b/src/services/graphql/topics.ts
@@ -1,13 +1,13 @@
-import {
- type EdgesResponse,
- type GraphQLEdgesInput,
- type PageLink,
- type RawArticle,
- type RawTopic,
- type RawTopicPreview,
- type Slug,
- type Topic,
- type TotalItems,
+import type {
+ EdgesResponse,
+ GraphQLEdgesInput,
+ PageLink,
+ RawArticle,
+ RawTopic,
+ RawTopicPreview,
+ Slug,
+ Topic,
+ TotalItems,
} from '../../types';
import {
getImageFromRawData,
@@ -59,7 +59,7 @@ export const getTopicsPreview = async (
* @param {RawTopic} data - The page raw data.
* @returns {Topic} The page data.
*/
-export const getTopicFromRawData = (data: RawTopic): Topic => {
+export const getTopicFromRawData = async (data: RawTopic): Promise<Topic> => {
const {
acfTopics,
contentParts,
@@ -84,9 +84,9 @@ export const getTopicFromRawData = (data: RawTopic): Topic => {
posts.forEach((post) => {
if (post.acfPosts.postsInThematic) {
- post.acfPosts.postsInThematic.forEach((thematic) =>
- thematics.push(getPageLinkFromRawData(thematic, 'thematic'))
- );
+ for (const thematic of post.acfPosts.postsInThematic) {
+ thematics.push(getPageLinkFromRawData(thematic, 'thematic'));
+ }
}
});
@@ -103,8 +103,8 @@ export const getTopicFromRawData = (data: RawTopic): Topic => {
id: databaseId,
intro: contentParts.beforeMore,
meta: {
- articles: acfTopics.postsInTopic.map((post) =>
- getArticleFromRawData(post)
+ articles: await Promise.all(
+ acfTopics.postsInTopic.map(async (post) => getArticleFromRawData(post))
),
cover: featuredImage?.node
? getImageFromRawData(featuredImage.node)
@@ -112,8 +112,8 @@ export const getTopicFromRawData = (data: RawTopic): Topic => {
dates: { publication: date, update: modified },
website: acfTopics.officialWebsite,
seo: {
- description: seo?.metaDesc || '',
- title: seo?.title || '',
+ description: seo?.metaDesc ?? '',
+ title: seo?.title ?? '',
},
thematics: getRelatedThematics(acfTopics.postsInTopic),
wordsCount: info.wordsCount,
diff --git a/src/utils/helpers/index.ts b/src/utils/helpers/index.ts
index 79077de..92f9424 100644
--- a/src/utils/helpers/index.ts
+++ b/src/utils/helpers/index.ts
@@ -3,6 +3,7 @@ export * from './images';
export * from './pages';
export * from './reading-time';
export * from './refs';
+export * from './rehype';
export * from './rss';
export * from './schema-org';
export * from './strings';
diff --git a/src/utils/helpers/pages.tsx b/src/utils/helpers/pages.tsx
index 62a582f..7b6bdca 100644
--- a/src/utils/helpers/pages.tsx
+++ b/src/utils/helpers/pages.tsx
@@ -109,9 +109,9 @@ export const getPostsWithUrl = (posts: Article[]): PostData[] =>
* @param {EdgesResponse<RawArticle>[]} rawData - The raw data.
* @returns {PostData[]} An array of posts.
*/
-export const getPostsList = (
+export const getPostsList = async (
rawData: EdgesResponse<RawArticle>[]
-): PostData[] => {
+): Promise<PostData[]> => {
const articlesList: RawArticle[] = [];
rawData.forEach((articleData) => {
articleData.edges.forEach((edge) => {
@@ -120,6 +120,8 @@ export const getPostsList = (
});
return getPostsWithUrl(
- articlesList.map((article) => getArticleFromRawData(article))
+ await Promise.all(
+ articlesList.map(async (article) => getArticleFromRawData(article))
+ )
);
};
diff --git a/src/utils/helpers/rehype.ts b/src/utils/helpers/rehype.ts
new file mode 100644
index 0000000..2716c62
--- /dev/null
+++ b/src/utils/helpers/rehype.ts
@@ -0,0 +1,23 @@
+/**
+ * Update a stringified HTML tree using unified plugins.
+ *
+ * It will parse the provided content to add id to each headings.
+ *
+ * @param {string} content - The page contents.
+ * @returns {string} The updated page contents.
+ */
+export const updateContentTree = async (content: string): Promise<string> => {
+ const { unified } = await import('unified');
+ const rehypeParse = (await import('rehype-parse')).default;
+ const rehypeSanitize = (await import('rehype-sanitize')).default;
+ const rehypeSlug = (await import('rehype-slug')).default;
+ const rehypeStringify = (await import('rehype-stringify')).default;
+
+ return unified()
+ .use(rehypeParse, { fragment: true })
+ .use(rehypeSlug)
+ .use(() => rehypeSanitize({ clobberPrefix: 'h-' }))
+ .use(rehypeStringify)
+ .processSync(content)
+ .toString();
+};
diff --git a/src/utils/helpers/rss.ts b/src/utils/helpers/rss.ts
index 6de60cc..d9c3b1e 100644
--- a/src/utils/helpers/rss.ts
+++ b/src/utils/helpers/rss.ts
@@ -18,8 +18,8 @@ const getAllArticles = async (): Promise<Article[]> => {
const rawArticles = await getArticles({ first: totalArticles });
const articles: Article[] = [];
- rawArticles.edges.forEach((edge) => {
- articles.push(getArticleFromRawData(edge.node));
+ rawArticles.edges.forEach(async (edge) => {
+ articles.push(await getArticleFromRawData(edge.node));
});
return articles;
diff --git a/src/utils/hooks/use-article.ts b/src/utils/hooks/use-article.ts
index 5cf0e51..f339f7f 100644
--- a/src/utils/hooks/use-article.ts
+++ b/src/utils/hooks/use-article.ts
@@ -1,10 +1,11 @@
+import { useEffect, useState } from 'react';
import useSWR from 'swr';
import {
articleBySlugQuery,
fetchAPI,
getArticleFromRawData,
} from '../../services/graphql';
-import type { Article, RawArticle } from '../../types';
+import type { Article, Maybe, RawArticle } from '../../types';
export type UseArticleConfig = {
/**
@@ -32,6 +33,19 @@ export const useArticle = ({
fetchAPI<RawArticle, typeof articleBySlugQuery>,
{}
);
+ const [article, setArticle] = useState<Maybe<Article>>();
- return data ? getArticleFromRawData(data.post) : fallback;
+ useEffect(() => {
+ const getArticle = async () => {
+ if (data) {
+ setArticle(await getArticleFromRawData(data.post));
+ } else {
+ setArticle(fallback);
+ }
+ };
+
+ getArticle();
+ }, [data, fallback]);
+
+ return article;
};
diff --git a/src/utils/hooks/use-headings-tree/use-headings-tree.test.ts b/src/utils/hooks/use-headings-tree/use-headings-tree.test.ts
index ad30a4f..2c8ff2d 100644
--- a/src/utils/hooks/use-headings-tree/use-headings-tree.test.ts
+++ b/src/utils/hooks/use-headings-tree/use-headings-tree.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from '@jest/globals';
-import { renderHook } from '@testing-library/react';
+import { act, renderHook } from '@testing-library/react';
import { useHeadingsTree } from './use-headings-tree';
const labels = {
@@ -9,7 +9,7 @@ const labels = {
};
describe('useHeadingsTree', () => {
- it('returns a ref object and the headings tree', () => {
+ it('returns a ref callback and the headings tree', () => {
const wrapper = document.createElement('div');
wrapper.innerHTML = `
@@ -19,12 +19,13 @@ describe('useHeadingsTree', () => {
<h2>${labels.secondH2}</h2>
<p>Totam cumque aut ipsum. Necessitatibus magnam necessitatibus. Qui illo nulla non ab. Accusamus voluptatem ab fugiat voluptas aspernatur velit dolore reprehenderit. Voluptatem quod minima asperiores voluptatum distinctio cumque quo.</p>`;
- const wrapperRef = { current: wrapper };
- const { result } = renderHook(() => useHeadingsTree(wrapperRef));
+ const { result } = renderHook(() => useHeadingsTree());
- expect(result.current.length).toBe(1);
- expect(result.current[0].label).toBe(labels.h1);
- expect(result.current[0].children.length).toBe(2);
+ act(() => result.current.ref(wrapper));
+
+ expect(result.current.tree.length).toBe(1);
+ expect(result.current.tree[0].label).toBe(labels.h1);
+ expect(result.current.tree[0].children.length).toBe(2);
});
it('can return a headings tree starting at the specified level', () => {
@@ -37,14 +38,13 @@ describe('useHeadingsTree', () => {
<h2>${labels.secondH2}</h2>
<p>Totam cumque aut ipsum. Necessitatibus magnam necessitatibus. Qui illo nulla non ab. Accusamus voluptatem ab fugiat voluptas aspernatur velit dolore reprehenderit. Voluptatem quod minima asperiores voluptatum distinctio cumque quo.</p>`;
- const wrapperRef = { current: wrapper };
- const { result } = renderHook(() =>
- useHeadingsTree(wrapperRef, { fromLevel: 2 })
- );
+ const { result } = renderHook(() => useHeadingsTree({ fromLevel: 2 }));
+
+ act(() => result.current.ref(wrapper));
- expect(result.current.length).toBe(2);
- expect(result.current[0].label).toBe(labels.firstH2);
- expect(result.current[1].label).toBe(labels.secondH2);
+ expect(result.current.tree.length).toBe(2);
+ expect(result.current.tree[0].label).toBe(labels.firstH2);
+ expect(result.current.tree[1].label).toBe(labels.secondH2);
});
it('can return a headings tree stopping at the specified level', () => {
@@ -57,22 +57,17 @@ describe('useHeadingsTree', () => {
<h2>${labels.secondH2}</h2>
<p>Totam cumque aut ipsum. Necessitatibus magnam necessitatibus. Qui illo nulla non ab. Accusamus voluptatem ab fugiat voluptas aspernatur velit dolore reprehenderit. Voluptatem quod minima asperiores voluptatum distinctio cumque quo.</p>`;
- const wrapperRef = { current: wrapper };
- const { result } = renderHook(() =>
- useHeadingsTree(wrapperRef, { toLevel: 1 })
- );
+ const { result } = renderHook(() => useHeadingsTree({ toLevel: 1 }));
+
+ act(() => result.current.ref(wrapper));
- expect(result.current.length).toBe(1);
- expect(result.current[0].label).toBe(labels.h1);
- expect(result.current[0].children).toStrictEqual([]);
+ expect(result.current.tree.length).toBe(1);
+ expect(result.current.tree[0].label).toBe(labels.h1);
+ expect(result.current.tree[0].children).toStrictEqual([]);
});
it('throws an error if the options are invalid', () => {
- const wrapperRef = { current: null };
-
- expect(() =>
- useHeadingsTree(wrapperRef, { fromLevel: 2, toLevel: 1 })
- ).toThrowError(
+ expect(() => useHeadingsTree({ fromLevel: 2, toLevel: 1 })).toThrowError(
'Invalid options: `fromLevel` must be lower or equal to `toLevel`.'
);
});
diff --git a/src/utils/hooks/use-headings-tree/use-headings-tree.ts b/src/utils/hooks/use-headings-tree/use-headings-tree.ts
index 6a081e7..68bdde8 100644
--- a/src/utils/hooks/use-headings-tree/use-headings-tree.ts
+++ b/src/utils/hooks/use-headings-tree/use-headings-tree.ts
@@ -1,4 +1,4 @@
-import { useEffect, useState, type RefObject } from 'react';
+import { useState, useCallback, type RefCallback } from 'react';
import type { HeadingLevel } from '../../../components';
export type HeadingsTreeNode = {
@@ -111,17 +111,26 @@ const buildHeadingsTreeFrom = (
return treeNodes;
};
+export type UseHeadingsTreeReturn<T extends HTMLElement> = {
+ /**
+ * A callback function to set a ref.
+ */
+ ref: RefCallback<T>;
+ /**
+ * The headings tree.
+ */
+ tree: HeadingsTreeNode[];
+};
+
/**
* React hook to retrieve the headings tree in a document or in a given wrapper.
*
- * @param {RefObject<T>} ref - A ref to the element where to look for headings.
* @param {UseHeadingsTreeOptions} options - The headings tree config.
- * @returns {HeadingsTreeNode[]} The headings tree.
+ * @returns {UseHeadingsTreeReturn<T>} The headings tree and a ref callback.
*/
export const useHeadingsTree = <T extends HTMLElement = HTMLElement>(
- ref: RefObject<T>,
options?: UseHeadingsTreeOptions
-): HeadingsTreeNode[] => {
+): UseHeadingsTreeReturn<T> => {
if (
options?.fromLevel &&
options.toLevel &&
@@ -134,15 +143,14 @@ export const useHeadingsTree = <T extends HTMLElement = HTMLElement>(
const [tree, setTree] = useState<HeadingsTreeNode[]>([]);
const requestedHeadingTags = getHeadingTagsList(options);
const query = requestedHeadingTags.join(', ');
+ const ref: RefCallback<T> = useCallback(
+ (el) => {
+ const headingNodes = el?.querySelectorAll<HTMLHeadingElement>(query);
- useEffect(() => {
- if (typeof window === 'undefined') return;
-
- const headingNodes =
- ref.current?.querySelectorAll<HTMLHeadingElement>(query);
-
- if (headingNodes) setTree(buildHeadingsTreeFrom(headingNodes));
- }, [query, ref]);
+ if (headingNodes) setTree(buildHeadingsTreeFrom(headingNodes));
+ },
+ [query]
+ );
- return tree;
+ return { ref, tree };
};
diff --git a/src/utils/hooks/use-posts-list/use-posts-list.ts b/src/utils/hooks/use-posts-list/use-posts-list.ts
index 661727f..980d531 100644
--- a/src/utils/hooks/use-posts-list/use-posts-list.ts
+++ b/src/utils/hooks/use-posts-list/use-posts-list.ts
@@ -1,4 +1,4 @@
-import { useCallback, useState } from 'react';
+import { useCallback, useEffect, useState } from 'react';
import type { PostData } from '../../../components';
import type { Maybe, RawArticle } from '../../../types';
import { getPostsList } from '../../helpers';
@@ -40,8 +40,15 @@ export const usePostsList = (
} = usePagination(config);
const [firstNewResultIndex, setFirstNewResultIndex] =
useState<Maybe<number>>(undefined);
+ const [posts, setPosts] = useState<Maybe<PostData[]>>(undefined);
- const posts = data ? getPostsList(data) : undefined;
+ useEffect(() => {
+ const getPosts = async () => {
+ if (data) setPosts(await getPostsList(data));
+ };
+
+ getPosts();
+ }, [data]);
const handleLoadMore = useCallback(async () => {
setFirstNewResultIndex(size * config.perPage + 1);
diff --git a/yarn.lock b/yarn.lock
index 27e02c7..fc3e029 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8582,6 +8582,11 @@ github-slugger@^1.0.0:
resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.5.0.tgz#17891bbc73232051474d68bd867a34625c955f7d"
integrity sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==
+github-slugger@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-2.0.0.tgz#52cf2f9279a21eb6c59dd385b410f0c0adda8f1a"
+ integrity sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==
+
glob-parent@^5.1.2, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
@@ -8828,6 +8833,74 @@ hasown@^2.0.0:
dependencies:
function-bind "^1.1.2"
+hast-util-from-html@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/hast-util-from-html/-/hast-util-from-html-2.0.1.tgz#9cd38ee81bf40b2607368b92a04b0905fa987488"
+ integrity sha512-RXQBLMl9kjKVNkJTIO6bZyb2n+cUH8LFaSSzo82jiLT6Tfc+Pt7VQCS+/h3YwG4jaNE2TA2sdJisGWR+aJrp0g==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ devlop "^1.1.0"
+ hast-util-from-parse5 "^8.0.0"
+ parse5 "^7.0.0"
+ vfile "^6.0.0"
+ vfile-message "^4.0.0"
+
+hast-util-from-parse5@^8.0.0:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz#654a5676a41211e14ee80d1b1758c399a0327651"
+ integrity sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ "@types/unist" "^3.0.0"
+ devlop "^1.0.0"
+ hastscript "^8.0.0"
+ property-information "^6.0.0"
+ vfile "^6.0.0"
+ vfile-location "^5.0.0"
+ web-namespaces "^2.0.0"
+
+hast-util-heading-rank@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz#2d5c6f2807a7af5c45f74e623498dd6054d2aba8"
+ integrity sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==
+ dependencies:
+ "@types/hast" "^3.0.0"
+
+hast-util-parse-selector@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27"
+ integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==
+ dependencies:
+ "@types/hast" "^3.0.0"
+
+hast-util-raw@^9.0.0:
+ version "9.0.1"
+ resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-9.0.1.tgz#2ba8510e4ed2a1e541cde2a4ebb5c38ab4c82c2d"
+ integrity sha512-5m1gmba658Q+lO5uqL5YNGQWeh1MYWZbZmWrM5lncdcuiXuo5E2HT/CIOp0rLF8ksfSwiCVJ3twlgVRyTGThGA==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ "@types/unist" "^3.0.0"
+ "@ungap/structured-clone" "^1.0.0"
+ hast-util-from-parse5 "^8.0.0"
+ hast-util-to-parse5 "^8.0.0"
+ html-void-elements "^3.0.0"
+ mdast-util-to-hast "^13.0.0"
+ parse5 "^7.0.0"
+ unist-util-position "^5.0.0"
+ unist-util-visit "^5.0.0"
+ vfile "^6.0.0"
+ web-namespaces "^2.0.0"
+ zwitch "^2.0.0"
+
+hast-util-sanitize@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/hast-util-sanitize/-/hast-util-sanitize-5.0.1.tgz#8e90068cd68e651c569960b77a1b25076579b4cf"
+ integrity sha512-IGrgWLuip4O2nq5CugXy4GI2V8kx4sFVy5Hd4vF7AR2gxS0N9s7nEAVUyeMtZKZvzrxVsHt73XdTsno1tClIkQ==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ "@ungap/structured-clone" "^1.2.0"
+ unist-util-position "^5.0.0"
+
hast-util-to-estree@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/hast-util-to-estree/-/hast-util-to-estree-3.1.0.tgz#f2afe5e869ddf0cf690c75f9fc699f3180b51b19"
@@ -8850,6 +8923,24 @@ hast-util-to-estree@^3.0.0:
unist-util-position "^5.0.0"
zwitch "^2.0.0"
+hast-util-to-html@^9.0.0:
+ version "9.0.0"
+ resolved "https://registry.yarnpkg.com/hast-util-to-html/-/hast-util-to-html-9.0.0.tgz#51c0ae2a3550b9aa988c094c4fc4e327af0dddd1"
+ integrity sha512-IVGhNgg7vANuUA2XKrT6sOIIPgaYZnmLx3l/CCOAK0PtgfoHrZwX7jCSYyFxHTrGmC6S9q8aQQekjp4JPZF+cw==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ "@types/unist" "^3.0.0"
+ ccount "^2.0.0"
+ comma-separated-tokens "^2.0.0"
+ hast-util-raw "^9.0.0"
+ hast-util-whitespace "^3.0.0"
+ html-void-elements "^3.0.0"
+ mdast-util-to-hast "^13.0.0"
+ property-information "^6.0.0"
+ space-separated-tokens "^2.0.0"
+ stringify-entities "^4.0.0"
+ zwitch "^2.0.4"
+
hast-util-to-jsx-runtime@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.2.0.tgz#ffd59bfcf0eb8321c6ed511bfc4b399ac3404bc2"
@@ -8865,6 +8956,26 @@ hast-util-to-jsx-runtime@^2.0.0:
unist-util-position "^5.0.0"
vfile-message "^4.0.0"
+hast-util-to-parse5@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz#477cd42d278d4f036bc2ea58586130f6f39ee6ed"
+ integrity sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ comma-separated-tokens "^2.0.0"
+ devlop "^1.0.0"
+ property-information "^6.0.0"
+ space-separated-tokens "^2.0.0"
+ web-namespaces "^2.0.0"
+ zwitch "^2.0.0"
+
+hast-util-to-string@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/hast-util-to-string/-/hast-util-to-string-3.0.0.tgz#2a131948b4b1b26461a2c8ac876e2c88d02946bd"
+ integrity sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA==
+ dependencies:
+ "@types/hast" "^3.0.0"
+
hast-util-whitespace@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621"
@@ -8872,6 +8983,17 @@ hast-util-whitespace@^3.0.0:
dependencies:
"@types/hast" "^3.0.0"
+hastscript@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-8.0.0.tgz#4ef795ec8dee867101b9f23cc830d4baf4fd781a"
+ integrity sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ comma-separated-tokens "^2.0.0"
+ hast-util-parse-selector "^4.0.0"
+ property-information "^6.0.0"
+ space-separated-tokens "^2.0.0"
+
he@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
@@ -8940,6 +9062,11 @@ html-tags@^3.1.0, html-tags@^3.3.1:
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce"
integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==
+html-void-elements@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7"
+ integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==
+
html-webpack-plugin@^5.5.0:
version "5.5.3"
resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.5.3.tgz#72270f4a78e222b5825b296e5e3e1328ad525a3e"
@@ -12778,6 +12905,43 @@ regjsparser@^0.9.1:
dependencies:
jsesc "~0.5.0"
+rehype-parse@^9.0.0:
+ version "9.0.0"
+ resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-9.0.0.tgz#3949faeec6f466ec57774215661e0d75469195d9"
+ integrity sha512-WG7nfvmWWkCR++KEkZevZb/uw41E8TsH4DsY9UxsTbIXCVGbAs4S+r8FrQ+OtH5EEQAs+5UxKC42VinkmpA1Yw==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ hast-util-from-html "^2.0.0"
+ unified "^11.0.0"
+
+rehype-sanitize@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz#16e95f4a67a69cbf0f79e113c8e0df48203db73c"
+ integrity sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ hast-util-sanitize "^5.0.0"
+
+rehype-slug@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/rehype-slug/-/rehype-slug-6.0.0.tgz#1d21cf7fc8a83ef874d873c15e6adaee6344eaf1"
+ integrity sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ github-slugger "^2.0.0"
+ hast-util-heading-rank "^3.0.0"
+ hast-util-to-string "^3.0.0"
+ unist-util-visit "^5.0.0"
+
+rehype-stringify@^10.0.0:
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/rehype-stringify/-/rehype-stringify-10.0.0.tgz#2031cf6fdd0355393706f0474ec794c75e5492f2"
+ integrity sha512-1TX1i048LooI9QoecrXy7nGFFbFSufxVRAfc6Y9YMRAi56l+oB0zP51mLSV312uRuvVLPV1opSlJmslozR1XHQ==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ hast-util-to-html "^9.0.0"
+ unified "^11.0.0"
+
relateurl@^0.2.7:
version "0.2.7"
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
@@ -14361,7 +14525,7 @@ unicode-property-aliases-ecmascript@^2.0.0:
resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd"
integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==
-unified@^11.0.0:
+unified@^11.0.0, unified@^11.0.4:
version "11.0.4"
resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.4.tgz#f4be0ac0fe4c88cb873687c07c64c49ed5969015"
integrity sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==
@@ -14625,6 +14789,14 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
+vfile-location@^5.0.0:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.2.tgz#220d9ca1ab6f8b2504a4db398f7ebc149f9cb464"
+ integrity sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==
+ dependencies:
+ "@types/unist" "^3.0.0"
+ vfile "^6.0.0"
+
vfile-message@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181"
@@ -14686,6 +14858,11 @@ wcwidth@^1.0.1:
dependencies:
defaults "^1.0.3"
+web-namespaces@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692"
+ integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==
+
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
@@ -15058,7 +15235,7 @@ yocto-queue@^1.0.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
-zwitch@^2.0.0:
+zwitch@^2.0.0, zwitch@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7"
integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==