refactor: 改用 TypeScript。close #1.
This commit is contained in:
31
pages/404.js
31
pages/404.js
@ -1,31 +0,0 @@
|
||||
import Link from '@/components/Link'
|
||||
import { PageSEO } from '@/components/SEO'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
|
||||
export default function FourZeroFour() {
|
||||
return (
|
||||
<>
|
||||
<PageSEO title={`Page Not Found - ${siteMetadata.title}`} />
|
||||
<div className="flex flex-col items-start justify-start md:mt-24 md:flex-row md:items-center md:justify-center md:space-x-6">
|
||||
<div className="space-x-2 pt-6 pb-8 md:space-y-5">
|
||||
<h1 className="text-6xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 md:border-r-2 md:px-6 md:text-8xl md:leading-14">
|
||||
404
|
||||
</h1>
|
||||
</div>
|
||||
<div className="max-w-md">
|
||||
<p className="mb-4 text-xl font-bold leading-normal md:text-2xl">
|
||||
Sorry we couldn't find this page.
|
||||
</p>
|
||||
<p className="mb-8">
|
||||
But don't worry, you can find plenty of other things on our homepage.
|
||||
</p>
|
||||
<Link href="/">
|
||||
<button className="focus:shadow-outline-blue inline rounded-lg border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium leading-5 text-white shadow transition-colors duration-150 hover:bg-blue-700 focus:outline-none dark:hover:bg-blue-500">
|
||||
Back to homepage
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
24
pages/404.tsx
Normal file
24
pages/404.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import Link from '@/components/Link'
|
||||
|
||||
export default function FourZeroFour() {
|
||||
return (
|
||||
<div className="flex flex-col items-start justify-start md:mt-24 md:flex-row md:items-center md:justify-center md:space-x-6">
|
||||
<div className="space-x-2 pt-6 pb-8 md:space-y-5">
|
||||
<h1 className="text-6xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 md:border-r-2 md:px-6 md:text-8xl md:leading-14">
|
||||
404
|
||||
</h1>
|
||||
</div>
|
||||
<div className="max-w-md">
|
||||
<p className="mb-4 text-xl font-bold leading-normal md:text-2xl">
|
||||
Sorry we couldn't find this page.
|
||||
</p>
|
||||
<p className="mb-8">But dont worry, you can find plenty of other things on our homepage.</p>
|
||||
<Link href="/">
|
||||
<button className="focus:shadow-outline-blue inline rounded-lg border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium leading-5 text-white shadow transition-colors duration-150 hover:bg-blue-700 focus:outline-none dark:hover:bg-blue-500">
|
||||
Back to homepage
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -5,6 +5,7 @@ import 'katex/dist/katex.css'
|
||||
import '@fontsource/inter/variable-full.css'
|
||||
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import type { AppProps } from 'next/app'
|
||||
import Head from 'next/head'
|
||||
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
@ -15,7 +16,7 @@ import { ClientReload } from '@/components/ClientReload'
|
||||
const isDevelopment = process.env.NODE_ENV === 'development'
|
||||
const isSocket = process.env.SOCKET
|
||||
|
||||
export default function App({ Component, pageProps }) {
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<ThemeProvider attribute="class" defaultTheme={siteMetadata.theme}>
|
||||
<Head>
|
@ -1,4 +1,5 @@
|
||||
import Document, { Html, Head, Main, NextScript } from 'next/document'
|
||||
|
||||
class MyDocument extends Document {
|
||||
render() {
|
||||
return (
|
||||
@ -20,8 +21,7 @@ class MyDocument extends Document {
|
||||
<link rel="manifest" href="/static/favicons/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/static/favicons/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<meta name="msapplication-TileColor" content="#000000" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fff" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />
|
||||
</Head>
|
||||
<body className="bg-white text-black antialiased dark:bg-gray-900 dark:text-white">
|
@ -1,21 +0,0 @@
|
||||
import { MDXLayoutRenderer } from '@/components/MDXComponents'
|
||||
import { getFileBySlug } from '@/lib/mdx'
|
||||
|
||||
const DEFAULT_LAYOUT = 'AuthorLayout'
|
||||
|
||||
export async function getStaticProps() {
|
||||
const authorDetails = await getFileBySlug('authors', ['default'])
|
||||
return { props: { authorDetails } }
|
||||
}
|
||||
|
||||
export default function About({ authorDetails }) {
|
||||
const { mdxSource, frontMatter } = authorDetails
|
||||
|
||||
return (
|
||||
<MDXLayoutRenderer
|
||||
layout={frontMatter.layout || DEFAULT_LAYOUT}
|
||||
mdxSource={mdxSource}
|
||||
frontMatter={frontMatter}
|
||||
/>
|
||||
)
|
||||
}
|
27
pages/about.tsx
Normal file
27
pages/about.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { MDXLayoutRenderer } from '@/components/MDXComponents'
|
||||
import { getFileBySlug } from '@/lib/mdx'
|
||||
import { GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
|
||||
|
||||
const DEFAULT_LAYOUT = 'AuthorLayout'
|
||||
|
||||
// @ts-ignore
|
||||
export const getStaticProps: GetStaticProps<{
|
||||
authorDetails: { mdxSource: string; frontMatter: AuthorFrontMatter }
|
||||
}> = async () => {
|
||||
const authorDetails = await getFileBySlug<AuthorFrontMatter>('authors', ['default'])
|
||||
const { mdxSource, frontMatter } = authorDetails
|
||||
return { props: { authorDetails: { mdxSource, frontMatter } } }
|
||||
}
|
||||
|
||||
export default function About({ authorDetails }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
const { mdxSource, frontMatter } = authorDetails
|
||||
|
||||
return (
|
||||
<MDXLayoutRenderer
|
||||
layout={frontMatter.layout || DEFAULT_LAYOUT}
|
||||
mdxSource={mdxSource}
|
||||
frontMatter={frontMatter}
|
||||
/>
|
||||
)
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default async (req, res) => {
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { email } = req.body
|
||||
if (!email) {
|
||||
return res.status(400).json({ error: 'Email is required' })
|
@ -1,5 +1,7 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
/* eslint-disable import/no-anonymous-default-export */
|
||||
export default async (req, res) => {
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { email } = req.body
|
||||
|
||||
if (!email) {
|
@ -1,33 +0,0 @@
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default async (req, res) => {
|
||||
const { email } = req.body
|
||||
if (!email) {
|
||||
return res.status(400).json({ error: 'Email is required' })
|
||||
}
|
||||
|
||||
try {
|
||||
const API_URL = process.env.EMAILOCTOPUS_API_URL
|
||||
const API_KEY = process.env.EMAILOCTOPUS_API_KEY
|
||||
const LIST_ID = process.env.EMAILOCTOPUS_LIST_ID
|
||||
|
||||
const data = { email_address: email, api_key: API_KEY }
|
||||
|
||||
const API_ROUTE = `${API_URL}lists/${LIST_ID}/contacts`
|
||||
|
||||
const response = await fetch(API_ROUTE, {
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (response.status >= 400) {
|
||||
return res.status(500).json({ error: `There was an error subscribing to the list.` })
|
||||
}
|
||||
|
||||
return res.status(201).json({ error: '' })
|
||||
} catch (error) {
|
||||
return res.status(500).json({ error: error.message || error.toString() })
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
/* eslint-disable import/no-anonymous-default-export */
|
||||
export default async (req, res) => {
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { email } = req.body
|
||||
if (!email) {
|
||||
return res.status(400).json({ error: 'Email is required' })
|
@ -1,3 +1,4 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import mailchimp from '@mailchimp/mailchimp_marketing'
|
||||
|
||||
mailchimp.setConfig({
|
||||
@ -6,7 +7,7 @@ mailchimp.setConfig({
|
||||
})
|
||||
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default async (req, res) => {
|
||||
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { email } = req.body
|
||||
|
||||
if (!email) {
|
||||
@ -14,7 +15,7 @@ export default async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const test = await mailchimp.lists.addListMember(process.env.MAILCHIMP_AUDIENCE_ID, {
|
||||
await mailchimp.lists.addListMember(process.env.MAILCHIMP_AUDIENCE_ID, {
|
||||
email_address: email,
|
||||
status: 'subscribed',
|
||||
})
|
@ -1,30 +0,0 @@
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default async (req, res) => {
|
||||
const { email } = req.body
|
||||
|
||||
if (!email) {
|
||||
return res.status(400).json({ error: 'Email is required' })
|
||||
}
|
||||
|
||||
try {
|
||||
const API_KEY = process.env.REVUE_API_KEY
|
||||
const revueRoute = `${process.env.REVUE_API_URL}subscribers`
|
||||
|
||||
const response = await fetch(revueRoute, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Token ${API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email, double_opt_in: false }),
|
||||
})
|
||||
|
||||
if (response.status >= 400) {
|
||||
return res.status(500).json({ error: `There was an error subscribing to the list.` })
|
||||
}
|
||||
|
||||
return res.status(201).json({ error: '' })
|
||||
} catch (error) {
|
||||
return res.status(500).json({ error: error.message || error.toString() })
|
||||
}
|
||||
}
|
@ -2,10 +2,16 @@ import { getAllFilesFrontMatter } from '@/lib/mdx'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import ListLayout from '@/layouts/ListLayout'
|
||||
import { PageSEO } from '@/components/SEO'
|
||||
import { GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||
import { ComponentProps } from 'react'
|
||||
|
||||
export const POSTS_PER_PAGE = 5
|
||||
|
||||
export async function getStaticProps() {
|
||||
export const getStaticProps: GetStaticProps<{
|
||||
posts: ComponentProps<typeof ListLayout>['posts']
|
||||
initialDisplayPosts: ComponentProps<typeof ListLayout>['initialDisplayPosts']
|
||||
pagination: ComponentProps<typeof ListLayout>['pagination']
|
||||
}> = async () => {
|
||||
const posts = await getAllFilesFrontMatter('blog')
|
||||
const initialDisplayPosts = posts.slice(0, POSTS_PER_PAGE)
|
||||
const pagination = {
|
||||
@ -16,7 +22,11 @@ export async function getStaticProps() {
|
||||
return { props: { initialDisplayPosts, posts, pagination } }
|
||||
}
|
||||
|
||||
export default function Blog({ posts, initialDisplayPosts, pagination }) {
|
||||
export default function Blog({
|
||||
posts,
|
||||
initialDisplayPosts,
|
||||
pagination,
|
||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
return (
|
||||
<>
|
||||
<PageSEO title={`Blog - ${siteMetadata.author}`} description={siteMetadata.description} />
|
@ -3,6 +3,10 @@ import PageTitle from '@/components/PageTitle'
|
||||
import generateRss from '@/lib/generate-rss'
|
||||
import { MDXLayoutRenderer } from '@/components/MDXComponents'
|
||||
import { formatSlug, getAllFilesFrontMatter, getFileBySlug, getFiles } from '@/lib/mdx'
|
||||
import { GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||
import { AuthorFrontMatter } from 'types/AuthorFrontMatter'
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||
import { Toc } from 'types/Toc'
|
||||
|
||||
const DEFAULT_LAYOUT = 'PostLayout'
|
||||
|
||||
@ -18,15 +22,23 @@ export async function getStaticPaths() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStaticProps({ params }) {
|
||||
// @ts-ignore
|
||||
export const getStaticProps: GetStaticProps<{
|
||||
post: { mdxSource: string; toc: Toc; frontMatter: PostFrontMatter }
|
||||
authorDetails: AuthorFrontMatter[]
|
||||
prev?: { slug: string; title: string }
|
||||
next?: { slug: string; title: string }
|
||||
}> = async ({ params }) => {
|
||||
const slug = (params.slug as string[]).join('/')
|
||||
const allPosts = await getAllFilesFrontMatter('blog')
|
||||
const postIndex = allPosts.findIndex((post) => formatSlug(post.slug) === params.slug.join('/'))
|
||||
const prev = allPosts[postIndex + 1] || null
|
||||
const next = allPosts[postIndex - 1] || null
|
||||
const post = await getFileBySlug('blog', params.slug.join('/'))
|
||||
const postIndex = allPosts.findIndex((post) => formatSlug(post.slug) === slug)
|
||||
const prev: { slug: string; title: string } = allPosts[postIndex + 1] || null
|
||||
const next: { slug: string; title: string } = allPosts[postIndex - 1] || null
|
||||
const post = await getFileBySlug<PostFrontMatter>('blog', slug)
|
||||
// @ts-ignore
|
||||
const authorList = post.frontMatter.authors || ['default']
|
||||
const authorPromise = authorList.map(async (author) => {
|
||||
const authorResults = await getFileBySlug('authors', [author])
|
||||
const authorResults = await getFileBySlug<AuthorFrontMatter>('authors', [author])
|
||||
return authorResults.frontMatter
|
||||
})
|
||||
const authorDetails = await Promise.all(authorPromise)
|
||||
@ -37,15 +49,27 @@ export async function getStaticProps({ params }) {
|
||||
fs.writeFileSync('./public/feed.xml', rss)
|
||||
}
|
||||
|
||||
return { props: { post, authorDetails, prev, next } }
|
||||
return {
|
||||
props: {
|
||||
post,
|
||||
authorDetails,
|
||||
prev,
|
||||
next,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function Blog({ post, authorDetails, prev, next }) {
|
||||
export default function Blog({
|
||||
post,
|
||||
authorDetails,
|
||||
prev,
|
||||
next,
|
||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
const { mdxSource, toc, frontMatter } = post
|
||||
|
||||
return (
|
||||
<>
|
||||
{frontMatter.draft !== true ? (
|
||||
{'draft' in frontMatter && frontMatter.draft !== true ? (
|
||||
<MDXLayoutRenderer
|
||||
layout={frontMatter.layout || DEFAULT_LAYOUT}
|
||||
toc={toc}
|
@ -3,8 +3,10 @@ import siteMetadata from '@/data/siteMetadata'
|
||||
import { getAllFilesFrontMatter } from '@/lib/mdx'
|
||||
import ListLayout from '@/layouts/ListLayout'
|
||||
import { POSTS_PER_PAGE } from '../../blog'
|
||||
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||
|
||||
export async function getStaticPaths() {
|
||||
export const getStaticPaths: GetStaticPaths<{ page: string }> = async () => {
|
||||
const totalPosts = await getAllFilesFrontMatter('blog')
|
||||
const totalPages = Math.ceil(totalPosts.length / POSTS_PER_PAGE)
|
||||
const paths = Array.from({ length: totalPages }, (_, i) => ({
|
||||
@ -17,12 +19,16 @@ export async function getStaticPaths() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStaticProps(context) {
|
||||
export const getStaticProps: GetStaticProps<{
|
||||
posts: PostFrontMatter[]
|
||||
initialDisplayPosts: PostFrontMatter[]
|
||||
pagination: { currentPage: number; totalPages: number }
|
||||
}> = async (context) => {
|
||||
const {
|
||||
params: { page },
|
||||
} = context
|
||||
const posts = await getAllFilesFrontMatter('blog')
|
||||
const pageNumber = parseInt(page)
|
||||
const pageNumber = parseInt(page as string)
|
||||
const initialDisplayPosts = posts.slice(
|
||||
POSTS_PER_PAGE * (pageNumber - 1),
|
||||
POSTS_PER_PAGE * pageNumber
|
||||
@ -41,7 +47,11 @@ export async function getStaticProps(context) {
|
||||
}
|
||||
}
|
||||
|
||||
export default function PostPage({ posts, initialDisplayPosts, pagination }) {
|
||||
export default function PostPage({
|
||||
posts,
|
||||
initialDisplayPosts,
|
||||
pagination,
|
||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
return (
|
||||
<>
|
||||
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />
|
@ -4,25 +4,26 @@ import Tag from '@/components/Tag'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { getAllFilesFrontMatter } from '@/lib/mdx'
|
||||
import formatDate from '@/lib/utils/formatDate'
|
||||
|
||||
import { GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||
import NewsletterForm from '@/components/NewsletterForm'
|
||||
|
||||
const MAX_DISPLAY = 5
|
||||
|
||||
export async function getStaticProps() {
|
||||
export const getStaticProps: GetStaticProps<{ posts: PostFrontMatter[] }> = async () => {
|
||||
const posts = await getAllFilesFrontMatter('blog')
|
||||
|
||||
return { props: { posts } }
|
||||
}
|
||||
|
||||
export default function Home({ posts }) {
|
||||
export default function Home({ posts }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
return (
|
||||
<>
|
||||
<PageSEO title={siteMetadata.title} description={siteMetadata.description} />
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
|
||||
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14">
|
||||
Hi.
|
||||
Latest
|
||||
</h1>
|
||||
<p className="text-lg leading-7 text-gray-500 dark:text-gray-400">
|
||||
{siteMetadata.description}
|
@ -13,7 +13,7 @@ export default function Projects() {
|
||||
Projects
|
||||
</h1>
|
||||
<p className="text-lg leading-7 text-gray-500 dark:text-gray-400">
|
||||
我的项目橱窗,欢迎交流。
|
||||
Showcase your projects with a hero image (16 x 9)
|
||||
</p>
|
||||
</div>
|
||||
<div className="container py-12">
|
@ -4,14 +4,15 @@ import Tag from '@/components/Tag'
|
||||
import siteMetadata from '@/data/siteMetadata'
|
||||
import { getAllTags } from '@/lib/tags'
|
||||
import kebabCase from '@/lib/utils/kebabCase'
|
||||
import { GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||
|
||||
export async function getStaticProps() {
|
||||
export const getStaticProps: GetStaticProps<{ tags: Record<string, number> }> = async () => {
|
||||
const tags = await getAllTags('blog')
|
||||
|
||||
return { props: { tags } }
|
||||
}
|
||||
|
||||
export default function Tags({ tags }) {
|
||||
export default function Tags({ tags }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
const sortedTags = Object.keys(tags).sort((a, b) => tags[b] - tags[a])
|
||||
return (
|
||||
<>
|
@ -6,7 +6,9 @@ import { getAllFilesFrontMatter } from '@/lib/mdx'
|
||||
import { getAllTags } from '@/lib/tags'
|
||||
import kebabCase from '@/lib/utils/kebabCase'
|
||||
import fs from 'fs'
|
||||
import { GetStaticProps, InferGetStaticPropsType } from 'next'
|
||||
import path from 'path'
|
||||
import { PostFrontMatter } from 'types/PostFrontMatter'
|
||||
|
||||
const root = process.cwd()
|
||||
|
||||
@ -23,30 +25,33 @@ export async function getStaticPaths() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStaticProps({ params }) {
|
||||
export const getStaticProps: GetStaticProps<{ posts: PostFrontMatter[]; tag: string }> = async (
|
||||
context
|
||||
) => {
|
||||
const tag = context.params.tag as string
|
||||
const allPosts = await getAllFilesFrontMatter('blog')
|
||||
const filteredPosts = allPosts.filter(
|
||||
(post) => post.draft !== true && post.tags.map((t) => kebabCase(t)).includes(params.tag)
|
||||
(post) => post.draft !== true && post.tags.map((t) => kebabCase(t)).includes(tag)
|
||||
)
|
||||
|
||||
// rss
|
||||
if (filteredPosts.length > 0) {
|
||||
const rss = generateRss(filteredPosts, `tags/${params.tag}/feed.xml`)
|
||||
const rssPath = path.join(root, 'public', 'tags', params.tag)
|
||||
const rss = generateRss(filteredPosts, `tags/${tag}/feed.xml`)
|
||||
const rssPath = path.join(root, 'public', 'tags', tag)
|
||||
fs.mkdirSync(rssPath, { recursive: true })
|
||||
fs.writeFileSync(path.join(rssPath, 'feed.xml'), rss)
|
||||
}
|
||||
|
||||
return { props: { posts: filteredPosts, tag: params.tag } }
|
||||
return { props: { posts: filteredPosts, tag } }
|
||||
}
|
||||
|
||||
export default function Tag({ posts, tag }) {
|
||||
export default function Tag({ posts, tag }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
// Capitalize first letter and convert space to dash
|
||||
const title = tag[0].toUpperCase() + tag.split(' ').join('-').slice(1)
|
||||
return (
|
||||
<>
|
||||
<TagSEO
|
||||
title={`${tag} - ${siteMetadata.author}`}
|
||||
title={`${tag} - ${siteMetadata.title}`}
|
||||
description={`${tag} tags - ${siteMetadata.author}`}
|
||||
/>
|
||||
<ListLayout posts={posts} title={title} />
|
Reference in New Issue
Block a user