diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..240bdfe --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": true, + "printWidth": 120, + "singleQuote": false, + "trailingComma": "all" +} diff --git a/components/Author.tsx b/components/Author.tsx index 911d090..320dff6 100644 --- a/components/Author.tsx +++ b/components/Author.tsx @@ -1,29 +1,29 @@ -import React from 'react'; -import { format } from 'fecha'; -import { PostData } from '../loader'; +import { format } from "fecha" +import React from "react" +import { PostData } from "../loader" -export const FollowButton = () => { +export function FollowButton() { return (
Follow
- ); -}; + ) +} -export const Author: React.FC<{ post: PostData }> = (props) => { +export function Author(props: { post: PostData }) { return (
{props.post.authorPhoto && ( - + author photo )} - +
- ); -}; + ) +} -export const AuthorLines: React.FC<{ post: PostData }> = (props) => { +export function AuthorLines(props: { post: PostData }) { return (

@@ -31,18 +31,18 @@ export const AuthorLines: React.FC<{ post: PostData }> = (props) => { {props.post.authorTwitter && ( - {' '} + {" "} {`@${props.post.authorTwitter}`}{' '} + >{`@${props.post.authorTwitter}`}{" "} )}

{props.post.datePublished - ? format(new Date(props.post.datePublished), 'MMMM Do, YYYY') - : ''} + ? format(new Date(props.post.datePublished), "MMMM Do, YYYY") + : ""}

- ); -}; + ) +} diff --git a/components/BlogPost.tsx b/components/BlogPost.tsx index 22b3f32..01da22d 100644 --- a/components/BlogPost.tsx +++ b/components/BlogPost.tsx @@ -1,30 +1,28 @@ -import React from 'react'; -import { Author } from './Author'; -import { Markdown } from './Markdown'; -import { PostData } from '../loader'; -import { PostMeta } from './PostMeta'; +import React from "react" +import { PostData } from "../loader" +import { Author } from "./Author" +import { Markdown } from "./Markdown" +import { PostMeta } from "./PostMeta" -export const BlogPost: React.FunctionComponent<{ post: PostData }> = ({ - post, -}) => { - const { title, subtitle } = post; +export function BlogPost({ post }: { post: PostData }) { + const { title, subtitle } = post return (
- + {post.bannerPhoto && ( - + blog post cover )}
{title &&

{title}

} {subtitle &&

{subtitle}

} -
- +
+
- +
- ); -}; + ) +} diff --git a/components/Code.tsx b/components/Code.tsx index cb55e12..805f656 100644 --- a/components/Code.tsx +++ b/components/Code.tsx @@ -1,23 +1,17 @@ -import React from 'react'; -import darcula from 'react-syntax-highlighter/dist/cjs/styles/prism/darcula'; -import { PrismLight, PrismAsyncLight } from "react-syntax-highlighter" +import React from "react" +import { PrismAsyncLight, PrismLight } from "react-syntax-highlighter" +import darcula from "react-syntax-highlighter/dist/cjs/styles/prism/darcula" const SyntaxHighlighter = typeof window === "undefined" ? PrismLight : PrismAsyncLight -export default class Code extends React.PureComponent<{ - language: string; - value?: string; -}> { - render() { - const { language, value } = this.props; - return ( - - {value} - - ); - } -} +export default function Code({ language, value }: { language: string, value?: string }) { + return ( + + {value} + + ) +}; diff --git a/components/Footer.tsx b/components/Footer.tsx index 85da7f8..e98f074 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -1,11 +1,11 @@ -import React from 'react'; -import { globals } from '../globals'; +import React from "react" +import { globals } from "../globals" -export const Footer: React.FC = () => ( -
+export function Footer() { + return

{`© ${globals.yourName} ${new Date().getFullYear()}`}

- RSS Feed + RSS Feed
-); +} diff --git a/components/Header.tsx b/components/Header.tsx index 068193b..b171e68 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -1,11 +1,12 @@ -import React from 'react'; -import { globals } from '../globals'; +import React from "react" +import { globals } from "../globals" -export const Header: React.FC = () => ( -
+export function Header() { + return
{globals.siteName} -
+
GitHub Motivation
-); +} + diff --git a/components/Markdown.tsx b/components/Markdown.tsx index c0f8383..1fb5a93 100644 --- a/components/Markdown.tsx +++ b/components/Markdown.tsx @@ -1,10 +1,10 @@ -import React from 'react'; -import Code from './Code'; -import ReactMarkdown from 'react-markdown/with-html'; +import React from "react" +import ReactMarkdown from "react-markdown/with-html" +import Code from "./Code" -export const Markdown: React.FC<{ source: string }> = (props) => { +export function Markdown(props: { source: string }) { return ( -
+
= (props) => { escapeHtml={false} />
- ); -}; + ) +} diff --git a/components/Meta.tsx b/components/Meta.tsx index ec929bd..475250f 100644 --- a/components/Meta.tsx +++ b/components/Meta.tsx @@ -1,24 +1,24 @@ -import React from 'react'; -import NextHead from 'next/head'; -import { globals } from '../globals'; +import NextHead from "next/head" +import React from "react" +import { globals } from "../globals" -export const Meta: React.FC<{ +export function Meta(props: { meta: { title: string; link?: string; desc?: string; image?: string; }; -}> = (props) => { - const { meta } = props; +}) { + const { meta } = props return ( {meta.title} - - {meta.link && } - {meta.desc && } - - + + {meta.link && } + {meta.desc && } + + {meta.desc && ( )} - - {meta.link && } - - - {meta.desc && } - - - {meta.image && } - {meta.image && } + + {meta.link && } + + + {meta.desc && } + + + {meta.image && } + {meta.image && } - ); -}; + ) +} diff --git a/components/PostCard.tsx b/components/PostCard.tsx index cb4d7e0..9bc0839 100644 --- a/components/PostCard.tsx +++ b/components/PostCard.tsx @@ -1,10 +1,10 @@ -import React from 'react'; -import { format } from 'fecha'; -import { PostData } from '../loader'; -import { Tag } from './Tag'; +import { format } from "fecha" +import React from "react" +import { PostData } from "../loader" +import { Tag } from "./Tag" -export const PostCard: React.FC<{ post: PostData }> = (props) => { - const post = props.post; +export function PostCard(props: { post: PostData }) { + const post = props.post return (
@@ -19,17 +19,17 @@ export const PostCard: React.FC<{ post: PostData }> = (props) => { {false && post.subtitle &&

{post.subtitle}

}

{props.post.datePublished - ? format(new Date(props.post.datePublished), 'MMMM Do, YYYY') - : ''} + ? format(new Date(props.post.datePublished), "MMMM Do, YYYY") + : ""}

-
+
{false && (
- {post.tags && (post.tags || []).map((tag) => )} + {post.tags && (post.tags || []).map((tag) => )}
)}
- ); -}; + ) +} diff --git a/components/PostMeta.tsx b/components/PostMeta.tsx index 758f4d2..43396d0 100644 --- a/components/PostMeta.tsx +++ b/components/PostMeta.tsx @@ -1,8 +1,8 @@ -import React from 'react'; -import { PostData } from '../loader'; -import { Meta } from './Meta'; +import React from "react" +import { PostData } from "../loader" +import { Meta } from "./Meta" -export const PostMeta: React.FC<{ post: PostData }> = ({ post }) => { +export function PostMeta({ post }: { post: PostData }) { return ( = ({ post }) => { image: post.bannerPhoto, }} /> - ); -}; + ) +} diff --git a/components/Tag.tsx b/components/Tag.tsx index 206d8aa..5c0448c 100644 --- a/components/Tag.tsx +++ b/components/Tag.tsx @@ -1,7 +1,7 @@ -import React from 'react'; +import React from "react" -export const Tag: React.FC<{ tag: string }> = (props) => { +export function Tag(props: { tag: string }) { return (
{props.tag}
- ); -}; + ) +} diff --git a/globals.ts b/globals.ts index 04512a3..5851e0c 100644 --- a/globals.ts +++ b/globals.ts @@ -1,11 +1,11 @@ export namespace globals { - export const yourName = 'Alyssa P. Hacker'; - export const siteName = 'My Awesome Blog'; - export const siteDescription = 'I write about code \'n stuff'; - export const siteCreationDate = 'March 3, 2020 04:00:00 GMT'; - export const twitterHandle = '@alyssaphacker'; - export const email = 'alyssa@example.com'; - export const url = 'https://alyssaphacker.com'; - export const accentColor = '#4fc2b4'; + export const yourName = "Alyssa P. Hacker"; + export const siteName = "My Awesome Blog"; + export const siteDescription = "I write about code 'n stuff"; + export const siteCreationDate = "March 3, 2020 04:00:00 GMT"; + export const twitterHandle = "@alyssaphacker"; + export const email = "alyssa@example.com"; + export const url = "https://alyssaphacker.com"; + export const accentColor = "#4fc2b4"; export const googleAnalyticsId = ``; // e.g. 'UA-999999999-1' } diff --git a/loader.ts b/loader.ts index 308234e..9bb072b 100644 --- a/loader.ts +++ b/loader.ts @@ -1,6 +1,6 @@ -import matter from 'gray-matter'; -import glob from 'glob'; -import { globals } from './globals'; +import glob from "glob"; +import matter from "gray-matter"; +import { globals } from "./globals"; export type PostData = { path: string; @@ -21,14 +21,14 @@ export type PostData = { type RawFile = { path: string; contents: string }; -export const loadMarkdownFile = async (path: string): Promise => { +export async function loadMarkdownFile(path: string): Promise { const mdFile = await import(`./md/${path}`); return { path, contents: mdFile.default }; -}; +} -export const mdToPost = (file: RawFile): PostData => { +export function mdToPost(file: RawFile): PostData { const metadata = matter(file.contents); - const path = file.path.replace('.md', ''); + const path = file.path.replace(".md", ""); const post = { path, title: metadata.data.title, @@ -46,37 +46,33 @@ export const mdToPost = (file: RawFile): PostData => { content: metadata.content, }; - if (!post.title) - throw new Error(`Missing required field: title.`); + if (!post.title) throw new Error(`Missing required field: title.`); - if (!post.content) - throw new Error(`Missing required field: content.`); + if (!post.content) throw new Error(`Missing required field: content.`); - if (!post.datePublished) - throw new Error(`Missing required field: datePublished.`); + if (!post.datePublished) throw new Error(`Missing required field: datePublished.`); return post as PostData; -}; +} -export const loadMarkdownFiles = async (path: string) => { +export async function loadMarkdownFiles(path: string) { const blogPaths = glob.sync(`./md/${path}`); - const postDataList = await Promise.all( - blogPaths.map((blogPath) => { + return await Promise.all( + blogPaths.map(blogPath => { const modPath = blogPath.slice(blogPath.indexOf(`md/`) + 3); return loadMarkdownFile(`${modPath}`); - }) + }), ); - return postDataList; -}; +} -export const loadPost = async (path: string): Promise => { +export async function loadPost(path: string): Promise { const file = await loadMarkdownFile(path); return mdToPost(file); -}; +} -export const loadBlogPosts = async (): Promise => { - return await (await loadMarkdownFiles(`blog/*.md`)) +export async function loadBlogPosts(): Promise { + return (await loadMarkdownFiles(`blog/*.md`)) .map(mdToPost) - .filter((p) => p.published) + .filter(p => p.published) .sort((a, b) => (b.datePublished || 0) - (a.datePublished || 0)); -}; +} diff --git a/package.json b/package.json index 563846f..e8a3259 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "build": "next build", "start": "next start", "export": "next export", - "deploy": "next build && next export && firebase deploy --only hosting" + "deploy": "next build && next export && firebase deploy --only hosting", + "format": "prettier --write \"*.ts\"" }, "dependencies": { "fecha": "^4.2.0", diff --git a/public/rss.xml b/public/rss.xml index e09a989..7b972b1 100644 --- a/public/rss.xml +++ b/public/rss.xml @@ -1,22 +1,22 @@ -<![CDATA[My Awesome Blog]]>https://alyssaphacker.comhttps://alyssaphacker.com/icon.pngMy Awesome Bloghttps://alyssaphacker.comRSS for NodeSun, 01 Nov 2020 23:18:44 GMTTue, 03 Mar 2020 04:00:00 GMT60<![CDATA[Dan Abramov knows about Devii]]>Dan Abramov knows about Devii!

+<![CDATA[My Awesome Blog]]>https://alyssaphacker.comhttps://alyssaphacker.com/icon.pngMy Awesome Bloghttps://alyssaphacker.comRSS for NodeTue, 14 Sep 2021 10:09:52 GMTTue, 03 Mar 2020 04:00:00 GMT60<![CDATA[Dan Abramov knows about Devii]]>Dan Abramov knows about Devii!

Seems like it might be useful! — Dan Abramov, taken entirely out of context

-

I don't want to brag, but Devii is kind of a big deal.

]]>
https://vriad.com/blog/dan-abramovhttps://vriad.com/blog/dan-abramovFri, 10 Jul 2020 23:51:18 GMT
<![CDATA[Choosing a tech stack for my personal dev blog in 2020]]> -

