Skip to content
Draft
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
2 changes: 2 additions & 0 deletions app/assets/css/main.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
@import "tailwindcss";
@import "@nuxt/ui";

@source "../../../content/**/*";
@import "tw-animate-css";
@import "./keyframes.css";
@import "./utilities.css";
Expand Down
26 changes: 26 additions & 0 deletions app/components/SocialLinks.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<template>
<div class="flex items-center">
<UButton
icon="i-simple-icons-discord"
to="https://join.wolfstar.rocks"
,
target="_blank"
variant="ghost"
color="neutral"
size="xs"
>
<span class="sr-only">Nuxt on Discord</span>
</UButton>
<UButton
icon="i-simple-icons-github"
to="https://github.com/wolfstar-project"
,
target="_blank"
variant="ghost"
color="neutral"
size="xs"
>
<span class="sr-only">Nuxt on GitHub</span>
</UButton>
Comment on lines +3 to +24
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Correct the link labels and add rel on the external buttons.

These controls point to WolfStar destinations, but the current accessible names still announce “Nuxt”. Also, target="_blank" should be paired with rel="noopener noreferrer".

♻️ Proposed fix
 		<UButton
 			icon="i-simple-icons-discord"
 			to="https://join.wolfstar.rocks"
-			,
 			target="_blank"
+			rel="noopener noreferrer"
 			variant="ghost"
 			color="neutral"
 			size="xs"
 		>
-			<span class="sr-only">Nuxt on Discord</span>
+			<span class="sr-only">Join WolfStar on Discord</span>
 		</UButton>
 		<UButton
 			icon="i-simple-icons-github"
 			to="https://github.com/wolfstar-project"
-			,
 			target="_blank"
+			rel="noopener noreferrer"
 			variant="ghost"
 			color="neutral"
 			size="xs"
 		>
-			<span class="sr-only">Nuxt on GitHub</span>
+			<span class="sr-only">WolfStar on GitHub</span>
 		</UButton>
As per coding guidelines, "Ensure correct accessible name, role, value, states, and properties for all interactive elements."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/SocialLinks.vue` around lines 3 - 24, The two UButton
instances (icon="i-simple-icons-discord" and icon="i-simple-icons-github") have
incorrect accessible labels and missing rel attributes; update the inner sr-only
span text to reflect the actual destinations (e.g., "WolfStar on Discord" and
"WolfStar on GitHub") and add rel="noopener noreferrer" to each UButton that
uses target="_blank" to prevent window.opener security issues and meet
accessibility/coding guidelines.

</div>
</template>
30 changes: 30 additions & 0 deletions app/composables/useBlog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { BlogArticle } from "~/types";

export const useBlog = () => {
const { data: articles, refresh } = useAsyncData<BlogArticle[]>(
"blog",
async () => {
return (
queryCollection("blog")
.where("extension", "=", "md")
/* .select('title', 'date', 'image', 'description', 'path', 'authors', 'category') */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Remove commented-out code.

Per coding guidelines, commented-out code should not be left without associated tickets. Either remove this line or create a tracking issue if field selection optimization is planned.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/composables/useBlog.ts` at line 10, Remove the commented-out select call
(/* .select('title', 'date', 'image', 'description', 'path', 'authors',
'category') */) from the useBlog composable so dead/commented code is not left
in the repo; if you intend to keep this as a planned optimization, create a
tracking ticket and reference that ticket ID in a short comment instead of
leaving the full commented line in the function (look for the select call in
useBlog.ts to locate the exact spot).

.order("date", "DESC")
.all()
.then((res) => res.filter((article) => article.path !== "/blog"))
);
Comment on lines +6 to +14
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add filter to exclude draft posts.

The query fetches all blog posts without filtering out drafts. Posts with draft: true in frontmatter will appear in the public listing. This was previously flagged in blog/index.vue, but since the query originates here, the fix should be applied in this composable.

