├── .gitignore ├── FUNDING.yml ├── LICENSE ├── README.md ├── components ├── Author.tsx ├── BlogPost.tsx ├── Code.tsx ├── Footer.tsx ├── Header.tsx ├── Markdown.tsx ├── Meta.tsx ├── PostCard.tsx ├── PostMeta.tsx └── Tag.tsx ├── globals.ts ├── index.html ├── loader.ts ├── md ├── blog │ ├── dan-abramov.md │ ├── devii.md │ └── the-ultimate-tech-stack.md ├── features.md └── introduction.md ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── blog │ └── [blog].tsx └── index.tsx ├── public ├── 404.html ├── favicon.ico ├── img │ ├── brook.jpg │ ├── brook_thumb.jpg │ ├── colin_square_small.jpg │ ├── danabramov.png │ ├── danabramov_thumb.png │ ├── pancakes.jpeg │ ├── pancakes_thumb.jpeg │ ├── profile.jpg │ └── rss-white.svg ├── index.html └── rss.xml ├── rssUtil.ts ├── sitemap.ts ├── styles └── base.css ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | /build/ 15 | /lib/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | .env* 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | firebase.json 30 | .firebaserc 31 | .firebase/* 32 | .nvmrc 33 | .vscode -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: colinhacks -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Colin McDonnell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Devii

2 |

3 | A developer blog starter for 2020.
Next.js
React
TypeScript
Markdown
syntax highlighting
SEO
RSS generation 4 |

5 |

6 | if you're happy and you know it, star this repo 7 |

8 |

9 | 10 | License 11 | 12 |

13 | 14 |
15 |
16 |
17 | 18 | ### A dev blog starter for 2020. 19 | 20 | - Works as a Markdown-based static-site generator out of the box: just add new blog posts to `/md/blog` 21 | - Supports exporting to fully static assets (powered by Next.js) 22 | - Hot reload (powered by Next.js) 23 | - Makes it easy to write custom pages/code in React + TypeScript 24 | - Provides a `Markdown.tsx` component with support for GitHub-style syntax highlighting 25 | - Automatic RSS feed generation 26 | - SEO best practices (title tag, meta tags, canonical URLs) 27 | 28 | Read more about the motivation + design behind Devii at [https://colinhacks.com/blog/devii](https://colinhacks.com/blog/devii). 29 | 30 | 39 | 40 | # Get started 41 | 42 | This repo contains the code for [https://devii.dev](https://devii.dev). 43 | 44 | devii.dev serves as both the documentation AND a working demo of Devii. After you clone/fork it, you can look through the code to learn how Devii works. Then you can rip out everything you don't like, customize everything else, and build your own tools and components on top of the foundation Devii provides! 45 | 46 | Your personal website is the online manifestation of you. Devii doesn't really provide much out of the box. It provides some nice Medium-style default styles for your blog posts and some tools for loading/rendering Markdown. But you'll have to implement your own homepage more or less from scratch. And that's the point! Don't settle for some theme. Build something that represents you. 47 | 48 | To get started: 49 | 50 | 1. Fork this repo 51 | 2. ``` 52 | git clone git@github.com:yourusername/devii.git my-blog 53 | cd my-blog 54 | yarn 55 | ``` 56 | 3. Start the development server with `yarn dev`. This should start a server on `http://localhost:3000`. 57 | 58 | ## Powered by Next.js 59 | 60 | The core of this repo is [Next.js](https://https://nextjs.org). We chose Next.js because it's the simplest, most elegant way to generate a static version of a React-based website. The documentation is excellent; read it first: [Next.js Documentation](https://nextjs.org/docs). 61 | 62 | ## Project structure 63 | 64 | Here's is an abbreviated version of the project structure. Certain config files (`next.config.js`, `next-end.d.ts`, `.gitignore`) have been removed for simplicity. 65 | 66 | ``` 67 | . 68 | ├── README.md 69 | ├── public // all static assets (images, css, etc) go here 70 | ├── pages // every .tsx component in this dir becomes a page of the final site 71 | | ├── index.tsx // the home page (which has access to the list of all blog posts) 72 | | ├── blog 73 | | ├── [blog].md // a template component that renders the blog posts under `/md/blog` 74 | ├── md 75 | | ├── blog 76 | | ├── devii.md // this page! 77 | ├── whatever.md // every MD file in this directory becomes a blog post 78 | ├── components 79 | | ├── BlogPost.tsx 80 | | ├── Code.tsx 81 | | ├── Footer.tsx 82 | | ├── Header.tsx 83 | | ├── Markdown.tsx 84 | | ├── Meta.tsx 85 | | ├── 86 | ├── loader.ts // contains utility functions for loading/parsing Markdown 87 | ├── node_modules 88 | ├── tsconfig.json 89 | ├── package.json 90 | ``` 91 | 92 | Next.js generates a new webpage for each file in the `pages` directory. If you want to add an About page to your blog, just add `about.tsx` inside `pages` and start writing the page. 93 | 94 | By default the repo only contains two pages: a home page (`/pages/index.tsx`) and a blog page (`/pages/[blog].md`). 95 | 96 | The file `[blog].ts` follows the Next.js convention of using square brackets to indicate a [dynamic route](https://nextjs.org/docs/routing/dynamic-routes). 97 | 98 | ## The home page 99 | 100 | The home page is intentionally minimal. You can put whatever you want in `index.tsx`; one of our goals in designing Devii was to place no restrictions on the developer. Use your imagination! Your website is the online manifestion of you. You can use whatever npm packages or styling libraries you like. 101 | 102 | ## Styling 103 | 104 | Devii is unopinionated about styling. Because your Devii site is a standard React app under the hood, you can use your favorite library from `npm` to do styling. 105 | 106 | Devii provides certain styles by default, notably in the Markdown renderer (`/components/Markdown.tsx`). Those styles are implemented using Next's built-in styling solution `styled-jsx`. Unfortunately it was necessary to make those styles global, since `styled-jsx` [doesn't play nice](https://github.com/vercel/styled-jsx/issues/573) with third-party components (in this case `react-markdown`). 107 | 108 | Feel free to re-implemement the built-in styles with your library of choice If you choose to use a separate styling library ([emotion](https://emotion.sh/) is pretty glorious) then you could re-implement the default styles 109 | 110 | ## Adding a new blog post 111 | 112 | Just add a Markdown file under `md/blog/` to create a new blog post: 113 | 114 | 1. Create a new Markdown file called `foo.md` within the `/md/blog` directory 115 | 2. Add in some basic Markdown content 116 | 3. Then go to `http://localhost:3000/blog/foo`. You should see the new post. 117 | 118 | ## Frontmatter support 119 | 120 | Every Markdown file can include a "frontmatter block" containing various metadata. Devii provides a `loadPost` utility that loads a Markdown file, parses it's frontmatter metadata, and returns a structured `PostData` object: 121 | 122 | ```ts 123 | type PostData = { 124 | path: string; 125 | title?: string; 126 | subtitle?: string; 127 | description?: string; // used for SEO 128 | canonicalUrl?: string; // used for SEO 129 | datePublished?: number; // Unix timestamp 130 | author?: string; 131 | authorPhoto?: string; 132 | authorHandle?: string; // twitter handle 133 | tags?: string[]; 134 | bannerPhoto?: string; 135 | thumbnailPhoto?: string; 136 | }; 137 | ``` 138 | 139 | For example, here is the frontmatter blog from the sample blog post (`md/blog/the-ultimate-tech-stack.md`): 140 | 141 | ``` 142 | --- 143 | title: Introducing Devii 144 | subtitle: Bringing the power of React, TypeScript, and static generation to dev blogs everywhere 145 | datePublished: 1589064522569 146 | author: Ben Bitdiddle 147 | tags: 148 | - Devii 149 | - Blogs 150 | authorPhoto: /img/profile.jpg 151 | bannerPhoto: /img/brook.jpg 152 | thumbnailPhoto: /img/brook.jpg 153 | --- 154 | ``` 155 | 156 | View `/loader.ts` to see how this works. 157 | 158 | ## Google Analytics 159 | 160 | Just add your Google Analytics ID (e.g. 'UA-999999999-1') to `globals.ts` and Devii will automatically add the appropriate Google Analytics snippet to your site. Go to `/pages/_app.ts` to see how this works or customize this behavior. 161 | 162 | ## Medium-inspired design 163 | 164 | The Markdown renderer (`Markdown.tsx`) provides a default style inspired by Medium. Just modify the CSS in `Markdown.tsx` to customize the design to your liking. 165 | 166 | ## GitHub-style code blocks 167 | 168 | You can easily drop code blocks into your blog posts using triple-backtick syntax ([just like GitHub](https://help.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks)). No more embedding CodePen iframes! 🚀 169 | 170 | Works out-of-the-box for all programming languages. Specify your language with a "language tag". So this: 171 | 172 |
173 |   ```ts
174 |   // pretty neat huh?
175 |   const test = (arg: string) => {
176 |     return arg.length > 5;
177 |   };
178 |   ```
179 | 180 | turns into 181 | 182 | ```ts 183 | // pretty neat huh? 184 | const test = (arg: string) => { 185 | return arg.length > 5; 186 | }; 187 | ``` 188 | 189 | View `/components/Code.tsx` to see how this works or customize this behavior. 190 | 191 | ## Markdown loading 192 | 193 | _You don't need to understand all of this to use Devii. Consider this an "advanced guide" you can use if you want to customize the structure of the site._ 194 | 195 | Markdown posts are loaded during Next.js static build step. Check out the [Data Fetching](https://nextjs.org/docs/basic-features/data-fetching) documentation to learn more about this. 196 | 197 | Here's the short version: if export a function called `getStaticProps` from one of your page components, Next.js will execute that function, take the result, and pass the `props` property (which should be another object) into your page as props. 198 | 199 | You can dynamically load and parse a Markdown file using `loadMarkdownFile`, a utility function implemented in `loader.ts`. It is an async function that returns a `PostData` TypeScript object containing all the metadata keys listed above: 200 | 201 | For an example of this, check out the `getStaticProps` implementation from the homepage. The function calls `loadBlogPosts` - a utilty function that loads _every_ blog posts in the `/md/blog/` directory, parses them, and returns `PostData[]`. 202 | 203 | ```ts 204 | export const getStaticProps = async () => { 205 | const posts = await loadBlogPosts(); 206 | return { props: { posts } }; 207 | }; 208 | ``` 209 | 210 | There are a few utility functions in `loader.ts` that Devii uses. All functions are _async_! All functions accept a _relative_ path which is expected to be \_relative to the `md/` directory. For instance `loadPost('blog/test.md'`) would load `/md/blog/test.md`. 211 | 212 | - `loadPost` loads/parses a Markdown file and returns a `PostData` 213 | - `loadBlogPosts`: loads/parses all the files in `/md/blog/`. Returns `PostData[]`. Used in `index.tsx` to load/render a list of all published blog posts 214 | - `loadMarkdownFile`: loads a Markdown file but doesn't parse it. Returns the string content. Useful if you want to implement some parts of a page in Markdown and other parts in React 215 | - `loadMarkdownFiles`: accepts a [glob](https://docs.python.org/3/library/glob.html) pattern and loads all the files inside `/md/` whose names match the pattern. Used internally by `loadBlogPosts` 216 | 217 | ## Static generation 218 | 219 | You can generate a fully static version of your site using `yarn build && yarn export`. This step is entirely powered by Next.js. The static site is exported to the `out` directory. 220 | 221 | After it's generated, use your static file hosting service of choice (Vercel, Netlify, Firebase Hosting, Amazon S3) to deploy your site. 222 | 223 | ## Global configs 224 | 225 | There is a `globals.ts` file in the project root containing some settings/configuration metadata about your site: 226 | 227 | - `yourName`: Your name, used for the copyright tags in the footer and the RSS feed, e.g. Alyssa P. Hacker 228 | - `siteName`: The title of your blog, e.g. `Alyssa's Cool Blog`; 229 | - `siteDescription`: A short description, used in the `meta` description tag, e.g. 'I write about code \'n stuff'; 230 | - `siteCreationDate`: Used in the generated RSS feed. Use this format: 'March 3, 2020 04:00:00 GMT'; 231 | - `twitterHandle`: The twitter handle for you or your blog/company, used in the Twitter meta tags. Include the @ symbol, e.g. '@alyssaphacker'; 232 | - `email`: Your email, used as the "webMaster" and "managingEditor" field of the generated RSS feed, e.g. `alyssa@example.com`; 233 | - `url`: The base URL of your website, used to "compute" default canonical links from relative paths, e.g. 'https://alyssaphacker.com'; 234 | - `accentColor`: The header and footer background color, e.g. `#4fc2b4`; 235 | 236 | ## RSS feed generation 237 | 238 | An RSS feed is auto-generated from your blog post feed. This feed is generated using the `rss` module (for converting JSON to RSS format) and `showdown` for converting the markdown files to RSS-compatible HTML. 239 | 240 | For RSS generation to work, all your posts must contain a `datePublished` timestamp in their frontmatter metadata. To examine or customize the RSS generation, check out the `rssUtil.ts` file in the root directory. 241 | 242 | ## SEO 243 | 244 | Every blog post page automatically populated meta tags based on the post metadata. This includes a `title` tag, `meta` tags, `og:` tags, Twitter metadata, and a `link` tag containing the canonical URL. 245 | 246 | The default value of the canonical URL is computed by concatenating the value of your `url` config (see Global Configs above) and the relative path of your post. Verify that the canonical URL is exactly equivalent to the URL in the browser when visiting your live site, otherwise your site's SEO may suffer. 247 | 248 | ## Insanely customizable 249 | 250 | There's nothing "under the hood" here. You can view and modify all the files that provide the functionality listed above. Devii just provides a project scaffold, some Markdown-loading loading utilities (in `loader.ts`), and some sensible styling defaults (especially in `Markdown.tsx`). 251 | 252 | To get started customizing, check out the source code of `index.tsx` (the home page), `BlogPost.tsx` (the blog post template), and `Markdown.tsx` (the Markdown renderer). 253 | 254 | Head to the GitHub repo to get started: [https://github.com/colinhacks/devii](https://github.com/colinhacks/devii). If you like this project, leave a ⭐️star⭐️ to help more people find Devii 😎 255 | 256 | # CLI 257 | 258 | ### `yarn dev` 259 | 260 | Starts the development server. Equivalent to `next dev`. 261 | 262 | ### `yarn build` 263 | 264 | Creates an optimized build of your site. Equivalent to `next build`. 265 | 266 | ### `yarn export` 267 | 268 | Exports your site to static files. All files are written to `/out`. Use your static file hosting service of choice (Firebase Hosting, Amazon S3, Vercel) to deploy your site. Equivalent to `next export`. 269 | -------------------------------------------------------------------------------- /components/Author.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { format } from 'fecha'; 3 | import { PostData } from '../loader'; 4 | 5 | export const FollowButton = () => { 6 | return ( 7 | 8 |
Follow
9 |
10 | ); 11 | }; 12 | 13 | export const Author: React.FC<{ post: PostData }> = (props) => { 14 | return ( 15 |
16 |
17 | {props.post.authorPhoto && ( 18 | 19 | )} 20 | 21 |
22 |
23 | ); 24 | }; 25 | 26 | export const AuthorLines: React.FC<{ post: PostData }> = (props) => { 27 | return ( 28 |
29 |

30 | {props.post.author && {props.post.author}} 31 | 32 | {props.post.authorTwitter && ( 33 | 34 | {' '} 35 | {`@${props.post.authorTwitter}`}{' '} 38 | 39 | )} 40 |

41 |

42 | {props.post.datePublished 43 | ? format(new Date(props.post.datePublished), 'MMMM Do, YYYY') 44 | : ''} 45 |

46 |
47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /components/BlogPost.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Author } from './Author'; 3 | import { Markdown } from './Markdown'; 4 | import { PostData } from '../loader'; 5 | import { PostMeta } from './PostMeta'; 6 | 7 | export const BlogPost: React.FunctionComponent<{ post: PostData }> = ({ 8 | post, 9 | }) => { 10 | const { title, subtitle } = post; 11 | return ( 12 |
13 | 14 | {post.bannerPhoto && ( 15 | 16 | )} 17 | 18 |
19 | {title &&

{title}

} 20 | {subtitle &&

{subtitle}

} 21 |
22 | 23 |
24 | 25 |
26 | 27 |
28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /components/Code.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import darcula from 'react-syntax-highlighter/dist/cjs/styles/prism/darcula'; 3 | import { PrismLight, PrismAsyncLight } from "react-syntax-highlighter" 4 | 5 | const SyntaxHighlighter = 6 | typeof window === "undefined" ? PrismLight : PrismAsyncLight 7 | 8 | export default class Code extends React.PureComponent<{ 9 | language: string; 10 | value?: string; 11 | }> { 12 | render() { 13 | const { language, value } = this.props; 14 | return ( 15 | 19 | {value} 20 | 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { globals } from '../globals'; 3 | 4 | export const Footer: React.FC = () => ( 5 |
6 |

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

7 | 8 | RSS Feed 9 | 10 |
11 | ); 12 | -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { globals } from '../globals'; 3 | 4 | export const Header: React.FC = () => ( 5 |
6 | {globals.siteName} 7 |
8 | GitHub 9 | Motivation 10 |
11 | ); 12 | -------------------------------------------------------------------------------- /components/Markdown.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Code from './Code'; 3 | import ReactMarkdown from 'react-markdown/with-html'; 4 | 5 | export const Markdown: React.FC<{ source: string }> = (props) => { 6 | return ( 7 |
8 | 16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /components/Meta.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NextHead from 'next/head'; 3 | import { globals } from '../globals'; 4 | 5 | export const Meta: React.FC<{ 6 | meta: { 7 | title: string; 8 | link?: string; 9 | desc?: string; 10 | image?: string; 11 | }; 12 | }> = (props) => { 13 | const { meta } = props; 14 | return ( 15 | 16 | {meta.title} 17 | 18 | {meta.link && } 19 | {meta.desc && } 20 | 21 | 22 | {meta.desc && ( 23 | 28 | )} 29 | 30 | {meta.link && } 31 | 32 | 33 | {meta.desc && } 34 | 35 | 36 | {meta.image && } 37 | {meta.image && } 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /components/PostCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { format } from 'fecha'; 3 | import { PostData } from '../loader'; 4 | import { Tag } from './Tag'; 5 | 6 | export const PostCard: React.FC<{ post: PostData }> = (props) => { 7 | const post = props.post; 8 | return ( 9 | 10 |
11 | {post.thumbnailPhoto && ( 12 |
16 | )} 17 |
18 | {post.title &&

{post.title}

} 19 | {false && post.subtitle &&

{post.subtitle}

} 20 |

21 | {props.post.datePublished 22 | ? format(new Date(props.post.datePublished), 'MMMM Do, YYYY') 23 | : ''} 24 |

25 |
26 | {false && ( 27 |
28 | {post.tags && (post.tags || []).map((tag) => )} 29 |
30 | )} 31 |
32 |
33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /components/PostMeta.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PostData } from '../loader'; 3 | import { Meta } from './Meta'; 4 | 5 | export const PostMeta: React.FC<{ post: PostData }> = ({ post }) => { 6 | return ( 7 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /components/Tag.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Tag: React.FC<{ tag: string }> = (props) => { 4 | return ( 5 |
{props.tag}
6 | ); 7 | }; 8 | -------------------------------------------------------------------------------- /globals.ts: -------------------------------------------------------------------------------- 1 | export namespace globals { 2 | export const yourName = 'Alyssa P. Hacker'; 3 | export const siteName = 'My Awesome Blog'; 4 | export const siteDescription = 'I write about code \'n stuff'; 5 | export const siteCreationDate = 'March 3, 2020 04:00:00 GMT'; 6 | export const twitterHandle = '@alyssaphacker'; 7 | export const email = 'alyssa@example.com'; 8 | export const url = 'https://alyssaphacker.com'; 9 | export const accentColor = '#4fc2b4'; 10 | export const googleAnalyticsId = ``; // e.g. 'UA-999999999-1' 11 | } 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | user/repo 6 | 7 | 8 | 12 | 16 | 17 | 18 | 23 |
24 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /loader.ts: -------------------------------------------------------------------------------- 1 | import matter from 'gray-matter'; 2 | import glob from 'glob'; 3 | import { globals } from './globals'; 4 | 5 | export type PostData = { 6 | path: string; 7 | title: string; 8 | subtitle?: string; 9 | content: string; 10 | description?: string; 11 | canonicalUrl?: string; 12 | published: boolean; 13 | datePublished: number; 14 | author?: string; 15 | authorPhoto?: string; 16 | authorTwitter?: string; 17 | tags?: string[]; 18 | bannerPhoto?: string; 19 | thumbnailPhoto?: string; 20 | }; 21 | 22 | type RawFile = { path: string; contents: string }; 23 | 24 | export const loadMarkdownFile = async (path: string): Promise => { 25 | const mdFile = await import(`./md/${path}`); 26 | return { path, contents: mdFile.default }; 27 | }; 28 | 29 | export const mdToPost = (file: RawFile): PostData => { 30 | const metadata = matter(file.contents); 31 | const path = file.path.replace('.md', ''); 32 | const post = { 33 | path, 34 | title: metadata.data.title, 35 | subtitle: metadata.data.subtitle || null, 36 | published: metadata.data.published || false, 37 | datePublished: metadata.data.datePublished || null, 38 | tags: metadata.data.tags || null, 39 | description: metadata.data.description || null, 40 | canonicalUrl: metadata.data.canonicalUrl || `${globals.url}/${path}`, 41 | author: metadata.data.author || null, 42 | authorPhoto: metadata.data.authorPhoto || null, 43 | authorTwitter: metadata.data.authorTwitter || null, 44 | bannerPhoto: metadata.data.bannerPhoto || null, 45 | thumbnailPhoto: metadata.data.thumbnailPhoto || null, 46 | content: metadata.content, 47 | }; 48 | 49 | if (!post.title) 50 | throw new Error(`Missing required field: title.`); 51 | 52 | if (!post.content) 53 | throw new Error(`Missing required field: content.`); 54 | 55 | if (!post.datePublished) 56 | throw new Error(`Missing required field: datePublished.`); 57 | 58 | return post as PostData; 59 | }; 60 | 61 | export const loadMarkdownFiles = async (path: string) => { 62 | const blogPaths = glob.sync(`./md/${path}`); 63 | const postDataList = await Promise.all( 64 | blogPaths.map((blogPath) => { 65 | const modPath = blogPath.slice(blogPath.indexOf(`md/`) + 3); 66 | return loadMarkdownFile(`${modPath}`); 67 | }) 68 | ); 69 | return postDataList; 70 | }; 71 | 72 | export const loadPost = async (path: string): Promise => { 73 | const file = await loadMarkdownFile(path); 74 | return mdToPost(file); 75 | }; 76 | 77 | export const loadBlogPosts = async (): Promise => { 78 | return await (await loadMarkdownFiles(`blog/*.md`)) 79 | .map(mdToPost) 80 | .filter((p) => p.published) 81 | .sort((a, b) => (b.datePublished || 0) - (a.datePublished || 0)); 82 | }; 83 | -------------------------------------------------------------------------------- /md/blog/dan-abramov.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dan Abramov knows about Devii 3 | published: true 4 | datePublished: 1594425078471 5 | author: Colin McDonnell 6 | tags: 7 | - Dan Abramov 8 | authorPhoto: /img/profile.jpg 9 | bannerPhoto: /img/danabramov.png 10 | thumbnailPhoto: /img/danabramov_thumb.png 11 | canonicalUrl: https://devii.dev/blog/dan-abramov 12 | --- 13 | 14 | Dan Abramov knows about Devii! 15 | 16 | > Seems like it might be useful! 17 | > — Dan Abramov, taken entirely out of context 18 | 19 | I don't want to brag, but Devii is kind of a big deal. 20 | -------------------------------------------------------------------------------- /md/blog/devii.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Devii's killer features 3 | subtitle: Bringing the power of React, TypeScript, and static generation to dev blogs everywhere 4 | published: true 5 | datePublished: 1589064522569 6 | author: Ben Bitdiddle 7 | tags: 8 | - Devii 9 | - Blogs 10 | authorPhoto: /img/profile.jpg 11 | bannerPhoto: /img/brook.jpg 12 | thumbnailPhoto: /img/brook.jpg 13 | canonicalUrl: https://devii.dev/blog/devii 14 | --- 15 | 16 | This page is built with Devii! Check out the source code for this under `/md/blog/test.md`. 17 | 18 | Devii is a starter kit for building a personal website with the best tools 2020 has to offer. 19 | 20 | - **Markdown-based**: Just add a Markdown file to `/md/blog` to add a new post to your blog! 21 | - **TypeScript + React**: aside from the parts that are rendered Markdown, everything else is fully built with TypeScript and functional React components. Implementing any sort of interactive widget is often hard using existing Markdown-centric static-site generators, but Devii makes it easy to mix Markdown and React on the same page. 22 | - **Frontmatter support**: Every post can include a frontmatter block containing metadata: `title`, `subtitle`, `datePublished` (timestamp), `author`, `authorPhoto`, and `bannerPhoto`. 23 | - **Medium-inspired styles**: The Markdown renderer (`Markdown.tsx`) contains default styles inspired by Medium. 24 | - **Static generation**: you can generate a fully static version of your site using `yarn build && yarn export`. Powered by Next.js. 25 | - **GitHub-style code blocks**: with syntax highlighting powered by [react-syntax-highlighter](https://github.com/conorhastings/react-syntax-highlighter). Works out-of-the-box for all programming languages. Just use Markdown's triple backtick syntax with a "language identifier", [just like GitHub](https://help.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks). 26 | 27 | ```ts 28 | // pretty neat huh? 29 | const test: (arg: string) => boolean = (arg) => { 30 | return arg.length > 5; 31 | }; 32 | ``` 33 | 34 | - **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! 35 | 36 | Head to the GitHub repo to get started: [https://github.com/colinhacks/devii](https://github.com/colinhacks/devii). If you like this project, leave a ⭐️star⭐️ to help more people find Devii 😎 37 | -------------------------------------------------------------------------------- /md/blog/the-ultimate-tech-stack.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Choosing a tech stack for my personal dev blog in 2020 3 | published: true 4 | datePublished: 1590463136775 5 | author: Colin McDonnell 6 | authorTwitter: colinhacks 7 | authorPhoto: /img/profile.jpg 8 | tags: 9 | - Static Site Generators 10 | - React 11 | - Next.js 12 | thumbnailPhoto: /img/pancakes_thumb.jpeg 13 | bannerPhoto: /img/pancakes.jpeg 14 | canonicalUrl: https://colinhacks.com/essays/devii 15 | --- 16 | 17 | > Originally published at [https://colinhacks.com/essays/devii](https://colinhacks.com/essays/devii). Check out the HN roast discussion here! 🤗 18 | 19 | I recently set out to build my personal website — the one you're reading now, as it happens! 20 | 21 | 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. 22 | 23 | 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. 24 | 25 | > Clone the repo here to get started with this setup: https://github.com/colinhacks/devii 26 | 27 | Let's quickly run through my list of design goals: 28 | 29 | ### React (+ TypeScript) 30 | 31 | 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. 32 | 33 | 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? 34 | 35 | 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](https://twitter.com/colinhacks). 36 | 37 | ### Good authoring experience 38 | 39 | 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 `

` tags in a div — is just annoying enough to bug me. The answer: Markdown of course! 40 | 41 | 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. 42 | 43 | Well, that doesn't work for me. I want my website to be React-first, with a sprinkling of Markdown when it makes my life easier. 44 | 45 | ### Static generation 46 | 47 | 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. 48 | 49 | 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. 😅 50 | 51 | ## My solution 52 | 53 | 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` 54 | 55 | ### Next.js 56 | 57 | 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). 58 | 59 | 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](https://nextjs.org/blog/next-9-3) back in March. So in the spirit of using technologies [in the spring of their life](https://www.youtube.com/watch?v=eBAX8MbRYFA), Next.js is a no-brainer. 60 | 61 | Here's a quick breakdown of the project structure. No need to understand every piece of it; but it may be useful to refer to throughout the rest of this post. 62 | 63 | ``` 64 | . 65 | ├── README.md 66 | ├── public // all static files (images, etc) go here 67 | ├── pages // every .tsx component in this dir becomes a page of the final site 68 | | ├── index.tsx // the home page (which has access to the list of all blog posts) 69 | | ├── blog 70 | | ├── [blog].md // a template component that renders the blog posts under `/md/blog` 71 | ├── md 72 | | ├── blog 73 | | ├── devii.md // this page! 74 | ├── whatever.md // every MD file in this directory becomes a blog post 75 | ├── components 76 | | ├── Code.tsx 77 | | ├── Markdown.tsx 78 | | ├── 79 | ├── loader.ts // contains utility functions for loading/parsing Markdown 80 | ├── node_modules 81 | ├── tsconfig.json 82 | ├── package.json 83 | ├── next.config.js 84 | ├── next-env.d.ts 85 | ├── .gitignore 86 | ``` 87 | 88 | 89 | 90 | ### TypeScript + React 91 | 92 | Both React and TypeScript are baked into the DNA of Next.js, so you get these for free when you set up a Next.js project. 93 | 94 | Gatsby, on the other hand, has a special plugin for TypeScript support, but it's not officially supported and seems to be [low on their priority list](https://github.com/gatsbyjs/gatsby/issues/18983). Also, after messing with it for an hour I couldn't get it to play nice with hot reload. 95 | 96 | ### Markdown authoring 97 | 98 | Using Next's special `getStaticProps` hook and glorious [dynamic imports](https://nextjs.org/docs/advanced-features/dynamic-import#with-no-ssr), it's trivial to a Markdown file and pass its contents into your React components as a prop. This achieves the holy grail I was searching for: the ability to easily mix React and Markdown. 99 | 100 | #### Frontmatter support 101 | 102 | Every Markdown file can include a "frontmatter block" containing metadata. I implemented a simple utility function (`loadPost`) that loads a Markdown file, parses its contents, and returns a TypeScript object with the following signature: 103 | 104 | ```ts 105 | type PostData = { 106 | path: string; // the relative URL to this page, can be used as an href 107 | content: string; // the body of the MD file 108 | title?: string; 109 | subtitle?: string; 110 | date?: number; 111 | author?: string; 112 | authorPhoto?: string; 113 | authorTwitter?: string; 114 | tags?: string[]; 115 | bannerPhoto?: string; 116 | thumbnailPhoto?: string; 117 | }; 118 | ``` 119 | 120 | I implemented a separate function `loadPosts` that loads _all_ the Markdown files under `/md/blog` and returns them as an array (`PostData[]`). I use `loadPosts` on this site's home page to render a list of all posts I've written. 121 | 122 | ### Medium-inspired design 123 | 124 | I used the wonderful [`react-markdown`](https://github.com/rexxars/react-markdown) package to render Markdown as a React component. My Markdown rendered component (`/components/Markdown.tsx`) provides some default styles inspired by Medium's design. Just modify the `style` pros in `Markdown.tsx` to customize the design to your liking. 125 | 126 | ### GitHub-style code blocks 127 | 128 | You can easily drop code blocks into your blog posts using triple-backtick syntax. Specify the programming language with a "language tag", [just like GitHub](https://help.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks)! 129 | 130 | To achieve this I implemented a custom `code` renderer (`/components/Code.tsx`) for `react-markdown` that uses [react-syntax-highlighter](https://github.com/conorhastings/react-syntax-highlighter#readme) to handle the highlighting. So this: 131 | 132 | 133 | 134 |

135 | ```ts
136 | // pretty neat huh?
137 | const test = (arg: string) => {
138 |   return arg.length > 5;
139 | };
140 | ```
141 | 142 | turns into this: 143 | 144 | ```ts 145 | // pretty neat huh? 146 | const test = (arg: string) => { 147 | return arg.length > 5; 148 | }; 149 | ``` 150 | 151 | ### RSS feed generation 152 | 153 | An RSS feed is auto-generated from your blog post feed. This feed is generated using the `rss` module (for converting JSON to RSS format) and `showdown` for converting the markdown files to RSS-compatible HTML. The feed is generated during the build step and written as a static file to `/rss.xml` in your static assets folder. It's dead simple. That's the joy of being able to easily write custom build scripts on top of Next.js's `getStaticProps` hooks! 154 | 155 | ### SEO 156 | 157 | Every blog post page automatically populated meta tags based on the post metadata. This includes a `title` tag, `meta` tags, `og:` tags, Twitter metadata, and a `link` tag containing the canonical URL. You can modify/augment this in the `PostMeta.ts` component. 158 | 159 | ### Static generation 160 | 161 | You can generate a fully static version of your site using `yarn build && yarn export`. This step is entirely powered by Next.js. The static site is exported to the `out` directory. 162 | 163 | After its generated, use your static file hosting service of choice (Firebase Hosting, Vercel, Netlify) to deploy your site. 164 | 165 | ### Insanely customizable 166 | 167 | 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`). 168 | 169 | To start customizing, modify `index.tsx` (the home page), `Essay.tsx` (the blog post template), and `Markdown.tsx` (the Markdown renderer). 170 | 171 | ## Get started 172 | 173 | Head to the GitHub repo to get started: [https://github.com/colinhacks/devii](https://github.com/colinhacks/devii). If you like this project, leave a ⭐️star⭐️ to help more people find Devii! 😎 174 | 175 | To jump straight into the code, clone the repo and start the development server like so: 176 | 177 | ```bash 178 | git clone git@github.com:colinhacks/devii.git mysite 179 | cd mysite 180 | yarn 181 | yarn dev 182 | ``` 183 | -------------------------------------------------------------------------------- /md/features.md: -------------------------------------------------------------------------------- 1 | It may not look like much, but Devii does a lot out of the box. 2 | 3 | **Markdown loading and rendering**: Using Next.js dynamic imports, you can load Markdown files and pass them into your Next.js pages as props. Easy peasy. 4 | 5 | **TypeScript + React**: Markdown is great for text-heavy, non-interactive content. For everything else, you'll want something a little more expressive. Devii makes it easy to mix Markdown and React on the same page. Just load your Markdown files with dynamic imports, pass it into your component as a prop, and render it with the `Markdown.tsx` component. 6 | 7 | **Built-in support for blogs**: Devii provides a utility for parsing Markdown blog posts with frontmatter metadata into a structured TypeScript object. Supported tags include: `title`, `subtitle`, `datePublished`, `tags`, `description`, `canonicalUrl`, `author`, `authorPhoto`, `authorTwitter`, `bannerPhoto`, and `thumbnailPhoto` 8 | 9 | **Medium-inspired styles**: The Markdown components (`Markdown.tsx`) contains default styles inspired by Medium. 10 | 11 | **Google Analytics**: Just add your Google Analytics ID (e.g. 'UA-999999999-1') to `globals.ts` and the appropriate snippet will be automatically added to every page. 12 | 13 | **RSS feed generation**: An RSS feed is auto-generated from your blog post feed. 14 | 15 | **SEO best practices**: Every blog post page automatically populated meta tags based on the post metadata. This includes a `title` tag, `meta` tags, `og:` tags, Twitter metadata, and a `link` tag containing the canonical URL. 16 | 17 | **GitHub-style code blocks**: with syntax highlighting powered by [react-syntax-highlighter](https://github.com/conorhastings/react-syntax-highlighter). Works out-of-the-box for all programming languages. Just use Markdown's triple backtick syntax with a "language identifier", [just like GitHub](https://help.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks). 18 | 19 | **Static generation**: you can generate a fully static version of your site using `yarn build && yarn export`. Powered by Next.js. 20 | 21 | **Zero magic**: You can view and modify every aspect of the site. If you're looking for a starting point, start modifying `index.tsx` (the home page), `BlogPost.tsx` (the blog post template), and `Markdown.tsx` (the Markdown component). And of course you can add entirely new pages/components as well! 22 | -------------------------------------------------------------------------------- /md/introduction.md: -------------------------------------------------------------------------------- 1 | Devii is a starter kit for building your personal developer website. Powered by the best technologies 2020 has to offer. 2 | 3 | It's not a a framework or a library, it's a just a simple project that contains some useful utilities and patterns that'll help you hit the ground running. In fact, the [GitHub repo for Devii](https://github.com/colinhacks/devii) contains the code for the site you're currently reading! 4 | 5 | Devii doesn't try to be a fully functional blog out of the box. After cloning/forking the repo, you'll need to delete the contents of `index.tsx` (the page you're reading now!) and implement your own homepage. Devii makes it easier — for instance, you can access a list of all your blog posts in `props.posts` — but you still have to build the site you're imagining in your mind's eye. 6 | 7 | And that's the point! After you clone/fork it, look through this code to learn how Devii works. Then rip out what you don't like, customize everything else, and build your own tools and components on top of the foundation Devii provides! 8 | 9 | Devii was designed to place _zero restrictions_ on what your site can be or become. You can use any React component or styling library, pull in data from third-party APIs, even implement user accounts. Your personal website is the online manifestation of you. Don't compromise. 10 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingSlash: true, 3 | webpack: function (config) { 4 | config.module.rules.push({ 5 | test: /\.md$/, 6 | use: 'raw-loader', 7 | }); 8 | config.resolve.alias = { 9 | ...config.resolve.alias, 10 | '~': __dirname, 11 | }; 12 | return config; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devii", 3 | "version": "0.5.0", 4 | "description": "A Medium inspired dev blog starter for Next.js", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/colinhacks/devii" 8 | }, 9 | "author": "Colin McDonnell ", 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/colinhacks/devii/issues" 13 | }, 14 | "homepage": "https://github.com/colinhacks/devii", 15 | "tags": [ 16 | "blog template", 17 | "react", 18 | "nextjs", 19 | "syntax highlighting", 20 | "static generation" 21 | ], 22 | "keywords": [ 23 | "blog template", 24 | "react", 25 | "nextjs", 26 | "syntax highlighting", 27 | "static generation" 28 | ], 29 | "scripts": { 30 | "dev": "next dev", 31 | "build": "next build", 32 | "start": "next start", 33 | "export": "next export", 34 | "deploy": "next build && next export && firebase deploy --only hosting" 35 | }, 36 | "dependencies": { 37 | "fecha": "^4.2.0", 38 | "glob": "^7.1.6", 39 | "gray-matter": "^4.0.2", 40 | "next": "9.5", 41 | "raw-loader": "^4.0.1", 42 | "react": "16.13.0", 43 | "react-dom": "16.13.0", 44 | "react-markdown": "^4.3.1", 45 | "react-syntax-highlighter": "^12.2.1", 46 | "rss": "^1.2.2", 47 | "showdown": "^1.9.1" 48 | }, 49 | "devDependencies": { 50 | "@types/glob": "^7.1.3", 51 | "@types/node": "14", 52 | "@types/react": "^16.9.35", 53 | "@types/react-syntax-highlighter": "^11.0.5", 54 | "@types/rss": "^0.0.28", 55 | "@types/showdown": "^1.9.3", 56 | "favicons": "^6.1.0", 57 | "typescript": "^3.8.3" 58 | } 59 | } -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Head from 'next/head'; 3 | import { Footer } from '../components/Footer'; 4 | import { globals } from '../globals'; 5 | import { Header } from '../components/Header'; 6 | import '../styles/base.css'; 7 | 8 | const App: React.FC = ({ Component, pageProps }: any) => { 9 | return ( 10 |
11 | 12 | {globals.googleAnalyticsId && ( 13 | 14 | )} 15 | {globals.googleAnalyticsId && ( 16 | 25 | )} 26 | 27 |
28 | 29 |
30 |
31 | ); 32 | }; 33 | 34 | export default App; 35 | -------------------------------------------------------------------------------- /pages/blog/[blog].tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import glob from 'glob'; 3 | import { BlogPost } from '../../components/BlogPost'; 4 | import { loadPost } from '../../loader'; 5 | 6 | function Post(props: any) { 7 | const { post } = props; 8 | return ; 9 | } 10 | 11 | export const getStaticPaths = () => { 12 | const blogs = glob.sync('./md/blog/*.md'); 13 | const slugs = blogs.map((file: string) => { 14 | const popped = file.split('/').pop(); 15 | if (!popped) throw new Error(`Invalid blog path: ${file}`); 16 | return popped.slice(0, -3).trim(); 17 | }); 18 | 19 | const paths = slugs.map((slug) => `/blog/${slug}`); 20 | return { paths, fallback: false }; 21 | }; 22 | 23 | export const getStaticProps = async ({ params }: any) => { 24 | const post = await loadPost(`blog/${params.blog}.md`); 25 | return { props: { post } }; 26 | }; 27 | 28 | export default Post; 29 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { generateRSS } from '../rssUtil'; 3 | import { Markdown } from '../components/Markdown'; 4 | import { PostData, loadBlogPosts, loadMarkdownFile } from '../loader'; 5 | import { PostCard } from '../components/PostCard'; 6 | 7 | const Home = (props: { 8 | introduction: string; 9 | features: string; 10 | readme: string; 11 | posts: PostData[]; 12 | }) => { 13 | return ( 14 |
15 | 16 | Introducing Devii 17 | 18 | 19 | 20 |
21 |

Introduction to Devii

22 | 23 |
24 | 25 |
26 |

Features

27 |
28 | 29 |
30 |
31 | 32 |
33 |

My blog posts

34 |

35 | This section demonstrates the power of dynamic imports. Every Markdown 36 | file under /md/blog is automatically parsed into a 37 | structured TypeScript object and available in the{' '} 38 | props.posts array. These blog post "cards" are 39 | implemented in the 40 | /components/PostCard.tsx component. 41 |

42 |
43 | {props.posts.map((post, j) => { 44 | return ; 45 | })} 46 |
47 |
48 | 49 |
50 |

Testimonials

51 |
52 |

53 | Seems like it might be useful! 54 |

55 |

56 | — Dan Abramov, taken{' '} 57 | 61 | {' '} 62 | utterly out of context 63 | 64 |

65 |
66 |
67 | 68 | {/*
69 |

README.md

70 |

71 | Below is the README.md for devii. It was imported and rendered using 72 | Next.js dynamic imports. The rest of this page (including this 73 | paragraph) are rendered with React. You can also read the README on 74 | GitHub at{' '} 75 | 76 | https://github.com/colinhacks/devii 77 | 78 | . 79 |

80 |
*/} 81 | 82 | {/*
83 |
84 | 85 |
86 |
*/} 87 | 88 |
89 |

