Skip to content
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

Port /blog to App router; add blog preview cards [#134] #1059

Merged
merged 7 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
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",
genehack marked this conversation as resolved.
Show resolved Hide resolved
"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;
genehack marked this conversation as resolved.
Show resolved Hide resolved
}

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);
Comment on lines +12 to +16
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The images of the most recent blog fail to load when going to /blog, but load fine at /blog/2024-10-22-oropouche-analysis-and-resources

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, i see that.

seems to be because the image URLs are relative to /blog (i.e., img/foo.png, not /blog/img/foo.png), maybe? The images are showing as 404 and the HTTP request was GET /img/oropouche_host_view.png.

oddly, it works fine on localhost... I will dig into the redirect() API and see if I need to do something different, but an initial fix might be tweaking the image URLs in the Oropouche post.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, yeah, this is because of how the redirect() API works, apparently.

I think I'm going to take this as the push from the universe to move ahead with converting the front page to be the five most recent blog posts, which will require some additional refactoring in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the end, because of how Next.JS generates URLs without trailing slashes, I had to update the blog image URLs; there was no other way to resolve this (not without other knock-on damage that would have been worse).

So the "multiple pages on the front page" thing will get kicked down the road a bit, and this PR is once again ready to be reviewed.

Note that I also added a section to the static-site README with some minimal info about blog post creation.

}
Loading