🔒 Proposed fix to filter draft posts
 			return (
 				queryCollection("blog")
 					.where("extension", "=", "md")
+					.where("draft", "=", false)
 					/* .select('title', 'date', 'image', 'description', 'path', 'authors', 'category') */
 					.order("date", "DESC")
 					.all()
 					.then((res) => res.filter((article) => article.path !== "/blog"))
 			);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async () => {
return (
queryCollection("blog")
.where("extension", "=", "md")
/* .select('title', 'date', 'image', 'description', 'path', 'authors', 'category') */
.order("date", "DESC")
.all()
.then((res) => res.filter((article) => article.path !== "/blog"))
);
async () => {
return (
queryCollection("blog")
.where("extension", "=", "md")
.where("draft", "=", false)
/* .select('title', 'date', 'image', 'description', 'path', 'authors', 'category') */
.order("date", "DESC")
.all()
.then((res) => res.filter((article) => article.path !== "/blog"))
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/composables/useBlog.ts` around lines 6 - 14, The blog query in the async
function using queryCollection("blog") doesn't exclude drafts, so update the
query to filter out frontmatter drafts by either adding a where clause for draft
(e.g., .where("draft", "!=", true) on the queryCollection("blog") chain) or, if
the query API doesn't support that operator, apply an additional filter in the
.then((res) => res.filter(...)) step to exclude articles where article.draft ===
true; target the async function that returns queryCollection("blog") so draft
posts are not returned to the caller.

},
{ default: () => [] },
);

async function fetchList() {
if (!articles.value?.length) {
return refresh();
}
}

return {
articles,
// featuredArticle,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Remove commented-out code.

The featuredArticle comment should be removed if not implemented, or tracked via an issue if planned.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/composables/useBlog.ts` at line 27, Remove the commented-out
"featuredArticle" line in the useBlog composable; if this feature is planned,
replace the comment with a short TODO referencing an issue number or create an
issue and add a TODO comment, otherwise simply delete the "// featuredArticle,"
token in the file (useBlog, export/composable where the comment appears).

fetchList,
};
};
34 changes: 34 additions & 0 deletions app/composables/useClipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useClipboard as _useClipboard } from "@vueuse/core";

type toastOptions = {
title?: string;
description?: string;
icon?: string;
color?:
| "primary"
| "secondary"
| "info"
| "success"
| "warning"
| "error"
| "important"
| "neutral";
};
Comment on lines +3 to +16
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Use PascalCase for type names.

As per coding guidelines, types and interfaces should use PascalCase.

