使用Garchi CMS和NUXT 3-第2部分的无头博客
#javascript #网络开发人员 #nuxt

part 1中,我们使用Garchi CMS建立了博客的后端。现在让我们继续使用NUXT3。

来构建前端。

要开始,请确保系统上安装了最新版本的节点JS。

在您的终端中,执行以下命令来创建新的NUXT 3应用程序。您可以自由地用您喜欢的任何其他名称替换“博客”。

npx nuxi@latest init blog

执行后,您应该看到以下输出:

Image description

现在,导航到“博客”文件夹并运行NPM i以安装必要的依赖项。

cd blog
npm i

为了加快这一过程,我将绕过NUXT 3和tailwind的基本面。您可以找到有关与Nuxt 3 here

集成的详细信息

接下来,将在我们的nuxt.config.ts文件中的部分1中创建的API键并添加API基本URL,如图像所示。

Image description

我们将在app.vue中将nuxtwelcome与nuxtpage交换,并在“页面”目录中创建index.vue。

记住,由于API密钥的计费目的,建议使用GARCHI API服务器端。因此,让我们通过生成一个名为“ blog.get.ts”的文件来创建NUXT 3中的API路由,该文件将映射到 /api /blog get request。该文件应位于“服务器/API”目录中。

在'blog.get.ts'中包含以下代码:

export default defineEventHandler(async (event) => {
    const categoryId = 71
    const config = useRuntimeConfig()

    const response = await $fetch(`${config.API_URL}/products/filter`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${config.API_KEY}`
        },
        body: {
            categories: [categoryId]
        }
    })

    return response
})

让我们解剖代码:

  1. 我们将利用Garchi CMS的过滤产品API,可以找到here 此API接受基于此的类别ID,空间ID或Pricesorder(用于电子商务)的列表。从我们的Garchi CMS仪表板中,我们知道类别ID并将使用它。由于API接受ID数组,因此我们将其作为数组传递。因此,这应该返回所有被归类为“博客”的项目,因为71(在我的情况下)是“博客”类别的ID。
  2. 我们向https://garchi.co.uk/api/v1/products/filter提出了邮政请求,目前,我们只是返回响应以检查输出。

由于这是NUXT端的GET API,因此我们可以在浏览器中执行它以查看结果。因此,我们将使用:
开始开发服务器

npm run dev

Image description

我们将使用此信息显示我们的博客文章列表。因此,我将在“组件”文件夹中构造一个组件,并将其标记为“ blogcard.vue”。但是首先,让我们根据收到的API响应创建类型。因此,在“资产/类型/post.ts”中包括此代码:


export type PostMeta = {
    id: number
    key: string
    value: string
    type: string
}

export type Post = {
    product_id: number
    slug: string
    name: string
    description?: string
    categories: {
        id: number
        name: string
    }[]
    main_image: string
    space: {
        uid: string
        name: string
    }
    product_meta: PostMeta[]
}

我们的“ blogcard.vue”中的代码应该看起来像这样:

<template>
  <article class="flex flex-col items-start justify-between">
    <div class="relative w-full">
      <img
        :src="post.main_image"
        alt=""
        class="aspect-[16/9] w-full rounded-2xl bg-gray-100 object-cover sm:aspect-[2/1] lg:aspect-[3/2]"
      />
      <div
        class="absolute inset-0 rounded-2xl ring-1 ring-inset ring-gray-900/10"
      />
    </div>
    <div class="max-w-xl">
      <div class="mt-8 flex items-center gap-x-4 text-xs">
        <NuxtLink
          class="relative z-10 rounded-full bg-gray-50 px-3 py-1.5 font-medium text-gray-600 hover:bg-gray-100"
        >
          {{ categoryTitle }}
        </NuxtLink>
      </div>
      <div class="group relative">
        <h3
          class="mt-3 text-lg font-semibold leading-6 text-gray-900 group-hover:text-gray-600"
        >
          <NuxtLink :to="`/blog/${post.slug}`">
            <span class="absolute inset-0" />
            {{ post.name }}
          </NuxtLink>
        </h3>
      </div>
      <div class="relative mt-8 flex items-center gap-x-4">
        <img
          :src="authorImage"
          alt=""
          class="h-10 w-10 rounded-full bg-gray-100"
        />
        <div class="text-sm leading-6">
          <p class="font-semibold text-gray-900">
            <span>
              <span class="absolute inset-0" />
              {{ authorName }}
            </span>
          </p>
        </div>
      </div>
    </div>
  </article>
</template>

<script setup lang="ts">
import { Post, PostMeta } from "~/assets/types/Post";

const postProps = defineProps<{
  post: Post;
}>();

const categoryTitle = computed(() =>
  postProps.post?.categories.map((category) => category.name).join(", ")
);

const authorImage = computed(
  () =>
    postProps.post?.product_meta?.find((m) => m.key == "avatar")?.value ?? ""
)
const authorName = computed(
  () =>
    postProps.post?.product_meta?.find((m) => m.key == "author")?.value ?? ""
)
</script>

如果您注意到,我将作者图像,AuthorName和categoryTitle作为计算属性。这是因为当我们将这些值添加为额外字段时,作者图和authorname将在元细节(product_meta)内。类别将是一系列类别,因此我们将使用','。

加入它。

我们的index内部。

<template>
    <Head>
        <Title>Blog</Title>
    </Head>
    <div class="max-w-6xl p-2 mx-auto xl:py-20
    grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 md:gap-5">
        <BlogCard v-for="(post, index) in data?.data" :key="index" :post="post" />
    </div>
</template>

<script setup lang="ts">
const {data} = await useFetch("/api/blog")
</script>

接下来,我们将创建一个动态路线,该路由将在Click上渲染我们的博客文章。因此,在页面/博客中创建[slug] .vue文件。您还可以创建一个布局以减少代码重写。但是在此示例中,我将重写课程,对此感到抱歉:)

也可以创建一个布局,以使我们的代码重写更少。但是对于此示例,我将重写类。对此很抱歉:)

以前,我们创建了一个API路线。这次,我们可以将NUXT 3的USEASYNCDATA与服务器密钥设置为true一起使用,因此它在服务器上运行。我们将使用Garchi CMS的Get Product Details

由于API在描述中返回HTML,因此我们需要使用Dompurify软件包清洁它。我们还可以仅允许YouTube标签标签,所以让我们来做。

首先,安装dompurify:

npm i dompurify

如果您使用的是打字稿,则可能需要添加类型:

npm i --save-dev @types/dompurify

我们的[slug]。vue应该看起来像这样:

<template>
  <Head>
    <Title>{{ article?.name }}</Title>
  </Head>
  <div class="max-w-6xl p-2 mx-auto xl:py-20 flex flex-col gap-9">
    <img
      :src="article?.main_image"
      alt=""
      class="aspect-square w-full xl:w-[30%] xl:h-1/2 mx-auto rounded-2xl bg-gray-100 object-cover object-center"
    />

    <h1 class="text-4xl">
      {{ article?.name }}
    </h1>

    <div class="flex items-center space-x-3">
      <img
        :src="authorImage"
        :alt="authorName"
        class="w-16 h-16 rounded-full ring-2 ring-gray-900 ring-offset-gray-800"
      />
      <span class="text-gray-600 font-semibold text-lg">
        By {{ authorName }}
      </span>
    </div>

    <div
      class="prose max-w-full"
      v-html="description || article?.description"
    ></div>
  </div>
</template>

<script setup lang="ts">
import { Post } from "assets/types/Post";
import * as DOMPurify from "dompurify";

const config = useRuntimeConfig();
const route = useRoute();

const { data, error } = await useAsyncData<{ data: Post[] }>(
  `article-${route.params.slug}`,
  () => {
    return $fetch(`${config.API_URL}/product/${route.params.slug}`, {
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${config.API_KEY}`,
      },
    });
  },
  {
    server: true,
  }
);

