diff --git a/components/Button.tsx b/components/Button.tsx index fbafbc9..c134765 100644 --- a/components/Button.tsx +++ b/components/Button.tsx @@ -1,11 +1,17 @@ -import { FC } from 'react' -import React from 'react' +import { FC } from "react"; +import React from "react"; export const Button: FC<{ title: string }> = ({ title }) => (
alert('Hi')} + style={{ + padding: 10, + backgroundColor: "#333", + color: "#fff", + display: "inline-block", + borderRadius: 4, + }} + onClick={() => alert("Hi")} > {title}
-) +); diff --git a/components/Header.tsx b/components/Header.tsx index 4103996..f097b02 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -1,8 +1,14 @@ -import Link from 'next/link' +import Link from "next/link"; function Icon() { return ( - + - ) + ); } function Logo() { @@ -25,7 +31,7 @@ function Logo() { Contentlayer - ) + ); } export function Header() { @@ -33,5 +39,5 @@ export function Header() {
- ) + ); } diff --git a/components/Tag.tsx b/components/Tag.tsx new file mode 100644 index 0000000..7010fb6 --- /dev/null +++ b/components/Tag.tsx @@ -0,0 +1,9 @@ +import Link from "next/link"; + +export function Tag({ tag }: { tag: string }) { + return ( + + {tag} + + ); +} diff --git a/components/Tags.tsx b/components/Tags.tsx new file mode 100644 index 0000000..b283e2c --- /dev/null +++ b/components/Tags.tsx @@ -0,0 +1,11 @@ +import { Tag } from "./Tag"; + +export function Tags({ tags }: { tags: string[] }) { + return ( +
+ {tags.map((tag) => ( + + ))} +
+ ); +} diff --git a/contentlayer.config.ts b/contentlayer.config.ts index 3e9372f..2c4f6d8 100644 --- a/contentlayer.config.ts +++ b/contentlayer.config.ts @@ -14,6 +14,14 @@ const Post = defineDocumentType(() => ({ description: 'The date of the post', required: true, }, + tags: { + type: 'list', + of: { + type: 'string' + }, + description: 'Optional tags associated with the post', + default: [] + } }, computedFields: { url: { diff --git a/lib/content.ts b/lib/content.ts new file mode 100644 index 0000000..2190179 --- /dev/null +++ b/lib/content.ts @@ -0,0 +1,44 @@ +import { allPosts, Post } from "contentlayer/generated"; +import { unique } from "typescript-array-utils"; +import { compareDesc, format, parseISO } from "date-fns"; + +// All tags used by blog posts +const allTags: string[] = unique(allPosts.flatMap((post) => post.tags)); + +// All tags except "this" one +const allOtherTags = (tag: string): string[] => + allTags.filter((t) => !t.includes(tag)); + +// Fetch post by slug (use only when all slugs are known in advance) +const getPostBySlug = (slug: string): Post => + allPosts.find((post) => post._raw.flattenedPath === slug); + +// All posts marked with a specific tag +const postsWithTag = (tag: string): Post[] | undefined => + allPosts.filter((post) => post.tags.includes(tag)); + +// Paths for all posts +const allPostPaths: string[] = allPosts.map((post) => post.url); + +// All posts sorted by date +const allPostsByDate: Post[] = allPosts.sort((a, b) => { + return compareDesc(new Date(a.date), new Date(b.date)); +}); + +// Paths for all tags +const allTagPaths: string[] = allTags.map((tag) => `/tags/${tag}`); + +// Formatters +const formatDate = (date: string): string => + format(parseISO(date), "LLLL d, yyyy"); + +export { + allTags, + allOtherTags, + getPostBySlug, + postsWithTag, + allPostPaths, + allPostsByDate, + allTagPaths, + formatDate, +}; diff --git a/package.json b/package.json index 599840b..c6ffc99 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "next": "12.1.6", "next-contentlayer": "latest", "react": "18.1.0", - "react-dom": "18.1.0" + "react-dom": "18.1.0", + "typescript-array-utils": "^0.1.4" }, "devDependencies": { "@types/react": "18.0.9", diff --git a/pages/_app.tsx b/pages/_app.tsx index c525292..73d34cb 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,8 +1,8 @@ -import Head from 'next/head' +import Head from "next/head"; -import '../styles/globals.css' +import "../styles/globals.css"; -import { Header } from '../components/Header' +import { Header } from "../components/Header"; // This default export is required in a new `pages/_app.js` file. export default function MyApp({ Component, pageProps }) { @@ -19,5 +19,5 @@ export default function MyApp({ Component, pageProps }) { - ) + ); } diff --git a/pages/index.tsx b/pages/index.tsx index 474aa9b..65f084e 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,25 +1,31 @@ import Link from "next/link"; -import { compareDesc, format, parseISO } from "date-fns"; -import { allPosts, Post } from "contentlayer/generated"; +import { Post } from "contentlayer/generated"; +import { Tags } from "components/Tags"; +import { allPostsByDate, formatDate } from "lib/content"; export async function getStaticProps() { - const posts: Post[] = allPosts.sort((a, b) => { - return compareDesc(new Date(a.date), new Date(b.date)); - }); - return { props: { posts } }; + return { props: { posts: allPostsByDate } }; } -function PostCard(post: Post) { +export function PostCard(post: Post) { + const formattedDate: string = formatDate(post.date); + return ( -
+

{post.title}

- +
+
+ +
+ + +
post.url); return { - paths, + paths: allPostPaths, fallback: false, }; } export async function getStaticProps({ params }) { - const post: Post = allPosts.find( - (post) => post._raw.flattenedPath === params.slug - ); + const post: Post = getPostBySlug(params.slug); return { props: { post, @@ -22,6 +20,8 @@ export async function getStaticProps({ params }) { } const PostLayout = ({ post }: { post: Post }) => { + const formattedDate: string = formatDate(post.date); + return ( <> @@ -30,9 +30,12 @@ const PostLayout = ({ post }: { post: Post }) => {

{post.title}

+
+ +
diff --git a/pages/tags/[tag].tsx b/pages/tags/[tag].tsx new file mode 100644 index 0000000..9c58291 --- /dev/null +++ b/pages/tags/[tag].tsx @@ -0,0 +1,54 @@ +import { Tags } from "components/Tags"; +import { Post } from "contentlayer/generated"; +import { allOtherTags, allTagPaths, postsWithTag } from "lib/content"; +import Head from "next/head"; +import { PostCard } from "pages"; + +export async function getStaticPaths() { + return { + paths: allTagPaths, + fallback: false, + }; +} + +export async function getStaticProps({ params }) { + const posts: Post[] | undefined = postsWithTag(params.tag); + + return { + props: { + posts, + tag: params.tag, + }, + }; +} + +const TagLayout = ({ posts, tag }: { posts: Post[]; tag: string }) => { + const otherTags: string[] = allOtherTags(tag); + + return ( + <> + + Posts with the tag {tag} + +
+

+ Posts with the tag {tag} +

+ +
+ {posts.map((post, idx) => ( + + ))} +
+ +
+

Other available tags

+ + +
+
+ + ); +}; + +export default TagLayout; diff --git a/pages/tags/index.tsx b/pages/tags/index.tsx new file mode 100644 index 0000000..6414097 --- /dev/null +++ b/pages/tags/index.tsx @@ -0,0 +1,12 @@ +import { Tags } from "components/Tags"; +import { allTags } from "lib/content"; + +export default function AllTags() { + return ( +
+

All tags used on the blog

+ + +
+ ); +} diff --git a/posts/change-me.md b/posts/change-me.md index 398ddec..91f5821 100644 --- a/posts/change-me.md +++ b/posts/change-me.md @@ -1,6 +1,7 @@ --- title: Change me! date: 2022-03-11 +tags: [cache, how-to] --- When you change a source file, Contentlayer automatically updates the content cache, which prompts Next.js to reload the content on screen. diff --git a/posts/click-me.md b/posts/click-me.md index 606cbd3..1ff4798 100644 --- a/posts/click-me.md +++ b/posts/click-me.md @@ -1,6 +1,7 @@ --- title: Click me! date: 2022-02-28 +tags: [html, how-to] --- Blog posts have their own pages. The content source is a markdown file, parsed to HTML by Contentlayer. diff --git a/posts/what-is-contentlayer.md b/posts/what-is-contentlayer.md index 07eef20..fa8d850 100644 --- a/posts/what-is-contentlayer.md +++ b/posts/what-is-contentlayer.md @@ -1,6 +1,7 @@ --- title: What is Contentlayer? date: 2022-02-22 +tags: [content, technical] --- **Contentlayer makes working with content easy.** It is a content preprocessor that validates and transforms your content into type-safe JSON you can easily import into your application.