Originally published at https://vriad.com/essays/devii. Check out the HN roast discussion here! 🤗

+

I don't want to brag, but Devii is kind of a big deal.

]]>
https://alyssaphacker.com/blog/dan-abramovhttps://alyssaphacker.com/blog/dan-abramovFri, 10 Jul 2020 23:51:18 GMT
<![CDATA[Choosing a tech stack for my personal dev blog in 2020]]> +

Originally published at https://colinhacks.com/essays/devii. Check out the HN roast discussion here! 🤗

I recently set out to build my personal website — the one you're reading now, as it happens!

Surprisingly, it was much harder than expected to put together a "tech stack" that met my criteria. My criteria are pretty straightforward; I would expect most React devs to have a similar list. Yet it was surprisingly hard to put all these pieces together.

Given the lack of a decent out-of-the-box solution, I worry that many developers are settling for static-site generators that place limits on the interactivity and flexibility of your website. We can do better.

-

Clone the repo here to get started with this setup: https://github.com/vriad/devii

+

Clone the repo here to get started with this setup: https://github.com/colinhacks/devii

Let's quickly run through my list of design goals:

React (+ TypeScript)

I want to build the site with React and TypeScript. I love them both wholeheartedly, I use them for my day job, and they're gonna be around for a long time. Plus writing untyped JS makes me feel dirty.