const article = computed<Post | null>(() => data.value?.data[0] ?? null);

const description = ref<string>("");

const authorImage = computed(
  () => article.value?.product_meta?.find((m) => m.key == "avatar")?.value ?? ""
);
const authorName = computed(
  () => article.value?.product_meta?.find((m) => m.key == "author")?.value ?? ""
);

if (error.value || !article.value) {
  throw createError({ statusCode: 404, message: "Not found" });
}

onMounted(() => {
  DOMPurify.addHook("uponSanitizeElement", (node: HTMLElement, data: any) => {
    if (data.tagName == "iframe") {
      const allowedSRC = "https://www.youtube.com/embed";
      const src = node.getAttribute("src");
      if (!src?.startsWith(allowedSRC)) {
        return node.parentNode?.removeChild(node);
      }
    }
  });

  let content = DOMPurify.sanitize(article.value?.description, {
    USE_PROFILES: { html: true },
    ADD_TAGS: ["iframe"],
    ADD_ATTR: ["autoplay", "allowfullscreen", "frameborder", "scrolling"],
  });

  description.value = content;
});
</script>

让我们逐步分解代码:

API以格式返回数据{data:post []}

我们有一个计算属性,该属性为我们提供了数据:post [0]。这是因为接受slug或项目ID作为URL参数的API将在响应数组中仅返回一个或不返回一个项目。

我们正在使用Dompurify对我们的文章进行消毒。以HTML格式进行的描述,仅允许某些IFRAME。由于挂钩的挂钩将在客户端发生,因此我正在显示Article.SERCRICTIO。我猜这可以完全在服务器端进行处理。

最后,我们使用尾风的版式插件来调整它。
最终输出是:

Image description

Image description

Image description

得出结论,这两部分文章的主要意图是证明Garchi CMS的使用和快速演示如何使用。

现在,希望您喜欢它:)