Skip to content

feat: add sticky TOC to post layout #1141

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion app/blog/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { Authors, Blog } from 'contentlayer/generated'
import PostSimple from '@/layouts/PostSimple'
import PostLayout from '@/layouts/PostLayout'
import PostBanner from '@/layouts/PostBanner'
import PostwithTocLayout from '@/layouts/PostwithTocLayout'
import { Metadata } from 'next'
import siteMetadata from '@/data/siteMetadata'
import { notFound } from 'next/navigation'
Expand All @@ -19,6 +20,7 @@ const layouts = {
PostSimple,
PostLayout,
PostBanner,
PostwithTocLayout,
}

export async function generateMetadata(props: {
Expand Down Expand Up @@ -112,7 +114,13 @@ export default async function Page(props: { params: Promise<{ slug: string[] }>
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<Layout content={mainContent} authorDetails={authorDetails} next={next} prev={prev}>
<Layout
content={mainContent}
authorDetails={authorDetails}
next={next}
prev={prev}
toc={post.toc}
>
<MDXLayoutRenderer code={post.body.code} components={components} toc={post.toc} />
</Layout>
</>
Expand Down
4 changes: 4 additions & 0 deletions css/tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,7 @@ input:-webkit-autofill:focus {
display: inline-block;
vertical-align: middle;
}

.toc-container a {
@apply text-primary-500 hover:text-primary-600 dark:hover:text-primary-400;
}
3 changes: 1 addition & 2 deletions data/blog/release-of-tailwind-nextjs-starter-blog-v2.0.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ tags: ['next-js', 'tailwind', 'guide', 'feature']
draft: false
summary: 'Release of Tailwind Nextjs Starter Blog template v2.0, refactored with Nextjs App directory and React Server Components setup.Discover the new features and how to migrate from V1.'
images: ['/static/images/twitter-card.png']
layout: PostwithTocLayout
---

## Introduction

Welcome to the release of Tailwind Nextjs Starter Blog template v2.0. This release is a major refactor of the codebase to support Nextjs App directory and React Server Components. Read on to discover the new features and how to migrate from V1.

<TOCInline toc={props.toc} exclude="Introduction" />

## V1 to V2

![Github Traffic](/static/images/github-traffic.png)
Expand Down
190 changes: 190 additions & 0 deletions layouts/PostwithTocLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { ReactNode } from 'react'
import { CoreContent } from 'pliny/utils/contentlayer'
import type { Blog, Authors } from 'contentlayer/generated'
import Comments from '@/components/Comments'
import Link from '@/components/Link'
import PageTitle from '@/components/PageTitle'
import SectionContainer from '@/components/SectionContainer'
import Image from '@/components/Image'
import Tag from '@/components/Tag'
import siteMetadata from '@/data/siteMetadata'
import ScrollTopAndComment from '@/components/ScrollTopAndComment'
import TOCInline from 'pliny/ui/TOCInline'

const editUrl = (path) => `${siteMetadata.siteRepo}/blob/main/data/${path}`
const discussUrl = (path) =>
`https://mobile.twitter.com/search?q=${encodeURIComponent(`${siteMetadata.siteUrl}/${path}`)}`

const postDateTemplate: Intl.DateTimeFormatOptions = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
}

interface TOCItem {
value: string
url: string
depth: number
}

interface LayoutProps {
content: CoreContent<Blog>
authorDetails: CoreContent<Authors>[]
next?: { path: string; title: string }
prev?: { path: string; title: string }
children: ReactNode
toc: TOCItem[]
}

export default function PostwithTocLayout({
content,
authorDetails,
next,
prev,
children,
toc,
}: LayoutProps) {
const { filePath, path, slug, date, title, tags } = content
const basePath = path.split('/')[0]
return (
<SectionContainer>
<ScrollTopAndComment />
<article>
<div className="xl:divide-y xl:divide-gray-200 xl:dark:divide-gray-700">
<header className="pt-6 xl:pb-6">
<div className="space-y-1 text-center">
<dl className="space-y-10">
<div>
<dt className="sr-only">Published on</dt>
<dd className="text-base leading-6 font-medium text-gray-500 dark:text-gray-400">
<time dateTime={date}>
{new Date(date).toLocaleDateString(siteMetadata.locale, postDateTemplate)}
</time>
</dd>
</div>
</dl>
<div>
<PageTitle>{title}</PageTitle>
</div>
</div>
</header>
<div className="grid-rows-[auto_1fr] divide-y divide-gray-200 pb-8 xl:grid xl:grid-cols-4 xl:gap-x-6 xl:divide-y-0 dark:divide-gray-700">
<dl className="pt-6 pb-10 xl:border-b xl:border-gray-200 xl:pt-11 xl:dark:border-gray-700">
<dt className="sr-only">Authors</dt>
<dd>
<ul className="flex flex-wrap justify-center gap-4 sm:space-x-12 xl:block xl:space-y-8 xl:space-x-0">
{authorDetails.map((author) => (
<li className="flex items-center space-x-2" key={author.name}>
{author.avatar && (
<Image
src={author.avatar}
width={38}
height={38}
alt="avatar"
className="h-10 w-10 rounded-full"
/>
)}
<dl className="text-sm leading-5 font-medium whitespace-nowrap">
<dt className="sr-only">Name</dt>
<dd className="text-gray-900 dark:text-gray-100">{author.name}</dd>
<dt className="sr-only">Twitter</dt>
<dd>
{author.twitter && (
<Link
href={author.twitter}
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
>
{author.twitter
.replace('https://twitter.com/', '@')
.replace('https://x.com/', '@')}
</Link>
)}
</dd>
</dl>
</li>
))}
</ul>
</dd>
</dl>
<div className="divide-y divide-gray-200 xl:col-span-3 xl:row-span-2 xl:pb-0 dark:divide-gray-700">
<div className="prose dark:prose-invert max-w-none pt-10 pb-8">{children}</div>
<div className="pt-6 pb-6 text-sm text-gray-700 dark:text-gray-300">
<Link href={discussUrl(path)} rel="nofollow">
Discuss on Twitter
</Link>
{` • `}
<Link href={editUrl(filePath)}>View on GitHub</Link>
</div>
{siteMetadata.comments && (
<div
className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300"
id="comment"
>
<Comments slug={slug} />
</div>
)}
</div>
<footer>
<div className="divide-gray-200 text-sm leading-5 font-medium xl:col-start-1 xl:row-start-2 xl:divide-y dark:divide-gray-700">
{tags && (
<div className="py-4 xl:py-8">
<h2 className="text-xs tracking-wide text-gray-500 uppercase dark:text-gray-400">
Tags
</h2>
<div className="flex flex-wrap">
{tags.map((tag) => (
<Tag key={tag} text={tag} />
))}
</div>
</div>
)}
{(next || prev) && (
<div className="flex justify-between py-4 xl:block xl:space-y-8 xl:py-8">
{prev && prev.path && (
<div>
<h2 className="text-xs tracking-wide text-gray-500 uppercase dark:text-gray-400">
Previous Article
</h2>
<div className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
<Link href={`/${prev.path}`}>{prev.title}</Link>
</div>
</div>
)}
{next && next.path && (
<div>
<h2 className="text-xs tracking-wide text-gray-500 uppercase dark:text-gray-400">
Next Article
</h2>
<div className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
<Link href={`/${next.path}`}>{next.title}</Link>
</div>
</div>
)}
</div>
)}
<div className="py-4 xl:py-8">
<h2 className="text-xs tracking-wide text-gray-500 uppercase dark:text-gray-400">
TABLE OF CONTENTS
</h2>
<div className="toc-container mt-3">
<TOCInline toc={toc} fromHeading={2} toHeading={3} collapse={true} />
</div>
</div>
</div>
<div className="pt-4 xl:pt-8">
<Link
href={`/${basePath}`}
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
aria-label="Back to the blog"
>
&larr; Back to the blog
</Link>
</div>
</footer>
</div>
</div>
</article>
</SectionContainer>
)
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"scripts": {
"start": "next dev",
"format": "prettier --write .",
"dev": "cross-env INIT_CWD=$PWD next dev",
"build": "cross-env INIT_CWD=$PWD next build && cross-env NODE_OPTIONS='--experimental-json-modules' node ./scripts/postbuild.mjs",
"serve": "next start",
Expand Down