feat: add basic webui
This commit is contained in:
132
apps/web/app/blog/[slug]/page.tsx
Normal file
132
apps/web/app/blog/[slug]/page.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { Sidebar } from '@/components/sidebar';
|
||||
import { blog } from '@konobangu/cms';
|
||||
import { Body } from '@konobangu/cms/components/body';
|
||||
import { Feed } from '@konobangu/cms/components/feed';
|
||||
import { Image } from '@konobangu/cms/components/image';
|
||||
import { TableOfContents } from '@konobangu/cms/components/toc';
|
||||
import { env } from '@konobangu/env';
|
||||
import { JsonLd } from '@konobangu/seo/json-ld';
|
||||
import { createMetadata } from '@konobangu/seo/metadata';
|
||||
import { ArrowLeftIcon } from '@radix-ui/react-icons';
|
||||
import type { Metadata } from 'next';
|
||||
import { draftMode } from 'next/headers';
|
||||
import Link from 'next/link';
|
||||
import { notFound } from 'next/navigation';
|
||||
import Balancer from 'react-wrap-balancer';
|
||||
|
||||
type BlogPostProperties = {
|
||||
readonly params: Promise<{
|
||||
slug: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export const generateMetadata = async ({
|
||||
params,
|
||||
}: BlogPostProperties): Promise<Metadata> => {
|
||||
const { slug } = await params;
|
||||
const post = await blog.getPost(slug);
|
||||
|
||||
if (!post) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return createMetadata({
|
||||
title: post._title,
|
||||
description: post.description,
|
||||
image: post.image.url,
|
||||
});
|
||||
};
|
||||
|
||||
export const generateStaticParams = async (): Promise<{ slug: string }[]> => {
|
||||
const posts = await blog.getPosts();
|
||||
|
||||
return posts.map(({ _slug }) => ({ slug: _slug }));
|
||||
};
|
||||
|
||||
const BlogPost = async ({ params }: BlogPostProperties) => {
|
||||
const { slug } = await params;
|
||||
const draft = await draftMode();
|
||||
|
||||
return (
|
||||
<Feed queries={[blog.postQuery(slug)]} draft={draft.isEnabled}>
|
||||
{/* biome-ignore lint/suspicious/useAwait: "Server Actions must be async" */}
|
||||
{async ([data]) => {
|
||||
'use server';
|
||||
|
||||
const [page] = data.blog.posts.items;
|
||||
|
||||
if (!page) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<JsonLd
|
||||
code={{
|
||||
'@type': 'BlogPosting',
|
||||
'@context': 'https://schema.org',
|
||||
datePublished: page.date,
|
||||
description: page.description,
|
||||
mainEntityOfPage: {
|
||||
'@type': 'WebPage',
|
||||
'@id': new URL(
|
||||
`/blog/${slug}`,
|
||||
env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL
|
||||
).toString(),
|
||||
},
|
||||
headline: page._title,
|
||||
image: page.image.url,
|
||||
dateModified: page.date,
|
||||
author: page.authors.at(0)?._title,
|
||||
isAccessibleForFree: true,
|
||||
}}
|
||||
/>
|
||||
<div className="container py-16">
|
||||
<Link
|
||||
className="mb-4 inline-flex items-center gap-1 text-muted-foreground text-sm focus:underline focus:outline-none"
|
||||
href="/blog"
|
||||
>
|
||||
<ArrowLeftIcon className="h-4 w-4" />
|
||||
Back to Blog
|
||||
</Link>
|
||||
<div className="mt-16 flex flex-col items-start gap-8 sm:flex-row">
|
||||
<div className="sm:flex-1">
|
||||
<div className="prose prose-neutral dark:prose-invert max-w-none">
|
||||
<h1 className="scroll-m-20 font-extrabold text-4xl tracking-tight lg:text-5xl">
|
||||
<Balancer>{page._title}</Balancer>
|
||||
</h1>
|
||||
<p className="leading-7 [&:not(:first-child)]:mt-6">
|
||||
<Balancer>{page.description}</Balancer>
|
||||
</p>
|
||||
{page.image ? (
|
||||
<Image
|
||||
src={page.image.url}
|
||||
width={page.image.width}
|
||||
height={page.image.height}
|
||||
alt={page.image.alt ?? ''}
|
||||
className="my-16 h-full w-full rounded-xl"
|
||||
priority
|
||||
/>
|
||||
) : undefined}
|
||||
<div className="mx-auto max-w-prose">
|
||||
<Body content={page.body.json.content} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sticky top-24 hidden shrink-0 md:block">
|
||||
<Sidebar
|
||||
toc={<TableOfContents data={page.body.json.toc} />}
|
||||
readingTime={`${page.body.readingTime} min read`}
|
||||
date={new Date(page.date)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Feed>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogPost;
|
||||
15
apps/web/app/blog/layout.tsx
Normal file
15
apps/web/app/blog/layout.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Toolbar } from '@konobangu/cms/components/toolbar';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
type BlogLayoutProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const BlogLayout = ({ children }: BlogLayoutProps) => (
|
||||
<>
|
||||
{children}
|
||||
<Toolbar />
|
||||
</>
|
||||
);
|
||||
|
||||
export default BlogLayout;
|
||||
87
apps/web/app/blog/page.tsx
Normal file
87
apps/web/app/blog/page.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
// import { blog } from '@konobangu/cms';
|
||||
// import { Feed } from '@konobangu/cms/components/feed';
|
||||
import { Image } from '@konobangu/cms/components/image';
|
||||
import { cn } from '@konobangu/design-system/lib/utils';
|
||||
import type { Blog, WithContext } from '@konobangu/seo/json-ld';
|
||||
import { JsonLd } from '@konobangu/seo/json-ld';
|
||||
import { createMetadata } from '@konobangu/seo/metadata';
|
||||
import type { Metadata } from 'next';
|
||||
import { draftMode } from 'next/headers';
|
||||
import Link from 'next/link';
|
||||
|
||||
const title = 'Blog';
|
||||
const description = 'Thoughts, ideas, and opinions.';
|
||||
|
||||
export const metadata: Metadata = createMetadata({ title, description });
|
||||
|
||||
const BlogIndex = async () => {
|
||||
const draft = await draftMode();
|
||||
|
||||
const jsonLd: WithContext<Blog> = {
|
||||
'@type': 'Blog',
|
||||
'@context': 'https://schema.org',
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<JsonLd code={jsonLd} />
|
||||
<div className="w-full py-20 lg:py-40">
|
||||
<div className="container mx-auto flex flex-col gap-14">
|
||||
<div className="flex w-full flex-col gap-8 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h4 className="max-w-xl font-regular text-3xl tracking-tighter md:text-5xl">
|
||||
Latest articles
|
||||
</h4>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
|
||||
{/* <Feed queries={[blog.postsQuery]} draft={draft.isEnabled}>
|
||||
{async ([data]) => {
|
||||
'use server';
|
||||
|
||||
if (!data.blog.posts.items.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return data.blog.posts.items.map((post, index) => (
|
||||
<Link
|
||||
href={`/blog/${post._slug}`}
|
||||
className={cn(
|
||||
'flex cursor-pointer flex-col gap-4 hover:opacity-75',
|
||||
!index && 'md:col-span-2'
|
||||
)}
|
||||
key={post._slug}
|
||||
>
|
||||
<Image
|
||||
src={post.image.url}
|
||||
alt={post.image.alt ?? ''}
|
||||
width={post.image.width}
|
||||
height={post.image.height}
|
||||
/>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{new Date(post.date).toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h3 className="max-w-3xl text-4xl tracking-tight">
|
||||
{post._title}
|
||||
</h3>
|
||||
<p className="max-w-3xl text-base text-muted-foreground">
|
||||
{post.description}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
));
|
||||
}}
|
||||
</Feed> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogIndex;
|
||||
Reference in New Issue
Block a user