Get started

90 | 91 | 92 | 93 |
94 |
95 | ); 96 | }; 97 | 98 | export default Home; 99 | 100 | export const getStaticProps = async () => { 101 | const introduction = await loadMarkdownFile('introduction.md'); 102 | const features = await loadMarkdownFile('features.md'); 103 | const readmeFile = await import(`../${'README.md'}`); 104 | const readme = readmeFile.default; 105 | const posts = await loadBlogPosts(); 106 | 107 | // comment out to turn off RSS generation during build step. 108 | await generateRSS(posts); 109 | 110 | const props = { 111 | introduction: introduction.contents, 112 | features: features.contents, 113 | readme: readme, 114 | posts, 115 | }; 116 | 117 | return { props }; 118 | }; 119 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Not Found 7 | 8 | 23 | 24 | 25 |
26 |

404

27 |

Page Not Found

28 |

The specified file was not found on this website. Please check the URL for mistakes and try again.

29 |

Why am I seeing this?

30 |

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinhacks/devii/ecc2954234e4e1d6310ce48080ffb5df3fcb1dc0/public/favicon.ico -------------------------------------------------------------------------------- /public/img/brook.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinhacks/devii/ecc2954234e4e1d6310ce48080ffb5df3fcb1dc0/public/img/brook.jpg -------------------------------------------------------------------------------- /public/img/brook_thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinhacks/devii/ecc2954234e4e1d6310ce48080ffb5df3fcb1dc0/public/img/brook_thumb.jpg -------------------------------------------------------------------------------- /public/img/colin_square_small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinhacks/devii/ecc2954234e4e1d6310ce48080ffb5df3fcb1dc0/public/img/colin_square_small.jpg -------------------------------------------------------------------------------- /public/img/danabramov.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinhacks/devii/ecc2954234e4e1d6310ce48080ffb5df3fcb1dc0/public/img/danabramov.png -------------------------------------------------------------------------------- /public/img/danabramov_thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinhacks/devii/ecc2954234e4e1d6310ce48080ffb5df3fcb1dc0/public/img/danabramov_thumb.png -------------------------------------------------------------------------------- /public/img/pancakes.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinhacks/devii/ecc2954234e4e1d6310ce48080ffb5df3fcb1dc0/public/img/pancakes.jpeg -------------------------------------------------------------------------------- /public/img/pancakes_thumb.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinhacks/devii/ecc2954234e4e1d6310ce48080ffb5df3fcb1dc0/public/img/pancakes_thumb.jpeg -------------------------------------------------------------------------------- /public/img/profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/colinhacks/devii/ecc2954234e4e1d6310ce48080ffb5df3fcb1dc0/public/img/profile.jpg -------------------------------------------------------------------------------- /public/img/rss-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Welcome to Firebase Hosting 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 32 | 33 | 34 |
35 |

Welcome

36 |

Firebase Hosting Setup Complete

37 |

You're seeing this because you've successfully setup Firebase Hosting. Now it's time to go build something extraordinary!

38 | Open Hosting Documentation 39 |
40 |

Firebase SDK Loading…

41 | 42 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /public/rss.xml: -------------------------------------------------------------------------------- 1 | <![CDATA[My Awesome Blog]]>https://alyssaphacker.comhttps://alyssaphacker.com/icon.pngMy Awesome Bloghttps://alyssaphacker.comRSS for NodeWed, 11 Nov 2020 16:27:25 GMTTue, 03 Mar 2020 04:00:00 GMT60<![CDATA[Dan Abramov knows about Devii]]>Dan Abramov knows about Devii!

2 |
3 |

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

5 |
6 |

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]]> 7 |

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

