From a333bfb3ef857bb585fc8dcc138b63d255de655a Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Thu, 16 Jan 2025 14:47:02 +0800 Subject: [PATCH 1/3] refactor: blog list page with tutorial selection dropdown header --- docusaurus.config.ts | 1 + plugins/tutorial-generator/utils.ts | 8 +- src/theme/BlogLayout/index.module.scss | 21 +++ src/theme/BlogLayout/index.tsx | 20 +-- src/theme/BlogListPage/index.module.scss | 5 + src/theme/BlogListPage/index.tsx | 107 ++++++++++++++ src/theme/BlogPostItem/Footer/index.tsx | 65 +++++++++ .../SelectionDropdown/index.module.scss | 15 ++ .../Header/SelectionDropdown/index.tsx | 20 ++- .../TitleWithSelectionDropdown/index.tsx | 138 +++++++++++------- 10 files changed, 327 insertions(+), 73 deletions(-) create mode 100644 src/theme/BlogLayout/index.module.scss create mode 100644 src/theme/BlogListPage/index.module.scss create mode 100644 src/theme/BlogListPage/index.tsx create mode 100644 src/theme/BlogPostItem/Footer/index.tsx diff --git a/docusaurus.config.ts b/docusaurus.config.ts index ca5bfd08d6b..ed51b87e7fb 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -373,6 +373,7 @@ const config: Config = { path: './tutorial', blogSidebarCount: 0, showReadingTime: false, + postsPerPage: 'ALL', }, ], [ diff --git a/plugins/tutorial-generator/utils.ts b/plugins/tutorial-generator/utils.ts index e21fc2dc049..f42e87b049a 100644 --- a/plugins/tutorial-generator/utils.ts +++ b/plugins/tutorial-generator/utils.ts @@ -1,10 +1,10 @@ import { type DocMetadata } from '@docusaurus/plugin-content-docs'; -export const getSdkDisplayName = (sdk: DocMetadata) => - String(sdk.frontMatter.tutorial_name ?? sdk.frontMatter.sidebar_label ?? ''); +export const getSdkDisplayName = (sdk?: DocMetadata) => + String(sdk?.frontMatter.tutorial_name ?? sdk?.frontMatter.sidebar_label ?? ''); -export const getConnectorDisplayName = (connector: DocMetadata) => - String(connector.frontMatter.tutorial_name ?? connector.frontMatter.sidebar_label ?? ''); +export const getConnectorDisplayName = (connector?: DocMetadata) => + String(connector?.frontMatter.tutorial_name ?? connector?.frontMatter.sidebar_label ?? ''); export const getSdkPath = (metadata: DocMetadata) => { const sdkName = String(metadata.frontMatter.tutorial_name ?? ''); diff --git a/src/theme/BlogLayout/index.module.scss b/src/theme/BlogLayout/index.module.scss new file mode 100644 index 00000000000..159b8ebd4a0 --- /dev/null +++ b/src/theme/BlogLayout/index.module.scss @@ -0,0 +1,21 @@ +.container { + max-width: 1044px; + margin: 0 auto; + + + .row { + display: flex; + gap: 24px; + + .main { + flex: 1; + display: flex; + flex-direction: column; + gap: 24px; + } + + .toc { + flex: 0 0 276px; + } + } +} \ No newline at end of file diff --git a/src/theme/BlogLayout/index.tsx b/src/theme/BlogLayout/index.tsx index cd5c110191e..085dee76bd9 100644 --- a/src/theme/BlogLayout/index.tsx +++ b/src/theme/BlogLayout/index.tsx @@ -1,9 +1,10 @@ import type { Props } from '@theme/BlogLayout'; import BlogSidebar from '@theme/BlogSidebar'; import Layout from '@theme/Layout'; -import clsx from 'clsx'; import ReactModal from 'react-modal'; +import styles from './index.module.scss'; + ReactModal.setAppElement('#__docusaurus'); export default function BlogLayout(props: Props): JSX.Element { @@ -12,18 +13,11 @@ export default function BlogLayout(props: Props): JSX.Element { return ( -
-
- -
- {children} -
- {toc &&
{toc}
} +
+
+ {hasSidebar && } +
{children}
+ {toc &&
{toc}
}
diff --git a/src/theme/BlogListPage/index.module.scss b/src/theme/BlogListPage/index.module.scss new file mode 100644 index 00000000000..d51d4329981 --- /dev/null +++ b/src/theme/BlogListPage/index.module.scss @@ -0,0 +1,5 @@ +.title { + font: var(--font-headline-1); + margin-top: 32px; + text-align: center; +} diff --git a/src/theme/BlogListPage/index.tsx b/src/theme/BlogListPage/index.tsx new file mode 100644 index 00000000000..6790f9f7a04 --- /dev/null +++ b/src/theme/BlogListPage/index.tsx @@ -0,0 +1,107 @@ +import { useHistory, useLocation } from '@docusaurus/router'; +import { PageMetadata, HtmlClassNameProvider, ThemeClassNames } from '@docusaurus/theme-common'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import { conditional } from '@silverhand/essentials'; +import BlogLayout from '@theme/BlogLayout'; +import type { Props } from '@theme/BlogListPage'; +import BlogListPageStructuredData from '@theme/BlogListPage/StructuredData'; +import BlogListPaginator from '@theme/BlogListPaginator'; +import BlogPostItems from '@theme/BlogPostItems'; +import SearchMetadata from '@theme/SearchMetadata'; +import clsx from 'clsx'; +import { useMemo } from 'react'; + +import { getConnectorPath, getSdkPath } from '@site/plugins/tutorial-generator/utils'; + +import TitleWithSelectionDropdown from '../BlogPostItem/Header/TitleWithSelectionDropdown'; + +import styles from './index.module.scss'; + +function BlogListPageMetadata(props: Props): JSX.Element { + const { metadata } = props; + const { + siteConfig: { title: siteTitle }, + } = useDocusaurusContext(); + + const { blogDescription, blogTitle, permalink } = metadata; + const isBlogOnlyMode = permalink === '/'; + const title = isBlogOnlyMode ? siteTitle : blogTitle; + return ( + <> + + + + ); +} + +function BlogListPageContent(props: Props): JSX.Element { + const { metadata, items, sidebar } = props; + + const { search } = useLocation(); + const { push } = useHistory(); + const searchParams = new URLSearchParams(search); + const sdk = conditional(searchParams.get('sdk')); + const connector = conditional(searchParams.get('connector')); + + const filteredItems = useMemo(() => { + if (!sdk && !connector) { + return items; + } + if (!sdk && connector) { + return items.filter((item) => item.content.metadata.tags[1]?.label === connector); + } + if (sdk && !connector) { + return items.filter((item) => item.content.metadata.tags[2]?.label === sdk); + } + return items.filter( + (item) => + item.content.metadata.tags[1]?.label === connector && + item.content.metadata.tags[2]?.label === sdk + ); + }, [items, sdk, connector]); + + return ( + +

+ { + if (selection) { + searchParams.set('connector', getConnectorPath(selection)); + } else { + searchParams.delete('connector'); + } + push({ + search: searchParams.toString(), + }); + }} + onSelectSdk={(selection) => { + if (selection) { + searchParams.set('sdk', getSdkPath(selection)); + } else { + searchParams.delete('sdk'); + } + push({ + search: searchParams.toString(), + }); + }} + /> +

+ + +
+ ); +} + +export default function BlogListPage(props: Props): JSX.Element { + return ( + + + + + + ); +} diff --git a/src/theme/BlogPostItem/Footer/index.tsx b/src/theme/BlogPostItem/Footer/index.tsx new file mode 100644 index 00000000000..1ea74fe8196 --- /dev/null +++ b/src/theme/BlogPostItem/Footer/index.tsx @@ -0,0 +1,65 @@ +import { useBlogPost } from '@docusaurus/plugin-content-blog/client'; +import { ThemeClassNames } from '@docusaurus/theme-common'; +import EditMetaRow from '@theme/EditMetaRow'; +import TagsListInline from '@theme/TagsListInline'; +import clsx from 'clsx'; + +export default function BlogPostItemFooter() { + const { metadata, isBlogPostPage } = useBlogPost(); + const { tags, editUrl, hasTruncateMarker, lastUpdatedBy, lastUpdatedAt } = metadata; + + // A post is truncated if it's in the "list view" and it has a truncate marker + const truncatedPost = !isBlogPostPage && hasTruncateMarker; + + const tagsExists = tags.length > 0; + + const renderFooter = tagsExists || truncatedPost || editUrl; + + if (!renderFooter || !isBlogPostPage) { + return null; + } + + // BlogPost footer - details view + const canDisplayEditMetaRow = !!(editUrl || lastUpdatedAt || lastUpdatedBy); + + return ( +
+ {tagsExists && ( +
+
+ +
+
+ )} + {canDisplayEditMetaRow && ( + + )} +
+ ); + + // BlogPost footer - list view - Charles commented out - We don't need tags in list view + // else { + // return ( + //
+ // {tagsExists && ( + //
+ // + //
+ // )} + // {truncatedPost && ( + //
+ // + //
+ // )} + //
+ // ); + // } +} diff --git a/src/theme/BlogPostItem/Header/SelectionDropdown/index.module.scss b/src/theme/BlogPostItem/Header/SelectionDropdown/index.module.scss index 7412995f782..83c8e79ee59 100644 --- a/src/theme/BlogPostItem/Header/SelectionDropdown/index.module.scss +++ b/src/theme/BlogPostItem/Header/SelectionDropdown/index.module.scss @@ -55,6 +55,21 @@ } } +.clearButton { + margin: 22px 0 6px; + padding: 0; + height: 20px; + color: var(--logto-link-color); + border: none; + background: transparent; + font: var(--font-body-2); + cursor: pointer; + + &:hover { + text-decoration: underline; + } +} + html[data-theme='dark'] { .dropdown { background: var(--logto-container-neutral-bg); diff --git a/src/theme/BlogPostItem/Header/SelectionDropdown/index.tsx b/src/theme/BlogPostItem/Header/SelectionDropdown/index.tsx index 6075b95a023..1b63fa3c9e1 100644 --- a/src/theme/BlogPostItem/Header/SelectionDropdown/index.tsx +++ b/src/theme/BlogPostItem/Header/SelectionDropdown/index.tsx @@ -1,3 +1,4 @@ +import Translate from '@docusaurus/Translate'; import { type DocMetadata } from '@docusaurus/plugin-content-docs'; import { type Nullable } from '@silverhand/essentials'; import { Fragment, type RefObject, useRef } from 'react'; @@ -14,9 +15,17 @@ type DropdownProps = { readonly options: Record; readonly onSelect: (option: { displayName: string; metadata: DocMetadata }) => void; readonly onClose: () => void; + readonly onReset?: () => void; }; -const SelectionDropdown = ({ isOpen, anchorRef, options, onSelect, onClose }: DropdownProps) => { +const SelectionDropdown = ({ + isOpen, + anchorRef, + options, + onSelect, + onClose, + onReset, +}: DropdownProps) => { const overlayRef = useRef(null); const { position, mutate } = usePosition({ @@ -75,6 +84,15 @@ const SelectionDropdown = ({ isOpen, anchorRef, options, onSelect, onClose }: Dr ); })} + {onReset && ( + + )}
); diff --git a/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.tsx b/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.tsx index c43a1853f61..d1a52925be0 100644 --- a/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.tsx +++ b/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.tsx @@ -2,6 +2,7 @@ import Translate, { translate } from '@docusaurus/Translate'; import { type PropBlogPostMetadata } from '@docusaurus/plugin-content-blog'; import { type DocMetadata } from '@docusaurus/plugin-content-docs'; import { useHistory } from '@docusaurus/router'; +import { conditional } from '@silverhand/essentials'; import { clsx } from 'clsx'; import { useMemo, useRef, useState } from 'react'; @@ -18,14 +19,19 @@ import Dropdown from '../SelectionDropdown'; import styles from './index.module.scss'; -type Props = { +type BlogPostProps = { readonly title: string; readonly metadata: PropBlogPostMetadata; - readonly isInListView?: boolean; +}; + +type ListViewProps = { readonly defaultSdkSlugPart?: string; readonly defaultConnectorSlugPart?: string; - readonly onSelectSdk?: (docMetadata: DocMetadata) => void; - readonly onSelectConnector?: (docMetadata: DocMetadata) => void; +}; + +type Props = (BlogPostProps | ListViewProps) & { + readonly onSelectSdk?: (docMetadata?: DocMetadata) => void; + readonly onSelectConnector?: (docMetadata?: DocMetadata) => void; }; type DropdownType = 'sdk' | 'connector'; @@ -34,54 +40,77 @@ const slugFirstPart = 'how-to-build-'; const slugMiddlePart = '-sign-in-with'; const slugLastPart = '-and-logto'; +/* eslint-disable no-template-curly-in-string */ +const sdkTemplateSlot = '${sdk}'; +const connectorTemplateSlot = '${connector}'; +/* eslint-enable no-template-curly-in-string */ + /** - * Escape potential parentheses in the SDK / connector name. + * Escape potential parentheses and dollar signs in the SDK / connector name. * The result will be used for the regex that splits the title into parts. */ -const normalizeName = (name: string) => name.replaceAll('(', '\\(').replaceAll(')', '\\)'); - -const TitleWithSelectionDropdown = ({ - title, - metadata, - isInListView, - defaultSdkSlugPart, - defaultConnectorSlugPart, - onSelectSdk, - onSelectConnector, -}: Props) => { - const history = useHistory(); - const slug = metadata.frontMatter.slug ?? ''; - const sdkName = String(metadata.frontMatter.sdk ?? ''); - const connectorName = String(metadata.frontMatter.connector ?? ''); - const [isDropdownOpen, setIsDropdownOpen] = useState(); +const normalizeName = (name: string) => + name.replaceAll('(', '\\(').replaceAll(')', '\\)').replaceAll('$', '\\$'); - const allTutorialsMetadata = useCategorizedTutorialMetadata(); +const TitleWithSelectionDropdown = (props: Props) => { + const isBlogPost = 'title' in props; + const { onSelectSdk, onSelectConnector } = props; + const listViewProps = conditional(!isBlogPost && props); + const blogPostProps = conditional(isBlogPost && props); + const { push } = useHistory(); const sdkNameRef = useRef(null); const connectorNameRef = useRef(null); + const allTutorialsMetadata = useCategorizedTutorialMetadata(); + const [isDropdownOpen, setIsDropdownOpen] = useState(); const defaultSdk = useMemo(() => { - if (!defaultSdkSlugPart) { + if (isBlogPost || !listViewProps?.defaultSdkSlugPart) { return; } - return allTutorialsMetadata.allSdks.find((data) => getSdkPath(data) === defaultSdkSlugPart); - }, [defaultSdkSlugPart, allTutorialsMetadata.allSdks]); + return allTutorialsMetadata.allSdks.find( + (data) => getSdkPath(data) === listViewProps.defaultSdkSlugPart + ); + }, [isBlogPost, listViewProps?.defaultSdkSlugPart, allTutorialsMetadata.allSdks]); const defaultConnector = useMemo(() => { - if (!defaultConnectorSlugPart) { + if (isBlogPost || !listViewProps?.defaultConnectorSlugPart) { return; } return allTutorialsMetadata.allConnectors.find( - (data) => getConnectorPath(data) === defaultConnectorSlugPart + (data) => getConnectorPath(data) === listViewProps.defaultConnectorSlugPart ); - }, [defaultConnectorSlugPart, allTutorialsMetadata.allConnectors]); + }, [isBlogPost, listViewProps?.defaultConnectorSlugPart, allTutorialsMetadata.allConnectors]); + + const sdkName = isBlogPost + ? String(blogPostProps?.metadata.frontMatter.sdk ?? '') + : getSdkDisplayName(defaultSdk); + const connectorName = isBlogPost + ? String(blogPostProps?.metadata.frontMatter.connector ?? '') + : getConnectorDisplayName(defaultConnector); - if (!sdkName && !connectorName) { - return title; + if (blogPostProps && (!sdkName || !connectorName)) { + return blogPostProps.title; } - const titleParts = title - .split(new RegExp(`(${normalizeName(sdkName)}|${normalizeName(connectorName)})`, 'g')) + const listViewTitle = translate({ + id: 'theme.blog.tutorial.title', + message: `Build ${connectorTemplateSlot} sign-in with ${sdkTemplateSlot}`, + }) + .replace(sdkTemplateSlot, sdkName || sdkTemplateSlot) + .replace(connectorTemplateSlot, connectorName || connectorTemplateSlot); + + console.log('listViewTitle', listViewTitle); + + const normalizedTitle = blogPostProps?.title ?? listViewTitle; + + const titleParts = normalizedTitle + .split( + new RegExp( + `(${normalizeName(sdkName || sdkTemplateSlot)}|${normalizeName(connectorName || connectorTemplateSlot)})`, + 'g' + ) + ) .filter(Boolean); const showDropdown = (type: DropdownType) => { @@ -105,7 +134,8 @@ const TitleWithSelectionDropdown = ({ // eslint-disable-next-line @silverhand/fp/no-mutation elementRef.current.textContent = displayName; } - if (!isInListView) { + if (isBlogPost) { + const slug = metadata.frontMatter.slug ?? ''; const selectedSlugPart = getPathFn(metadata); const targetSlug = type === 'sdk' @@ -113,19 +143,19 @@ const TitleWithSelectionDropdown = ({ selectedSlugPart + slugLastPart : slugFirstPart + selectedSlugPart + slug.slice(slug.indexOf(slugMiddlePart)); - // eslint-disable-next-line @silverhand/fp/no-mutating-methods - history.push(`/tutorial/${targetSlug}`); + + push(`/tutorial/${targetSlug}`); } }; return ( <> {titleParts.map((part) => { - if (part === sdkName) { + if (part === sdkName || part === sdkTemplateSlot) { return ( - {isInListView ? ( - defaultSdk ? ( - getSdkDisplayName(defaultSdk) - ) : ( - - Your SDK - - ) + {part === sdkTemplateSlot ? ( + + Your SDK + ) : ( part )} ); } - if (part === connectorName) { + if (part === connectorName || part === connectorTemplateSlot) { return ( - {isInListView ? ( - defaultConnector ? ( - getConnectorDisplayName(defaultConnector) - ) : ( - - Your provider - - ) + {part === connectorTemplateSlot ? ( + + Your provider + ) : ( part )} @@ -201,6 +223,9 @@ const TitleWithSelectionDropdown = ({ onClose={() => { setIsDropdownOpen(undefined); }} + onReset={() => { + onSelectSdk?.(); + }} /> { setIsDropdownOpen(undefined); }} + onReset={() => { + onSelectConnector?.(); + }} /> ); From c3057a88b41eb40bed6f1ae90c525f1dbf7e7320 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Thu, 16 Jan 2025 18:43:03 +0800 Subject: [PATCH 2/3] fix: fixup --- src/theme/BlogLayout/index.module.scss | 26 +++++++++++++++-- src/theme/BlogLayout/index.tsx | 3 +- src/theme/BlogListPage/index.module.scss | 8 +++++- src/theme/BlogListPage/index.tsx | 7 ++++- src/theme/BlogPostItem/Container/index.tsx | 5 ++++ src/theme/BlogPostItem/Header/Title/index.tsx | 11 ++------ .../Header/Title/styles.module.css | 9 ++++-- .../TitleWithSelectionDropdown/index.tsx | 14 +++------- src/theme/BlogPostItem/index.module.scss | 9 ++++++ src/theme/BlogPostItem/index.tsx | 28 +++++++++++++++++++ src/theme/BlogPostItems/index.tsx | 23 +++++++++++++++ 11 files changed, 116 insertions(+), 27 deletions(-) create mode 100644 src/theme/BlogPostItem/Container/index.tsx create mode 100644 src/theme/BlogPostItem/index.module.scss create mode 100644 src/theme/BlogPostItem/index.tsx create mode 100644 src/theme/BlogPostItems/index.tsx diff --git a/src/theme/BlogLayout/index.module.scss b/src/theme/BlogLayout/index.module.scss index 159b8ebd4a0..6f07cc34808 100644 --- a/src/theme/BlogLayout/index.module.scss +++ b/src/theme/BlogLayout/index.module.scss @@ -1,8 +1,9 @@ .container { - max-width: 1044px; + max-width: 1000px; + width: 100%; margin: 0 auto; + padding: 0 24px; - .row { display: flex; gap: 24px; @@ -12,10 +13,29 @@ display: flex; flex-direction: column; gap: 24px; + overflow: hidden; } .toc { flex: 0 0 276px; } } -} \ No newline at end of file + + &.hasToc { + max-width: 1200px; + } +} + +@media (max-width: 996px) { + .container { + max-width: 100vw; + + &.hasToc { + max-width: 100vw; + } + + .row .toc { + display: none; + } + } +} diff --git a/src/theme/BlogLayout/index.tsx b/src/theme/BlogLayout/index.tsx index 085dee76bd9..2c58e1468aa 100644 --- a/src/theme/BlogLayout/index.tsx +++ b/src/theme/BlogLayout/index.tsx @@ -1,6 +1,7 @@ import type { Props } from '@theme/BlogLayout'; import BlogSidebar from '@theme/BlogSidebar'; import Layout from '@theme/Layout'; +import clsx from 'clsx'; import ReactModal from 'react-modal'; import styles from './index.module.scss'; @@ -13,7 +14,7 @@ export default function BlogLayout(props: Props): JSX.Element { return ( -
+
{hasSidebar && }
{children}
diff --git a/src/theme/BlogListPage/index.module.scss b/src/theme/BlogListPage/index.module.scss index d51d4329981..419ac2cd317 100644 --- a/src/theme/BlogListPage/index.module.scss +++ b/src/theme/BlogListPage/index.module.scss @@ -1,5 +1,11 @@ .title { font: var(--font-headline-1); - margin-top: 32px; + margin-block: 32px 0; text-align: center; } + +.subtitle { + font: var(--font-body-0); + text-align: center; + margin-bottom: 16px; +} diff --git a/src/theme/BlogListPage/index.tsx b/src/theme/BlogListPage/index.tsx index 6790f9f7a04..bdbd65413b7 100644 --- a/src/theme/BlogListPage/index.tsx +++ b/src/theme/BlogListPage/index.tsx @@ -1,3 +1,4 @@ +import Translate from '@docusaurus/Translate'; import { useHistory, useLocation } from '@docusaurus/router'; import { PageMetadata, HtmlClassNameProvider, ThemeClassNames } from '@docusaurus/theme-common'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; @@ -36,7 +37,6 @@ function BlogListPageMetadata(props: Props): JSX.Element { function BlogListPageContent(props: Props): JSX.Element { const { metadata, items, sidebar } = props; - const { search } = useLocation(); const { push } = useHistory(); const searchParams = new URLSearchParams(search); @@ -88,6 +88,11 @@ function BlogListPageContent(props: Props): JSX.Element { }} /> +

+ + Follow our step-by-step tutorial to set up an authentication system right away. + +

diff --git a/src/theme/BlogPostItem/Container/index.tsx b/src/theme/BlogPostItem/Container/index.tsx new file mode 100644 index 00000000000..1c0421eddde --- /dev/null +++ b/src/theme/BlogPostItem/Container/index.tsx @@ -0,0 +1,5 @@ +import type { Props } from '@theme/BlogPostItem/Container'; + +export default function BlogPostItemContainer({ children, className }: Props): JSX.Element { + return
{children}
; +} diff --git a/src/theme/BlogPostItem/Header/Title/index.tsx b/src/theme/BlogPostItem/Header/Title/index.tsx index cbd0d192110..aa50115c31b 100644 --- a/src/theme/BlogPostItem/Header/Title/index.tsx +++ b/src/theme/BlogPostItem/Header/Title/index.tsx @@ -1,5 +1,4 @@ import BrowserOnly from '@docusaurus/BrowserOnly'; -import Link from '@docusaurus/Link'; import { useBlogPost } from '@docusaurus/plugin-content-blog/client'; import Admonition from '@theme/Admonition'; import type { Props } from '@theme/BlogPostItem/Header/Title'; @@ -12,7 +11,7 @@ import styles from './styles.module.css'; const Content = ({ className }: Props): JSX.Element => { const { metadata, isBlogPostPage } = useBlogPost(); - const { permalink, title } = metadata; + const { title } = metadata; const TitleHeading = isBlogPostPage ? 'h1' : 'h2'; const shouldReplace = window.location.pathname.startsWith('/blog'); @@ -72,13 +71,7 @@ const Content = ({ className }: Props): JSX.Element => { className={clsx(styles.title, !isBlogPostPage && styles.listTitle, className)} itemProp="headline" > - {isBlogPostPage ? ( - - ) : ( - - {title} - - )} + {isBlogPostPage ? : title} ); diff --git a/src/theme/BlogPostItem/Header/Title/styles.module.css b/src/theme/BlogPostItem/Header/Title/styles.module.css index c08d14098f5..5469a814927 100644 --- a/src/theme/BlogPostItem/Header/Title/styles.module.css +++ b/src/theme/BlogPostItem/Header/Title/styles.module.css @@ -6,8 +6,13 @@ /* Make it smaller while in the list */ .listTitle { - font-size: 2rem; - line-height: 50px; + font: var(--font-body-0); + font-weight: 500; + margin: 0; +} + +.highlight { + color: var(--logto-link-color); } /** diff --git a/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.tsx b/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.tsx index d1a52925be0..2c6ceb600fe 100644 --- a/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.tsx +++ b/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.tsx @@ -2,7 +2,7 @@ import Translate, { translate } from '@docusaurus/Translate'; import { type PropBlogPostMetadata } from '@docusaurus/plugin-content-blog'; import { type DocMetadata } from '@docusaurus/plugin-content-docs'; import { useHistory } from '@docusaurus/router'; -import { conditional } from '@silverhand/essentials'; +import { cond, conditional } from '@silverhand/essentials'; import { clsx } from 'clsx'; import { useMemo, useRef, useState } from 'react'; @@ -100,8 +100,6 @@ const TitleWithSelectionDropdown = (props: Props) => { .replace(sdkTemplateSlot, sdkName || sdkTemplateSlot) .replace(connectorTemplateSlot, connectorName || connectorTemplateSlot); - console.log('listViewTitle', listViewTitle); - const normalizedTitle = blogPostProps?.title ?? listViewTitle; const titleParts = normalizedTitle @@ -135,7 +133,7 @@ const TitleWithSelectionDropdown = (props: Props) => { elementRef.current.textContent = displayName; } if (isBlogPost) { - const slug = metadata.frontMatter.slug ?? ''; + const slug = blogPostProps?.metadata.frontMatter.slug ?? ''; const selectedSlugPart = getPathFn(metadata); const targetSlug = type === 'sdk' @@ -223,9 +221,7 @@ const TitleWithSelectionDropdown = (props: Props) => { onClose={() => { setIsDropdownOpen(undefined); }} - onReset={() => { - onSelectSdk?.(); - }} + onReset={cond(!isBlogPost && onSelectSdk)} /> { onClose={() => { setIsDropdownOpen(undefined); }} - onReset={() => { - onSelectConnector?.(); - }} + onReset={cond(!isBlogPost && onSelectConnector)} /> ); diff --git a/src/theme/BlogPostItem/index.module.scss b/src/theme/BlogPostItem/index.module.scss new file mode 100644 index 00000000000..8c5f3a4d900 --- /dev/null +++ b/src/theme/BlogPostItem/index.module.scss @@ -0,0 +1,9 @@ +.listViewItemContainer { + display: flex; + align-items: center; + border: 1px solid var(--logto-line-divider); + border-radius: 12px; + box-shadow: var(--logto-shadow-1); + padding: 20px; + width: 100%; +} diff --git a/src/theme/BlogPostItem/index.tsx b/src/theme/BlogPostItem/index.tsx new file mode 100644 index 00000000000..70e975a7533 --- /dev/null +++ b/src/theme/BlogPostItem/index.tsx @@ -0,0 +1,28 @@ +import { useBlogPost } from '@docusaurus/plugin-content-blog/client'; +import type { Props } from '@theme/BlogPostItem'; +import BlogPostItemContainer from '@theme/BlogPostItem/Container'; +import BlogPostItemContent from '@theme/BlogPostItem/Content'; +import BlogPostItemFooter from '@theme/BlogPostItem/Footer'; +import BlogPostItemHeader from '@theme/BlogPostItem/Header'; +import clsx from 'clsx'; + +import styles from './index.module.scss'; + +// Apply a bottom margin in list view - Charles commented out +// function useContainerClassName() { +// const {isBlogPostPage} = useBlogPost(); +// return !isBlogPostPage ? 'margin-bottom--xl' : undefined; +// } + +export default function BlogPostItem({ children, className }: Props): JSX.Element { + const { isBlogPostPage } = useBlogPost(); + return ( + + + {children} + + + ); +} diff --git a/src/theme/BlogPostItems/index.tsx b/src/theme/BlogPostItems/index.tsx new file mode 100644 index 00000000000..be7e4f884e7 --- /dev/null +++ b/src/theme/BlogPostItems/index.tsx @@ -0,0 +1,23 @@ +import Link from '@docusaurus/Link'; +import { BlogPostProvider } from '@docusaurus/plugin-content-blog/client'; +import BlogPostItem from '@theme/BlogPostItem'; +import type { Props } from '@theme/BlogPostItems'; + +export default function BlogPostItems({ + items, + component: BlogPostItemComponent = BlogPostItem, +}: Props): JSX.Element { + return ( + <> + {items.map(({ content: BlogPostContent }) => ( + + + + + + + + ))} + + ); +} From 92babfe8dba821a09c74a18bf0003817f64348b7 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Thu, 16 Jan 2025 22:08:46 +0800 Subject: [PATCH 3/3] fix: fixup --- i18n/de/code.json | 4 +- i18n/es/code.json | 4 +- i18n/fr/code.json | 4 +- i18n/pt-BR/code.json | 4 +- .../use-categorized-tutorial-metadata.ts | 24 ++++--- src/theme/BlogLayout/index.module.scss | 1 + src/theme/BlogPostItem/Header/Info/index.tsx | 14 ++-- src/theme/BlogPostItem/Header/Title/index.tsx | 7 +- .../Header/Title/styles.module.css | 15 +--- .../TitleWithHighlights/index.module.scss | 3 + .../Header/TitleWithHighlights/index.tsx | 44 ++++++++++++ .../index.module.scss | 17 ----- .../TitleWithSelectionDropdown/index.tsx | 29 +++++--- src/theme/BlogPostItem/index.module.scss | 28 +++++++- src/theme/BlogPostItem/index.tsx | 72 ++++++++++++++++++- src/theme/BlogPostItems/index.module.scss | 3 + src/theme/BlogPostItems/index.tsx | 4 +- 17 files changed, 210 insertions(+), 67 deletions(-) create mode 100644 src/theme/BlogPostItem/Header/TitleWithHighlights/index.module.scss create mode 100644 src/theme/BlogPostItem/Header/TitleWithHighlights/index.tsx create mode 100644 src/theme/BlogPostItems/index.module.scss diff --git a/i18n/de/code.json b/i18n/de/code.json index e5b58ff1062..51f8b37ec7a 100644 --- a/i18n/de/code.json +++ b/i18n/de/code.json @@ -483,10 +483,10 @@ "message": "Datenschutz" }, "theme.common.sdk.placeholder": { - "message": "Ihr SDK" + "message": "dein SDK" }, "theme.common.connector.placeholder": { - "message": "Ihr Anbieter" + "message": "dein anbieter" }, "theme.common.sdk.native": { "message": "Nativ" diff --git a/i18n/es/code.json b/i18n/es/code.json index b8c8b3d6d0d..4fc76aafe0b 100644 --- a/i18n/es/code.json +++ b/i18n/es/code.json @@ -483,10 +483,10 @@ "message": "Privacidad" }, "theme.common.sdk.placeholder": { - "message": "Tu SDK" + "message": "tu SDK" }, "theme.common.connector.placeholder": { - "message": "Tu proveedor" + "message": "tu proveedor" }, "theme.common.sdk.native": { "message": "Nativo" diff --git a/i18n/fr/code.json b/i18n/fr/code.json index bbc6b582315..d1089f7ea6a 100644 --- a/i18n/fr/code.json +++ b/i18n/fr/code.json @@ -483,10 +483,10 @@ "message": "Confidentialité" }, "theme.common.sdk.placeholder": { - "message": "Votre SDK" + "message": "ton SDK" }, "theme.common.connector.placeholder": { - "message": "Votre fournisseur" + "message": "ton fournisseur" }, "theme.common.sdk.native": { "message": "Natif" diff --git a/i18n/pt-BR/code.json b/i18n/pt-BR/code.json index 908b8822b68..1f4e9970d5e 100644 --- a/i18n/pt-BR/code.json +++ b/i18n/pt-BR/code.json @@ -483,10 +483,10 @@ "message": "Privacidade" }, "theme.common.sdk.placeholder": { - "message": "Seu SDK" + "message": "seu SDK" }, "theme.common.connector.placeholder": { - "message": "Seu provedor" + "message": "seu provedor" }, "theme.common.sdk.native": { "message": "Nativo" diff --git a/src/hooks/use-categorized-tutorial-metadata.ts b/src/hooks/use-categorized-tutorial-metadata.ts index 01df481aa16..ba8f5367d93 100644 --- a/src/hooks/use-categorized-tutorial-metadata.ts +++ b/src/hooks/use-categorized-tutorial-metadata.ts @@ -1,4 +1,5 @@ import { type DocMetadata } from '@docusaurus/plugin-content-docs'; +import { useMemo } from 'react'; import metadata from '@site/tutorial/build-with-logto/metadata.json'; @@ -47,16 +48,19 @@ const useCategorizedTutorialMetadata = () => { { nativeSdks: [], traditionalSdks: [], spaSdks: [] } ); - return { - allSdks: sdks, - allConnectors: [...socialConnectors, ...emailConnectors, ...smsConnectors], - nativeSdks, - traditionalSdks, - spaSdks, - socialConnectors, - emailConnectors, - smsConnectors, - }; + return useMemo( + () => ({ + allSdks: sdks, + allConnectors: [...socialConnectors, ...emailConnectors, ...smsConnectors], + nativeSdks, + traditionalSdks, + spaSdks, + socialConnectors, + emailConnectors, + smsConnectors, + }), + [sdks, socialConnectors, emailConnectors, smsConnectors, nativeSdks, traditionalSdks, spaSdks] + ); }; export default useCategorizedTutorialMetadata; diff --git a/src/theme/BlogLayout/index.module.scss b/src/theme/BlogLayout/index.module.scss index 6f07cc34808..9434191bfc3 100644 --- a/src/theme/BlogLayout/index.module.scss +++ b/src/theme/BlogLayout/index.module.scss @@ -14,6 +14,7 @@ flex-direction: column; gap: 24px; overflow: hidden; + margin-bottom: 50px; } .toc { diff --git a/src/theme/BlogPostItem/Header/Info/index.tsx b/src/theme/BlogPostItem/Header/Info/index.tsx index 1438845c20f..6a6342f21cc 100644 --- a/src/theme/BlogPostItem/Header/Info/index.tsx +++ b/src/theme/BlogPostItem/Header/Info/index.tsx @@ -47,13 +47,10 @@ function Spacer() { return <>{' · '}; } -export default function BlogPostItemHeaderInfo({ className }: Props): JSX.Element { +export default function BlogPostItemHeaderInfo({ className }: Props) { const { metadata } = useBlogPost(); const { date, readingTime } = metadata; - // Charles edited this to remove the time from generated "Build X with Y tutorials" - const isTutorial = metadata.frontMatter.slug?.startsWith('how-to-build-'); - const dateTimeFormat = useDateTimeFormat({ day: 'numeric', month: 'long', @@ -61,11 +58,18 @@ export default function BlogPostItemHeaderInfo({ className }: Props): JSX.Elemen timeZone: 'UTC', }); + // Charles edited this to remove the time from generated "Build X with Y tutorials" + const isTutorial = metadata.frontMatter.slug?.startsWith('how-to-build-'); + + if (isTutorial) { + return null; + } + const formatDate = (blogDate: string) => dateTimeFormat.format(new Date(blogDate)); return (
- {!isTutorial && } + {readingTime !== undefined && ( <> diff --git a/src/theme/BlogPostItem/Header/Title/index.tsx b/src/theme/BlogPostItem/Header/Title/index.tsx index aa50115c31b..e7ff2f8ba48 100644 --- a/src/theme/BlogPostItem/Header/Title/index.tsx +++ b/src/theme/BlogPostItem/Header/Title/index.tsx @@ -5,6 +5,7 @@ import type { Props } from '@theme/BlogPostItem/Header/Title'; import { clsx } from 'clsx'; import { useCallback, useEffect } from 'react'; +import TitleWithHighlights from '../TitleWithHighlights'; import TitleWithSelectionDropdown from '../TitleWithSelectionDropdown'; import styles from './styles.module.css'; @@ -71,7 +72,11 @@ const Content = ({ className }: Props): JSX.Element => { className={clsx(styles.title, !isBlogPostPage && styles.listTitle, className)} itemProp="headline" > - {isBlogPostPage ? : title} + {isBlogPostPage ? ( + + ) : ( + + )} ); diff --git a/src/theme/BlogPostItem/Header/Title/styles.module.css b/src/theme/BlogPostItem/Header/Title/styles.module.css index 5469a814927..64c62c33bd4 100644 --- a/src/theme/BlogPostItem/Header/Title/styles.module.css +++ b/src/theme/BlogPostItem/Header/Title/styles.module.css @@ -9,18 +9,5 @@ font: var(--font-body-0); font-weight: 500; margin: 0; -} - -.highlight { - color: var(--logto-link-color); -} - -/** - Blog post title should be smaller on smaller devices -**/ -@media (max-width: 576px) { - .listTitle { - font-size: 1.5rem; - line-height: 40px; - } + color: var(--logto-color-text); } diff --git a/src/theme/BlogPostItem/Header/TitleWithHighlights/index.module.scss b/src/theme/BlogPostItem/Header/TitleWithHighlights/index.module.scss new file mode 100644 index 00000000000..75c7f290c0b --- /dev/null +++ b/src/theme/BlogPostItem/Header/TitleWithHighlights/index.module.scss @@ -0,0 +1,3 @@ +.highlight { + color: var(--logto-link-color); +} diff --git a/src/theme/BlogPostItem/Header/TitleWithHighlights/index.tsx b/src/theme/BlogPostItem/Header/TitleWithHighlights/index.tsx new file mode 100644 index 00000000000..e9e7f02f601 --- /dev/null +++ b/src/theme/BlogPostItem/Header/TitleWithHighlights/index.tsx @@ -0,0 +1,44 @@ +import { type PropBlogPostMetadata } from '@docusaurus/plugin-content-blog'; +import { condString } from '@silverhand/essentials'; + +import styles from './index.module.scss'; + +type Props = { + readonly metadata: PropBlogPostMetadata; +}; + +/** + * Escape potential parentheses in the SDK / connector name. + * The result will be used for the regex that splits the title into parts. + */ +const normalizeName = (name: string) => name.replaceAll('(', '\\(').replaceAll(')', '\\)'); + +const TitleWithHighlights = ({ metadata }: Props) => { + const { frontMatter, title } = metadata; + const sdkName = condString('sdk' in frontMatter && frontMatter.sdk); + const connectorName = condString('connector' in frontMatter && frontMatter.connector); + + const titleParts = title + .split(new RegExp(`(${normalizeName(connectorName)}|${normalizeName(sdkName)})`, 'g')) + .filter(Boolean); + + return titleParts.map((part) => { + if (part === sdkName) { + return ( + + {part} + + ); + } + if (part === connectorName) { + return ( + + {part} + + ); + } + return part; + }); +}; + +export default TitleWithHighlights; diff --git a/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.module.scss b/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.module.scss index 47e46be5048..778c06f017b 100644 --- a/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.module.scss +++ b/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.module.scss @@ -27,20 +27,3 @@ user-select: none; } } - -/* Hide dropdown anchors from title on smaller devices */ -@media (max-width: 576px) { - .dropdownAnchor { - padding: 0; - outline: none; - cursor: default; - - &::after { - display: none; - } - - &.active { - outline: none; - } - } -} diff --git a/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.tsx b/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.tsx index 2c6ceb600fe..eb6ee4df5e3 100644 --- a/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.tsx +++ b/src/theme/BlogPostItem/Header/TitleWithSelectionDropdown/index.tsx @@ -20,7 +20,6 @@ import Dropdown from '../SelectionDropdown'; import styles from './index.module.scss'; type BlogPostProps = { - readonly title: string; readonly metadata: PropBlogPostMetadata; }; @@ -53,7 +52,7 @@ const normalizeName = (name: string) => name.replaceAll('(', '\\(').replaceAll(')', '\\)').replaceAll('$', '\\$'); const TitleWithSelectionDropdown = (props: Props) => { - const isBlogPost = 'title' in props; + const isBlogPost = 'metadata' in props; const { onSelectSdk, onSelectConnector } = props; const listViewProps = conditional(!isBlogPost && props); const blogPostProps = conditional(isBlogPost && props); @@ -90,7 +89,7 @@ const TitleWithSelectionDropdown = (props: Props) => { : getConnectorDisplayName(defaultConnector); if (blogPostProps && (!sdkName || !connectorName)) { - return blogPostProps.title; + return blogPostProps.metadata.title; } const listViewTitle = translate({ @@ -100,12 +99,12 @@ const TitleWithSelectionDropdown = (props: Props) => { .replace(sdkTemplateSlot, sdkName || sdkTemplateSlot) .replace(connectorTemplateSlot, connectorName || connectorTemplateSlot); - const normalizedTitle = blogPostProps?.title ?? listViewTitle; + const normalizedTitle = blogPostProps?.metadata.title ?? listViewTitle; const titleParts = normalizedTitle .split( new RegExp( - `(${normalizeName(sdkName || sdkTemplateSlot)}|${normalizeName(connectorName || connectorTemplateSlot)})`, + `(${normalizeName(connectorName || connectorTemplateSlot)}|${normalizeName(sdkName || sdkTemplateSlot)})`, 'g' ) ) @@ -166,7 +165,7 @@ const TitleWithSelectionDropdown = (props: Props) => { > {part === sdkTemplateSlot ? ( - Your SDK + your SDK ) : ( part @@ -194,7 +193,7 @@ const TitleWithSelectionDropdown = (props: Props) => { > {part === connectorTemplateSlot ? ( - Your provider + your provider ) : ( part @@ -221,7 +220,13 @@ const TitleWithSelectionDropdown = (props: Props) => { onClose={() => { setIsDropdownOpen(undefined); }} - onReset={cond(!isBlogPost && onSelectSdk)} + onReset={cond( + !isBlogPost && + (() => { + onSelectSdk?.(undefined); + setIsDropdownOpen(undefined); + }) + )} /> { onClose={() => { setIsDropdownOpen(undefined); }} - onReset={cond(!isBlogPost && onSelectConnector)} + onReset={cond( + !isBlogPost && + (() => { + onSelectConnector?.(undefined); + setIsDropdownOpen(undefined); + }) + )} /> ); diff --git a/src/theme/BlogPostItem/index.module.scss b/src/theme/BlogPostItem/index.module.scss index 8c5f3a4d900..b5b21d9376e 100644 --- a/src/theme/BlogPostItem/index.module.scss +++ b/src/theme/BlogPostItem/index.module.scss @@ -5,5 +5,31 @@ border-radius: 12px; box-shadow: var(--logto-shadow-1); padding: 20px; - width: 100%; + width: 100%; + transition: background-color var(--ifm-transition-fast) ease; + + &:hover { + background-color: var(--logto-container-info-bg); + } +} + +.logo { + display: flex; + align-items: center; + justify-content: center; + width: 60px; + height: 60px; + margin-inline-end: 12px; + border-radius: 10px; + background: var(--logto-container-neutral-bg); + flex-shrink: 0; + + img { + width: 36px; + height: 36px; + } + + + .logo { + margin-inline-end: 28px; + } } diff --git a/src/theme/BlogPostItem/index.tsx b/src/theme/BlogPostItem/index.tsx index 70e975a7533..cef0d1ee541 100644 --- a/src/theme/BlogPostItem/index.tsx +++ b/src/theme/BlogPostItem/index.tsx @@ -1,11 +1,18 @@ import { useBlogPost } from '@docusaurus/plugin-content-blog/client'; +import { type DocMetadata } from '@docusaurus/plugin-content-docs'; +import { condString } from '@silverhand/essentials'; import type { Props } from '@theme/BlogPostItem'; import BlogPostItemContainer from '@theme/BlogPostItem/Container'; import BlogPostItemContent from '@theme/BlogPostItem/Content'; import BlogPostItemFooter from '@theme/BlogPostItem/Footer'; import BlogPostItemHeader from '@theme/BlogPostItem/Header'; +import ThemedImage from '@theme/ThemedImage'; import clsx from 'clsx'; +import { getConnectorDisplayName, getSdkDisplayName } from '@site/plugins/tutorial-generator/utils'; +import useCategorizedTutorialMetadata from '@site/src/hooks/use-categorized-tutorial-metadata'; +import { useCurrentLocalePrefix } from '@site/src/hooks/useCurrentLocalePrefix'; + import styles from './index.module.scss'; // Apply a bottom margin in list view - Charles commented out @@ -14,12 +21,75 @@ import styles from './index.module.scss'; // return !isBlogPostPage ? 'margin-bottom--xl' : undefined; // } +const getLogoFilenames = (locale: string, data?: DocMetadata) => { + const lastSegmentInSlug = data?.slug.slice(data.slug.lastIndexOf('/') + 1) ?? ''; + const logoFilename = condString( + data?.frontMatter.sidebar_custom_props?.logoFilename ?? lastSegmentInSlug + '.svg' + ); + const darkLogoFilename = condString( + data?.frontMatter.sidebar_custom_props?.darkLogoFilename ?? logoFilename + ); + + return { + logoFilename: `${locale}/img/logo/${logoFilename}`, + darkLogoFilename: `${locale}/img/logo/${darkLogoFilename}`, + fallbackLogoFilename: `${locale}/img/logo/broken-image.svg`, + }; +}; + export default function BlogPostItem({ children, className }: Props): JSX.Element { - const { isBlogPostPage } = useBlogPost(); + const locale = useCurrentLocalePrefix(); + const { isBlogPostPage, frontMatter } = useBlogPost(); + const { allConnectors, allSdks } = useCategorizedTutorialMetadata(); + + const isGeneratedTutorial = frontMatter.slug?.startsWith('how-to-build-'); + const isTutorialListView = isGeneratedTutorial && !isBlogPostPage; + + const blogSdkName = condString('sdk' in frontMatter && frontMatter.sdk); + const blogConnectorName = condString('connector' in frontMatter && frontMatter.connector); + + const connectorMetadata = allConnectors.find( + (data) => getConnectorDisplayName(data) === blogConnectorName + ); + const sdkMetadata = allSdks.find((data) => getSdkDisplayName(data) === blogSdkName); + + const sdkLogos = getLogoFilenames(locale, sdkMetadata); + const connectorLogos = getLogoFilenames(locale, connectorMetadata); + return ( + {isTutorialListView && ( + <> +
+ { + // eslint-disable-next-line @silverhand/fp/no-mutation + currentTarget.src = connectorLogos.fallbackLogoFilename; + }} + /> +
+
+ { + // eslint-disable-next-line @silverhand/fp/no-mutation + currentTarget.src = sdkLogos.fallbackLogoFilename; + }} + /> +
+ + )} {children} diff --git a/src/theme/BlogPostItems/index.module.scss b/src/theme/BlogPostItems/index.module.scss new file mode 100644 index 00000000000..28d87bd27e2 --- /dev/null +++ b/src/theme/BlogPostItems/index.module.scss @@ -0,0 +1,3 @@ +.link:hover { + text-decoration: none; +} diff --git a/src/theme/BlogPostItems/index.tsx b/src/theme/BlogPostItems/index.tsx index be7e4f884e7..9cec965cbe5 100644 --- a/src/theme/BlogPostItems/index.tsx +++ b/src/theme/BlogPostItems/index.tsx @@ -3,6 +3,8 @@ import { BlogPostProvider } from '@docusaurus/plugin-content-blog/client'; import BlogPostItem from '@theme/BlogPostItem'; import type { Props } from '@theme/BlogPostItems'; +import styles from './index.module.scss'; + export default function BlogPostItems({ items, component: BlogPostItemComponent = BlogPostItem, @@ -11,7 +13,7 @@ export default function BlogPostItems({ <> {items.map(({ content: BlogPostContent }) => ( - +