Bump script ver and refactor md metadata gen

Signed-off-by: Paul W. <lambdapaul@protonmail.com>
This commit is contained in:
Paul W. 2023-10-30 00:18:38 -04:00
parent b5ca20b93d
commit 35d56f5cde
No known key found for this signature in database
GPG Key ID: 0023B93C0FF1E1D4
38 changed files with 412 additions and 8679 deletions

2
.gitignore vendored
View File

@ -4,5 +4,5 @@ dist/
.DS_Store
.cache/
*.bun
**/*.wip.md
**/.*.md
.env

BIN
bun.lockb

Binary file not shown.

View File

@ -1,49 +0,0 @@
import Image from 'next/image';
import { parse as URIparse } from 'uri-js';
import date from '../lib/date';
import style from '../styles/github-profile.module.css';
function CardRow({label, children}: {label: string, children: JSX.Element | string}) {
return !children? <></> : (
<div className={style.cardRow}>
<span className={style.cardLabel}>{label}</span>
<span className={style.cardValue}>{children}</span>
</div>
);
}
function GitHubProfile({user}: any) {
return (<>
<div className={style.card}>
<div className={style.avatarContainer}>
<Image
layout='fixed'
width={256}
height={256}
src={user.avatar_url}
alt={`${user.login}'s GitHub profile avatar`}
/>
</div>
<div className={style.cardTable}>
<CardRow label='Name'>{user.name}</CardRow>
<CardRow label='Username'>{user.login}</CardRow>
<CardRow label='URL'><a href={user.html_url}>{user.html_url}</a></CardRow>
{user.blog && <CardRow label='Blog'>
<a href={ !URIparse(user.blog).scheme ? `//${user.blog}`: user.blog}>{user.blog}</a>
</CardRow>}
<CardRow label='Location'>{user.location}</CardRow>
<CardRow label='About'>{user.bio}</CardRow>
<CardRow label='Created'>{user.created_at ? date.toRelativeDate(user.created_at) : ''}</CardRow>
{user.updated_at && <CardRow label='Last Updated'>{user.updated_at ? date.toRelativeDate(user.updated_at) : ''}</CardRow>}
{user.twitter_username && <CardRow label='Twitter'>
<a className='link extern blue button'
href={`https://twitter.com/${user.twitter_username}`}>
@{user.twitter_username}
</a>
</CardRow>}
</div>
</div>
</>);
}
export default GitHubProfile;

View File