8 | 9 |

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

10 |

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.

11 |

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.

12 |
13 |

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

14 |
15 |

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

16 |

React (+ TypeScript)

17 |

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.

18 |

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?

19 |

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.

20 |

Good authoring experience

21 |

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!

22 |

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.

23 |

Well, that doesn't work for me. I want my website to be React-first, with a sprinkling of Markdown when it makes my life easier.

24 |

Static generation

25 |

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.

26 |

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. 😅

27 |

My solution

28 |

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

29 |

Next.js

30 |

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).

31 |

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.

32 |

Here's a quick breakdown of the project structure. No need to understand every piece of it; but it may be useful to refer to throughout the rest of this post.

33 |
.
 34 | ├── README.md
 35 | ├── public // all static files (images, etc) go here
 36 | ├── pages // every .tsx component in this dir becomes a page of the final site
 37 | |   ├── index.tsx // the home page (which has access to the list of all blog posts)
 38 | |   ├── blog
 39 | |       ├── [blog].md // a template component that renders the blog posts under `/md/blog`
 40 | ├── md
 41 | |   ├── blog
 42 | |       ├── devii.md // this page!
 43 |         ├── whatever.md // every MD file in this directory becomes a blog post
 44 | ├── components
 45 | |   ├── Code.tsx
 46 | |   ├── Markdown.tsx
 47 | |   ├── <various others>
 48 | ├── loader.ts // contains utility functions for loading/parsing Markdown
 49 | ├── node_modules
 50 | ├── tsconfig.json
 51 | ├── package.json
 52 | ├── next.config.js
 53 | ├── next-env.d.ts
 54 | ├── .gitignore
 55 | 
