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
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client';

import { useRouter_UNSTABLE as useRouter } from 'waku';
import { scrollTo } from '../../utils/dom.js';

interface AnchorProps {
Expand All @@ -9,7 +8,6 @@ interface AnchorProps {

export const Anchor = (props: AnchorProps) => {
const { anchorId } = props;
const router = useRouter();

return (
<a
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,24 @@ import { type ReactNode, useEffect } from 'react';
import { scrollTo } from '../utils/dom.js';

interface MDXWrapperProps {
children: ReactNode;
children?: ReactNode;
html?: string;
}

export const MDXWrapper = (props: MDXWrapperProps) => {
useEffect(() => {
if (typeof window === 'undefined') return;
if (window.location.hash.startsWith('#')) {
// requestAnimationFrame can sure dom tree already rendered
// maybe can change to ref
requestAnimationFrame(() => {
scrollTo(decodeURIComponent(window.location.hash.slice(1)));
});
}
}, []);

if (props.html) {
// biome-ignore lint/security/noDangerouslySetInnerHtml: HTML rendered from trusted MDX compilation
return <div dangerouslySetInnerHTML={{ __html: props.html }} />;
}

return <div>{props.children}</div>;
};
2 changes: 1 addition & 1 deletion src/components/menu.tsx → app/components/menu.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Link } from 'waku';
import { Link } from '@remix-run/react';

interface MenuLinkProps {
href: `/${string}`;
Expand Down
File renamed without changes.
File renamed without changes.
74 changes: 74 additions & 0 deletions app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type {
LinksFunction,
LoaderFunctionArgs,
MetaFunction,
} from '@remix-run/node';
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from '@remix-run/react';

import { Footer } from './components/footer.js';
import { Header } from './components/header.js';
import stylesHref from './styles.css';

Check failure on line 17 in app/root.tsx

View workflow job for this annotation

GitHub Actions / build

Cannot find module './styles.css' or its corresponding type declarations.

export const loader = async (_args: LoaderFunctionArgs) => {
return {
description: "PerfectPan's Blog",
icon: '/images/favicon.png',
title: "PerfectPan's Blog",
};
};

export const meta: MetaFunction<typeof loader> = ({ data }) => {
const title = data?.title ?? "PerfectPan's Blog";
const description = data?.description ?? "PerfectPan's Blog";
return [
{ title },
{ name: 'description', content: description },
{ property: 'og:title', content: title },
{ property: 'og:description', content: description },
];
};

export const links: LinksFunction = () => [
{ rel: 'stylesheet', href: stylesHref },
{ rel: 'icon', type: 'image/png', href: '/images/favicon.png' },
];

export default function App() {
return (
<html lang='en'>
<head>
<Meta />
<Links />
</head>
<body className='dark:bg-wash-dark dark:text-white'>
<div id='__waku'>
<Header />
<div className='flex flex-col min-h-screen px-6'>
<main className='flex flex-grow items-center justify-center *:min-h-64 *:min-w-64'>
<Outlet />
</main>
<Footer />
</div>
</div>
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}

export function ErrorBoundary() {
return (
<div className='flex flex-col items-center justify-center text-2xl'>
Something went wrong.
</div>
);
}
13 changes: 13 additions & 0 deletions app/routes/$.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { json } from '@remix-run/node';

export const loader = () => {
return json({}, { status: 404 });
};

export default function NotFoundPage() {
return (
<div className='flex flex-col items-center justify-center text-2xl'>
你闯入了无人之境...
</div>
);
}
34 changes: 16 additions & 18 deletions src/pages/index.tsx → app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
import { Link } from 'waku';
import type { LoaderFunctionArgs, MetaFunction } from '@remix-run/node';
import { Link } from '@remix-run/react';

export default async function HomePage() {
const data = await getData();
export const loader = async (_args: LoaderFunctionArgs) => {
return { title: "Home | PerfectPan's Blog" };
};

export const meta: MetaFunction<typeof loader> = ({ data }) => {
const title = data?.title ?? "PerfectPan's Blog";
return [
{ title },
{ name: 'description', content: title },
{ property: 'og:title', content: title },
{ property: 'og:description', content: title },
];
};

export default function HomePage() {
return (
<div className='flex flex-col items-center'>
<title>{data.title}</title>
<img className='m-0' src='/images/xm.jpg' alt='' />
<div className='mt-8 text-[2.5rem] font-black'>
是个什么都不会的废物.jpg
Expand All @@ -31,17 +43,3 @@ export default async function HomePage() {
</div>
);
}

const getData = async () => {
const data = {
title: "Home | PerfectPan's Blog",
};

return data;
};

export const getConfig = async () => {
return {
render: 'static',
};
};
64 changes: 64 additions & 0 deletions app/routes/blog.$slug.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { LoaderFunctionArgs, MetaFunction } from '@remix-run/node';
import { json } from '@remix-run/node';
import { Link, useLoaderData } from '@remix-run/react';
import { MDXWrapper } from '../components/mdx-wrapper.js';
import { Utterances } from '../components/utterances.js';
import { findBlogFileName, readBlog, renderMdxToHtml } from '../utils/index.js';

export const loader = async ({ params }: LoaderFunctionArgs) => {
const slug = params.slug;
if (!slug) {
throw new Response('Not Found', { status: 404 });
}

const fileName = await findBlogFileName(slug);
if (!fileName) {
throw new Response('Not Found', { status: 404 });
}

const { source, metadata } = await readBlog(fileName);
const html = await renderMdxToHtml(source);
const formattedDate = new Date(metadata.date).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
});

return json({
slug,
html,
metadata: { ...metadata, formattedDate },
});
};

export const meta: MetaFunction<typeof loader> = ({ data }) => {
if (!data) return [];
const title = `${data.metadata.title} | PerfectPan's Blog`;
const description = data.metadata.description;

return [
{ title },
{ name: 'description', content: description },
{ property: 'og:title', content: title },
{ property: 'og:description', content: description },
];
};

export default function BlogArticlePage() {
const { slug, html, metadata } = useLoaderData<typeof loader>();

return (
<div className='mx-auto w-full max-w-[80ch] pt-24 lg:pt-32'>
<div className='flex flex-col gap-2 m-auto mb-8'>
<div className='text-3xl font-black'>{metadata.title}</div>
<div className='opacity-60'>{metadata.formattedDate}</div>
</div>
<MDXWrapper html={html} />
<Link to='/blog' className='mt-4 inline-block'>
<span className='opacity-70'>&gt;&nbsp;&nbsp;&nbsp;</span>
<span className='underline opacity-70 hover:opacity-100'>cd ..</span>
</Link>
<Utterances slug={slug} />
</div>
);
}
59 changes: 59 additions & 0 deletions app/routes/blog._index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { LoaderFunctionArgs, MetaFunction } from '@remix-run/node';
import { json } from '@remix-run/node';
import { Link, useLoaderData } from '@remix-run/react';
import { getBlogList } from '../utils/posts.server.js';

export const loader = async (_args: LoaderFunctionArgs) => {
const blogList = await getBlogList();
return json({ blogList });
};

export const meta: MetaFunction = () => [
{ title: "Blog | PerfectPan's Blog" },
{ name: 'description', content: "Blog | PerfectPan's Blog" },
{ property: 'og:title', content: "Blog | PerfectPan's Blog" },
{ property: 'og:description', content: "Blog | PerfectPan's Blog" },
];

export default function BlogPage() {
const { blogList } = useLoaderData<typeof loader>();

return (
<div className='flex flex-col gap-8'>
<div className='mx-auto w-full max-w-[80ch] pt-24 lg:pt-32'>
{blogList.map((group) => {
return (
<div key={group.year}>
<div className='text-3xl mb-4'>{group.year}</div>
{group.blogs.map((blog) => (
<div
key={blog.name}
className='mt-2 mb-6 opacity-70 hover:opacity-100'
>
<Link
to={`/blog/${blog.name}`}
className='flex gap-2 items-center'
>
<span className='text-lg leading-[1.2em]'>
{blog.title}
</span>
<span className='text-sm opacity-50'>
{new Date(blog.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
})}
</span>
</Link>
</div>
))}
</div>
);
})}
</div>
<Link to='/' className='mt-4 inline-block'>
<span className='opacity-70'>&gt;&nbsp;&nbsp;&nbsp;</span>
<span className='underline opacity-70 hover:opacity-100'>cd ..</span>
</Link>
</div>
);
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
5 changes: 5 additions & 0 deletions app/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { withResolver } from './promise.js';
export { getMetaData } from './markdown.server.js';
export { groupedByDate } from './_.js';
export { renderMdxToHtml } from './mdx.server.js';
export { findBlogFileName, getBlogList, readBlog } from './posts.server.js';
40 changes: 40 additions & 0 deletions app/utils/markdown.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { remark } from 'remark';
import frontmatter from 'remark-frontmatter';
import yaml from 'yaml';
import { withResolver } from './promise.js';

export interface BlogMetaData {
date: string;
title: string;
description: string;
name: string;
tag?: string[];
}

export const getMetaData = async (fileName: string) => {
const absolutePath = path.join(process.cwd(), 'content', 'blog', fileName);
const source = await fs.readFile(absolutePath, 'utf8');
const { promise, resolve, reject } = withResolver<BlogMetaData>();
remark()
.use(frontmatter)
.use(() => (tree) => {
// biome-ignore lint/suspicious/noExplicitAny: mdast node type is hard
const yamlNode = (tree as any).children.find(
// biome-ignore lint/suspicious/noExplicitAny: mdast node type is hard
(node: any) => node.type === 'yaml',
);
if (yamlNode) {
const data = yaml.parse(yamlNode.value);
resolve({ ...data, name: fileName });
}
})
.process(source, (err) => {
if (err) {
reject(err);
}
});

return promise;
};
File renamed without changes.
Loading
Loading