@ -1,12 +1,8 @@
import Meta from './meta';
import Title from './title';
type ChildrenType = JSX.Element | Array<ChildrenType>;
type LayoutProps = {
name: string,
title?: string,
ancestors?: Array<{ name: string, path: string }>
children?: ChildrenType,
removeContainer?: boolean,
};
@ -22,8 +18,7 @@ function Container(props: {children?: ChildrenType, ignore?: boolean}) {
function Layout(props : LayoutProps) {
return (
<>
<Meta name={props.name} ancestors={props.ancestors} />
<Title title={props.title} name={props.name} ancestors={props.ancestors} />
<Title />
<Container ignore={props.removeContainer}>{props.children}</Container>
</>
);

View File

@ -48,11 +48,6 @@ export function toListItem(record: Record<string, any>): listItem | null {
});
}
const s = {
"af": 123,
"asdf" : 123
}
export function mapChild(
obj: listItem | string,
level: number,

View File

@ -1,24 +0,0 @@
import Head from 'next/head';
function Meta({name, ancestors}
: {name: string, ancestors?: Array<{ name: string, path: string }> }) {
function path(): string {
if (!ancestors)
return name;
let path = '';
ancestors.forEach((obj) => {
path = `${path}${obj.name} /`;
});
return `PaulW.XYZ / ${path} ${name}`;
}
return (
<Head>
<title>{path()}</title>
</Head>
);
}
export default Meta;

View File

@ -1,5 +1,5 @@
import Link from 'next/link';
import Pages from '../public/pages.json';
import Pages from '../public/external.json';
function QuickLinks() {
return (

View File

@ -1,17 +1,18 @@
import Link from "next/link";
import { INoteMeta } from "../lib/slug";
import NotesInfo from '../public/notes.json';
function RecentNotes({ notesMeta }: { notesMeta: INoteMeta[] }) {
function RecentNotes() {
const notes = Object.entries(NotesInfo);
return (
<div className='block'>
<div className='h2'>Recent Notes</div>
{notesMeta?.slice(0, 10)
.map((note: any) => {
return <Link key={note.slug} href={`/notes/${note.slug}`} className={`button link`}>{note.title}</Link>
{notes?.slice(0, 10)
.map(([slug, note]: any) => {
return <Link key={slug} href={`/notes/${slug}`} className={`button link`}>{note.title}</Link>
})
}
{
notesMeta.length > 10 &&
notes.length > 10 &&
<div>
<Link href='/notes' className='h5'>More...</Link>
</div>

View File

@ -1,23 +1,24 @@
import Link from "next/link";
import date from "../lib/date";
import { IPostMeta } from "../lib/slug";
import style from '../styles/recent-posts.module.css';
import PostsInfo from '../public/posts.json';
function RecentPosts({ postsMeta }: { postsMeta: IPostMeta[] }) {
if (!postsMeta.length)
function RecentPosts() {
const posts = Object.entries(PostsInfo);
if (!posts.length)
return <></>;
return (
<div className='block'>
<div className='h2'>Recent Posts</div>
<div className={style.container}>
{postsMeta?.slice(0, 10)
.map((post: any) => {
{posts?.slice(0, 10)
.map(([slug, post]: any) => {
return <div className={style.block} key={post.slug}>
<span className={style.postDate}>
{date.toRelativeDate(new Date(post.created_at))}
{date.toRelativeDate(new Date(post.otime))}
</span>
<div className={style.postTitle}>
<Link href={`/posts/${post.slug}`}>
<Link href={`/posts/${slug}`}>
{post.title}
</Link>
</div>
@ -25,7 +26,7 @@ function RecentPosts({ postsMeta }: { postsMeta: IPostMeta[] }) {
})}
</div>
{
postsMeta.length > 10 &&
posts.length > 10 &&
<div className={style.more}>
<Link href='/posts' className='h5'>More...</Link>
</div>

View File

@ -1,12 +1,10 @@
import Link from 'next/link';
import { useRouter } from 'next/router';
import style from '../styles/title.module.css';
type propsObj = {
name: string,
title?: string,
ancestors?: Array<{ name: string, path: string }>
};
import SiteMap from '../public/sitemap.json';
import Head from 'next/head';
import { SiteSubPages } from '../lib/site';
function createPathElements(ancestors: Array<{ name: string, path: string }>) {
let currentPath = '';
@ -21,19 +19,42 @@ function createPathElements(ancestors: Array<{ name: string, path: string }>) {
});
};
function Title({ name, title, ancestors }: propsObj) {
const pathElements = ancestors && createPathElements(ancestors) || <></>;
function Title() {
const router = useRouter();
const pagePath = router.asPath;
const splitPath: Array<{ name: string, path: string }> = [];
let currRoot: SiteSubPages = SiteMap.subpages;
let title: string | null = null;
if (pagePath !== '/') {
const subPaths = pagePath.split('/');
for (const p of subPaths.slice(1, subPaths.length)) {
splitPath.push({ name: currRoot[p].title, path: p });
if (currRoot === undefined
|| currRoot[p] === undefined
|| currRoot[p].subpages !== undefined)
currRoot = currRoot[p].subpages!;
}
if (splitPath !== undefined && splitPath.length > 0)
title = splitPath.pop()!.name;
}
const pathElements = splitPath && createPathElements(splitPath) || <></>;
return (
<>
<Head>
<title>{title && `${title} | PaulW.XYZ` || 'PaulW.XYZ'}</title>
</Head>
<div className={style.container}>
<h1 className={style.title}>
{title || name}
{title || 'PaulW.XYZ'}
</h1>
</div>
<div className={`${style.nav} h1`}>
{name
? <><Link href='/'>PaulW.XYZ</Link> / {pathElements}{name}</>
{title
? <><Link href='/'>PaulW.XYZ</Link> / {pathElements}{title}</>
: <>PaulW.XYZ /</>}
</div>
</>

View File

@ -75,6 +75,6 @@ function isValid(date: any) {
const DateTool = {
toRelativeDate,
isValid
};;
};
export default DateTool;

9
lib/read-markdown.ts Normal file
View File

@ -0,0 +1,9 @@
import { readFile } from 'fs/promises';
import path from 'path';
export default async function readMarkdown(directory: string, slug: string, withoutTitle: boolean = false): Promise<string> {
const content = await readFile(path.join(process.cwd(), directory, `${slug}.md`), 'utf-8');
if (withoutTitle)
return content.substring(content.indexOf('\n') + 1, content.length);
return content;
}

11
lib/site.ts Normal file
View File

@ -0,0 +1,11 @@
export interface Site {
title: string;
subpages?: SiteSubPages;
mtime?: string;
otime?: string;
}
export interface SiteSubPages {
[slug: string]: Site;
}

35
lib/slug.d.ts vendored
View File

@ -1,35 +0,0 @@
interface IPost {
slug: string;
rawslug: string;
content: string;
title: string;
}
interface INote {
slug: string;
rawslug: string;
content: string;
title: string;
}
interface INoteMeta {
title: string;
slug: string;
last_updated?: string;
}
interface IPostMeta {
title: string;
slug: string;
created_at: string;
last_updated?: string;
}
export function getAllPosts(filter?: Array<any>): IPost[];
export function getAllNotes(filter?: Array<any>): INote[];
export function getPost(rawslug: string, filter?: Array<any>): IPost;
export function getNote(rawslug: string, filter?: Array<any>): INote;
export function getPostsMeta(): IPostMeta[];
export function getNotesMeta(): INoteMeta[];

View File

@ -1,139 +0,0 @@
const fs = require('fs');
const matter = require('gray-matter');
const { join } = require('path');
const cacheDir = join(process.cwd(), '.cache');
function getDir(name) {
return join(process.cwd(), name);
}
function getCacheFileName(name) {
return join(cacheDir, `${name}.cache.json`)
}
function get(dir, rawslug, filter = []) {
const slug = rawslug.replace(/\.md$/, '');
const path = join(dir, `${slug}.md`);
const file = fs.readFileSync(path, 'utf-8');
const { data, content } = matter(file);
if (data['last_updated'] === undefined)
data['last_updated'] = '';
if (filter.length === 0)
return { ...data, content, slug, rawslug };
let post = {};
for (const [_, entry] of filter.entries()) {
if (entry === 'slug')
post[entry] = slug;
if (entry === 'rawslug')
post[entry] = rawslug;
if (entry === 'content')
post[entry] = content;
if (typeof data[entry] !== 'undefined') {
post[entry] = data[entry]
}
}
return post;
}
function getAllPosts(filter = []) {
const files = fs.readdirSync(getDir('posts'));
return files
.filter(c => (!c.match(/^\.]/) && c.match(/\.md$/)))
.map(file => {
return get(getDir('posts'), file, filter)
})
.filter(c => (c.title && c.slug && c.created_at && (new Date(c.created_at)).toString() !== 'Invalid Date'))
.sort((a, b) => {
const dA = new Date(a['created_at']);
const dB = new Date(b['created_at']);
return dB - dA;
});
}
function getAllNotes(filter = []) {
const files = fs.readdirSync(getDir('notes'));
return files
.filter(c => (!c.match(/^\.]/) && c.match(/\.md$/)))
.map(file => {
return get(getDir('notes'), file, filter)
})
.filter(c => (c.title && c.slug && c.last_updated && (new Date(c.last_updated)).toString() !== 'Invalid Date'))
.sort((a, b) => {
const dA = new Date(a['last_updated']);
const dB = new Date(b['last_updated']);
return dB - dA;
});
}
const cats = {
notes: {
name: 'notes',
getAll: getAllNotes,
filter: ['title', 'slug', 'last_updated'],
},
posts: {
name: 'posts',
getAll: getAllPosts,
filter: ['title', 'slug', 'created_at', 'last_updated'],
}
};
function cacheMeta({name, getAll, filter}) {
const items = getAll(filter);
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir);
}
fs.writeFile(getCacheFileName(name), JSON.stringify(items), (e) => {
if (e)
console.error(e);
});
return items;
}
function getMeta(cat) {
try {
const file = fs.readFileSync(getCacheFileName(cat.name), 'utf-8');
return JSON.parse(file);
} catch (e) {
if (cat.name)
return cacheMeta(cat);
}
}
function getPostsMeta() {
return getMeta(cats.posts);
};
function getNotesMeta() {
return getMeta(cats.notes);
};
function cache() {
Object.entries(cats).map(([_, v]) => {
return cacheMeta(v);
});
}
const getPost = (s, f) => {return get(getDir('posts'), s, f)};
const getNote = (s, f) => {return get(getDir('notes'), s, f)};
module.exports = {
getAllPosts,
getAllNotes,
getPostsMeta,
getNotesMeta,
getPost,
getNote,
cache
};

View File

@ -4,18 +4,6 @@ module.exports = {
defaultLocale: 'en-US'
},
webpack: (config, _options) => {
const { cache } = require('./lib/slug');
config.plugins.push(
{
apply: (compiler) => {
compiler.hooks.beforeCompile.tap('cachePostDataBC', _ => {
cache();
});
}
}
)
config.module.rules.push(
{
test: /\.ya?ml$/,

View File

@ -1,8 +1,4 @@
---
title: MOS 6502 Microprocessor
last_updated: '2022-09-29T02:41:26.738Z'
---
# MOS 6502 Microprocessor
<table>
<tr>
<td>Name</td>

View File

@ -1,7 +1,4 @@
---
title: Nintendo Switch
last_updated: '2022-10-02T23:01:34.000Z'
---
# Nintendo Switch
<a href='https://www.nintendo.com/switch' class='link button extern blue'>Official Website</a>
<a href='https://developer.nintendo.com/' class='link button extern blue'>Developer Portal</a>

View File

@ -1,7 +1,4 @@
---
title: Steam Deck
last_updated: '2022-09-29T03:55:40.476Z'
---
# Steam Deck
<a href='https://www.steamdeck.com/' class='link button extern blue'>Official Website</a>

View File

@ -1,7 +1,4 @@
---
title: Steam Client
last_updated: '2022-09-29T03:15:58.777Z'
---
# Steam Client
<a href='https://store.steampowered.com' class='link button extern blue'>Steam Store</a>
<a href='https://developer.valvesoftware.com/wiki/SteamCMD' class='link button extern blue'>SteamCMD</a>

View File

@ -1,8 +1,4 @@
---
title: Zilog Z80 Microprocessor
last_updated: '2022-09-29T02:41:18.085Z'
---
# Zilog Z80 Microprocessor
<table>
<tr>
<td>Name</td>

8230
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,6 @@
},
"dependencies": {
"dotenv": "^16.3.1",
"gray-matter": "^4.0.3",
"js-yaml-loader": "^1.2.2",
"next": "^13.5.1",
"normalize.css": "^8.0.1",

View File

@ -3,7 +3,9 @@ import Layout from '../components/layout';
function NotFoundPage() {
return (
<Layout title='Page Not Found' name='... ??? / 404: Not Found'>
<Layout
// title='Page Not Found' name='... ??? / 404: Not Found'
>
<section className='block text center'>
<h1>Error 404</h1>
<p>

View File

@ -3,22 +3,18 @@ import ReactMarkdown from 'react-markdown';
import ReadmeMd from '../README.md';
import License from '../LICENSE.txt';
import Layout from '../components/layout';
import GitHubProfile from '../components/github-profile';
function AboutPage({usr}: any) {
function AboutPage() {
return (
<Layout name='About' title='About PaulW.XYZ'>
<Layout >
<section className='block'>
<p>This is a personal website written by <a href='https://github.com/LambdaPaul'>@LambdaPaul</a>.</p>
<p>Why did I write this?
I do not really know, at least the content I put here.
I wanted a place on the web where I wanted to put everything I think is worth looking at some point in the future.
It seems wise to have things up here even though they may not be worthwhile, as many things ultimately are not.</p>
I do not really know, at least the content I put here.
I wanted a place on the web where I wanted to put everything I think is worth looking at some point in the future.
It seems wise to have things up here even though they may not be worthwhile, as many things ultimately are not.</p>
<p>Got any questions, concerns, or issues? Contact me via email: <code>lambdapaul [at] pm [dot] me</code>.</p>
</section>
<section className='block'>
{usr && <GitHubProfile user={usr} />}
</section>
<hr />
<section className='block'>
<p>Source for this site is available at <a className='button link extern blue' href='https://github.com/LambdaPaul/www'>GitHub / LambdaPaul / www</a></p>
@ -38,16 +34,4 @@ function AboutPage({usr}: any) {
);
}
export async function getStaticProps() {
try {
const res = await fetch('https://api.github.com/users/lambdapaul');
const usr = await res.json();
return {
props: { usr }
};
} catch (e) {
return {props: {}}
}
}
export default AboutPage;

View File

@ -4,36 +4,30 @@ import Layout from '../components/layout';
import QuickLinks from '../components/quick-links';
import RecentNotes from '../components/recent-notes';
import RecentPosts from '../components/recent-posts';
import { getNotesMeta, getPostsMeta, INoteMeta, IPostMeta } from '../lib/slug';
import RootInfo from '../public/home.json';
function Nav() {
const nav = { 'Posts': '/posts', 'Notes': '/notes', 'About': '/about', };
const nav = RootInfo;
return (
<div className='block' style={{ textAlign: 'center' }}>
{
Object.entries(nav).map(([k, v], i) => {
return <Link key={i} href={v} className='button green'>{k}</Link>
Object.entries(nav).map(([slug, info], i) => {
return <Link key={i} href={slug} className='button green'>{info.title}</Link>
})
}
</div>
)
}
function HomePage({ postsMeta, notesMeta }: { postsMeta: IPostMeta[], notesMeta: INoteMeta[] }) {
function HomePage() {
return (
<Layout name='' title='PaulW.XYZ'>
<Layout>
<Nav />
<QuickLinks />
<RecentNotes notesMeta={notesMeta} />
<RecentPosts postsMeta={postsMeta} />
<RecentNotes />
<RecentPosts />
</Layout>
)
}
export async function getStaticProps() {
return {
props: { postsMeta: getPostsMeta(), notesMeta: getNotesMeta() }
};
}
export default HomePage;

View File

@ -1,12 +1,23 @@
import Layout from '../../components/layout';
import { getAllNotes, getNote } from '../../lib/slug';
import ReactMarkdown from 'react-markdown';
import SyntaxHighlighter from 'react-syntax-highlighter';
import { monokaiSublime as hlTheme } from 'react-syntax-highlighter/dist/cjs/styles/hljs';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import readMarkdown from '../../lib/read-markdown';
function Markdown({content}: any) {
import NotesInfo from '../../public/notes.json';
interface Note {
title: string,
mtime: string,
}
interface Notes {
[slug: string]: Note;
}
function Markdown({ content }: any) {
return <ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
@ -36,7 +47,7 @@ function Markdown({content}: any) {
function Note({ note }: any) {
return (<>
<Layout name={note.title} title={note.title} ancestors={[{ name: 'Notes', path: 'notes' }]}>
<Layout >
<section className='block'>
<Markdown content={note.content} />
</section>
@ -46,20 +57,26 @@ function Note({ note }: any) {
}
export async function getStaticProps({ params }: any) {
const note = getNote(params.note);
const note: string = params.note;
const notesInfo: Notes = NotesInfo;
const noteInfo: Note = notesInfo[note];
return {
props: { note }
};
props: {
note: {
...noteInfo,
content: await readMarkdown('notes', note, true)
}
}
}
}
export async function getStaticPaths() {
const notes = getAllNotes();
return {
paths: notes.map((note: any) => {
paths: Object.keys(NotesInfo).map((note: string) => {
return {
params: {
note: note.slug
note
}
}
}),

View File

@ -1,36 +1,40 @@
import Link from 'next/link';
import Layout from '../../components/layout';
import date from '../../lib/date';
import { getNotesMeta, INoteMeta } from '../../lib/slug';
function NotesPage({ notesMeta }: { notesMeta: INoteMeta[] }) {
import date from '../../lib/date';
import NotesInfo from '../../public/notes.json';
function NoteEntry(props: { path: string, note: { title: string, mtime: string } }) {
return (
<Layout name='Notes'>
<table>
<tr>
<td style={{ flex: '1 0 50%' }}>
<Link href={props.path}>
{props.note.title}
</Link>
</td>
<td style={{ fontStyle: 'italic' }}>
{props.note.mtime && date.toRelativeDate(new Date(props.note.mtime))}
</td>
</tr>
);
}
function NotesPage() {
const notes = Object.entries(NotesInfo);
return (
<Layout>
{!notes || notes.length === 0 && <>No notes found</> || <table>
<tbody>
{notesMeta && notesMeta.map((note: INoteMeta, i) => {
return (
<tr key={i}>
<td style={{flex: '1 0 50%'}}>
<Link href={`/notes/${note.slug}`}>
{note.title}
</Link>
</td>
<td style={{fontStyle: 'italic'}}>
{note.last_updated && date.toRelativeDate(new Date(note.last_updated))}
</td>
</tr>
)})}
{notes.map(([slug, note]: any, i: number) => {
return <NoteEntry path={`/notes/${slug}`} note={note} key={i} />
})}
</tbody>
</table>
</table>}
</Layout>
)
}
export async function getStaticProps() {
return {
props: { notesMeta: getNotesMeta() }
};
}
export default NotesPage;

View File

@ -1,16 +1,28 @@
import Layout from '../../components/layout';
import { getAllPosts, getPost } from '../../lib/slug';
import ReactMarkdown from 'react-markdown';
import style from '../../styles/post.module.css';
import PostsInfo from '../../public/posts.json';
import readMarkdown from '../../lib/read-markdown';
function Post({ post }: any) { // eh
interface Post {
title: string;
mtime: string;
otime?: string;
}
interface Posts {
[slug: string]: Post
}
function Post({ post }: { post: Post & { content: string, cover?: string } }) {
return (<>
<Layout removeContainer={true} name={post.title} title={post.title} ancestors={[{ name: 'Posts', path: 'posts' }]}>
<Layout removeContainer={true} >
{<div className={style.imageBlock}
style={{ backgroundImage:
post.cover ?
`url(/assets/images/${post.cover})` :
'linear-gradient(to bottom right, #565a0f, #08432c 15%, rgb(5, 39, 10) 40%, rgb(0, 22, 46) 80%)'
style={{
backgroundImage:
post.cover ?
`url(/assets/images/${post.cover})` :
'linear-gradient(to bottom right, #565a0f, #08432c 15%, rgb(5, 39, 10) 40%, rgb(0, 22, 46) 80%)'
}}></div>}
<div className={style.spacer}></div>
<section className={`${style.block} block`}>
@ -25,20 +37,24 @@ function Post({ post }: any) { // eh
}
export async function getStaticProps({ params }: any) {
const post = getPost(params.post);
const postsInfo: Posts = PostsInfo;
const post: Post = postsInfo[params.post];
return {
props: { post }
};
props: {
post: {
...post,
content: await readMarkdown('posts', params.post, true)
}
}
}
}
export async function getStaticPaths() {
const posts = getAllPosts();
return {
paths: posts.map((post: any) => {
paths: Object.keys(PostsInfo).map((post: string) => {
return {
params: {
post: post.slug
post
}
}
}),

View File

@ -1,47 +1,53 @@
import Link from 'next/link';
import Layout from '../../components/layout';
import date from '../../lib/date';
import { getPostsMeta, IPostMeta } from '../../lib/slug';
import PostsInfo from '../../public/posts.json';
function PostsPage({ postsMeta }: { postsMeta: IPostMeta[] }) {
function PostsPage() {
return (
<Layout name='Posts'>
<table>
<tbody>
{
postsMeta.length &&
postsMeta.map((post: IPostMeta, i) => {
return <tr key={i} style={{alignItems: 'center'}}>
<td style={{display: 'inline-block', textAlign: 'right', fontSize: '0.9rem'}}>
<div style={{fontStyle: 'italics', fontSize: '.8rem'}}>{
post.last_updated && `updated ${date.toRelativeDate(new Date(post.last_updated))}`
}</div>
<div>{ date.toRelativeDate(new Date(post.created_at)) }</div>
</td>
<td style={{
flex: '1 1 60%',
alignItems: 'center',
fontFamily: `'EB Garamond', 'Garamond', 'Times New Roman', Times, serif`}}>
<Link href={`/posts/${post.slug}`} style={{textDecoration: 'none'}}>{post.title}</Link>
</td>
</tr>
}) ||
<div className='text center'>
<div>**crickets**</div>
<div>No posts found...</div>
<div><Link href='/' className='link button green back'>Go Home</Link></div>
</div>
}
</tbody>
</table>
<Layout>
{Object.keys(PostsInfo).length && <Posts /> || <NoPosts />}
</Layout>
)
}
export async function getStaticProps() {
return {
props: { postsMeta: getPostsMeta() }
};
function NoPosts() {
return (<><div className='text center'>
<div>**crickets**</div>
<div>No posts found...</div>
<div><Link href='/' className='link button green back'>Go Home</Link></div>
</div></>);
}
function Posts() {
const posts = Object.entries(PostsInfo);
return (
<>
<table>
<tbody>
{
posts.map(([slug, post]: any, i: number) => {
return <tr key={i} style={{ alignItems: 'center' }}>
<td style={{ display: 'inline-block', textAlign: 'right', fontSize: '0.9rem' }}>
<div style={{ fontStyle: 'italics', fontSize: '.8rem' }}>{
post.mtime && `Updated ${date.toRelativeDate(new Date(post.mtime))}`
}</div>
<div>{date.toRelativeDate(new Date(post.otime))}</div>
</td>
<td style={{
flex: '1 1 60%',
alignItems: 'center',
fontFamily: `'EB Garamond', 'Garamond', 'Times New Roman', Times, serif`
}}>
<Link href={`/posts/${slug}`} style={{ textDecoration: 'none' }}>{post.title}</Link>
</td>
</tr>
})}
</tbody>
</table>
</>
)
}
export default PostsPage;

31
pages/sitemap.tsx Normal file
View File

@ -0,0 +1,31 @@
import Link from 'next/link';
import Layout from '../components/layout';
import { Site } from '../lib/site';
import SiteMap from '../public/sitemap.json';
function traverseMap(head: Site, cwd = '', depth = 0) {
if (head.subpages === undefined)
return [];
let elements = [];
for (const [slug, info] of Object.entries(head.subpages)) {
const path = `${cwd}/${slug}`;
const children = (<><ul> {traverseMap(info, path, depth + 1)}</ul></>);
elements.push(<>
<li>
<Link className='button' href={path}>{info.title}</Link> {children}
</li>
</>);
}
return elements;
}
function SiteMapPage() {
return <Layout>
<ul>{traverseMap(SiteMap)}</ul>
</Layout>;
}
export default SiteMapPage;

14
public/home.json Normal file
View File

@ -0,0 +1,14 @@
{
"posts": {
"title": "Posts"
},
"notes": {
"title": "Notes"
},
"about": {
"title": "About"
},
"sitemap": {
"title": "Site Map"
}
}

1
public/notes.json Normal file
View File

@ -0,0 +1 @@
{"mos-6502":{"title":"MOS 6502 Microprocessor","mtime":"2023-10-29T18:05:52.440Z"},"nintendo-switch":{"title":"Nintendo Switch","mtime":"2023-10-29T18:06:23.793Z"},"steam-deck":{"title":"Steam Deck","mtime":"2023-10-29T18:06:45.197Z"},"steam":{"title":"Steam Client","mtime":"2023-10-29T18:06:53.897Z"},"zilog-z80":{"title":"Zilog Z80 Microprocessor","mtime":"2023-10-29T18:07:08.580Z"}}

1
public/posts.json Normal file
View File

@ -0,0 +1 @@
{}

1
public/sitemap.json Normal file
View File

@ -0,0 +1 @@
{"title":"PaulW.XYZ","subpages":{"posts":{"title":"Posts","subpages":{}},"notes":{"title":"Notes","subpages":{"mos-6502":{"title":"MOS 6502 Microprocessor","mtime":"2023-10-29T18:05:52.440Z"},"nintendo-switch":{"title":"Nintendo Switch","mtime":"2023-10-29T18:06:23.793Z"},"steam-deck":{"title":"Steam Deck","mtime":"2023-10-29T18:06:45.197Z"},"steam":{"title":"Steam Client","mtime":"2023-10-29T18:06:53.897Z"},"zilog-z80":{"title":"Zilog Z80 Microprocessor","mtime":"2023-10-29T18:07:08.580Z"}}},"about":{"title":"About"},"sitemap":{"title":"Site Map"}}}

View File

@ -0,0 +1,136 @@
const fs = require('fs/promises');
const { createReadStream, write } = require('fs');
const path = require('path');
const readline = require('readline/promises');
const { info } = require('console');
async function readFirstLines(filePath, lineCount = 1) {
return new Promise((resolve, reject) => {
try {
const stream = createReadStream(filePath, 'utf-8');
const rl = readline.createInterface({ input: stream });
let counter = 0;
const lines = [];
rl.on('line', (line) => {
counter++;
lines.push(line);
if (counter >= lineCount) {
rl.close();
rl.removeAllListeners();
}
});
rl.on('close', () => {
resolve(lines)
});
} catch (e) {
reject(e)
}
});
}
async function getTitle(filePath) {
const firstLines = await readFirstLines(filePath);
if (firstLines === undefined || firstLines.length === 0)
return null;
let title = firstLines[0];
if (title.substring(0, 2) !== '# ')
return null;
title = title
.substring(1, firstLines[0].length)
.trim();
if (title.length < 3)
return null;
return title;
}
async function getMarkdownMetadata(dir) {
const dirPath = path.join(process.cwd(), dir);
const files = (await fs.readdir(dirPath, 'utf-8'))
.filter((file) => {
return /^[^.].*.md$/.test(file);
})
const out = {};
for (const file of files) {
const filePath = path.join(dirPath, file);
const title = await getTitle(filePath);
if (title === null)
continue;
const slug = file.replace(/\.md$/, '');
// const pagePath = path.join('/', dir, slug);
out[slug] = {
title: title,
// path: pagePath,
mtime: (await fs.stat(filePath)).mtime,
};
}
return out;
}
async function readFilesMetadata(dir) {
const filePath = jsonFilePath(dir);
try {
const fileContent = await fs.readFile(filePath, 'utf-8');
const metadata = JSON.parse(fileContent);
return metadata;
} catch {
return [];
}
}
async function writeFilesMetadata(filePath, metadata) {
try {
await fs.writeFile(filePath, JSON.stringify(metadata), 'utf-8');
} catch (error) {
console.error(error);
}
}
function jsonFilePath(dir) {
return path.join(process.cwd(), 'public', `${dir}.json`);
}
async function generateNotesMetadata() {
const dir = 'notes';
await writeFilesMetadata(jsonFilePath(dir), await getMarkdownMetadata(dir));
}
async function generatePostsMetadata() {
const dir = 'posts';
const currMetadata = await readFilesMetadata(dir);
const generatedMetadata = await getMarkdownMetadata(dir);
const newMetadata = {};
for (const [name, data] of Object.entries(generatedMetadata)) {
let otime;
if (currMetadata[name] !== undefined && currMetadata[name].otime !== undefined)
otime = currMetadata[name].otime
else
otime = data.mtime;
newMetadata[name] = { ...data, otime }
}
await writeFilesMetadata(jsonFilePath(dir), newMetadata);
}
async function generateSiteMap() {
await generateNotesMetadata();
await generatePostsMetadata();
const sitemap = {
title: 'PaulW.XYZ',
subpages: await readFilesMetadata('home')
};
const pages = ['posts', 'notes'];
for (const page of pages) {
sitemap.subpages[page].subpages = await readFilesMetadata(page);
}
await writeFilesMetadata(jsonFilePath('sitemap'), sitemap);
}
generateSiteMap();

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es6",
"target": "es2017",
"lib": [
"dom",
"dom.iterable",