Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ Thumbs.db
# Maizzle
**/public/emails/*
!**/public/emails/README.md

# generated files
/apps/web/public/rss/
68 changes: 68 additions & 0 deletions apps/web/components/ArticleLayout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import Head from 'next/head';
import { useRouter } from 'next/router';
import { formatDate } from '../lib/formatDate';
import { Prose } from './Prose';

function ArrowLeftIcon(props) {
return (
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}>
<path
d="M7.25 11.25 3.75 8m0 0 3.5-3.25M3.75 8h8.5"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}

export function ArticleLayout({
children,
meta,
isRssFeed = false,
previousPathname,
}) {
const router = useRouter();

if (isRssFeed) {
return children;
}

return (
<>
<Head>
<title>{`${meta.title} - Jason Ruesch`}</title>
<meta name="description" content={meta.description} />
</Head>
<div className="pt-16 pb-40">
<div className="w-full pt-6">
<div className="space-y-4">
{previousPathname && (
<button
type="button"
onClick={() => router.back()}
aria-label="Go back to articles"
className="group mb-8 flex h-10 w-10 items-center justify-center rounded-full bg-white shadow-md shadow-neutral-800/5 ring-1 ring-neutral-900/5 transition dark:border dark:border-neutral-700/50 dark:bg-neutral-800 dark:ring-0 dark:ring-white/10 dark:hover:border-neutral-700 dark:hover:ring-white/20"
>
<ArrowLeftIcon className="h-4 w-4 stroke-neutral-500 transition group-hover:stroke-neutral-700 dark:stroke-neutral-500 dark:group-hover:stroke-neutral-400" />
</button>
)}
<article>
<header className="flex flex-col">
<h1 className="mt-4">{meta.title}</h1>
<time
dateTime={meta.date}
className="order-first flex items-center text-base text-neutral-400 dark:text-neutral-500"
>
<span className="h-4 w-0.5 rounded-full bg-neutral-200 dark:bg-neutral-500" />
<span className="ml-3">{formatDate(meta.date)}</span>
</time>
</header>
<Prose className="mt-4">{children}</Prose>
</article>
</div>
</div>
</div>
</>
);
}
7 changes: 7 additions & 0 deletions apps/web/components/Prose.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import clsx from 'clsx'

export function Prose({ children, className }) {
return (
<div className={clsx(className, 'prose dark:prose-invert')}>{children}</div>
)
}
8 changes: 8 additions & 0 deletions apps/web/lib/formatDate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function formatDate(dateString) {
return new Date(`${dateString}T00:00:00Z`).toLocaleDateString('en-US', {
day: 'numeric',
month: 'long',
year: 'numeric',
timeZone: 'UTC',
})
}
53 changes: 53 additions & 0 deletions apps/web/lib/generateRssFeed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import ReactDOMServer from 'react-dom/server';
import { Feed } from 'feed';
import { mkdir, writeFile } from 'fs/promises';

import { getAllArticles } from './getAllArticles';

export async function generateRssFeed() {
let articles = await getAllArticles();
let siteUrl = process.env.NEXT_PUBLIC_SITE_URL;
let author = {
name: 'Jason Ruesch',
email: 'spencer@planetaria.tech',
};

let feed = new Feed({
title: author.name,
description: 'Your blog description',
author,
id: siteUrl,
link: siteUrl,
image: `${siteUrl}/favicon.ico`,
favicon: `${siteUrl}/favicon.ico`,
copyright: `All rights reserved ${new Date().getFullYear()}`,
feedLinks: {
rss2: `${siteUrl}/rss/feed.xml`,
json: `${siteUrl}/rss/feed.json`,
},
});

for (let article of articles) {
let url = `${siteUrl}/articles/${article.slug}`;
let html = ReactDOMServer.renderToStaticMarkup(
<article.component isRssFeed />
);

feed.addItem({
title: article.title,
id: url,
link: url,
description: article.description,
content: html,
author: [author],
contributor: [author],
date: new Date(article.date),
});
}

await mkdir('./apps/web/public/rss', { recursive: true });
await Promise.all([
writeFile('./apps/web/public/rss/feed.xml', feed.rss2(), 'utf8'),
writeFile('./apps/web/public/rss/feed.json', feed.json1(), 'utf8'),
]);
}
23 changes: 23 additions & 0 deletions apps/web/lib/getAllArticles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import glob from 'fast-glob';
import * as path from 'path';

async function importArticle(articleFilename) {
let { meta, default: component } = await import(
`../pages/articles/${articleFilename}`
);
return {
slug: articleFilename.replace(/(\/index)?\.mdx$/, ''),
...meta,
component,
};
}

export async function getAllArticles() {
let articleFilenames = await glob(['*.mdx', '*/index.mdx'], {
cwd: path.join(process.cwd(), 'apps/web/pages/articles'),
});

let articles = await Promise.all(articleFilenames.map(importArticle));

return articles.sort((a, z) => new Date(z.date) - new Date(a.date));
}
17 changes: 0 additions & 17 deletions apps/web/next.config.js

This file was deleted.

19 changes: 19 additions & 0 deletions apps/web/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import nextMDX from '@next/mdx';
import remarkGfm from 'remark-gfm';
import rehypePrism from '@mapbox/rehype-prism';