♻️ Proposed fix
-type toastOptions = {
+type ToastOptions = {
 	title?: string;
 	description?: string;
 	icon?: string;
 	color?:
 		| "primary"
 		| "secondary"
 		| "info"
 		| "success"
 		| "warning"
 		| "error"
 		| "important"
 		| "neutral";
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/composables/useClipboard.ts` around lines 3 - 16, The type name
toastOptions uses camelCase; rename it to PascalCase (ToastOptions) and update
all references in this file (and any imports/usages) to the new name;
specifically change the declaration of toastOptions to ToastOptions and update
any variables, function signatures, or generic/type annotations that reference
toastOptions (e.g., in useClipboard-related functions) so the codebase follows
the PascalCase type naming convention.


export const useClipboard = () => {
const { copy: _copy, copied } = _useClipboard();

const toast = useToast();

const copy = (source: string, optionsOptions?: toastOptions) => {
_copy(source);
if (optionsOptions) {
toast.add(optionsOptions);
}
};
Comment thread
RedStar071 marked this conversation as resolved.

return {
copy,
copied,
};
};
2 changes: 1 addition & 1 deletion app/composables/useFooter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const useFooter = () => {
class: "link-hover",
icon: "ph:newspaper-clipping-duotone",
label: "Blog",
to: "https://blog.wolfstar.rocks",
to: "/blog",
ui: { linkLeadingIcon: "bg-primary" },
Comment on lines 22 to 26
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useFooter now routes the Blog link internally (/blog), but the existing useFooter tests only check that the column/labels exist, not that the Blog link points to the correct destination. Add an assertion for the Blog entry’s to so future refactors don’t accidentally revert to the external blog URL.

Copilot generated this review using guidance from repository custom instructions.
},
],
Expand Down
8 changes: 8 additions & 0 deletions app/composables/useHeader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export function useHeader() {
label: "Commands",
to: "/commands",
},
{
label: "Blog",
to: "/blog",
},
Comment on lines +58 to +61
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useHeader was updated to include a new "Blog" link in both desktopLinks and mobileLinks, but the existing useHeader composable tests don’t assert that this link is present. Add a small test assertion for the new label/path to prevent accidental regression of the navigation.

Copilot generated this review using guidance from repository custom instructions.
]);

const mobileLinks = computed(() => [
Expand Down Expand Up @@ -88,6 +92,10 @@ export function useHeader() {
label: "Commands",
to: "/commands",
},
{
label: "Blog",
to: "/blog",
},
...(currentApp.value.invite !== "#"
? [
{
Expand Down
189 changes: 189 additions & 0 deletions app/pages/blog/[slug].vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<script setup lang="ts">
import { isNullOrUndefined } from "@sapphire/utilities/isNullOrUndefined";
import { kebabCase } from "scule";

const route = useRoute();
const { copy } = useClipboard();

const [{ data: article }, { data: surround }] = await Promise.all([
useAsyncData(kebabCase(route.path), () => queryCollection("blog").path(route.path).first()),
useAsyncData(`${kebabCase(route.path)}-surround`, () => {
return queryCollectionItemSurroundings("blog", route.path, {
fields: ["description"],
}).order("date", "DESC");
}),
]);

if (isNullOrUndefined(article.value)) {
throw createError({ statusCode: 404, statusMessage: "Article not found", fatal: true });
}
Comment on lines +17 to +19
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Treat drafted posts as not found on the detail route.

The schema includes draft, but this guard only handles missing entries. A known slug with draft: true will still render here, and the PR explicitly prerenders /blog/**.

🔒 Proposed fix
-if (isNullOrUndefined(article.value)) {
+if (isNullOrUndefined(article.value) || article.value.draft) {
 	throw createError({ statusCode: 404, statusMessage: "Article not found", fatal: true });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/blog/`[slug].vue around lines 17 - 19, The route currently only
throws a 404 when article.value is null/undefined; update the guard in
blog/[slug].vue to also treat drafted posts as not found by checking
article.value.draft (e.g., if isNullOrUndefined(article.value) ||
article.value.draft) and then call createError({ statusCode: 404, statusMessage:
"Article not found", fatal: true }) the same way; reference the existing
isNullOrUndefined check, the article.value object, and the createError call so
the same behavior applies for draft: true.


const title = article.value.seo?.title || article.value.title;
const description = article.value.seo?.description || article.value.description;

useSeoMeta({
titleTemplate: "%s · Wolfstar Blog",
title,
Comment on lines +24 to +26
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Align page title format with repository accessibility convention.

Line 28 uses %s · Wolfstar Blog; guideline requires Unique page - section - site.

♻️ Proposed fix
-	titleTemplate: "%s · Wolfstar Blog",
+	titleTemplate: "%s - blog - Wolfstar",

Based on learnings: "Set a descriptive page title using the format 'Unique page - section - site'."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/blog/`[slug].vue around lines 27 - 29, The page title format used
in the useSeoMeta call needs to follow the repository convention "Unique page -
section - site"; update the titleTemplate passed to useSeoMeta (the
titleTemplate property in the useSeoMeta invocation) to use that dash-separated
format instead of "%s · Wolfstar Blog", keeping title as the dynamic part so the
title value is inserted into the new template.

description,
ogDescription: description,
ogTitle: `${title} · Wolfstar Blog`,
});

if (article.value.image) {
defineOgImage({ url: article.value.image });
} else {
defineOgImageComponent("Docs", {
headline: "Blog",
title,
description,
});
Comment thread
RedStar071 marked this conversation as resolved.
}

function formatSocialIntentQueryText(handle: string | undefined): string {
const credit = handle ? ` by @${handle}` : "";
const body = article.value!.title + credit;
const link = `${getOrigin()}${article.value!.path}`;
return encodeURIComponent(`${body}\n\n${link}`);
}

const authorHandles: { twitter?: string; bluesky?: string } = {
twitter: article.value.authors?.[0]?.twitter,
bluesky: article.value.authors?.[0]?.bluesky,
};

const socialLinks = computed(() =>
!article.value
? []
: [
{
label: "Bluesky",
icon: "i-simple-icons-bluesky",
to: `https://bsky.app/intent/compose?text=${formatSocialIntentQueryText(authorHandles.bluesky)}`,
},
{
label: "X",
icon: "i-simple-icons-x",
to: `https://x.com/intent/tweet?text=${formatSocialIntentQueryText(authorHandles.twitter)}`,
},
],
);

function copyLink() {
copy(`${getOrigin()}${article.value?.path || "/"}`, {
title: "Link copied to clipboard",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
icon: "i-lucide-copy-check",
});
}

const links = [
{
icon: "i-lucide-pen",
label: "Edit this article",
to: `https://github.com/wolfstar-project/wolfstar.rocks/edit/main/content/${article.value.stem}.md`,
target: "_blank",
},
{
icon: "i-lucide-star",
label: "Star on GitHub",
to: "https://github.com/wolfstar-project/wolfstar.rocks",
target: "_blank",
},
];
</script>

<template>
<UContainer>
<UPage v-if="article">
<UPageHeader
:title="article.title"
:description="article.description"
:ui="{ headline: 'flex flex-col gap-y-8 items-start' }"
>
<template #headline>
<UBreadcrumb
:items="[
{ label: 'Blog', icon: 'i-lucide-newspaper', to: '/blog' },
{ label: article.title },
]"
class="max-w-full"
/>
<div class="flex items-center space-x-2">
<span>
{{ article.category }}
</span>
<span class="text-muted"
>&middot;&nbsp;&nbsp;<time>{{
new Date(article.date).toLocaleDateString("en", {
year: "numeric",
month: "short",
day: "numeric",
})
}}</time></span
Comment on lines +114 to +121
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd /tmp && find . -type f -name "[slug].vue" 2>/dev/null | head -5

