Commit c50018d

HPCesia <me@hpcesia.com>
2025-01-20 08:28:13
feat: posts routing
1 parent 212d918
src/components/widgets/Button.astro
@@ -30,6 +30,10 @@ if (href && onclick) throw new AstroParameterConflictError('href', 'onclick');
     &:hover {
       @apply bg-[var(--theme-color-light)] dark:bg-[var(--theme-color-dark)];
     }
+
+    &:active {
+      @apply brightness-75 scale-95;
+    }
   }
 </style>
 
src/components/widgets/MetaIcon.astro
@@ -0,0 +1,25 @@
+---
+import type { ComponentProps } from 'astro/types';
+import { Icon } from 'astro-icon/components';
+
+type Props = ComponentProps<typeof Icon>;
+
+const { class: className, ...rest } = Astro.props;
+---
+
+<Icon {...rest} class:list={['meta-icon', className]} />
+
+<style lang="scss">
+  .meta-icon {
+    width: 2rem;
+    height: 2rem;
+    align-items: center;
+    margin-right: 0.5rem;
+    display: flex;
+    justify-content: center;
+
+    @apply text-[var(--theme-color-light-darken)] dark:text-[var(--theme-color-dark-lighten)];
+    @apply bg-[var(--theme-color-light-transparent)] dark:bg-[var(--theme-color-dark-transparent)];
+    @apply rounded-md;
+  }
+</style>
src/components/widgets/PostCard.astro
@@ -0,0 +1,86 @@
+---
+import MetaIcon from './MetaIcon.astro';
+import ReadMoreButton from './ReadMoreButton.astro';
+import PostCardCover from './PostCardCover.astro';
+
+interface Props {
+  class?: string;
+  title: string;
+  url: string;
+  published: Date;
+  updated?: Date;
+  tags: string[];
+  category?: string;
+  cover?: string;
+  description: string;
+}
+
+const { title, url, published, updated, tags, category, cover } = Astro.props;
+const className = Astro.props.class;
+
+const hasCover = cover !== '' && cover !== undefined && cover !== null;
+
+const metas: ({ icon: string; text: string; link?: string } | undefined)[] = [
+  {
+    icon: 'material-symbols:calendar-clock-outline-rounded',
+    text: published.toLocaleDateString(),
+  },
+  updated && {
+    icon: 'material-symbols:edit-calendar-outline-rounded',
+    text: updated.toLocaleDateString(),
+  },
+  category
+    ? {
+        icon: 'material-symbols:category-outline-rounded',
+        text: category,
+        link: `/archives/categories/${category}/1/`,
+      }
+    : undefined,
+  ...tags.map((tag) => {
+    return {
+      icon: 'material-symbols:tag-rounded',
+      text: tag,
+      link: `/archives/tags/${tag}/1/`,
+    };
+  }),
+];
+---
+
+<div
+  class:list={[
+    'theme-card-bg theme-border',
+    'border-2 rounded-xl p-4 flex max-md:flex-col-reverse w-full',
+    className,
+  ]}
+>
+  <div class="items-center px-12 py-7 w-full h-full mr-auto">
+    <div class="text-2xl mb-5"><a href={url}>{title}</a></div>
+    <div class="flex flex-wrap items-center mb-3 gap-x-4 gap-y-2 theme-text-second">
+      {
+        metas.map((meta) => {
+          return (
+            meta && (
+              <div class="flex items-center gap-1">
+                <MetaIcon name={meta.icon} />
+                {meta.link ? (
+                  <a href={meta.link} class="text-sm font-medium">
+                    {meta.text}
+                  </a>
+                ) : (
+                  <span class="text-sm font-medium">{meta.text}</span>
+                )}
+              </div>
+            )
+          );
+        })
+      }
+    </div>
+  </div>
+  {
+    hasCover ? (
+      <PostCardCover url={url} title={title} cover={cover} />
+    ) : (
+      <ReadMoreButton href={url} title={title} />
+    )
+  }
+</div>
src/components/widgets/PostCardCover.astro
@@ -0,0 +1,38 @@
+---
+import { Icon } from 'astro-icon/components';
+import { Image } from 'astro:assets';
+
+interface Props {
+  url: string;
+  title: string;
+  cover: string;
+}
+
+const { url, title, cover } = Astro.props;
+---
+
+<a href={url} title={title} class="group">
+  <div><Icon name="material-symbols:chevron-right-rounded" /></div>
+  <Image src={cover} alt={title} inferSize={true} />
+</a>
+
+<style>
+  a {
+    @apply min-h-48 w-full md:w-3/4 md:max-w-96 relative;
+    @apply active:brightness-75 active:scale-95 duration-100;
+  }
+
+  img {
+    @apply w-full h-full object-cover rounded-md;
+  }
+
+  div {
+    @apply absolute inset-0 w-full h-full bg-black/60;
+    @apply flex items-center justify-center;
+    @apply opacity-0 group-hover:opacity-100 duration-300;
+  }
+
+  svg {
+    @apply w-24 h-24 text-white;
+  }
+</style>
src/components/widgets/ReadMoreButton.astro
@@ -0,0 +1,26 @@
+---
+import type { ComponentProps } from 'astro/types';
+import { Icon } from 'astro-icon/components';
+
+type Props = Omit<ComponentProps<typeof Icon>, 'name'>;
+
+const { href, title, ...rest } = Astro.props;
+---
+
+<a href={href} title={title}>
+  <Icon name="material-symbols:chevron-right-rounded" {...rest} />
+</a>
+
+<style>
+  a {
+    @apply max-md:hidden duration-100;
+    @apply hover:brightness-125;
+    @apply active:brightness-75 active:scale-95;
+  }
+
+  svg {
+    @apply min-h-48 h-full w-12 rounded-md;
+    @apply text-[var(--theme-color-light-darken)] dark:text-[var(--theme-color-dark-lighten)];
+    @apply bg-[var(--theme-color-light-transparent)] dark:bg-[var(--theme-color-dark-transparent)];
+  }
+</style>
src/components/CatagoryBar.astro
@@ -1,5 +0,0 @@
----
-
----
-
-<div id="catagory-bar" class="flex w-full rounded-md px-2 py-3"></div>
src/components/CategoryBar.astro
@@ -0,0 +1,5 @@
+---
+
+---
+
+<div id="category-bar" class="flex w-full rounded-md px-2 py-3"></div>
src/components/Navbar.astro
@@ -16,10 +16,10 @@ if (!title) title = 'Astral Halo';
 <div
   id="navbar"
   transition:persist
