-
Notifications
You must be signed in to change notification settings - Fork 48
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1059 from nextstrain/add-blog-preview-cards-134
Port `/blog` to App router; add blog preview cards [#134]
- Loading branch information
Showing
24 changed files
with
533 additions
and
367 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import { Metadata } from "next"; | ||
import { redirect } from "next/navigation"; | ||
import React from "react"; | ||
|
||
import { BigSpacer } from "../../../components/spacers"; | ||
import { | ||
siteLogo, | ||
siteTitle, | ||
siteTitleAlt, | ||
siteUrl, | ||
} from "../../../data/BaseConfig"; | ||
|
||
import { getBlogPosts, markdownToHtml } from "../utils"; | ||
|
||
import styles from "./styles.module.css"; | ||
|
||
// just to avoid having to repeat this in a couple method sigs... | ||
interface BlogPostParams { | ||
id: string; | ||
} | ||
|
||
// return a list of params that will get handed to this page at build | ||
// time, to statically build out all the blog posts | ||
export function generateStaticParams(): BlogPostParams[] { | ||
return getBlogPosts().map((post) => { | ||
return { id: post.blogUrlName }; | ||
}); | ||
} | ||
|
||
// generate opengraph and other metadata tags | ||
export async function generateMetadata({ | ||
params, | ||
}: { | ||
params: BlogPostParams; | ||
}): Promise<Metadata> { | ||
const { id } = params; | ||
|
||
// set up some defaults that are independent of the specific blog post | ||
const baseUrl = new URL(siteUrl); | ||
const metadata: Metadata = { | ||
metadataBase: baseUrl, | ||
openGraph: { | ||
description: siteTitleAlt, | ||
images: [ | ||
{ | ||
url: `${siteUrl}${siteLogo}`, | ||
}, | ||
], | ||
siteName: siteTitle, | ||
title: siteTitle, | ||
type: "website", | ||
url: baseUrl, | ||
}, | ||
}; | ||
|
||
// this is the specific post we're rendering | ||
const blogPost = getBlogPosts().find((post) => post.blogUrlName === id); | ||
|
||
if (blogPost) { | ||
const description = `Nextstrain blog post from ${blogPost.date}; author(s): ${blogPost.author}`; | ||
|
||
metadata.title = blogPost.title; | ||
metadata.description = description; | ||
metadata.openGraph!.description = description; | ||
metadata.openGraph!.title = `${siteTitle}: ${blogPost.title}`; | ||
metadata.openGraph!.url = `/blog/${blogPost.blogUrlName}`; | ||
} | ||
|
||
return metadata; | ||
} | ||
|
||
export default async function BlogPost({ | ||
params, | ||
}: { | ||
params: BlogPostParams; | ||
}): Promise<React.ReactElement> { | ||
const { id } = params; | ||
|
||
// we need this list to build the archive list in the sidebar | ||
const allBlogPosts = getBlogPosts(); | ||
|
||
// and then this is the specific post we're rendering | ||
const blogPost = allBlogPosts.find((post) => post.blogUrlName === id); | ||
|
||
// if for some reason we didn't find the post, 404 on out | ||
if (!blogPost) { | ||
redirect("/404"); | ||
} | ||
|
||
const html = await markdownToHtml(blogPost.mdstring); | ||
|
||
return ( | ||
<> | ||
<BigSpacer count={2} /> | ||
|
||
<article className="container"> | ||
<div className="row"> | ||
<div className="col-lg-8"> | ||
<time className={styles.blogPostDate} dateTime={blogPost.date}> | ||
{blogPost.date} | ||
</time> | ||
<h1 className={styles.blogPostTitle}>{blogPost.title}</h1> | ||
<h2 className={styles.blogPostAuthor}>{blogPost.author}</h2> | ||
<div | ||
className={styles.blogPostBody} | ||
dangerouslySetInnerHTML={{ | ||
__html: html, | ||
}} | ||
/> | ||
</div> | ||
<div className="col-lg-1" /> | ||
<div className={`${styles.blogSidebar} col-lg-3`}> | ||
<h2>Blog Archives</h2> | ||
<ul> | ||
{allBlogPosts.map((p) => ( | ||
<li key={p.blogUrlName}> | ||
<a href={p.blogUrlName}>{p.sidebarTitle}</a> ({p.date}) | ||
</li> | ||
))} | ||
</ul> | ||
</div> | ||
</div> | ||
</article> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
.blogPostTitle { | ||
clear: both; /* this is to let the title fall under the floated date, not under it */ | ||
color: black; | ||
font-size: 3.5rem; | ||
font-weight: 400; | ||
line-height: 40px; | ||
text-align: left; | ||
width: 100%; | ||
} | ||
|
||
.blogPostAuthor { | ||
color: black; | ||
font-size: 2rem; | ||
font-weight: 300; | ||
margin: 1rem 0 2rem; | ||
} | ||
|
||
.blogPostDate { | ||
color: black; | ||
float: right; | ||
font-size: 1.4rem; | ||
font-weight: 300; | ||
min-height: 2rem; | ||
} | ||
|
||
.blogPostBody { | ||
color: black; | ||
font-size: 1.6rem; | ||
font-weight: 300; | ||
line-height: var(--niceLineHeight); | ||
margin-top: 0px; | ||
padding-bottom: 25px; | ||
width: 100%; | ||
} | ||
|
||
.blogPostBody img { | ||
max-width: 100%; | ||
} | ||
|
||
.blogPostBody h1 { | ||
color: black; | ||
font-size: 3rem; | ||
margin-top: 20px; | ||
text-align: left; | ||
} | ||
.blogPostBody h2 { | ||
font-size: 2.4rem; | ||
font-weight: 300; | ||
margin-top: 10px; | ||
} | ||
.blogPostBody h3 { | ||
font-size: 1.8rem; | ||
font-weight: 300; | ||
margin-top: 10px; | ||
} | ||
.blogPostBody p { | ||
margin-top: 10px; | ||
} | ||
.blogPostBody li { | ||
margin-left: 3rem; | ||
} | ||
|
||
.blogSidebar { | ||
font-size: 14px; | ||
} | ||
.blogSidebar ul { | ||
list-style: none; | ||
} | ||
.blogSidebar ul li { | ||
margin: 1.2rem 0; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { redirect } from "next/navigation"; | ||
|
||
import { getBlogPosts } from "./utils"; | ||
|
||
export default function Index(): void { | ||
const mostRecentPost = getBlogPosts()[0]; | ||
|
||
// _technically_ getBlogPosts() could return an empty array and then | ||
// mostRecentPost would be undefined -- to make the type checker | ||
// happy, if for some reason mostRecentPost is undefined, we will | ||
// detect that and redirect to the 404 page | ||
const redirectTo = mostRecentPost | ||
? `/blog/${mostRecentPost.blogUrlName}` | ||
: `/404`; | ||
|
||
redirect(redirectTo); | ||
} |
Oops, something went wrong.