Repository: wolfstar-project/wolfstar.rocks

Length of output: 57


🏁 Script executed:

git ls-files app/pages/blog/ | head -20

Repository: wolfstar-project/wolfstar.rocks

Length of output: 125


🏁 Script executed:

head -150 app/pages/blog/[slug].vue | tail -50

Repository: wolfstar-project/wolfstar.rocks

Length of output: 1428


🏁 Script executed:

cat -n app/pages/blog/[slug].vue | sed -n '110,125p'

Repository: wolfstar-project/wolfstar.rocks

Length of output: 563


🏁 Script executed:

cat -n app/pages/blog/index.vue | grep -A 10 -B 10 "article.date\|toLocaleDateString"

Repository: wolfstar-project/wolfstar.rocks

Length of output: 770


🏁 Script executed:

rg "new Date\(article\.date\)" app/

Repository: wolfstar-project/wolfstar.rocks

Length of output: 240


🏁 Script executed:

rg "toLocaleDateString" app/ -B 2 -A 2

Repository: wolfstar-project/wolfstar.rocks

Length of output: 638


Fix date interpretation timezone issue in both blog files.

new Date("2024-02-25") is interpreted as midnight UTC, causing the date to display as the previous day for visitors in western U.S. time zones. Apply the UTC-safe formatting fix to both app/pages/blog/[slug].vue (lines 114-121) and app/pages/blog/index.vue (lines 39-45).

🕒 UTC-safe formatting
 <script setup lang="ts">
+const BLOG_DATE_FORMAT = new Intl.DateTimeFormat("en", {
+	year: "numeric",
+	month: "short",
+	day: "numeric",
+	timeZone: "UTC",
+});
 </script>