56 | 57 |

TypeScript + React

58 |

Both React and TypeScript are baked into the DNA of Next.js, so you get these for free when you set up a Next.js project.

59 |

Gatsby, on the other hand, has a special plugin for TypeScript support, but it's not officially supported and seems to be low on their priority list. Also, after messing with it for an hour I couldn't get it to play nice with hot reload.

60 |

Markdown authoring

61 |

Using Next's special getStaticProps hook and glorious dynamic imports, it's trivial to a Markdown file and pass its contents into your React components as a prop. This achieves the holy grail I was searching for: the ability to easily mix React and Markdown.

62 |

Frontmatter support

63 |

Every Markdown file can include a "frontmatter block" containing metadata. I implemented a simple utility function (loadPost) that loads a Markdown file, parses its contents, and returns a TypeScript object with the following signature:

64 |
type PostData = {
 65 |   path: string; // the relative URL to this page, can be used as an href
 66 |   content: string; // the body of the MD file
 67 |   title?: string;
 68 |   subtitle?: string;
 69 |   date?: number;
 70 |   author?: string;
 71 |   authorPhoto?: string;
 72 |   authorTwitter?: string;
 73 |   tags?: string[];
 74 |   bannerPhoto?: string;
 75 |   thumbnailPhoto?: string;
 76 | };
 77 | 
