-
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.
Deletes old Pages Router blog files, as well as displayMarkdown component.
- Loading branch information
Showing
8 changed files
with
325 additions
and
268 deletions.
There are no files selected for viewing
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,76 @@ | ||
import { redirect } from "next/navigation"; | ||
import React from "react"; | ||
|
||
import FlexCenter from "../../../components/flex-center"; | ||
import { BigSpacer } from "../../../components/spacers"; | ||
|
||
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 }; | ||
}); | ||
} | ||
|
||
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={4} /> | ||
|
||
<FlexCenter> | ||
<div className="row"> | ||
<div className="col-lg-8"> | ||
<h3 className={styles.blogPostDate}>{blogPost.date}</h3> | ||
<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> | ||
</FlexCenter> | ||
</> | ||
); | ||
} |
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: 500; | ||
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); | ||
} |
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,89 @@ | ||
import { marked } from "marked"; | ||
import sanitizeHtml, { Attributes, IOptions, Tag } from "sanitize-html"; | ||
|
||
import { siteUrl } from "../../data/BaseConfig"; | ||
|
||
export default async function parseMarkdown(mdString: string): Promise<string> { | ||
const rawDescription = await marked.parse(mdString); | ||
|
||
const sanitizerConfig: IOptions = { | ||
allowedTags, // see below | ||
allowedAttributes: { "*": allowedAttributes }, // see below | ||
nonTextTags: ["style", "script", "textarea", "option"], | ||
transformTags: { | ||
a: transformA, // see below | ||
}, | ||
}; | ||
|
||
const cleanDescription: string = sanitizeHtml( | ||
rawDescription, | ||
sanitizerConfig, | ||
); | ||
|
||
return cleanDescription; | ||
} | ||
|
||
function transformA(tagName: string, attribs: Attributes): Tag { | ||
// small helper to keep things dry | ||
const _setAttribs: (attribs: Attributes) => void = (attribs) => { | ||
attribs.target = "_blank"; | ||
attribs.rel = "noreferrer nofollow"; | ||
}; | ||
|
||
const href = attribs.href; | ||
if (href) { | ||
const baseUrl = new URL(siteUrl); | ||
try { // sometimes the `href` isn't a valid URL… | ||
const linkUrl = new URL(href); | ||
if (linkUrl.hostname !== baseUrl.hostname) { | ||
_setAttribs(attribs); | ||
} | ||
} catch { | ||
_setAttribs(attribs); | ||
} | ||
} | ||
|
||
return { tagName, attribs }; | ||
} | ||
|
||
// All of these tags may not be necessary, this list was adopted from https://github.com/nextstrain/auspice/blob/master/src/util/parseMarkdown.js | ||
const allowedTags = ['div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'em', 'strong', 'del', 'ol', 'ul', 'li', 'a', 'img']; | ||
allowedTags.push('#text', 'code', 'pre', 'hr', 'table', 'thead', 'tbody', 'th', 'tr', 'td', 'sub', 'sup'); | ||
// We want to support SVG elements, requiring the following tags (we exclude "foreignObject", "style" and "script") | ||
allowedTags.push("svg", "altGlyph", "altGlyphDef", "altGlyphItem", "animate", "animateColor", "animateMotion", "animateTransform"); | ||
allowedTags.push("circle", "clipPath", "color-profile", "cursor", "defs", "desc", "ellipse", "feBlend", "feColorMatrix", "feComponentTransfer"); | ||
allowedTags.push("feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feFlood", "feFuncA"); | ||
allowedTags.push("feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset"); | ||
allowedTags.push("fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence", "filter", "font", "font-face"); | ||
allowedTags.push("font-face-format", "font-face-name", "font-face-src", "font-face-uri", "g", "glyph", "glyphRef"); | ||
allowedTags.push("hkern", "image", "line", "linearGradient", "marker", "mask", "metadata", "missing-glyph", "mpath", "path"); | ||
allowedTags.push("pattern", "polygon", "polyline", "radialGradient", "rect", "set", "stop", "switch", "symbol"); | ||
allowedTags.push("text", "textPath", "title", "tref", "tspan", "use", "view", "vkern"); | ||
|
||
const allowedAttributes = ['href', 'src', 'width', 'height', 'alt']; | ||
// We add the following Attributes for SVG via https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute | ||
// Certain values have been excluded here, e.g. "style" | ||
allowedAttributes.push("accent-height", "accumulate", "additive", "alignment-baseline", "allowReorder", "alphabetic", "amplitude", "arabic-form", "ascent", "attributeName", "attributeType", "autoReverse", "azimuth"); | ||
allowedAttributes.push("baseFrequency", "baseline-shift", "baseProfile", "bbox", "begin", "bias", "by"); | ||
allowedAttributes.push("calcMode", "cap-height", "class", "clip", "clipPathUnits", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "cursor", "cx", "cy"); | ||
allowedAttributes.push("d", "decelerate", "descent", "diffuseConstant", "direction", "display", "divisor", "dominant-baseline", "dur", "dx", "dy"); | ||
allowedAttributes.push("edgeMode", "elevation", "enable-background", "end", "exponent", "externalResourcesRequired"); | ||
allowedAttributes.push("fill", "fill-opacity", "fill-rule", "filter", "filterRes", "filterUnits", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "format", "from", "fr", "fx", "fy"); | ||
allowedAttributes.push("g1", "g2", "glyph-name", "glyph-orientation-horizontal", "glyph-orientation-vertical", "glyphRef", "gradientTransform", "gradientUnits"); | ||
allowedAttributes.push("hanging", "height", "href", "hreflang", "horiz-adv-x", "horiz-origin-x"); | ||
allowedAttributes.push("id", "ideographic", "image-rendering", "in", "in2", "intercept"); | ||
allowedAttributes.push("k", "k1", "k2", "k3", "k4", "kernelMatrix", "kernelUnitLength", "kerning", "keyPoints", "keySplines", "keyTimes"); | ||
allowedAttributes.push("lang", "lengthAdjust", "letter-spacing", "lighting-color", "limitingConeAngle", "local"); | ||
allowedAttributes.push("marker-end", "marker-mid", "marker-start", "markerHeight", "markerUnits", "markerWidth", "mask", "maskContentUnits", "maskUnits", "mathematical", "max", "media", "method", "min", "mode"); | ||
allowedAttributes.push("name", "numOctaves"); | ||
allowedAttributes.push("offset", "opacity", "operator", "order", "orient", "orientation", "origin", "overflow", "overline-position", "overline-thickness"); | ||
allowedAttributes.push("panose-1", "paint-order", "path", "pathLength", "patternContentUnits", "patternTransform", "patternUnits", "ping", "pointer-events", "points", "pointsAtX", "pointsAtY", "pointsAtZ", "preserveAlpha", "preserveAspectRatio", "primitiveUnits"); | ||
allowedAttributes.push("r", "radius", "referrerPolicy", "refX", "refY", "rel", "rendering-intent", "repeatCount", "repeatDur", "requiredExtensions", "requiredFeatures", "restart", "result", "rotate", "rx", "ry"); | ||
allowedAttributes.push("scale", "seed", "shape-rendering", "slope", "spacing", "specularConstant", "specularExponent", "speed", "spreadMethod", "startOffset", "stdDeviation", "stemh", "stemv", "stitchTiles", "stop-color", "stop-opacity", "strikethrough-position", "strikethrough-thickness", "string", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "surfaceScale", "systemLanguage"); | ||
allowedAttributes.push("tabindex", "tableValues", "target", "targetX", "targetY", "text-anchor", "text-decoration", "text-rendering", "textLength", "to", "transform", "type"); | ||
allowedAttributes.push("u1", "u2", "underline-position", "underline-thickness", "unicode", "unicode-bidi", "unicode-range", "units-per-em"); | ||
allowedAttributes.push("v-alphabetic", "v-hanging", "v-ideographic", "v-mathematical", "values", "vector-effect", "version", "vert-adv-y", "vert-origin-x", "vert-origin-y", "viewBox", "viewTarget", "visibility"); | ||
allowedAttributes.push("width", "widths", "word-spacing", "writing-mode"); | ||
allowedAttributes.push("x", "x-height", "x1", "x2", "xChannelSelector"); | ||
allowedAttributes.push("y", "y1", "y2", "yChannelSelector"); | ||
allowedAttributes.push("z", "zoomAndPan"); |
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,72 @@ | ||
import fs from "fs"; | ||
import matter from "gray-matter"; | ||
import path from "path"; | ||
import { fileURLToPath } from "url"; | ||
|
||
import parseMarkdown from "./parseMarkdown"; | ||
|
||
export interface BlogPost { | ||
author: string; | ||
blogUrlName: string; | ||
date: string; | ||
mdstring: string; | ||
sidebarTitle: string; | ||
title: string; | ||
} | ||
|
||
// Scans the ./static-site/content/blog directory for .md files and | ||
// returns a chronologically-sorted array of posts, each with some | ||
// basic metadata and the raw (unsanitized) markdown contents. | ||
export function getBlogPosts(): BlogPost[] { | ||
const __dirname = path.dirname(fileURLToPath(import.meta.url)); | ||
|
||
const postsDirectory = path.join(__dirname, "..", "..", "content", "blog"); | ||
|
||
const markdownFiles = fs | ||
.readdirSync(postsDirectory) | ||
.filter((fileName) => fileName.endsWith(".md")); | ||
|
||
const blogPosts: BlogPost[] = markdownFiles | ||
.map((fileName): BlogPost | false => { | ||
const { data: frontmatter, content: mdstring } = matter( | ||
fs.readFileSync(path.join(postsDirectory, fileName), "utf8"), | ||
); | ||
|
||
// Our blog posts have frontmatter which includes author, date | ||
// (YYYY-MM-DD format), post title, and an optional sidebar title. | ||
// If the sidebar title isn't provided, the post title will be | ||
// used for the sidebar title. | ||
const { author, date, title } = frontmatter; | ||
|
||
if (!author || !date || !title) { | ||
// console warning printed server-side | ||
console.warn( | ||
`Blog post ${fileName} skipped due to empty/incomplete frontmatter`, | ||
); | ||
// we will filter these `false` values out momentarily | ||
return false; | ||
} else { | ||
const blogUrlName = fileName.replace(/\.md$/, ""); | ||
const sidebarTitle: string = frontmatter.sidebarTitle || title; | ||
return { author, date, title, blogUrlName, sidebarTitle, mdstring }; | ||
} | ||
}) | ||
// type guard to filter out false entries generated because of bad frontmatter | ||
.filter((post: false | BlogPost): post is BlogPost => { | ||
return !!post; | ||
}) | ||
// YYYY-MM-DD strings sort alphabetically | ||
.sort((a, b) => (a.date > b.date ? -1 : 1)); | ||
|
||
return blogPosts; | ||
} | ||
|
||
export async function markdownToHtml(mdString:string): Promise<string> { | ||
try { | ||
return await parseMarkdown(mdString) | ||
} | ||
catch(error) { | ||
console.error(`Error parsing markdown: ${error}`); | ||
return '<p>There was an error parsing markdown content.</p>'; | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.