Skip to content

Commit

Permalink
add nextraConfig.unstable_readingTime option for blog theme (#684)
Browse files Browse the repository at this point in the history
  • Loading branch information
dimaMachina committed Aug 29, 2022
1 parent 71528f1 commit 64ae4b5
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 97 deletions.
6 changes: 6 additions & 0 deletions .changeset/purple-melons-begin.md
@@ -0,0 +1,6 @@
---
'nextra': patch
'nextra-theme-blog': patch
---

add `nextraConfig.unstable_readingTime` option for blog theme
3 changes: 2 additions & 1 deletion examples/blog/next.config.mjs
Expand Up @@ -4,7 +4,8 @@ const withNextra = nextra({
theme: 'nextra-theme-blog',
themeConfig: './theme.config.jsx',
unstable_staticImage: true,
unstable_defaultShowCopyCode: true
unstable_defaultShowCopyCode: true,
unstable_readingTime: true
})

export default withNextra({
Expand Down
2 changes: 1 addition & 1 deletion examples/blog/pages/posts/code-blocks.mdx
Expand Up @@ -2,7 +2,7 @@
title: Code blocks
date: 2022/7/29
description: En example of using code blocks in your blog.
tag: web development
tag: web development,JavaScript,GraphQL,C++,Java,React,Next.js,The Guild,MacBook Pro
author: Dimitri POSTOLOV
---

Expand Down
6 changes: 2 additions & 4 deletions packages/nextra-theme-blog/src/article-layout.tsx
Expand Up @@ -3,14 +3,12 @@ import Meta from './meta'
import MDXTheme from './mdx-theme'
import { useBlogContext } from './blog-context'
import { BasicLayout } from './basic-layout'
import { getParent } from './utils/parent'

export const ArticleLayout = ({ children }: { children: ReactNode }) => {
const { opts, config } = useBlogContext()
const { back } = getParent({ opts, config })
const { config } = useBlogContext()
return (
<BasicLayout>
<Meta {...opts.frontMatter} back={back} config={config} />
<Meta />
<MDXTheme>
{children}
{config.postFooter}
Expand Down
34 changes: 11 additions & 23 deletions packages/nextra-theme-blog/src/mdx-theme.tsx
@@ -1,4 +1,6 @@
import React, {
RefObject,
createRef,
ComponentProps,
createContext,
ReactElement,
Expand All @@ -9,34 +11,26 @@ import React, {
} from 'react'
import { MDXProvider } from '@mdx-js/react'
import Link from 'next/link'
import ReactDOM from 'react-dom'
import { createPortal } from 'react-dom'
import { Code, Pre, Td, Th, Tr } from 'nextra/components'
import { useBlogContext } from './blog-context'

export const HeadingContext = createContext<
React.RefObject<HTMLHeadingElement | null>
>(React.createRef())

const useHeadingRef = () => {
const ref = useContext(HeadingContext)
return ref
}
RefObject<HTMLHeadingElement | null>
>(createRef())

const H1 = ({ children }: { children?: ReactNode }): ReactElement => {
const ref = useHeadingRef()
const {
opts: { hasJsxInH1 }
} = useBlogContext()
const ref = useContext(HeadingContext)
const { opts } = useBlogContext()
const [showHeading, setShowHeading] = useState(false)
useEffect(() => {
if (ref.current && hasJsxInH1) {
if (ref.current && opts.hasJsxInH1) {
setShowHeading(true)
}
}, [])
return (
<>{showHeading ? ReactDOM.createPortal(children, ref.current!) : null}</>
)
return <>{showHeading && createPortal(children, ref.current!)}</>
}

const createHeaderLink =
(Tag: `h${2 | 3 | 4 | 5 | 6}`) =>
({ children, id, ...props }: ComponentProps<'h2'>): ReactElement => {
Expand All @@ -48,13 +42,7 @@ const createHeaderLink =
)
}

const A = ({
children,
...props
}: {
children?: React.ReactNode
href?: string
}) => {
const A = ({ children, ...props }: ComponentProps<'a'>) => {
const isExternal = props.href && props.href.startsWith('https://')
if (isExternal) {
return (
Expand Down
111 changes: 58 additions & 53 deletions packages/nextra-theme-blog/src/meta.tsx
@@ -1,67 +1,72 @@
import React, { ReactElement } from 'react'
import Link from 'next/link'
import ThemeSwitch from './theme-switch'
import type { NextraBlogTheme } from './types'
import { split } from './utils/get-tags'
import { useBlogContext } from './blog-context'
import { getParent } from './utils/parent'

interface MetaProps {
author?: string
date?: string
tag?: string | string[]
back?: string | null
config: NextraBlogTheme
}

export default function Meta({
author,
date,
tag,
back,
config
}: MetaProps): ReactElement {
export default function Meta(): ReactElement {
const { opts, config } = useBlogContext()
const { author, date, tag } = opts.frontMatter
const { back } = getParent({ opts, config })
const tags = tag ? split(tag) : []

const tagsEl = tags.map(t => (
<Link key={t} href="/tags/[tag]" as={`/tags/${t}`}>
<a
className="
select-none
rounded-md
px-1
transition-colors
text-sm
text-gray-400
hover:text-gray-500
dark:text-gray-300
dark:hover:text-gray-200
bg-gray-200
hover:bg-gray-300
dark:bg-gray-600
dark:hover:bg-gray-700
"
>
{t}
</a>
</Link>
))

const readingTime = opts.readingTime?.text

return (
<div className="mb-8 flex items-center gap-3">
<div className="flex flex-1 flex-wrap items-center gap-1 text-gray-400 not-prose">
{author}
{author && date && ','}
{date && (
<time dateTime={new Date(date).toISOString()}>
{new Date(date).toDateString()}
</time>
<div
className={
'mb-8 flex gap-3 ' + (readingTime ? 'items-start' : 'items-center')
}
>
<div className="grow text-gray-400">
<div className="flex flex-wrap items-center gap-1 not-prose">
{author}
{author && date && ','}
{date && (
<time dateTime={new Date(date).toISOString()}>
{new Date(date).toDateString()}
</time>
)}
{(author || date) && tags.length > 0 && ' • '}
{readingTime || tagsEl}
</div>
{readingTime && (
<div className="flex flex-wrap items-center gap-1 mt-1 not-prose">{tagsEl}</div>
)}
{(author || date) && tags.length > 0 && '•'}
{tags.map(t => (
<Link key={t} href="/tags/[tag]" as={`/tags/${t}`}>
<a
className="
select-none
rounded-md
px-1
transition-colors
text-sm
text-gray-400
hover:text-gray-500
dark:text-gray-300
dark:hover:text-gray-200
bg-gray-200
hover:bg-gray-300
dark:bg-gray-600
dark:hover:bg-gray-700
"
>
{t}
</a>
</div>
<div className="flex items-center gap-3">
{back && (
<Link href={back} passHref>
<a>Back</a>
</Link>
))}
)}
{config.darkMode && <ThemeSwitch />}
</div>
{back && (
<Link href={back} passHref>
<a>Back</a>
</Link>
)}
{config.darkMode && <ThemeSwitch />}
</div>
)
}
1 change: 1 addition & 0 deletions packages/nextra/package.json
Expand Up @@ -90,6 +90,7 @@
"rehype-mdx-title": "^1.0.0",
"rehype-pretty-code": "0.2.4",
"remark-gfm": "^3.0.1",
"remark-reading-time": "^2.0.1",
"shiki": "0.10.1",
"slash": "^3.0.0"
},
Expand Down
13 changes: 9 additions & 4 deletions packages/nextra/src/compile.ts
Expand Up @@ -3,9 +3,10 @@ import { Processor } from '@mdx-js/mdx/lib/core'
import remarkGfm from 'remark-gfm'
import rehypePrettyCode from 'rehype-pretty-code'
import { rehypeMdxTitle } from 'rehype-mdx-title'
import readingTime from 'remark-reading-time'
import { remarkStaticImage } from './mdx-plugins/static-image'
import { remarkHeadings } from './mdx-plugins/remark'
import { LoaderOptions, PageOpts } from './types'
import { LoaderOptions, PageOpts, ReadingTime } from './types'
import structurize from './mdx-plugins/structurize'
import { parseMeta, attachMeta } from './mdx-plugins/rehype-handler'
import theme from './theme.json'
Expand Down Expand Up @@ -45,6 +46,7 @@ export async function compileMdx(
| 'unstable_staticImage'
| 'unstable_flexsearch'
| 'unstable_defaultShowCopyCode'
| 'unstable_readingTime'
> = {},
filePath = ''
) {
Expand All @@ -59,7 +61,8 @@ export async function compileMdx(
remarkHeadings,
nextraOptions.unstable_staticImage && remarkStaticImage,
nextraOptions.unstable_flexsearch &&
structurize(structurizedData, nextraOptions.unstable_flexsearch)
structurize(structurizedData, nextraOptions.unstable_flexsearch),
nextraOptions.unstable_readingTime && readingTime
].filter(truthy),
rehypePlugins: [
...(mdxOptions.rehypePlugins || []),
Expand All @@ -76,16 +79,18 @@ export async function compileMdx(
]
})
try {
const result = String(await compiler.process(source))
const vFile = await compiler.process(source)
const result = String(vFile)
.replace('export const __nextra_title__', 'const __nextra_title__')
.replace('export default MDXContent;', '')

const readingTime = vFile.data.readingTime as ReadingTime | undefined
return {
result,
...(compiler.data('headingMeta') as Pick<
PageOpts,
'headings' | 'hasJsxInH1'
>),
...(readingTime && { readingTime }),
structurizedData
}
} catch (err) {
Expand Down
26 changes: 15 additions & 11 deletions packages/nextra/src/loader.ts
Expand Up @@ -66,6 +66,7 @@ async function loader(
unstable_defaultShowCopyCode,
unstable_flexsearch,
unstable_staticImage,
unstable_readingTime,
mdxOptions,
pageMapCache,
newNextLinkBehavior
Expand Down Expand Up @@ -111,16 +112,18 @@ async function loader(
// Extract frontMatter information if it exists
const { data: frontMatter, content } = grayMatter(source)

const { result, headings, structurizedData, hasJsxInH1 } = await compileMdx(
content,
mdxOptions,
{
unstable_defaultShowCopyCode,
unstable_staticImage,
unstable_flexsearch
},
mdxPath
)
const { result, headings, structurizedData, hasJsxInH1, readingTime } =
await compileMdx(
content,
mdxOptions,
{
unstable_readingTime,
unstable_defaultShowCopyCode,
unstable_staticImage,
unstable_flexsearch
},
mdxPath
)
// @ts-expect-error
const cssImport = OFFICIAL_THEMES.includes(theme)
? `import '${theme}/style.css'`
Expand Down Expand Up @@ -184,7 +187,8 @@ export default MDXContent`.trimStart()
hasJsxInH1,
timestamp,
unstable_flexsearch,
newNextLinkBehavior
newNextLinkBehavior,
readingTime
}

const pageNextRoute =
Expand Down
9 changes: 9 additions & 0 deletions packages/nextra/src/types.ts
Expand Up @@ -71,6 +71,14 @@ export type PageOpts = {
timestamp?: number
unstable_flexsearch?: Flexsearch
newNextLinkBehavior?: boolean
readingTime?: ReadingTime
}

export type ReadingTime = {
text: string
minutes: number
time: number
words: number
}

type Theme = string
Expand All @@ -82,6 +90,7 @@ export type NextraConfig = {
unstable_defaultShowCopyCode?: boolean
unstable_flexsearch?: Flexsearch
unstable_staticImage?: boolean
unstable_readingTime?: boolean
mdxOptions?: Pick<ProcessorOptions, 'rehypePlugins' | 'remarkPlugins'> & {
rehypePrettyCodeOptions?: Partial<RehypePrettyCodeOptions>
}
Expand Down

0 comments on commit 64ae4b5

Please sign in to comment.