I don't want limitations on what my personal website can be/become. Sure, at present my site consists of two simple, static blog posts. But down the road, I may want to build a page that contains an interactive visualization, a filterable table, or a demo of a React component I'm open-sourcing. Even something simple (like the email newsletter signup form at the bottom of this page) was much more pleasant to implement in React; how did we use to build forms again?

-

Plus: I want access to the npm ecosystem and all my favorite UI, animation, and styling libraries. I sincerely hope I never write another line of raw CSS ever again; CSS-in-JS 4 lyfe baby. If you want to start a Twitter feud with me about this, by all means at me.

+

Plus: I want access to the npm ecosystem and all my favorite UI, animation, and styling libraries. I sincerely hope I never write another line of raw CSS ever again; CSS-in-JS 4 lyfe baby. If you want to start a Twitter feud with me about this, by all means at me.

Good authoring experience

If it's obnoxious to write new blog posts, I won't do it. That's a regrettable law of the universe. Even writing blog posts with plain HTML — just a bunch of <p> tags in a div — is just annoying enough to bug me. The answer: Markdown of course!

Static site generators (SSGs) like Hugo and Jekyll provide an undeniably wonderful authoring experience. All you have to do is touch a new .md file in the proper directory and get to writing. Unfortunately all Markdown-based SSGs I know of are too restrictive. Mixing React and Markdown on the same page is either impossible or tricky. If it's possible, it likely requires some plugin/module/extension, config file, blob of boilerplate, or egregious hack. Sorry Hugo, I'm not going to re-write my React code using React.createElement like it's 2015.

