介绍

我正在为自己建立一个项目,在这个项目中我需要构建一个博客。作为一名技术作家,一旦我理解了Markdown文章,我就会写Markdown文章。我想建立一个能够呈现Markdown的博客,而不是使用文本格式化器。在本文中,我将向您介绍如何使用Supabase和Chakra UI构建一个Markdown呈现的博客。

Supabase (opens new window)将用于将文章数据存储在数据库中并将文章的封面图像存储在存储中。Chakra UI (opens new window)将用于为元素提供样式。通过同时使用这两个工具,我们可以轻松地构建博客。

在项目中,我们将创建3个路由:

  • /(根目录): 它将以网格格式显示所有文章。
  • /create: 通过提供名称、描述和文章的Markdown版本,可以用于创建文章。
  • /[slug]: 它将呈现Markdown文章。

现在,让我们开始构建这个项目。

设置环境 #

让我们设置构建项目所需的环境。就前端框架而言,我们将使用带有应用程序目录的NextJS。这将有助于路由和导航。使用以下命令安装NextJS项目:

npx create-next-app@latest supabase-blog

使用下面的设置进行NextJS安装:

NextJS setting

注意:要运行上述命令,您需要在系统上预先安装NodeJS和NPM。

安装完成NextJS项目后,我们现在可以安装依赖项。

ChakraUI #

在项目的根目录中,运行以下命令以安装Chakra UI。

npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion chakra-react-select react-toastify

我还额外添加了 chakra-react-selectreact-toastify 用于改进下拉选择和通知用户响应。安装完成后,我们需要从应用程序目录更新 layout.js。以下是代码:

"use client";
import { Inter } from "next/font/google";
import "./globals.css";
import { ChakraProvider } from "@chakra-ui/react";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import theme from "../theme/theme";

const inter = Inter({ subsets: ["latin"] });
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <ChakraProvider theme={theme}>
          <ToastContainer />
          {children}
        </ChakraProvider>
      </body>
    </html>
  );
}

我们在文件顶部使用了 “use client” 来对Chakra UI进行客户端渲染。除此之外都是很容易理解的。导入的 theme 是Charka UI组件的自定义主题。您可以在这里 (opens new window)查看所有自定义组件。 现在,Supabase设置已完成。

附加依赖 #

我们需要两个更多的依赖项来完成设置。您可以使用以下命令进行安装:

npm i chakra-ui-markdown-renderer formik

第一个将用于将Markdown语法转换为具有所需Chakra UI主题的HTML组件。 Formik用于处理文章数据的表单输入。

项目结构 #

如上所述,我们将拥有3个路由。以下是应用程序目录中的结构。

--app
    --page.js
    --create
            --page.js
    --[slug]
            --page.js

让我们首先从根目录开始。

根/(root) #

在根目录中,我们将显示文章。以下是该文件的代码。

// 未为所有代码文件添加导入。您可以从下面提到的GitHub存储库中获取整个代码。

const BlogTags = (props) => {
  const { marginTop = 0, tags } = props;
  return (
    <HStack spacing={2} marginTop={marginTop}>
      {tags.map((tag) => {
        return (
          <Tag size={"md"} variant="solid" colorScheme="orange" key={tag}>
            {tag}
          </Tag>
        );
      })}
    </HStack>
  );
};

const ArticleList = () => {
  const [articleData, setArticleData] = useState();
  const getArticleData = async () => {
    const { data: articleData, error: artilceError } = await supabase
      .from("article")
      .select();
    if (artilceError) {
      console.log(artilceError);
    } else {
      setArticleData(articleData);
      console.log(articleData);
    }
  };
  useEffect(() => {
    getArticleData();
  }, []);
  return (
    <Box marginTop="20" className="mainContainer">
      <Heading variant="primary-heading" mt={20}>
        Stories by Suraj Vishwakarma
      </Heading>
      <Flex justifyContent="space-between">
        <Heading variant="secondary-heading" marginTop="10" marginBottom="10">
          Latest articles
        </Heading>
        <Link href="/create">
          <Button variant="primary-button">Create</Button>
        </Link>
      </Flex>
      <SimpleGrid
        templateColumns={{
          base: "repeat(1, 1fr)",
          lg: "repeat(6, 1fr)",
        }}
        spacing="40px"
      >
        {!articleData &&
          [0, 1, 2].map((item) => {
            return (
              <GridItem colSpan={{ base: 6, lg: 2 }} key={item}>
                <Wrap spacing="30px" marginBottom="10">
                  <WrapItem
                    width={{ base: "100%", sm: "100%", md: "100%", lg: "100%" }}
                  >
                    <Box w="100%" height="100%">
                      <Box borderRadius="lg" overflow="hidden">
                        <Box
                          textDecoration="none"
                          _hover={{ textDecoration: "none" }}
                        >
                          <Skeleton colorScheme="purple">
                            <div
                              style={{ borderRadius: "10px", height: "400px" }}
                            />
                          </Skeleton>
                        </Box>
                      </Box>
                    </Box>
                  </WrapItem>
                </Wrap>
              </GridItem>
            );
          })}
        {articleData &&
          articleData.map((item, index) => {
            return (
              <GridItem colSpan={{ base: 6, lg: 2 }} key={index}>
                <Wrap spacing="30px" marginBottom="10">
                  <WrapItem
                    width={{ base: "100%", sm: "100%", md: "100%", lg: "100%" }}
                  >
                    <Box w="100%" height="100%">
                      <Box borderRadius="lg" overflow="hidden">
                        <Box
                          textDecoration="none"
                          _hover={{ textDecoration: "none" }}
                        >
                          <Link href={`/${item.slug}`}>
                            <Image
                              transform="scale(1.0)"
                              src={item.thumbnail}
                              alt="some text"
                              objectFit="contain"
                              width="100%"
                              transition="0.3s ease-in-out"
                              _hover={{
                                transform: "scale(1.05)",
                              }}
                            />
                          </Link>
                        </Box>
                      </Box>
                      <BlogTags tags={item.tags} marginTop={3} />
                      <Heading fontSize="xl" marginTop="2">
                        <Link href={`/${item.slug}`}>
                          <Text
                            textDecoration="none"
                            _hover={{ textDecoration: "none" }}
                          >
                            {item.title}
                          </Text>
                        </Link>
                      </Heading>
                      <Text as="p" fontSize="md" marginTop="2">
                        {item.description}
                      </Text>
                    </Box>
                  </WrapItem>
                </Wrap>
              </GridItem>
            );
          })}
      </SimpleGrid>
    </Box>
  );
};
export default ArticleList;