78 |

I implemented a separate function loadPosts that loads all the Markdown files under /md/blog and returns them as an array (PostData[]). I use loadPosts on this site's home page to render a list of all posts I've written.

79 |

Medium-inspired design

80 |

I used the wonderful react-markdown package to render Markdown as a React component. My Markdown rendered component (/components/Markdown.tsx) provides some default styles inspired by Medium's design. Just modify the style pros in Markdown.tsx to customize the design to your liking.

81 |

GitHub-style code blocks

82 |

You can easily drop code blocks into your blog posts using triple-backtick syntax. Specify the programming language with a "language tag", just like GitHub!

83 |

To achieve this I implemented a custom code renderer (/components/Code.tsx) for react-markdown that uses react-syntax-highlighter to handle the highlighting. So this:

84 | 85 |
// pretty neat huh?
 86 | const test = (arg: string) => {
 87 |   return arg.length > 5;
 88 | };
 89 | 
90 |

turns into this:

91 |
// pretty neat huh?
 92 | const test = (arg: string) => {
 93 |   return arg.length > 5;
 94 | };
 95 | 
96 |

RSS feed generation

97 |

An RSS feed is auto-generated from your blog post feed. This feed is generated using the rss module (for converting JSON to RSS format) and showdown for converting the markdown files to RSS-compatible HTML. The feed is generated during the build step and written as a static file to /rss.xml in your static assets folder. It's dead simple. That's the joy of being able to easily write custom build scripts on top of Next.js's getStaticProps hooks!