/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ['jsx', 'tsx', 'mdx'],
reactStrictMode: true,
};

const withMDX = nextMDX({
extension: /\.mdx?$/,
options: {
remarkPlugins: [remarkGfm],
rehypePlugins: [rehypePrism],
},
});

export default withMDX(nextConfig);
22 changes: 18 additions & 4 deletions apps/web/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
import { AppProps } from 'next/app';
import Head from 'next/head';
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { ThemeProvider } from 'next-themes';
import { Inter, Alegreya_Sans_SC } from '@next/font/google';
import { Beams, Header, Navbar, PageTransitions } from '@jasonruesch/web/ui';
import './styles.css';

import '../styles/tailwind.css';
import 'focus-visible';

const inter = Inter({ subsets: ['latin'] });
const alegreyaSansSC = Alegreya_Sans_SC({
subsets: ['latin'],
weight: ['100', '300', '400', '500', '700', '800', '900'],
});

function CustomApp({ Component, pageProps }: AppProps) {
function usePrevious(value) {
const ref = useRef();

useEffect(() => {
ref.current = value;
}, [value]);

return ref.current;
}

function CustomApp({ Component, pageProps, router }: AppProps) {
const previousPathname = usePrevious(router.pathname);

const [isHydrated, setIsHydrated] = useState(false);
useEffect(() => {
setIsHydrated(true);
Expand Down Expand Up @@ -48,7 +62,7 @@ function CustomApp({ Component, pageProps }: AppProps) {
<main className="flex min-h-screen bg-neutral-100 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-50">
<Beams className="z-10" />
<div className="relative z-20 mx-auto w-full max-w-screen-lg px-4 sm:px-8">
<Component {...pageProps} />
<Component previousPathname={previousPathname} {...pageProps} />
</div>
</main>
</PageTransitions>
Expand Down
44 changes: 44 additions & 0 deletions apps/web/pages/articles/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Link from 'next/link';
import Head from 'next/head';
import { LogoImageNeutral } from '@jasonruesch/web/ui';
import { getAllArticles } from '../../lib/getAllArticles';
import { formatDate } from '../../lib/formatDate';

export default function Articles({ articles }) {
return (
<>
<Head>
<title>Articles - Jason Ruesch</title>
<meta name="description" content="" />
</Head>
<div className="pt-16 pb-40">
<div className="w-full pt-6">
<div className="space-y-4">
<h1>Articles</h1>
{articles.map((article) => (
<div key={article.slug} article={article}>
<Link href={`/articles/${article.slug}`}>
<h3>{article.title}</h3>
</Link>
<small>{formatDate(article.date)}</small>
<p>{article.description}</p>
</div>
))}
</div>

<div className="absolute inset-x-0 bottom-0 h-40 py-14">
<LogoImageNeutral className="mx-auto h-12 w-12" />
</div>
</div>
</div>
</>
);
}

export async function getStaticProps() {
return {
props: {
articles: (await getAllArticles()).map(({ component, ...meta }) => meta),
},
};
}
13 changes: 13 additions & 0 deletions apps/web/pages/articles/my-first-article.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ArticleLayout } from '../../components/ArticleLayout';

export const meta = {
author: 'Jason Ruesch',
date: '2023-02-06',
title: 'My First Article',
description:
'This is a description of my first article. It will be used in the meta description tag.',
};

export default (props) => <ArticleLayout meta={meta} {...props} />;

This is a description of my first article. It will be used in the meta description tag.
13 changes: 13 additions & 0 deletions apps/web/pages/articles/my-second-article.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ArticleLayout } from '../../components/ArticleLayout';

export const meta = {
author: 'Jason Ruesch',
date: '2023-02-07',
title: 'My Second Article',
description:
'This is a description of my second article. It will be used in the meta description tag.',
};

export default (props) => <ArticleLayout meta={meta} {...props} />;

This is a description of my second article. It will be used in the meta description tag.
3 changes: 3 additions & 0 deletions apps/web/postcss.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ module.exports = {
tailwindcss: {
config: join(__dirname, 'tailwind.config.js'),
},
'postcss-focus-visible': {
replaceWith: '[data-focus-visible-added]',
},
autoprefixer: {},
},
};
47 changes: 47 additions & 0 deletions apps/web/styles/prism.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
pre[class*='language-'] {
color: theme('colors.neutral.100');
}

.token.tag,
.token.class-name,
.token.selector,
.token.selector .class,
.token.selector.class,
.token.function {
color: theme('colors.red.400');
}

.token.attr-name,
.token.keyword,
.token.rule,
.token.pseudo-class,
.token.important {
color: theme('colors.neutral.300');
}

.token.module {
color: theme('colors.red.400');
}

.token.attr-value,
.token.class,
.token.string,
.token.property {
color: theme('colors.teal.300');
}

.token.punctuation,
.token.attr-equals {
color: theme('colors.neutral.500');
}

.token.unit,
.language-css .token.function {
color: theme('colors.cyan.200');
}

.token.comment,
.token.operator,
.token.combinator {
color: theme('colors.neutral.400');
}
7 changes: 4 additions & 3 deletions apps/web/pages/styles.css → apps/web/styles/tailwind.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import './prism.css';
@import 'tailwindcss/utilities';

@layer base {
:root,
Expand Down
Loading