我们在此处使用useEffect hook调用getArticle()函数来获取文章数据。在返回部分,我们正在显示文章。完成后,该路由将如下所示:Home Page (opens new window)

创建路由 #

在应用程序目录中创建一个名为create的目录。这个路由将用于创建文章。这里有4个文件。

--page.js     // 将呈现页面。
--ArticleSetting.js     // 它将包含标题、描述、标签等数据。
--WriteArticle.js     // 用于放置文章的markdown版本。
--tagOption.js     // 这个文件包含可以选择的标签选项。

所有文件在代码行数方面都相当庞大。因此,与其在此处放置所有内容,您可以从此GitHub存储库中查看它这里 (opens new window)。我们可以从这些路由中讨论一些有趣的代码片段。

将图片上传到存储 #

<FormControl>
  <Input
    variant={"form-input-file"}
    name="thumbnail"
    type="file"
    onChange={ async (e) => {
          const timestamp = Date.now();
          const { data, error } = await supabase.storage
            .from("thumbnail")
            .upload(`${timestamp}-${e.target.files[0].name}`, e.target.files[0], {
              cacheControl: "3600",
              upsert: false,
            });
          if (error) {
            console.log(error);
            return;
          }
          const path = data.path.replace(/ /g, "%20");
          const SUPABASE_REFERENCE = "hkvyihwfkphuwgetdtee";
          const URL = `https://${SUPABASE_REFERENCE}.supabase.co/storage/v1/object/public/thumbnail/${path}`;
          setImgURL(URL);
          setFieldValue("thumbnail", URL);
        }}
        onBlur={handleBlur}
      />
    </FormControl>;

在这里,我们使用了Chakra UI的Input组件来获取图片,并在每次更改时将图片上传到Supabase的存储桶中。您可以从Supabase的Dashboard→设置中获取SUPABASE_REFERENCE

这是在WriteArticle.js中。它用于将Markdown呈现为HTML元素。MarkdownTheme在路径中的主题目录中提供。以下是其代码。

import { Text, Heading, Link, Code, ListItem, UnorderedList } from '@chakra-ui/react';

const MarkdownTheme = {
    p: (props) => {
        const { children } = props;
        return (
            <Text variant={"secondary-text"} lineHeight={2} p="0.5em 0">
                {children}
            </Text>
        );
    },
    h1: (props) => {
        const { children } = props;
        return (
            <Heading variant={"secondary-heading"} lineHeight={2}>
                {children}
            </Heading>
        );
    },
    h2: (props) => {
        const { children } = props;
        return (
            <Heading variant={"secondary-heading"} lineHeight={2}>
                {children}
            </Heading>
        );
    },
    a: (props) => {
        const { href, children } = props;
        return (
            <Link href={href} variant={"secondary-text"} textDecoration="underline" cursor="pointer" fontWeight="bold" lineHeight={2}>
                {children}
            </Link>
        );
    },
    code: (props) => {
        const { children } = props;
        if (children.includes("\n")) {
            return (
                <Code children={children} colorScheme='purple' width="100%" p="1em 1em"/>
            );
        } else {
            return (
                <a style={{backgroundColor:"lightgray", padding:"0 0.2em"}}>{children}</a>
            );
        }
    },
    li: (props) => {
        const { children } = props;
        return (
            <UnorderedList>
                <ListItem>
                    <Text variant={"secondary-text"} lineHeight={2}>
                        {children}
                    </Text>
                </ListItem>
            </UnorderedList>
        );
    },
};