98 |

SEO

99 |

Every blog post page automatically populated meta tags based on the post metadata. This includes a title tag, meta tags, og: tags, Twitter metadata, and a link tag containing the canonical URL. You can modify/augment this in the PostMeta.ts component.

100 |

Static generation

101 |

You can generate a fully static version of your site using yarn build && yarn export. This step is entirely powered by Next.js. The static site is exported to the out directory.

102 |

After its generated, use your static file hosting service of choice (Firebase Hosting, Vercel, Netlify) to deploy your site.

103 |

Insanely customizable

104 |

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).

105 |

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

106 |

Get started

107 |

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! 😎

108 |

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

109 |
git clone git@github.com:colinhacks/devii.git mysite
110 | cd mysite
111 | yarn
112 | yarn dev
113 | 
]]>
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.

114 |

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

115 |
    116 |
  • Markdown-based: Just add a Markdown file to /md/blog to add a new post to your blog!
  • 117 |
  • TypeScript + React: aside from the parts that are rendered Markdown, everything else is fully built with TypeScript and functional React components. Implementing any sort of interactive widget is often hard using existing Markdown-centric static-site generators, but Devii makes it easy to mix Markdown and React on the same page.
  • 118 |
  • Frontmatter support: Every post can include a frontmatter block containing metadata: title, subtitle, datePublished (timestamp), author, authorPhoto, and bannerPhoto.
  • 119 |
  • Medium-inspired styles: The Markdown renderer (Markdown.tsx) contains default styles inspired by Medium.
  • 120 |
  • Static generation: you can generate a fully static version of your site using yarn build && yarn export. Powered by Next.js.
  • 121 |
  • GitHub-style code blocks: with syntax highlighting powered by react-syntax-highlighter. Works out-of-the-box for all programming languages. Just use Markdown's triple backtick syntax with a "language identifier", just like GitHub.
  • 122 |
123 |
  // pretty neat huh?
124 |   const test: (arg: string) => boolean = (arg) => {
125 |     return arg.length > 5;
126 |   };
127 | 
128 |
    129 |
  • 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!
  • 130 |
131 |

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
-------------------------------------------------------------------------------- /rssUtil.ts: -------------------------------------------------------------------------------- 1 | import RSS from 'rss'; 2 | import fs from 'fs'; 3 | import showdown from 'showdown'; 4 | import { globals } from './globals'; 5 | import { PostData } from './loader'; 6 | 7 | export const generateRSS = async (posts: PostData[]) => { 8 | posts.map((post) => { 9 | if (!post.canonicalUrl) 10 | throw new Error( 11 | "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." 12 | ); 13 | return post; 14 | }); 15 | 16 | const feed = new RSS({ 17 | title: globals.siteName, 18 | description: globals.siteDescription, 19 | feed_url: `${globals.url}/rss.xml`, 20 | site_url: globals.url, 21 | image_url: `${globals.url}/icon.png`, 22 | managingEditor: globals.email, 23 | webMaster: globals.email, 24 | copyright: `${new Date().getFullYear()} ${globals.yourName}`, 25 | language: 'en', 26 | pubDate: globals.siteCreationDate, 27 | ttl: 60, 28 | }); 29 | 30 | let isValid = true; 31 | for (const post of posts) { 32 | const converter = new showdown.Converter(); 33 | const html = converter.makeHtml(post.content); 34 | if (!post.datePublished) { 35 | isValid = false; 36 | console.warn( 37 | 'All posts must have a publishedDate timestamp when generating RSS feed.' 38 | ); 39 | console.warn('Not generating rss.xml.'); 40 | } 41 | feed.item({ 42 | title: post.title, 43 | description: html, 44 | url: `${globals.url}/${post.path}`, 45 | categories: post.tags || [], 46 | author: post.author || 'Colin McDonnell', 47 | date: new Date(post.datePublished || 0).toISOString(), 48 | }); 49 | } 50 | 51 | if (!isValid) return; 52 | 53 | // writes RSS.xml to public directory 54 | const path = `${process.cwd()}/public/rss.xml`; 55 | fs.writeFileSync(path, feed.xml(), 'utf8'); 56 | console.log(`generated RSS feed`); 57 | }; 58 | -------------------------------------------------------------------------------- /sitemap.ts: -------------------------------------------------------------------------------- 1 | export const sitemap = ''; 2 | import glob from 'glob'; 3 | import { globals } from './globals'; 4 | import { getStaticPaths as getBlogPaths } from './pages/blog/[blog]'; 5 | 6 | export const generateSitemap = async () => { 7 | const pagesDir = './pages/**/*.*'; 8 | const posts = await glob.sync(pagesDir); 9 | 10 | const pagePaths = posts 11 | .filter((path) => !path.includes('[')) 12 | .filter((path) => !path.includes('/_')) 13 | .map((path) => path.slice(1)); 14 | 15 | const blogPaths = await getBlogPaths().paths; 16 | 17 | const sitemap = ` 18 | 19 | 20 | 21 | ${globals.url} 22 | 2020-06-01 23 | 24 | ${[...pagePaths, ...blogPaths].map((path) => { 25 | const item = [``]; 26 | item.push(` ${globals.url}${path}`); 27 | item.push(` 2020-06-01`); 28 | return [``]; 29 | })} 30 | `; 31 | 32 | return sitemap; 33 | }; 34 | -------------------------------------------------------------------------------- /styles/base.css: -------------------------------------------------------------------------------- 1 | /* reset */ 2 | html, 3 | body, 4 | #__next { 5 | min-height: 100%; 6 | padding: 0; 7 | margin: 0; 8 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 9 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 10 | } 11 | 12 | * { 13 | box-sizing: border-box; 14 | } 15 | 16 | /* font sizing */ 17 | body { 18 | color: #333; 19 | font-size: 100%; 20 | font-family: Helvetica, sans-serif; 21 | line-height: 1.6; 22 | text-rendering: optimizeLegibility; 23 | -moz-osx-font-smoothing: grayscale; 24 | -webkit-font-smoothing: antialiased; 25 | } 26 | 27 | h1, 28 | h2, 29 | h3, 30 | h4, 31 | h5, 32 | h6 { 33 | font-weight: 700; 34 | line-height: 1.6; 35 | margin-bottom: 0.5em; 36 | margin-top: 1em; 37 | text-align: left; 38 | } 39 | 40 | h1 { 41 | font-size: 2.6em; 42 | } 43 | h2 { 44 | font-size: 1.8em; 45 | } 46 | h3 { 47 | font-size: 1.4em; 48 | } 49 | h4 { 50 | font-size: 1.2em; 51 | } 52 | h5 { 53 | font-size: 1.1em; 54 | } 55 | 56 | h6, 57 | p, 58 | li { 59 | font-size: 1em; 60 | } 61 | 62 | p { 63 | margin: 0; 64 | padding: 1em 0px; 65 | } 66 | 67 | blockquote { 68 | border-left: 5px solid turquoise; 69 | padding: 15px 0px 15px 15px; 70 | background-color: #f4f4f4; 71 | border-radius: 4px; 72 | width: 100%; 73 | max-width: 550px; 74 | margin: 0.4em 0px; 75 | } 76 | 77 | blockquote p { 78 | padding: 0px; 79 | } 80 | 81 | code { 82 | background-color: #00000010; 83 | padding: 3px 3px; 84 | border-radius: 2px; 85 | } 86 | 87 | pre { 88 | margin: 20px 0px !important; 89 | } 90 | 91 | ol pre, 92 | ol p { 93 | margin: 0px 0px !important; 94 | } 95 | 96 | a { 97 | color: turquoise; 98 | } 99 | 100 | a:hover { 101 | color: #c00; 102 | } 103 | 104 | a:visited { 105 | color: purple; 106 | } 107 | 108 | /* page sections */ 109 | .container { 110 | display: flex; 111 | flex-direction: column; 112 | justify-content: center; 113 | align-items: center; 114 | min-height: 100vh; 115 | } 116 | 117 | .content { 118 | width: 100%; 119 | } 120 | 121 | .content > * { 122 | padding: 0px 10px; 123 | } 124 | 125 | .content img { 126 | width: 100%; 127 | } 128 | 129 | .header, 130 | .footer { 131 | width: 100%; 132 | display: flex; 133 | flex-direction: row; 134 | align-items: center; 135 | justify-content: space-between; 136 | background-color: turquoise; 137 | color: white; 138 | } 139 | 140 | .header a, 141 | .footer a { 142 | color: #fff; 143 | padding: 15px; 144 | text-decoration: none; 145 | } 146 | 147 | .header p, 148 | .footer p { 149 | margin: 0; 150 | padding: 15px; 151 | } 152 | 153 | .section { 154 | width: 100%; 155 | display: flex; 156 | flex-direction: column; 157 | align-items: center; 158 | padding-bottom: 30px; 159 | } 160 | 161 | .section .narrow { 162 | max-width: 550px; 163 | } 164 | 165 | .section h1, 166 | .section h2, 167 | .section h3, 168 | .section h4, 169 | .section h5, 170 | .section h6, 171 | .section p { 172 | width: 100%; 173 | max-width: 550px; 174 | } 175 | 176 | .medium-wide { 177 | max-width: 550px; 178 | } 179 | 180 | hr { 181 | height: 1px; 182 | color: #eee; 183 | opacity: 0.2; 184 | margin: 15px 0px; 185 | } 186 | 187 | /* utilities */ 188 | .centered { 189 | text-align: center; 190 | } 191 | 192 | .alternate { 193 | background-color: #eee; 194 | } 195 | 196 | .flex-spacer { 197 | flex: 1; 198 | } 199 | 200 | /* page areas */ 201 | .introduction { 202 | max-width: 550px; 203 | margin: auto; 204 | } 205 | 206 | .fork-button { 207 | background-color: turquoise; 208 | border-radius: 10px; 209 | border: none; 210 | color: white; 211 | font-size: 14pt; 212 | padding: 10px 30px; 213 | cursor: pointer; 214 | } 215 | .fork-button:hover { 216 | background-color: mediumturquoise; 217 | } 218 | 219 | .author-container { 220 | width: 100%; 221 | max-width: 550px; 222 | margin: 0px; 223 | padding: 0px; 224 | } 225 | 226 | .author { 227 | display: flex; 228 | flex-direction: row; 229 | align-items: center; 230 | justify-content: flex-start; 231 | } 232 | 233 | .author-image { 234 | border-radius: 35px; 235 | height: 70px; 236 | margin: 0px 10px 0px 0px; 237 | width: 70px; 238 | } 239 | 240 | .author-line { 241 | line-height: 1.2; 242 | margin: 2px; 243 | padding: 0; 244 | } 245 | 246 | .author-line.subtle { 247 | opacity: 0.6; 248 | } 249 | 250 | .blog-post { 251 | display: flex; 252 | flex-direction: column; 253 | justify-content: center; 254 | align-items: center; 255 | width: 100%; 256 | margin: auto; 257 | } 258 | 259 | .blog-post-inner { 260 | width: 100%; 261 | } 262 | 263 | .blog-post-image { 264 | width: 100%; 265 | max-width: 1600px; 266 | } 267 | 268 | .blog-post-title { 269 | width: 100%; 270 | max-width: 550px; 271 | margin-bottom: 15px; 272 | padding: 40px 10px; 273 | } 274 | 275 | .blog-post-title h1 { 276 | letter-spacing: -1px; 277 | line-height: 1.2; 278 | margin: 3px 0px; 279 | } 280 | 281 | .blog-post-title h2 { 282 | margin: 0px; 283 | padding: 3px 0px; 284 | color: #808080; 285 | letter-spacing: 0px; 286 | line-height: 1.2; 287 | font-size: 13pt; 288 | font-weight: normal; 289 | } 290 | 291 | .blog-post-content { 292 | width: 100%; 293 | max-width: 550px; 294 | padding: 0px 10px; 295 | } 296 | 297 | .follow-button { 298 | display: inline-block; 299 | border: 1px solid turquoise; 300 | border-radius: 4px; 301 | padding: 2px 10px; 302 | color: turquoise; 303 | font-size: 10pt; 304 | margin-bottom: 2px; 305 | margin-left: 4px; 306 | } 307 | 308 | .tag { 309 | display: inline-block; 310 | padding: 3px 12px; 311 | border-radius: 20px; 312 | background-color: #dddddd; 313 | color: #333333; 314 | margin-right: 7px; 315 | box-shadow: 0px 1px 1px #00000030; 316 | } 317 | 318 | .post-card-container { 319 | display: grid; 320 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 321 | grid-row-gap: 8px; 322 | grid-column-gap: 8px; 323 | width: 100%; 324 | padding: 15px 30px; 325 | } 326 | 327 | .post-card { 328 | text-decoration: inherit; 329 | color: inherit; 330 | margin: 0px; 331 | display: flex; 332 | flex-direction: row; 333 | justify-content: center; 334 | height: 300px; 335 | } 336 | 337 | .post-card-inner { 338 | opacity: 0.92; 339 | box-shadow: 0px 2px 10px #00000040; 340 | width: 100%; 341 | max-width: 500px; 342 | overflow: hidden; 343 | border-radius: 8px; 344 | display: flex; 345 | flex-direction: column; 346 | height: 100%; 347 | } 348 | 349 | .post-card-thumbnail { 350 | width: 100%; 351 | flex: 1; 352 | background-repeat: no-repeat; 353 | background-size: cover; 354 | } 355 | 356 | .post-card-title { 357 | padding: 15px 10px; 358 | display: flex; 359 | flex-direction: column; 360 | border-top: 1px solid #00000020; 361 | } 362 | 363 | .post-card-title h2 { 364 | margin: 0px; 365 | font-size: 1.4em; 366 | line-height: 1.2; 367 | letter-spacing: -1px; 368 | text-align: center; 369 | } 370 | 371 | .post-card-title p { 372 | text-align: center; 373 | margin: 0px; 374 | padding: 0px; 375 | font-size: 12pt; 376 | color: #888; 377 | padding-top: 0px; 378 | } 379 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "strict": true, 20 | "noImplicitAny": true, 21 | "noImplicitThis": true, 22 | "alwaysStrict": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "noImplicitReturns": true, 26 | "noFallthroughCasesInSwitch": true, 27 | }, 28 | "exclude": [ 29 | "node_modules" 30 | ], 31 | "include": [ 32 | "next-env.d.ts", 33 | "**/*.ts", 34 | "**/*.tsx", 35 | "next.config.js" 36 | ] 37 | } --------------------------------------------------------------------------------