@@ -25,7 +25,7 @@

As much as I love the Jamstack, it doesn't cut it from an SEO perspective. Many blogs powered by a "headless CMS" require two round trips before rendering the blog content (one to fetch the static JS bundle and another to fetch the blog content from a CMS). This degrades page load speeds and user experience, which accordingly degrades your rankings on Google.

Instead I want every page of my site to be pre-rendered to a set of fully static assets, so I can deploy them to a CDN and get fast page loads everywhere. You could get the same benefits with server-side rendering, but that requires an actual server and worldwide load balancing to achieve comparable page load speeds. I love overengineering things as much as the next guy, even I have a line. 😅

My solution

-

I describe my final architecture design below, along with my rationale for each choice. I distilled this setup into a website starter/boilerplate available here: https://github.com/vriad/devii. Below, I allude to certain files/functions I implemented; to see the source code of these, just clone the repo git clone git@github.com:vriad/devii.git

+

I describe my final architecture design below, along with my rationale for each choice. I distilled this setup into a website starter/boilerplate available here: https://github.com/colinhacks/devii. Below, I allude to certain files/functions I implemented; to see the source code of these, just clone the repo git clone git@github.com:colinhacks/devii.git

Next.js

I chose to build my site with Next.js. This won't be a surprising decision to anyone who's played with statically-rendered or server-side rendered React in recent years. Next.js is quickly eating everyone else's lunch in this market, especially Gatsby's (sorry Gatsby fans).