export default MarkdownTheme;

在上面的代码中,您可以看到每个HTML标记将根据Chakra主题进行呈现。您可以参考基本语法 (opens new window)指南了解哪个语法映射到哪个HTML标记。

总的来说,这是演示此路由功能的gif。

Create Route (opens new window)

/[slug] #

此路由用于将博客呈现为可以查看的HTML页面。这里有两个文件:page.js将在加载时呈现骨架,如果找到文章,则将使用DisplayArticle.jsx组件显示文章。以下是代码。

page.js #

const ArticlePage = () => {
    const [articleData, setArticleData] = useState(null);
    const router = useRouter();
    const pathname = usePathname();
    const handleFetchParam = async () => {
        const slugArr = pathname.split("/");
        const slug = slugArr[slugArr.length - 1];
        console.log(slug);
        const { data: articleData, error: artilceError } = await supabase
            .from("article")
            .select()
            .eq("slug", slug);
        if (artilceError) {
            console.log(artilceError);
        } else {
            setArticleData(articleData[0]);
            console.log(articleData[0]);
        }
    };
    useEffect(() => {
        handleFetchParam();
    }, []);
    return (
        <Box margin="0 auto">
            {!articleData && (
                <Stack
                    spacing={10}
                    margin="0 auto"
                    marginTop={20}
                    marginBottom={20}
                    width={{
                        xl: "60%",
                        "2xl": "50%",
                        lg: "70%",
                        base: "100%",
                        md: "80%",
                    }}
                >
                    <Skeleton colorScheme="purple">
                        <div style={{ borderRadius: "10px", height: "300px" }} />
                    </Skeleton>
                    <Stack>
                        <Skeleton colorScheme="purple">
                            <div style={{ borderRadius: "10px", height: "100px" }} />
                        </Skeleton>
                        <Skeleton colorScheme="purple">
                            <div style={{ borderRadius: "10px", height: "40px" }} />
                        </Skeleton>
                        <Skeleton colorScheme="purple">
                            <div style={{ borderRadius: "10px", height: "100px" }} />
                        </Skeleton>
                    </Stack>
                    <Box>
                        <Skeleton colorScheme="purple">
                            <div style={{ borderRadius: "10px", height: "600px" }} />
                        </Skeleton>
                    </Box>
                </Stack>
            )}
            <Box
                width={{ xl: "60%", "2xl": "50%", lg: "70%", base: "100%", md: "80%" }}
                margin="0 auto"
                marginBottom={40}
            >
                {articleData != null && <DisplayArticle articleData={articleData} />}
            </Box>
        </Box>
    );
};
export default ArticlePage;

代码是不言自明的,我们只是渲染加载骨架,然后在获取articleData时,我们渲染DisplayArticle组件。 ```jsx kraUIRenderer(MarkdownTheme)} skipHtml> {articleData.markdown} ); }; const BlogAuthor = (props) => { return ( <Image borderRadius="full" boxSize="40px" src={props.image} alt={Avatar of ${props.name}} /> {props.name} {props.date} ); }; const BlogTags = (props) => { const { marginTop = 0, tags } = props; return ( {tags.map((tag) => { return ( <Tag size={"md"} variant="solid" colorScheme="orange" key={tag}> {tag} ); })} ); };


您可以在此处看到渲染文章的屏幕截图。

[![Rendered Article](https://imagedelivery.net/8B08sdLvw783CQcaKhUoYw/12c4cb95-4e6c-49db-b7c2-5aa36e0ebb00/public)](https://res.cloudinary.com/practicaldev/image/fetch/s--fEG-a00B--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://paper-attachments.dropboxusercontent.com/s_A1F14CB9F9D9154D0EBE8EC24C80324C8F0438DE22B4D8CBB6325295C0479B1E_1713428565865_image.png)

## [](#github-repository)GitHub 代码库

我已经创建了文件的 GitHub 代码库。如果您想重新创建该项目,可以参考该代码库获取整个代码。

这是链接: [https://github.com/surajondev/supabase-blog](https://github.com/surajondev/supabase-blog)

## [](#conclusion)总结

使用 Supabase 和 Chakra UI 构建一个支持 Markdown 的博客提供了一种有效的方式来创建一个动态、数据库支持的博客平台。通过将 Supabase 用于数据存储和文件处理与 Chakra UI 用于时尚且响应式的 UI 组件相结合,开发人员可以快速开发具有丰富功能的博客。

本文介绍了如何设置路由、CRUD 操作、Markdown 渲染和使用 Next.js 进行增强路由和导航的图示。GitHub 代码库提供了一个全面的指南,可以复制和扩展这个项目,使开发人员能够根据自己的特定需求和设计偏好定制和扩展自己的 Markdown 驱动博客。这个项目是将现代技术整合起来构建一个可扩展且视觉上吸引人的博客平台供内容创作者使用的实际示例。

希望本文有助于理解如何创建博客。感谢阅读本文。```