1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-09-13 23:55:35 -04:00

simplify note parsing logic

This commit is contained in:
2022-01-02 23:05:51 -05:00
parent ca614e1a1a
commit 49fb053649
15 changed files with 164 additions and 154 deletions

View File

@@ -2,90 +2,106 @@ import Link from "next/link";
import Image from "next/image";
import TweetEmbed from "react-tweet-embed";
import Gist from "react-gist";
import getNodeText from "../lib/get-node-text";
import Video from "./video/FullPageVideo";
import CopyButton from "./clipboard/CopyButton";
import getNodeText from "../lib/get-node-text";
import { OctocatOcticon } from "./icons/octicons";
import type { LinkProps } from "next/link";
import type { ImageProps } from "next/image";
import type { ReactPlayerProps } from "react-player";
// The following components are all passed into <MDXProvider /> as replacement HTML tags or drop-in React components
// available in .mdx files containing post content, since they're not directly aware of the components in this folder.
const mdxComponents = {
a: ({
href,
target,
rel,
className,
children,
}: LinkProps & {
target?: string;
rel?: string;
className?: string;
children?: unknown;
}) => (
<Link href={href} passHref={true}>
<a className={className} target={target} rel={rel}>
{children}
</a>
</Link>
),
img: (props: ImageProps) => {
const CustomLink = ({
href,
target,
rel,
className,
children,
}: LinkProps & {
target?: string;
rel?: string;
className?: string;
children?: unknown;
}) => (
<Link href={href} passHref={true}>
<a className={className} target={target} rel={rel}>
{children}
</a>
</Link>
);
const CustomImg = (props: ImageProps) => {
return (
// height and width are part of the props, so they get automatically passed here with {...props}
<div className={props.className}>
{/* eslint-disable-next-line jsx-a11y/alt-text */}
<Image {...props} />
</div>
);
};
const CustomCode = (props: any) => {
if (props.className?.split(" ").includes("hljs")) {
return (
// height and width are part of the props, so they get automatically passed here with {...props}
<div className={props.className}>
{/* eslint-disable-next-line jsx-a11y/alt-text */}
<Image {...props} />
<div>
<CopyButton content={getNodeText(props.children)} />
<code {...props}>{props.children}</code>
<style jsx>{`
div {
position: relative;
max-width: 100%;
overflow-x: scroll;
margin: 1em 0;
}
`}</style>
</div>
);
},
code: (props: any) => {
if (props.className?.split(" ").includes("hljs")) {
return (
<div>
<CopyButton content={getNodeText(props.children)} />
<code {...props}>{props.children}</code>
<style jsx>{`
div {
position: relative;
max-width: 100%;
overflow-x: scroll;
margin: 1em 0;
}
`}</style>
</div>
);
} else {
return <code {...props}>{props.children}</code>;
}
},
video: (props: ReactPlayerProps) => <Video {...props} />,
tweet: (props: { id: string }) => (
<TweetEmbed
id={props.id}
options={{
dnt: true,
align: "center",
}}
/>
),
gist: (props: { id: string; file?: string }) => <Gist {...props} />,
octocat: (props: { repo: string }) => (
<a className="no-underline" href={`https://github.com/${props.repo}`} target="_blank" rel="noopener noreferrer">
<OctocatOcticon verticalAlign="text-top" fill="currentColor" />
<style jsx>{`
a {
margin: 0 0.3em;
color: var(--text);
}
} else {
return <code {...props}>{props.children}</code>;
}
};
a:hover {
color: var(--link);
}
`}</style>
</a>
),
const CustomVideo = (props: ReactPlayerProps) => <Video {...props} />;
const CustomTweet = (props: { id: string }) => (
<TweetEmbed
id={props.id}
options={{
dnt: true,
align: "center",
}}
/>
);
const CustomGist = (props: { id: string; file?: string }) => <Gist {...props} />;
const CustomGitHubLink = (props: { repo: string }) => (
<a className="no-underline" href={`https://github.com/${props.repo}`} target="_blank" rel="noopener noreferrer">
<OctocatOcticon verticalAlign="text-top" fill="currentColor" />
<style jsx>{`
a {
margin: 0 0.3em;
color: var(--text);
}
a:hover {
color: var(--link);
}
`}</style>
</a>
);
const mdxComponents = {
a: CustomLink,
img: CustomImg,
code: CustomCode,
video: CustomVideo,
tweet: CustomTweet,
gist: CustomGist,
octocat: CustomGitHubLink,
};
export default mdxComponents;

View File

@@ -1,5 +1,6 @@
import Link from "next/link";
import { format, parseISO } from "date-fns";
import groupBy from "lodash.groupby";
import styles from "./List.module.scss";
@@ -9,15 +10,16 @@ type NoteProps = {
slug: string;
};
const List = ({ notesByYear }) => {
const List = ({ notes }) => {
const notesByYear = groupBy(notes, "year");
const sections = [];
Object.entries(notesByYear).forEach(([year, notes]: [string, NoteProps[]]) => {
Object.entries(notesByYear).forEach(([year, yearNotes]: [string, NoteProps[]]) => {
sections.push(
<section key={year} className={styles.section}>
<h2 className={styles.year}>{year}</h2>
<ul className={styles.list}>
{notes.map((note) => (
{yearNotes.map((note) => (
<li key={note.slug} className={styles.row}>
<span className={styles.date}>{format(parseISO(note.date), "MMM d")}</span>
<span>

View File

@@ -1,37 +1,40 @@
import { Feed } from "feed";
import { getAllNotes } from "./parse-notes";
import * as config from "./config";
export const buildFeed = (notes: any[]) => {
export const buildFeed = () => {
const baseURL = config.baseURL || "http://localhost:3000"; // necessary for local testing
const feed = new Feed({
id: `${config.baseURL}/`,
link: `${config.baseURL}/`,
id: `${baseURL}/`,
link: `${baseURL}/`,
title: config.siteName,
description: config.longDescription,
copyright: "https://creativecommons.org/licenses/by/4.0/",
updated: new Date(),
image: `${config.baseURL}/static/images/me.jpg`,
image: `${baseURL}/static/images/me.jpg`,
feedLinks: {
rss: `${config.baseURL}/feed.xml`,
atom: `${config.baseURL}/feed.atom`,
rss: `${baseURL}/feed.xml`,
atom: `${baseURL}/feed.atom`,
},
author: {
name: config.authorName,
link: config.baseURL,
link: baseURL,
email: "jake@jarv.is",
},
});
notes.forEach((note: { title: any; slug: any; description: any; image: any; date: string | number | Date }) => {
const notes = getAllNotes();
notes.forEach((note: any) => {
feed.addItem({
title: note.title,
link: `${config.baseURL}/notes/${note.slug}/`,
guid: `${config.baseURL}/notes/${note.slug}/`,
link: `${baseURL}/notes/${note.slug}/`,
guid: `${baseURL}/notes/${note.slug}/`,
description: note.description,
image: note.image ? `${config.baseURL}${note.image}` : "",
image: note.image ? `${baseURL}${note.image}` : "",
author: [
{
name: config.authorName,
link: config.baseURL,
link: baseURL,
},
],
date: new Date(note.date),

View File

@@ -27,4 +27,7 @@ export const facebookAppId = "3357248167622283";
export const webmentionId = "jarv.is";
export const monetization = "$ilp.uphold.com/BJp6d2FrEB69";
// Next.js constants
export const NOTES_DIR = "notes";
// ...note / TODO: there is still a metric poop ton of this kind of info hard-coded.

View File

@@ -1,38 +1,29 @@
import fs from "fs";
import path from "path";
import matter from "gray-matter";
import { format, parseISO } from "date-fns";
import { NOTES_DIR } from "./config";
export const NOTES_PATH = path.join(process.cwd(), "notes");
export const getNoteData = (file: string) => {
const slug = file.replace(/\.mdx$/, "");
const fullPath = path.join(process.cwd(), NOTES_DIR, `${slug}.mdx`);
const contents = fs.readFileSync(fullPath, "utf8");
const { data } = matter(contents);
export const getNoteSlugs = () => fs.readdirSync(NOTES_PATH);
// Return all md(x) files in NOTES_PATH
export const notePaths = getNoteSlugs().filter((notePath) => /\.mdx?$/.test(notePath));
export const getNoteBySlug = (slug, fields = []) => {
const realSlug = slug.replace(/\.mdx$/, "");
const fullPath = path.join(NOTES_PATH, `${realSlug}.mdx`);
const fileContents = fs.readFileSync(fullPath, "utf8");
const { data, content } = matter(fileContents);
const items = {};
// Ensure only the minimal needed data is exposed
fields.forEach((field) => {
if (field === "slug") {
items[field] = realSlug;
} else if (field === "content") {
items[field] = content;
} else if (typeof data[field] !== "undefined") {
items[field] = data[field];
}
});
return items;
return {
...data,
slug: slug,
date: parseISO(data.date).toISOString(), // validate/normalize the date string provided from front matter
year: parseInt(format(parseISO(data.date), "yyyy")),
};
};
export const getAllNotes = (fields = []) =>
getNoteSlugs()
.map((slug) => getNoteBySlug(slug, fields))
// all .mdx files in NOTES_DIR
export const getNoteFiles = () =>
fs.readdirSync(path.join(process.cwd(), NOTES_DIR)).filter((notePath) => /\.mdx$/.test(notePath));
export const getAllNotes = () =>
getNoteFiles()
.map((file) => getNoteData(file))
// sort notes by date in descending order
.sort((note1: any, note2: any) => (note1.date > note2.date ? -1 : 1));

View File

@@ -8,7 +8,7 @@ tags:
- Coronavirus
- Public Health
- GitHub
image: "images/covid19dashboards.png"
image: "/static/images/notes/coronavirus-open-source/covid19dashboards.png"
---
We're all quickly learning that worldwide pandemics can bring out both [the best](https://www.vox.com/culture/2020/3/13/21179293/coronavirus-italy-covid19-music-balconies-sing) and [the worst](https://twitter.com/9NewsAUS/status/1236088663093608448) of humanity. But one thing has become readily apparent to me — outside of the large teams of medical professionals risking their lives right this minute, the open source community stands alone in its ability to rapidly organize in the midst of chaos to give back to the world and, in this case, make it safer for all of us.

View File

@@ -8,7 +8,7 @@ tags:
- Subdomain Takeover
- Bug Bounty
- Tutorial
image: "images/hackerone-2.png"
image: "/static/images/notes/finding-candidates-subdomain-takeovers/hackerone-2.png"
---
A **subdomain takeover** occurs when a subdomain (like _example_.jarv.is) points to a shared hosting account that is abandoned by its owner, leaving the endpoint available to claim for yourself.

View File

@@ -8,7 +8,7 @@ tags:
- Continuous Integration
- Docker
- Open Source
image: "images/actions-flow.png"
image: "/static/images/notes/github-actions/actions-flow.png"
---
<img

View File

@@ -14,7 +14,7 @@
"url": "https://github.com/jakejarvis/jarv.is.git"
},
"scripts": {
"dev": "next dev",
"dev": "NODE_OPTIONS='--inspect' next dev",
"build": "next build",
"postbuild": "next-sitemap",
"start": "next start",
@@ -85,7 +85,7 @@
"eslint-config-next": "~12.0.7",
"eslint-config-prettier": "~8.3.0",
"eslint-plugin-prettier": "~4.0.0",
"lint-staged": "^12.1.4",
"lint-staged": "^12.1.5",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.5",
"postcss-focus": "^5.0.1",

View File

@@ -1,14 +1,12 @@
import { getAllNotes } from "../lib/parse-notes";
import { buildFeed } from "../lib/build-feed";
import type { GetServerSideProps } from "next";
const AtomPage = () => null;
export const getServerSideProps: GetServerSideProps = async (context) => {
const notes = getAllNotes(["title", "date", "image", "slug", "description"]);
const feed = buildFeed(notes);
const feed = buildFeed();
const { res } = context;
res.setHeader("content-type", "application/atom+xml");
res.write(feed.atom1());
res.end();

View File

@@ -1,14 +1,12 @@
import { getAllNotes } from "../lib/parse-notes";
import { buildFeed } from "../lib/build-feed";
import type { GetServerSideProps } from "next";
const RssPage = () => null;
export const getServerSideProps: GetServerSideProps = async (context) => {
const notes = getAllNotes(["title", "date", "image", "slug", "description"]);
const feed = buildFeed(notes);
const feed = buildFeed();
const { res } = context;
res.setHeader("content-type", "application/rss+xml");
res.write(feed.rss2());
res.end();

View File

@@ -8,8 +8,8 @@ import Layout from "../../components/Layout";
import Container from "../../components/Container";
import Content from "../../components/Content";
import Meta from "../../components/notes/Meta";
import { notePaths, NOTES_PATH } from "../../lib/parse-notes";
import mdxComponents from "../../components/mdxComponents";
import { getNoteFiles } from "../../lib/parse-notes";
import * as config from "../../lib/config";
import type { GetStaticProps, GetStaticPaths } from "next";
@@ -59,7 +59,9 @@ const Note = ({ source, frontMatter, slug }) => (
<Container>
<Meta {...frontMatter} slug={slug} />
<Content>
<MDXRemote {...source} components={mdxComponents} />
<div className="markdown">
<MDXRemote {...source} components={mdxComponents} />
</div>
</Content>
</Container>
</Layout>
@@ -67,7 +69,7 @@ const Note = ({ source, frontMatter, slug }) => (
);
export const getStaticProps: GetStaticProps = async ({ params }) => {
const filePath = path.join(NOTES_PATH, `${params.slug}.mdx`);
const filePath = path.join(process.cwd(), config.NOTES_DIR, `${params.slug}.mdx`);
const source = fs.readFileSync(filePath);
const { content, data } = matter(source);
@@ -98,7 +100,7 @@ export const getStaticProps: GetStaticProps = async ({ params }) => {
};
export const getStaticPaths: GetStaticPaths = async () => {
const paths = notePaths
const paths = getNoteFiles()
// Remove file extensions for page paths
.map((notePath) => notePath.replace(/\.mdx?$/, ""))
// Map the path into the static paths object required by Next.js

View File

@@ -1,28 +1,23 @@
import { format, parseISO } from "date-fns";
import groupBy from "lodash.groupby";
import Layout from "../../components/Layout";
import Container from "../../components/Container";
import List from "../../components/notes/List";
import { getAllNotes } from "../../lib/parse-notes";
import type { GetStaticProps } from "next";
const Notes = ({ notesByYear }) => (
const Notes = ({ notes }) => (
<Layout>
<Container title="Notes" description="Recent posts by Jake Jarvis.">
<List notesByYear={notesByYear} />
<List notes={notes} />
</Container>
</Layout>
);
export const getStaticProps: GetStaticProps = async () => {
const allNotes = getAllNotes(["date", "slug", "title"]);
// parse year of each note
allNotes.map((note: any) => (note.year = parseInt(format(parseISO(note.date), "yyyy"))));
const notes = getAllNotes();
return {
props: {
notesByYear: groupBy(allNotes, "year"),
notes,
},
};
};

View File

@@ -1,8 +1,10 @@
@use "sass:meta";
// hacky way to assign different HLJS themes to light/dark mode
@include meta.load-css("../node_modules/highlight.js/styles/stackoverflow-light.css");
.markdown {
@include meta.load-css("../node_modules/highlight.js/styles/stackoverflow-light.css");
}
[data-theme="dark"] {
[data-theme="dark"] .markdown {
@include meta.load-css("../node_modules/highlight.js/styles/stackoverflow-dark.css");
}

View File

@@ -2586,7 +2586,7 @@ eslint-import-resolver-typescript@^2.4.0:
resolve "^1.20.0"
tsconfig-paths "^3.9.0"
eslint-module-utils@^2.7.1:
eslint-module-utils@^2.7.2:
version "2.7.2"
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.2.tgz#1d0aa455dcf41052339b63cada8ab5fd57577129"
integrity sha512-zquepFnWCY2ISMFwD/DqzaM++H+7PDzOpUvotJWm/y1BAFt5R4oeULgdrTejKqLkz7MA/tgstsUMNYc7wNdTrg==
@@ -2595,23 +2595,23 @@ eslint-module-utils@^2.7.1:
find-up "^2.1.0"
eslint-plugin-import@^2.25.2:
version "2.25.3"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.3.tgz#a554b5f66e08fb4f6dc99221866e57cfff824766"
integrity sha512-RzAVbby+72IB3iOEL8clzPLzL3wpDrlwjsTBAQXgyp5SeTqqY+0bFubwuo+y/HLhNZcXV4XqTBO4LGsfyHIDXg==
version "2.25.4"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz#322f3f916a4e9e991ac7af32032c25ce313209f1"
integrity sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==
dependencies:
array-includes "^3.1.4"
array.prototype.flat "^1.2.5"
debug "^2.6.9"
doctrine "^2.1.0"
eslint-import-resolver-node "^0.3.6"
eslint-module-utils "^2.7.1"
eslint-module-utils "^2.7.2"
has "^1.0.3"
is-core-module "^2.8.0"
is-glob "^4.0.3"
minimatch "^3.0.4"
object.values "^1.1.5"
resolve "^1.20.0"
tsconfig-paths "^3.11.0"
tsconfig-paths "^3.12.0"
eslint-plugin-jsx-a11y@^6.5.1:
version "6.5.1"
@@ -3729,10 +3729,10 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
lint-staged@^12.1.4:
version "12.1.4"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-12.1.4.tgz#a92ec8509f13018caaafade61d515c2d5873316e"
integrity sha512-RgDz9nsFsE0/5eL9Vat0AvCuk0+j5mEuzBIVfrRH5FRtt5wibYe8zTjZs2nuqLFrLAGQGYnj8+HJxolcj08i/A==
lint-staged@^12.1.5:
version "12.1.5"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-12.1.5.tgz#e05582fc39aed5cb13b9dd1dfb8330407246d809"
integrity sha512-WyKb+0sNKDTd1LwwAfTBPp0XmdaKkAOEbg4oHE4Kq2+oQVchg/VAcjVQtSqZih1izNsTURjc2EkhG/syRQUXdA==
dependencies:
cli-truncate "^3.1.0"
colorette "^2.0.16"
@@ -5591,7 +5591,7 @@ trough@^2.0.0:
resolved "https://registry.yarnpkg.com/trough/-/trough-2.0.2.tgz#94a3aa9d5ce379fc561f6244905b3f36b7458d96"
integrity sha512-FnHq5sTMxC0sk957wHDzRnemFnNBvt/gSY99HzK8F7UP5WAbvP70yX5bd7CjEQkN+TjdxwI7g7lJ6podqrG2/w==
tsconfig-paths@^3.11.0, tsconfig-paths@^3.9.0:
tsconfig-paths@^3.12.0, tsconfig-paths@^3.9.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz#19769aca6ee8f6a1a341e38c8fa45dd9fb18899b"
integrity sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg==