Skip to content

Commit

Permalink
Merge pull request #1059 from nextstrain/add-blog-preview-cards-134
Browse files Browse the repository at this point in the history
Port `/blog` to App router; add blog preview cards [#134]
  • Loading branch information
genehack authored Nov 15, 2024
2 parents 41e5936 + 65c6296 commit 5503367
Show file tree
Hide file tree
Showing 24 changed files with 533 additions and 367 deletions.
37 changes: 28 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@keyv/sqlite": "^3.6.6",
"@renovatebot/pep440": "^2.1.20",
"@smithy/node-http-handler": "^2.1.8",
"@types/sanitize-html": "^2.13.0",
"argparse": "^1.0.10",
"aws-sdk": "^2.908.0",
"chalk": "^2.4.1",
Expand Down Expand Up @@ -63,7 +64,7 @@
"luxon": "^3.4.4",
"make-fetch-happen": "^11.1.1",
"mapbox-gl": "^3.2.0",
"marked": "^0.7.0",
"marked": "^14.1.3",
"mime": "^2.5.2",
"neat-csv": "^7.0.0",
"negotiator": "^0.6.2",
Expand Down
20 changes: 20 additions & 0 deletions static-site/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,26 @@ After the above PR is merged, the new team member can then be added to the [docs
2. Merge the PR and follow [instructions to release a new version of the theme on PyPI](https://github.com/nextstrain/sphinx-theme#releasing).
3. Once the new version is available on PyPI, trigger RTD rebuilds for the latest/stable doc versions to update the footer.

### Writing blog posts

To author a new blog post, create a new file under `/static-site/content/blog/` following the existing file naming convention (`YYYY-MM-DD-the-title-here.md`, e.g., `2024-11-14-blog-posts-are-awesome.md`). The file should start with a block of YAML front matter, such as:

``` yaml
---
author: "James Hadfield"
date: "2018-05-14"
title: "New nextstrain.org website"
sidebarTitle: "New Nextstrain Website"
---
```

followed by the content of the blog post, marked up using [Markdown](https://en.wikipedia.org/wiki/Markdown). Please observe the following conventions:

* Images associated with blog posts should be placed in [`/static-site/public/blog/img/`](./public/blog/img)
* All images associated with a given blog post should start with a common filename prefix, which should be clearly related to the blog post; see the existing files in the directory for examples
* Image URLs in the post should be given in an origin-relative format; i.e., they should start with `/blog/img/`
* Links to other pages and resources on `nextstrain.org` should also be given in origin-relative form; i.e., they should NOT start with `https://nextstraing.org`, only with a `/`


## Deploying
The static documentation is automatically rebuilt every time the (parent) repo is pushed to master.
Expand Down
126 changes: 126 additions & 0 deletions static-site/app/blog/[id]/page.tsx
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>
</>
);
}
71 changes: 71 additions & 0 deletions static-site/app/blog/[id]/styles.module.css
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;
}
17 changes: 17 additions & 0 deletions static-site/app/blog/page.tsx
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);
}
Loading

0 comments on commit 5503367

Please sign in to comment.