Next.js is by far the most elegant way (for now) to do any static generation or server-side rendering with React. They just released their next-generation (pun intended) static site generator in the 9.3 release back in March. So in the spirit of using technologies in the spring of their life, Next.js is a no-brainer.

@@ -104,13 +104,13 @@ const test = (arg: string) => {

There's nothing "under the hood" here. You can view and modify all the files that provide the functionality described above. Devii just provides a project scaffold, some Markdown-loading loading utilities (in loader.ts), and some sensible styling defaults (especially in Markdown.tsx).

To start customizing, modify index.tsx (the home page), Essay.tsx (the blog post template), and Markdown.tsx (the Markdown renderer).

Get started

-

Head to the GitHub repo to get started: https://github.com/vriad/devii. If you like this project, leave a ⭐️star⭐️ to help more people find Devii! 😎

+

Head to the GitHub repo to get started: https://github.com/colinhacks/devii. If you like this project, leave a ⭐️star⭐️ to help more people find Devii! 😎

To jump straight into the code, clone the repo and start the development server like so:

-
git clone git@github.com:vriad/devii.git mysite
+
git clone git@github.com:colinhacks/devii.git mysite
 cd mysite
 yarn
 yarn dev
-
]]>https://vriad.com/blog/the-ultimate-tech-stackhttps://vriad.com/blog/the-ultimate-tech-stackTue, 26 May 2020 03:18:56 GMT<![CDATA[Devii's killer features]]>This page is built with Devii! Check out the source code for this under /md/blog/test.md.