-  class="flex w-full h-[60px] items-center border-b-2 theme-border theme-card-bg"
+  class="flex fixed w-full h-16 items-center border-b-2 theme-border theme-card-bg"
 >
   <div id="nav-left" class="left-0 flex mr-auto w-fit">
-    <Button id="site-name-wrapper" href="/">
+    <Button id="site-name" href="/">
       <span class="text-xl font-bold">{title}</span>
     </Button>
   </div>
@@ -27,11 +27,10 @@ if (!title) title = 'Astral Halo';
     {
       navbarConfig.navbarCenterItems.map((item) => (
         <Button
-          id="nav-menu-item"
           href={item.href}
           onclick={item.onclick}
           title={item.text}
-          class="!px-4"
+          class="nav-menu-item !px-4"
         >
           <span class="text-xl tracking-wide">{item.text}</span>
         </Button>
@@ -42,7 +41,12 @@ if (!title) title = 'Astral Halo';
     <div class="flex max-md:hidden">
       {
         navbarConfig.navbarRightItems.onlyWide.map((item) => (
-          <Button id="nav-menu-item" href={item.href} onclick={item.onclick} title={item.text}>
+          <Button
+            class="nav-menu-item"
+            href={item.href}
+            onclick={item.onclick}
+            title={item.text}
+          >
             <Icon name={item.icon} class="text-2xl" />
           </Button>
         ))
@@ -51,16 +55,22 @@ if (!title) title = 'Astral Halo';
     <div class="flex">
       {
         navbarConfig.navbarRightItems.always.map((item) => (
-          <Button id="nav-menu-item" href={item.href} onclick={item.onclick} title={item.text}>
+          <Button
+            class="nav-menu-item"
+            href={item.href}
+            onclick={item.onclick}
+            title={item.text}
+          >
             <Icon name={item.icon} class="text-2xl" />
           </Button>
         ))
       }
     </div>
     <div class="flex md:hidden">
-      <Button id="toggle-sidebar-btn">
+      <Button id="nav-toggle-sidebar-btn">
         <Icon name="material-symbols:menu-rounded" class="text-2xl" />
       </Button>
     </div>
   </div>
 </div>
+<div id="navbar-placeholder" class="pt-20"></div>
src/components/PostCards.astro
@@ -1,1 +0,0 @@
-<div id="post-cards"></div>
src/components/PostPage.astro
@@ -0,0 +1,39 @@
+---
+import type { BlogPostData } from '@/types/data';
+import { siteConfig } from '@/config';
+import PostCard from './widgets/PostCard.astro';
+
+interface Props {
+  posts: {
+    body: string;
+    data: BlogPostData;
+  }[];
+  currentPage: number;
+  postsPerPage?: number;
+}
+
+let { posts, currentPage, postsPerPage } = Astro.props;
+
+postsPerPage = postsPerPage || siteConfig.postsPerPage;
+
+posts = posts.slice((currentPage - 1) * postsPerPage, currentPage * postsPerPage);
+---
+
+<div id="post-page" class="flex flex-col gap-4">
+  {
+    posts.map((post) => {
+      const data = post.data;
+      return (
+        <PostCard
+          title={data.title}
+          url={'/posts/' + data.abbrlink + '/'}
+          published={data.published}
+          tags={data.tags}
+          category={data.category}
+          cover={data.cover}
+          description={data.description}
+        />
+      );
+    })
+  }
+</div>
src/components/Sidebar.astro
@@ -10,12 +10,12 @@ import DarkModeButton from './widgets/DarkModeButton.astro';
 <div id="sidebar" transition:persist>
   <div
     id="sidebar-mask"
-    class="fixed z-40 hidden w-full h-full bg-black/10 backdrop-blur-md backdrop-saturate-100"
+    class="fixed z-40 opacity-0 pointer-events-none w-full h-full bg-black/10 backdrop-blur-md backdrop-saturate-100 duration-500 ease-in-out"
   >
   </div>
   <div
     id="sidebar-menu"
-    class="fixed md:hidden h-full z-50 -right-1/3 w-1/3 duration-500 ease-in-out border-l-2 theme-border theme-card-bg"
+    class="fixed h-full z-50 -right-1/3 w-1/3 duration-500 ease-in-out border-l-2 theme-border theme-card-bg"
   >
     <div id="sidebar-site-data"></div>
     <DarkModeButton
@@ -54,26 +54,30 @@ import DarkModeButton from './widgets/DarkModeButton.astro';
 </div>
 
 <script>
-  const toggleSidebarBtns = document.querySelectorAll('button#toggle-sidebar-btn');
+  const toggleSidebarBtns = document.getElementById('nav-toggle-sidebar-btn');
   const sidebarMask = document.getElementById('sidebar-mask');
   const sidebarMenu = document.getElementById('sidebar-menu');
 
-  toggleSidebarBtns.forEach((btn) => {
-    btn.addEventListener('click', () => {
-      sidebarMask?.classList.toggle('hidden');
-      sidebarMenu?.classList.toggle('-translate-x-full');
-    });
+  toggleSidebarBtns?.addEventListener('click', () => {
+    sidebarMask?.classList.remove('opacity-0');
+    sidebarMask?.classList.remove('pointer-events-none');
+    sidebarMenu?.classList.remove('-translate-x-full');
+    sidebarMenu?.removeAttribute('inert');
   });
 
   sidebarMask?.addEventListener('click', () => {
-    sidebarMask?.classList.add('hidden');
+    sidebarMask?.classList.add('opacity-0');
+    sidebarMask?.classList.add('pointer-events-none');
     sidebarMenu?.classList.remove('-translate-x-full');
+    sidebarMenu?.setAttribute('inert', 'true');
   });
 
   window.addEventListener('resize', () => {
     if (window.innerWidth > 768) {
-      sidebarMask?.classList.add('hidden');
+      sidebarMask?.classList.add('opacity-0');
+      sidebarMask?.classList.add('pointer-events-none');
       sidebarMenu?.classList.remove('-translate-x-full');
+      sidebarMenu?.setAttribute('inert', 'true');
     }
   });
 </script>
src/components/SideToolBar.astro
@@ -28,10 +28,12 @@ import DarkModeButton from './widgets/DarkModeButton.astro';
 
   stbTrigger?.addEventListener('click', () => {
     stbShow?.classList.toggle('translate-x-full');
+    stbShow?.toggleAttribute('inert');
   });
 
   stbShowMore?.addEventListener('click', () => {
     stbHide?.classList.toggle('translate-x-full');
+    stbHide?.toggleAttribute('inert');
   });
 
   window.addEventListener('scroll', () => {
@@ -39,6 +41,8 @@ import DarkModeButton from './widgets/DarkModeButton.astro';
     else {
       stbShow?.classList.add('translate-x-full');
       stbHide?.classList.add('translate-x-full');
+      stbShow?.toggleAttribute('inert', true);
+      stbHide?.toggleAttribute('inert', true);
     }
   });
 </script>
src/content/posts/manual.md
@@ -0,0 +1,41 @@
+---
+title: Maunal
+abbrlink: '12345678'
+published: 2025-01-16 22:01:22
+category: TEST
+tags:
+  - test1
+  - test2
+  - test3
+  - test4
+  - test5
+  - test6
+  - test7
+  - test8
+  - test9
+  - test10
+---
+
+THIS IS A TEST
+
+# h1
+some content
+## h2
+some content
+### h3
+some content
+#### h4
+some content
+##### h5
+some content
+
+# h1 again
+some content
+## h2 again
+some content
+### h3 again
+some content
+#### h4 again
+some content
+##### h5 again
+some content
\ No newline at end of file
src/pages/archives/categories/[category]/[page].astro
@@ -0,0 +1,41 @@
+---
+import { getSortedPosts } from '@utils/content-utils';
+import MainLayout from '@layouts/MainLayout.astro';
+import PostPage from '@components/PostPage.astro';
+import { siteConfig } from '@/config';
+import { i18n } from '@i18n/translation';
+import I18nKey from '@i18n/i18nKey';
+
+export async function getStaticPaths() {
+  const posts = await getSortedPosts();
+  const categories = [
+    ...new Set(posts.map((post) => post.data.category || i18n(I18nKey.uncategorized))),
+  ];
+  return categories
+    .map((category) => {
+      const categoryPosts = posts.filter((post) => post.data.category === category);
+      const pageNum = Math.ceil(categoryPosts.length / siteConfig.postsPerPage);
+      return Array.from({ length: pageNum }, (_, i) => ({
+        params: { category, page: (i + 1).toString() },
+        props: {
+          category,
+          posts: categoryPosts.slice(
+            i * siteConfig.postsPerPage,
+            (i + 1) * siteConfig.postsPerPage
+          ),
+          currentPage: i + 1,
+        },
+      }));
+    })
+    .flat();
+}
+
+const { category, posts, currentPage } = Astro.props;
+---
+
+<MainLayout>
+  <div id="main-content">
+    <PostPage posts={posts} currentPage={currentPage} />
+  </div>
+  <div id="aside-content"></div>
+</MainLayout>
src/pages/archives/categories/index.astro
@@ -0,0 +1,7 @@
+---
+import MainLayout from '@layouts/MainLayout.astro';
+---
+
+<MainLayout>
+  <div></div>
+</MainLayout>
src/pages/archives/tags/[tag]/[page].astro
@@ -0,0 +1,34 @@
+---
+import { getSortedPosts } from '@utils/content-utils';
+import MainLayout from '@layouts/MainLayout.astro';
+import PostPage from '@components/PostPage.astro';
+import { siteConfig } from '@/config';
+
+export async function getStaticPaths() {
+  const posts = await getSortedPosts();
+  const tags = [...new Set(posts.map((post) => post.data.tags).flat())];
+  return tags
+    .map((tag) => {
+      const tagPosts = posts.filter((post) => post.data.tags.includes(tag));
+      const pageNum = Math.ceil(tagPosts.length / siteConfig.postsPerPage);
+      return Array.from({ length: pageNum }, (_, i) => ({
+        params: { tag, page: (i + 1).toString() },
+        props: {
+          tag,
+          posts: tagPosts.slice(i * siteConfig.postsPerPage, (i + 1) * siteConfig.postsPerPage),
+          currentPage: i + 1,
+        },
+      }));
+    })
+    .flat();
+}
+
+const { tag, posts, currentPage } = Astro.props;
+---
+
+<MainLayout>
+  <div id="main-content">
+    <PostPage posts={posts} currentPage={currentPage} />
+  </div>
+  <div id="aside-content"></div>
+</MainLayout>
src/pages/archives/tags/index.astro
@@ -0,0 +1,7 @@
+---
+import MainLayout from '@layouts/MainLayout.astro';
+---
+
+<MainLayout>
+  <div></div>
+</MainLayout>
src/pages/posts/[article].astro
@@ -0,0 +1,22 @@
+---
+import { getCollection, render } from 'astro:content';
+import MainLayout from '@layouts/MainLayout.astro';
+
+export async function getStaticPaths() {
+  const articles = await getCollection('posts');
+  return articles.map((article) => ({
+    params: { article: article.data.abbrlink },
+    props: { article },
+  }));
+}
+
+const { article } = Astro.props;
+const { Content } = await render(article);
+---
+
+<MainLayout>
+  <div id="main-content">
+    <Content />
+  </div>
+  <div id="aside-content"></div>
+</MainLayout>
src/pages/[...page].astro
@@ -0,0 +1,32 @@
+---
+import { getSortedPosts } from '@utils/content-utils';
+import { siteConfig } from '@/config';
+import categoryBar from '@components/categoryBar.astro';
+import PostPage from '@components/PostPage.astro';
+import MainLayout from '@layouts/MainLayout.astro';
+
+export async function getStaticPaths() {
+  const articles = await getSortedPosts();
+  const pageNum = Math.ceil(articles.length / siteConfig.postsPerPage);
+  let pages = Array.from({ length: pageNum }).map((_, i) => ({
+    params: { page: i + 1 === 1 ? undefined : 'page/' + (i + 1).toString() },
+    props: { page: i + 1 },
+  }));
+  return pages;
+}
+
+const { page } = Astro.props;
+const articles = await getSortedPosts();
+const posts = articles.slice(
+  (page - 1) * siteConfig.postsPerPage,
+  page * siteConfig.postsPerPage
+);
+---
+
+<MainLayout>
+  <div id="main-content">
+    <categoryBar></categoryBar>
+    <PostPage posts={posts} currentPage={page} />
+  </div>
+  <div id="aside-content"></div>
+</MainLayout>
src/pages/about.astro
@@ -6,5 +6,5 @@ import { i18n } from '@i18n/translation';
 ---
 
 <MainLayout title={i18n(I18nKey.about)}>
-  <div></div>
+  <div>测试</div>
 </MainLayout>
src/pages/index.astro
@@ -1,12 +0,0 @@
----
-import PostCards from '@components/PostCards.astro';
-import CatagoryBar from '@components/widgets/CatagoryBar.astro';
-import MainLayout from '@layouts/MainLayout.astro';
----
-
-<MainLayout>
-  <div id="main-content">
-    <CatagoryBar />
-    <PostCards />
-  </div>
-</MainLayout>
src/styles/globals.scss
@@ -16,6 +16,14 @@
     @apply text-[var(--theme-text-color-light)] dark:text-[var(--theme-text-color-dark)];
   }
 
+  .theme-text-hl {
+    @apply text-[var(--theme-color-light)] dark:text-[var(--theme-color-dark)];
+  }
+
+  .theme-text-second {
+    @apply text-[var(--theme-text-color-second-light)] dark:text-[var(--theme-text-color-second-dark)];
+  }
+
   .theme-border {
     @apply border-[var(--theme-border-color-light)] dark:border-[var(--theme-border-color-dark)];
   }
src/styles/variables.scss
@@ -8,6 +8,8 @@ $theme-card-bg-color-light: $theme-bg-color-light;
 $theme-card-bg-color-dark: $theme-bg-color-dark;
 $theme-text-color-light: theme('colors.neutral.900');
 $theme-text-color-dark: theme('colors.neutral.100');
+$theme-text-color-secondary-light: theme('colors.neutral.600');
+$theme-text-color-secondary-dark: theme('colors.neutral.400');
 $theme-border-color-light: theme('colors.neutral.200');
 $theme-border-color-dark: theme('colors.neutral.800');
 
@@ -26,6 +28,8 @@ $theme-border-color-dark: theme('colors.neutral.800');
   --theme-card-bg-color-dark: #{$theme-card-bg-color-dark};
   --theme-text-color-light: #{$theme-text-color-light};
   --theme-text-color-dark: #{$theme-text-color-dark};
+  --theme-text-color-second-light: #{$theme-text-color-secondary-light};
+  --theme-text-color-second-dark: #{$theme-text-color-secondary-dark};
   --theme-border-color-light: #{$theme-border-color-light};
   --theme-border-color-dark: #{$theme-border-color-dark};
 }
src/types/Color.ts
@@ -1,5 +0,0 @@
-export type HexColor = `#${string}`;
-export type RGBColor = `rgb(${number}, ${number}, ${number})`;
-export type RGBAColor = `rgba(${number}, ${number}, ${number}, ${number})`;
-
-export type Color = HexColor | RGBColor | RGBAColor;
src/types/config.ts
@@ -1,9 +1,9 @@
-import type { Color } from './Color';
-
 export type SiteConfig = {
   title: string;
+  subtitle: string;
   lang: string;
   favicon: (string | { src: string; theme?: 'light' | 'dark' })[];
+  postsPerPage: number;
 };
 
 export type ProfileConfig = {
src/types/data.ts
@@ -0,0 +1,11 @@
+export type BlogPostData = {
+  body: string;
+  title: string;
+  abbrlink: string;
+  published: Date;
+  description: string;
+  tags: string[];
+  draft?: boolean;
+  cover?: string;
+  category?: string;
+};
src/utils/content-utils.ts
@@ -0,0 +1,17 @@
+import { getCollection } from 'astro:content';
+import type { BlogPostData } from '@/types/data';
+
+export async function getSortedPosts(): Promise<{ body: string; data: BlogPostData }[]> {
+  const allBlogPosts = (await getCollection('posts')) as unknown as {
+    body: string;
+    data: BlogPostData;
+  }[];
+  const sortedBlogPosts = allBlogPosts.sort(
+    (a: { data: BlogPostData }, b: { data: BlogPostData }) => {
+      const dateA = new Date(a.data.published);
+      const dateB = new Date(b.data.published);
+      return dateA > dateB ? -1 : 1;
+    }
+  );
+  return sortedBlogPosts;
+}
src/utils/url-utils.ts
@@ -0,0 +1,14 @@
+export function pathsEqual(path1: string, path2: string) {
+  const normalizedPath1 = path1.replace(/^\/|\/$/g, '').toLowerCase();
+  const normalizedPath2 = path2.replace(/^\/|\/$/g, '').toLowerCase();
+  return normalizedPath1 === normalizedPath2;
+}
+
+function joinUrl(...parts: string[]): string {
+  const joined = parts.join('/');
+  return joined.replace(/\/+/g, '/');
+}
+
+export function url(path: string) {
+  return joinUrl('', import.meta.env.BASE_URL, path);
+}
src/config.ts
@@ -11,8 +11,10 @@ import { i18n } from '@i18n/translation';
 
 export const siteConfig: SiteConfig = {
   title: 'Astral Halo',
+  subtitle: '',
   lang: 'zh_CN', // "en" | "zh_CN" | "zh_TW"
   favicon: [''],
+  postsPerPage: 10,
 };
 
 export const profileConfig: ProfileConfig = {
@@ -24,9 +26,9 @@ export const profileConfig: ProfileConfig = {
 
 export const navbarConfig: NavbarConfig = {
   navbarCenterItems: [
-    { text: i18n(I18nKey.archive), href: '/archive/' },
-    { text: i18n(I18nKey.categories), href: '/categories/' },
-    { text: i18n(I18nKey.tags), href: '/tags/' },
+    { text: i18n(I18nKey.archive), href: '/archives/' },
+    { text: i18n(I18nKey.categories), href: '/archives/categories/' },
+    { text: i18n(I18nKey.tags), href: '/archives/tags/' },
     { text: i18n(I18nKey.about), href: '/about/' },
   ],
   navbarRightItems: {
src/content.config.ts
@@ -0,0 +1,24 @@
+import { defineCollection, z } from 'astro:content';
+import { glob } from 'astro/loaders';
+
+const postsCollection = defineCollection({
+  loader: glob({
+    pattern: '**/*.md',
+    base: 'src/content/posts',
+  }),
+  schema: z.object({
+    title: z.string(),
+    abbrlink: z.string(),
+    published: z.date(),
+    updated: z.date().optional(),
+    draft: z.boolean().optional().default(false),
+    description: z.string().optional().default(''),
+    cover: z.string().optional().default(''),
+    tags: z.array(z.string()).optional().default([]),
+    category: z.string().optional().default(''),
+    lang: z.string().optional().default(''),
+  }),
+});
+export const collections = {
+  posts: postsCollection,
+};