@@
-								new Date(article.date).toLocaleDateString("en", {
-									year: "numeric",
-									month: "short",
-									day: "numeric",
-								})
+								BLOG_DATE_FORMAT.format(new Date(`${article.date}T00:00:00Z`))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/blog/`[slug].vue around lines 114 - 121, The date parsing using new
Date(article.date) is vulnerable to timezone shifts; change the rendering to
parse the YYYY-MM-DD string as UTC (e.g., split article.date into year, month,
day and use new Date(Date.UTC(year, month-1, day))) before calling
toLocaleDateString, and apply the same fix where article.date is used in both
the blog detail time element (the time element rendering article.date in the
[slug].vue template) and the blog listing (the time rendering article.date in
index.vue) so the displayed date is UTC-safe across time zones.

>
</div>
</template>

<div class="mt-4 flex flex-wrap items-center gap-6">
<UUser
v-for="(author, index) in article.authors"
:key="index"
v-bind="author"
:description="author.to ? `@${author.to.split('/').pop()}` : undefined"
/>
</div>
</UPageHeader>

<UPage class="lg:gap-24">
<UPageBody>
<ContentRenderer v-if="article.body" :value="article" />

<div class="not-prose mt-12 flex items-center justify-between">
<ULink to="/blog" class="text-primary"> ← Back to blog </ULink>
<div class="flex items-center justify-end gap-1.5">
<UButton
icon="i-lucide-link"
variant="ghost"
color="neutral"
@click="copyLink"
>
<span class="sr-only">Copy URL</span>
Copy URL
</UButton>
<UButton
v-for="(link, index) in socialLinks"
:key="index"
v-bind="link"
variant="ghost"
color="neutral"
target="_blank"
>
<span class="sr-only">Wolfstar on {{ link.label }}</span>
</UButton>
</div>
</div>

<USeparator v-if="surround?.length" />

<UContentSurround :surround="surround" />
</UPageBody>

<template #right>
<UContentToc
v-if="article.body && article.body.toc"
:links="article.body.toc.links"
title="Table of Contents"
highlight
>
<template #bottom>
<div class="hidden space-y-6 lg:block">
<UPageLinks title="Links" :links="links" />
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<USeparator type="dashed" />
<SocialLinks />
</div>
</template>
</UContentToc>
</template>
</UPage>
</UPage>
</UContainer>
</template>
65 changes: 65 additions & 0 deletions app/pages/blog/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<script setup lang="ts">
const { data: page } = await useAsyncData("blog-landing", () => queryCollection("blog").first());
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: "Page not found", fatal: true });
}

const { articles, fetchList } = useBlog();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

fetchList is destructured but never called.

The fetchList function is imported from useBlog() but is not invoked anywhere in this component. If articles need to be refreshed on navigation (e.g., when returning to this page), consider calling fetchList() or remove the unused destructuring.

♻️ Option 1: Call fetchList on mount
 const { articles, fetchList } = useBlog();
+
+await fetchList();
♻️ Option 2: Remove unused import
-const { articles, fetchList } = useBlog();
+const { articles } = useBlog();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { articles, fetchList } = useBlog();
const { articles } = useBlog();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/blog/index.vue` at line 7, The component destructures fetchList
from useBlog() but never uses it; either call fetchList() when the page mounts
(e.g., invoke fetchList inside the component's setup/onMounted lifecycle to
refresh articles) or remove fetchList from the destructuring to eliminate the
unused variable; locate the destructuring line with "const { articles, fetchList
} = useBlog()" and either add the lifecycle call to fetchList or change it to
"const { articles } = useBlog()".


useSeoMeta({
titleTemplate: "%s",
title: page.value.title,
description: page.value.description,
ogDescription: page.value.description,
ogTitle: page.value.title,
});
</script>

<template>
<UContainer v-if="page">
<UPageHero :title="page.title" :description="page.description" orientation="horizontal">
</UPageHero>

<UPageBody>
<UContainer>
<UBlogPosts class="mb-12 md:grid-cols-2 lg:grid-cols-3">
<h2 class="sr-only">Post</h2>
<UBlogPost
v-for="(article, index) in articles"
:key="article.path"
:to="article.path"
:title="article.title"
:description="article.description"
:image="{
src: article.image,
width: index === 0 ? 672 : 437,
height: index === 0 ? 378 : 246,
alt: `${article.title} image`,
}"
:date="
new Date(article.date).toLocaleDateString('en', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
"
:authors="
article.authors.map((author) => ({
...author,
avatar: {
...author.avatar,
loading: 'lazy',
alt: `${author.name} avatar`,
},
}))
"
:badge="{ label: article.category, color: 'primary', variant: 'subtle' }"
:variant="index === 0 ? 'outline' : 'subtle'"
:orientation="index === 0 ? 'horizontal' : 'vertical'"
:class="[index === 0 && 'col-span-full']"
/>
</UBlogPosts>
</UContainer>
</UPageBody>
</UContainer>
</template>
3 changes: 3 additions & 0 deletions app/types/blog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { BlogCollectionItem } from "@nuxt/content";

export type BlogArticle = BlogCollectionItem;
Loading