+
]]>
https://alyssaphacker.com/blog/the-ultimate-tech-stackhttps://alyssaphacker.com/blog/the-ultimate-tech-stackTue, 26 May 2020 03:18:56 GMT
<![CDATA[Devii's killer features]]>This page is built with Devii! Check out the source code for this under /md/blog/test.md.

Devii is a starter kit for building a personal website with the best tools 2020 has to offer.

  • Markdown-based: Just add a Markdown file to /md/blog to add a new post to your blog!
  • @@ -128,4 +128,4 @@ yarn dev
    • Utterly customizable: We provide a minimal interface to get you started, but you can customize every aspect of the rendering and styling by just modifying index.tsx (the home page), BlogPost.tsx (the blog post template), and Markdown.tsx (the Markdown renderer). And of course you can add entirely new pages as well!
    -

    Head to the GitHub repo to get started: https://github.com/vriad/devii. If you like this project, leave a ⭐️star⭐️ to help more people find Devii 😎

    ]]>https://vriad.com/blog/deviihttps://vriad.com/blog/deviiSat, 09 May 2020 22:48:42 GMT \ No newline at end of file +

    Head to the GitHub repo to get started: https://github.com/colinhacks/devii. If you like this project, leave a ⭐️star⭐️ to help more people find Devii 😎

    ]]>https://alyssaphacker.com/blog/deviihttps://alyssaphacker.com/blog/deviiSat, 09 May 2020 22:48:42 GMT \ No newline at end of file diff --git a/rssUtil.ts b/rssUtil.ts index 5be790d..5602e9b 100644 --- a/rssUtil.ts +++ b/rssUtil.ts @@ -1,14 +1,14 @@ -import RSS from 'rss'; -import fs from 'fs'; -import showdown from 'showdown'; -import { globals } from './globals'; -import { PostData } from './loader'; +import RSS from "rss"; +import fs from "fs"; +import showdown from "showdown"; +import { globals } from "./globals"; +import { PostData } from "./loader"; export const generateRSS = async (posts: PostData[]) => { - posts.map((post) => { + posts.map(post => { if (!post.canonicalUrl) throw new Error( - "Missing canonicalUrl. A canonical URL is required for RSS feed generation. If you don't care about RSS, uncomment `generateRSS(posts)` at the bottom of index.tsx." + "Missing canonicalUrl. A canonical URL is required for RSS feed generation. If you don't care about RSS, uncomment `generateRSS(posts)` at the bottom of index.tsx.", ); return post; }); @@ -22,7 +22,7 @@ export const generateRSS = async (posts: PostData[]) => { managingEditor: globals.email, webMaster: globals.email, copyright: `${new Date().getFullYear()} ${globals.yourName}`, - language: 'en', + language: "en", pubDate: globals.siteCreationDate, ttl: 60, }); @@ -33,17 +33,15 @@ export const generateRSS = async (posts: PostData[]) => { const html = converter.makeHtml(post.content); if (!post.datePublished) { isValid = false; - console.warn( - 'All posts must have a publishedDate timestamp when generating RSS feed.' - ); - console.warn('Not generating rss.xml.'); + console.warn("All posts must have a publishedDate timestamp when generating RSS feed."); + console.warn("Not generating rss.xml."); } feed.item({ title: post.title, description: html, url: `${globals.url}/${post.path}`, categories: post.tags || [], - author: post.author || 'Colin McDonnell', + author: post.author || "Colin McDonnell", date: new Date(post.datePublished || 0).toISOString(), }); } @@ -52,6 +50,6 @@ export const generateRSS = async (posts: PostData[]) => { // writes RSS.xml to public directory const path = `${process.cwd()}/public/rss.xml`; - fs.writeFileSync(path, feed.xml(), 'utf8'); + fs.writeFileSync(path, feed.xml(), "utf8"); console.log(`generated RSS feed`); }; diff --git a/sitemap.ts b/sitemap.ts index 3e3b3d2..68fe203 100644 --- a/sitemap.ts +++ b/sitemap.ts @@ -1,18 +1,18 @@ -export const sitemap = ''; -import glob from 'glob'; -import { globals } from './globals'; -import { getStaticPaths as getBlogPaths } from './pages/blog/[blog]'; +export const sitemap = ""; +import glob from "glob"; +import { globals } from "./globals"; +import { getStaticPaths as getBlogPaths } from "./pages/blog/[blog]"; -export const generateSitemap = async () => { - const pagesDir = './pages/**/*.*'; - const posts = await glob.sync(pagesDir); +export async function generateSitemap() { + const pagesDir = "./pages/**/*.*"; + const posts = glob.sync(pagesDir); const pagePaths = posts - .filter((path) => !path.includes('[')) - .filter((path) => !path.includes('/_')) - .map((path) => path.slice(1)); + .filter(path => !path.includes("[")) + .filter(path => !path.includes("/_")) + .map(path => path.slice(1)); - const blogPaths = await getBlogPaths().paths; + const blogPaths = getBlogPaths().paths; const sitemap = ` @@ -21,7 +21,7 @@ export const generateSitemap = async () => { ${globals.url} 2020-06-01 -${[...pagePaths, ...blogPaths].map((path) => { +${[...pagePaths, ...blogPaths].map(path => { const item = [``]; item.push(` ${globals.url}${path}`); item.push(` 2020-06-01`); @@ -30,4 +30,4 @@ ${[...pagePaths, ...blogPaths].map((path) => { `; return sitemap; -}; +}