How to add a comments section to doc pages and blog entries using giscus
This post details how to outfit your Docusaurus site's doc pages and blog posts with a comments section using giscus.
This blog post was originally inspired by the post on Canny, a site meant for lisitng Docusaurus feature requests. Specifically, a blog post by Dipak Parmar was used us the starting point.
Note: All examples in this post were run using Docusaurus 2.1.0
.
Follow the steps below to add a comments section similar to what appears at the end of this post. Since what you are reading is a blog entry, it is clear that comments have been enabled for blog posts on this site. But comments can also be enabled for documentation entries such as this site's introduction page. Choose what works best for you according to the steps below.
As Docusaurus itself notes: swizzling allows deeper site customizations. The swizzling customizations we are going to make are very minor. We do not need to worry much about "copying a large amount of internal code" which we will then have to maintain ourselves. If the Docusaurus maintainers change something at some point that interferes with our changes, then we can easily re-swizzle and update components to mesh cleanly with our original updates.
To get an overview of all the themes and components available to swizzle, run the following:
npm run swizzle -- --list
We will be making updates to DocItem/Layout
(for the docs) and BlogPostItem/Content
(for the blog), both deemed components "unsafe" to make changes to. But, as noted above, our changes will be minimal and these components can easily be re-swizzled if we ever run into problems in the future.
Giscus
What is giscus? Quick answer according to its homepage:
A comments system powered by GitHub Discussions. Let visitors leave comments and reactions on your website via GitHub! Heavily inspired by utterances.
Essentially, giscus will make it possible for us to let users comment on our docs/blog without having to use some sort of database.
How giscus works
As noted on the homepage for giscus:
When giscus loads, the GitHub Discussions search API is used to find the Discussion associated with the page based on a chosen mapping (URL,
pathname
,<title>
, etc.). If a matching discussion cannot be found, the giscus bot will automatically create a discussion the first time someone leaves a comment or reaction.To comment, visitors must authorize the giscus app [on GitHub] to post on their behalf using the GitHub OAuth flow. Alternatively, visitors can comment on the GitHub Discussion directly. You can moderate the comments on GitHub.
Since doc pages and blog entries will be unique by path name, our chosen mapping for giscus will be pathname
.
Giscus installation and configuration
Follow the steps outlined below to install and configure giscus to your liking.
Enable comments for doc pages
Start by swizzling the DocItem/Layout
component:
npm run swizzle @docusaurus/theme-classic DocItem/Layout -- --eject
You will likely encounter a prompt that looks as follows (as of Oct. 27, 2022, at least):
> software-development-handbook@0.0.0 swizzle
> docusaurus swizzle "@docusaurus/theme-classic" "DocItem/Layout" "--eject"
[WARNING]
Swizzle action eject is unsafe to perform on DocItem/Layout.
It is more likely to be affected by breaking changes in the future
If you want to swizzle it, use the `--danger` flag, or confirm that you understand the risks.
? Do you really want to swizzle this unsafe internal component? › - Use arrow-keys. Return to submit.
❯ NO: cancel and stay safe
YES: I know what I am doing!
[Exit]
Press the down arrow to point ❯
to the highlighted line above to confirm the swizzling directive. Two files will then be "ejected" to /src/theme/DocItem/Layout
in your project, namely index.js
and styles.modules.css
(you do not need to modify this file unless you want to change the default styling). These originally ejected files are shown below in the two tabs on the right for reference. The first tab includes the modifications we need to make to the index.js
file in order for giscus to work with our doc pages (all changes/additions from the originally swizzled file have been highlighted):
- index.js (updated with giscus)
- index.js
- styles.modules.css
import React from 'react';
import clsx from 'clsx';
import { useWindowSize } from '@docusaurus/theme-common';
import { useDoc } from '@docusaurus/theme-common/internal';
import DocItemPaginator from '@theme/DocItem/Paginator';
import DocVersionBanner from '@theme/DocVersionBanner';
import DocVersionBadge from '@theme/DocVersionBadge';
import DocItemFooter from '@theme/DocItem/Footer';
import DocItemTOCMobile from '@theme/DocItem/TOC/Mobile';
import DocItemTOCDesktop from '@theme/DocItem/TOC/Desktop';
import DocItemContent from '@theme/DocItem/Content';
import DocBreadcrumbs from '@theme/DocBreadcrumbs';
import styles from './styles.module.css';
import Giscus from '@giscus/react';
import { useColorMode } from '@docusaurus/theme-common';
/**
* Decide if the toc should be rendered, on mobile or desktop viewports
*/
function useDocTOC() {
const { frontMatter, toc } = useDoc();
const windowSize = useWindowSize();
const hidden = frontMatter.hide_table_of_contents;
const canRender = !hidden && toc.length > 0;
const mobile = canRender ? <DocItemTOCMobile /> : undefined;
const desktop =
canRender && (windowSize === 'desktop' || windowSize === 'ssr') ? (
<DocItemTOCDesktop />
) : undefined;
return {
hidden,
mobile,
desktop,
};
}
export default function DocItemLayout({ children }) {
const docTOC = useDocTOC();
const { colorMode } = useColorMode();
const giscus = (
<React.Fragment>
<hr />
<br></br>
<Giscus
id="comments"
repo="farlowdw/software-development-handbook"
repoId="R_kgDOHmzzBQ"
category="Announcements"
categoryId="DIC_kwDOHmzzBc4CSLOr"
mapping="pathname"
reactionsEnabled="1"
emitMetadata="0"
inputPosition="top"
theme={colorMode}
lang="en"
loading="lazy"
/>
</React.Fragment>
)
return (
<div className="row">
<div className={clsx('col', !docTOC.hidden && styles.docItemCol)}>
<DocVersionBanner />
<div className={styles.docItemContainer}>
<article>
<DocBreadcrumbs />
<DocVersionBadge />
{docTOC.mobile}
<DocItemContent>{children}</DocItemContent>
<DocItemFooter />
</article>
<DocItemPaginator />
{giscus}
</div>
</div>
{docTOC.desktop && <div className="col col--3">{docTOC.desktop}</div>}
</div>
);
}
The Giscus
component comes from our work in the giscus installation and configuration section. The hr
and br
tags included above this component are merely to make the comments section more appealing from a presentational standpoint. Do what works best for you.
import React from 'react';
import clsx from 'clsx';
import {useWindowSize} from '@docusaurus/theme-common';
import {useDoc} from '@docusaurus/theme-common/internal';
import DocItemPaginator from '@theme/DocItem/Paginator';
import DocVersionBanner from '@theme/DocVersionBanner';
import DocVersionBadge from '@theme/DocVersionBadge';
import DocItemFooter from '@theme/DocItem/Footer';
import DocItemTOCMobile from '@theme/DocItem/TOC/Mobile';
import DocItemTOCDesktop from '@theme/DocItem/TOC/Desktop';
import DocItemContent from '@theme/DocItem/Content';
import DocBreadcrumbs from '@theme/DocBreadcrumbs';
import styles from './styles.module.css';
/**
* Decide if the toc should be rendered, on mobile or desktop viewports
*/
function useDocTOC() {
const {frontMatter, toc} = useDoc();
const windowSize = useWindowSize();
const hidden = frontMatter.hide_table_of_contents;
const canRender = !hidden && toc.length > 0;
const mobile = canRender ? <DocItemTOCMobile /> : undefined;
const desktop =
canRender && (windowSize === 'desktop' || windowSize === 'ssr') ? (
<DocItemTOCDesktop />
) : undefined;
return {
hidden,
mobile,
desktop,
};
}
export default function DocItemLayout({children}) {
const docTOC = useDocTOC();
return (
<div className="row">
<div className={clsx('col', !docTOC.hidden && styles.docItemCol)}>
<DocVersionBanner />
<div className={styles.docItemContainer}>
<article>
<DocBreadcrumbs />
<DocVersionBadge />
{docTOC.mobile}
<DocItemContent>{children}</DocItemContent>
<DocItemFooter />
</article>
<DocItemPaginator />
</div>
</div>
{docTOC.desktop && <div className="col col--3">{docTOC.desktop}</div>}
</div>
);
}
.docItemContainer header + *,
.docItemContainer article > *:first-child {
margin-top: 0;
}
@media (min-width: 997px) {
.docItemCol {
max-width: 75% !important;
}
}
Congratulations! You should now have comments enabled for your doc pages.
Enable comments for blog entries
Start by swizzling the BlogPostItem/Content
component:
npm run swizzle @docusaurus/theme-classic BlogPostItem/Content -- --eject
You will likely encounter a prompt that looks as follows (as of Oct. 27, 2022, at least):
> software-development-handbook@0.0.0 swizzle
> docusaurus swizzle "@docusaurus/theme-classic" "BlogPostItem/Content" "--eject"
[WARNING]
Swizzle action eject is unsafe to perform on BlogPostItem/Content.
It is more likely to be affected by breaking changes in the future
If you want to swizzle it, use the `--danger` flag, or confirm that you understand the risks.
? Do you really want to swizzle this unsafe internal component? › - Use arrow-keys. Return to submit.
❯ NO: cancel and stay safe
YES: I know what I am doing!
[Exit]
Press the down arrow to point ❯
to the highlighted line above to confirm the swizzling directive. A single file will then be "ejected" to /src/theme/BlogPostItem/Content
in your project, namely index.js
. The originally ejected file is shown below in the tab on the right for reference. The first tab includes the modifications we need to make to the index.js
file in order for giscus to work with our blog entries (all changes/additions from the originally swizzled file have been highlighted):
- index.js (updated with giscus)
- index.js
import React from 'react';
import clsx from 'clsx';
import {blogPostContainerID} from '@docusaurus/utils-common';
import {useBlogPost} from '@docusaurus/theme-common/internal';
import MDXContent from '@theme/MDXContent';
import Giscus from '@giscus/react';
import { useColorMode } from '@docusaurus/theme-common';
export default function BlogPostItemContent({children, className}) {
const {isBlogPostPage} = useBlogPost();
const { colorMode } = useColorMode();
const giscus = (
<React.Fragment>
<hr />
<br></br>
<Giscus
id="comments"
repo="farlowdw/software-development-handbook"
repoId="R_kgDOHmzzBQ"
category="Announcements"
categoryId="DIC_kwDOHmzzBc4CSLOr"
mapping="pathname"
reactionsEnabled="1"
emitMetadata="0"
inputPosition="top"
theme={colorMode}
lang="en"
loading="lazy"
/>
</React.Fragment>
)
return (
<div
// This ID is used for the feed generation to locate the main content
id={isBlogPostPage ? blogPostContainerID : undefined}
className={clsx('markdown', className)}
itemProp="articleBody">
<MDXContent>
{children}
{isBlogPostPage && giscus}
</MDXContent>
</div>
);
}
The Giscus
component comes from our work in the giscus installation and configuration section. The hr
and br
tags included above this component are merely to make the comments section more appealing from a presentational standpoint. Do what works best for you.
import React from 'react';
import clsx from 'clsx';
import {blogPostContainerID} from '@docusaurus/utils-common';
import {useBlogPost} from '@docusaurus/theme-common/internal';
import MDXContent from '@theme/MDXContent';
export default function BlogPostItemContent({children, className}) {
const {isBlogPostPage} = useBlogPost();
return (
<div
// This ID is used for the feed generation to locate the main content
id={isBlogPostPage ? blogPostContainerID : undefined}
className={clsx('markdown', className)}
itemProp="articleBody">
<MDXContent>{children}</MDXContent>
</div>
);
}
Congratulations! You should now have comments enabled for your blog entries.
Selectively disable comments
As the Docusaurus docs note, we can take advantage of useLocation
to get the current page's location:
import {useLocation} from '@docusaurus/router';
export function someFunction() {
// React router provides the current component's route, even in SSR
const location = useLocation();
...
}
This means we can use location.pathname
to programmatically determine whether or not a giscus comments section should be made available for specific doc pages or blog entries based on the paths for those entries.
But where should such paths be specified? Here are two sensible options:
- Directly within swizzled component: The paths could be specified within each swizzled component directly; for example, doc paths for which we do not want giscus comment sections could be specified in
/src/theme/DocItem/Layout/index.js
directly. - Within Docusaurus config (preferred): The first option may seem like a good choice at first, but it quickly becomes problematic if we ever want to revert back to the current
DocItem
orBlogPostItem
being maintained by the Docusaurus team and re-swizzle them to use giscus comment sections. The code shown in previous sections of this article would not be problematic to add back to freshly swizzled components, but adding all of the paths back might be cumbersome depending on how many were added in the first place.
A convenient solution is to make use of the useDocusaurusContext
hook, which gives us access to the siteConfig
object from docusaurus.config.js
. How does this help? As the docs note:
Docusaurus guards
docusaurus.config.js
from unknown fields. To add a custom field, define it oncustomFields
.
Hence, we can add a customFields
property to our exported config object in docusaurus.config.js
, which will then be accessible as a property on the siteConfig
object, which is made available by means of the useDocusaurusContext
hook.
The takeaway is that we can add our paths to the customFields
object, which will then not only be accessible globally but also, most importantly, within whatever components we want to swizzle:
customFields: {
forbiddenGiscusDocPaths: [
'/docs/tutorials/intro'
],
forbiddenGiscusBlogPaths: [
'/blog/2022/10/27/giscus-comments'
],
}
The tabs below indicate how we can build upon our previous work to achieve this (line changes are highlighted):
- Doc pages
- Blog entries
import React from 'react';
import clsx from 'clsx';
import { useWindowSize } from '@docusaurus/theme-common';
import { useDoc } from '@docusaurus/theme-common/internal';
import DocItemPaginator from '@theme/DocItem/Paginator';
import DocVersionBanner from '@theme/DocVersionBanner';
import DocVersionBadge from '@theme/DocVersionBadge';
import DocItemFooter from '@theme/DocItem/Footer';
import DocItemTOCMobile from '@theme/DocItem/TOC/Mobile';
import DocItemTOCDesktop from '@theme/DocItem/TOC/Desktop';
import DocItemContent from '@theme/DocItem/Content';
import DocBreadcrumbs from '@theme/DocBreadcrumbs';
import styles from './styles.module.css';
import Giscus from '@giscus/react';
import { useColorMode } from '@docusaurus/theme-common';
import { useLocation } from '@docusaurus/router';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
/**
* Decide if the toc should be rendered, on mobile or desktop viewports
*/
function useDocTOC() {
const { frontMatter, toc } = useDoc();
const windowSize = useWindowSize();
const hidden = frontMatter.hide_table_of_contents;
const canRender = !hidden && toc.length > 0;
const mobile = canRender ? <DocItemTOCMobile /> : undefined;
const desktop =
canRender && (windowSize === 'desktop' || windowSize === 'ssr') ? (
<DocItemTOCDesktop />
) : undefined;
return {
hidden,
mobile,
desktop,
};
}
export default function DocItemLayout({ children }) {
const docTOC = useDocTOC();
const { colorMode } = useColorMode();
const location = useLocation();
const {siteConfig} = useDocusaurusContext();
const {forbiddenGiscusDocPaths} = siteConfig.customFields;
const giscus = (
<React.Fragment>
<hr />
<br></br>
<Giscus
id="comments"
repo="farlowdw/software-development-handbook"
repoId="R_kgDOHmzzBQ"
category="Announcements"
categoryId="DIC_kwDOHmzzBc4CSLOr"
mapping="pathname"
reactionsEnabled="1"
emitMetadata="0"
inputPosition="top"
theme={colorMode}
lang="en"
loading="lazy"
/>
</React.Fragment>
)
return (
<div className="row">
<div className={clsx('col', !docTOC.hidden && styles.docItemCol)}>
<DocVersionBanner />
<div className={styles.docItemContainer}>
<article>
<DocBreadcrumbs />
<DocVersionBadge />
{docTOC.mobile}
<DocItemContent>{children}</DocItemContent>
<DocItemFooter />
</article>
<DocItemPaginator />
{!forbiddenGiscusDocPaths.includes(location.pathname) && giscus}
</div>
</div>
{docTOC.desktop && <div className="col col--3">{docTOC.desktop}</div>}
</div>
);
}
The changes above mean that all doc pages will have a giscus comments section except the doc page at the following path:
/docs/tutorials/intro
To prevent comments for other paths, simply add these paths to the forbiddenGiscusDocPaths
array.
import React from 'react';
import clsx from 'clsx';
import {blogPostContainerID} from '@docusaurus/utils-common';
import {useBlogPost} from '@docusaurus/theme-common/internal';
import MDXContent from '@theme/MDXContent';
import Giscus from '@giscus/react';
import { useColorMode } from '@docusaurus/theme-common';
import { useLocation } from '@docusaurus/router';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
export default function BlogPostItemContent({children, className}) {
const {isBlogPostPage} = useBlogPost();
const { colorMode } = useColorMode();
const {siteConfig} = useDocusaurusContext();
const {forbiddenGiscusBlogPaths} = siteConfig.customFields;
const location = useLocation();
const giscus = (
<React.Fragment>
<hr />
<br></br>
<Giscus
id="comments"
repo="farlowdw/software-development-handbook"
repoId="R_kgDOHmzzBQ"
category="Announcements"
categoryId="DIC_kwDOHmzzBc4CSLOr"
mapping="pathname"
reactionsEnabled="1"
emitMetadata="0"
inputPosition="top"
theme={colorMode}
lang="en"
loading="lazy"
/>
</React.Fragment>
)
return (
<div
// This ID is used for the feed generation to locate the main content
id={isBlogPostPage ? blogPostContainerID : undefined}
className={clsx('markdown', className)}
itemProp="articleBody">
<MDXContent>
{children}
{isBlogPostPage && !forbiddenGiscusBlogPaths.includes(location.pathname) && giscus}
</MDXContent>
</div>
);
}
The changes above mean that all blog entries will have a giscus comments section except this blog entry because it is accessed via the following path (note: this is just for illustrative reasons; the comments have not been disabled for this post for obvious reasons):
/blog/2022/10/27/giscus-comments
To prevent comments for other paths, simply add these paths to the forbiddenGiscusBlogPaths
array.