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,11 +3,10 @@ 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?
@ -16,9 +15,6 @@ function AboutPage({usr}: any) {
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,10 +1,21 @@
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';
import NotesInfo from '../../public/notes.json';
interface Note {
title: string,
mtime: string,
}
interface Notes {
[slug: string]: Note;
}
function Markdown({ content }: any) {
return <ReactMarkdown
@ -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>
<tbody>
{notesMeta && notesMeta.map((note: INoteMeta, i) => {
return (
<tr key={i}>
<tr>
<td style={{ flex: '1 0 50%' }}>
<Link href={`/notes/${note.slug}`}>
{note.title}
<Link href={props.path}>
{props.note.title}
</Link>
</td>
<td style={{ fontStyle: 'italic' }}>
{note.last_updated && date.toRelativeDate(new Date(note.last_updated))}
{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>
{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,13 +1,25 @@
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:
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%)'
@ -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",