feat(blog): add blog section with Nuxt Content and Valibot schemas#90
feat(blog): add blog section with Nuxt Content and Valibot schemas#90RedStar071 wants to merge 4 commits intomainfrom
Conversation
- Add @nuxt/content module with Valibot-based collection schema - Create blog listing page (app/pages/blog/index.vue) with UBlogPosts - Create blog post page (app/pages/blog/[slug].vue) with TOC and surround - Migrate wolfstar-v7 post from blog.wolfstar.rocks - Add Blog link to header and footer navigation - Configure blog route rules for prerendering and SEO
✅ Deploy Preview for wolfstar-rocks-et34281 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughAdds a blog system: Nuxt Content integration, collection schema, blog index and dynamic post pages, header/footer navigation updates, composables (blog, clipboard), types, a sample post, UI component for social links, CSS content source, and dependency/config updates. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Browser
participant NuxtPage as Nuxt Page
participant NuxtContent as `@nuxt/content`
participant Files as Content Filesystem
User->>Browser: navigate to /blog or /blog/[slug]
Browser->>NuxtPage: mount page component
NuxtPage->>NuxtContent: queryCollection("blog") / queryCollection(...).path(...)
NuxtContent->>Files: read matching markdown files
Files-->>NuxtContent: return post(s) + metadata
NuxtContent-->>NuxtPage: deliver data (article, surrounds, list)
NuxtPage->>NuxtPage: compute SEO, TOC, social links
NuxtPage->>Browser: render HTML (list or article + TOC + controls)
Browser-->>User: display blog page
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #90 +/- ##
=======================================
Coverage 67.38% 67.38%
=======================================
Files 79 79
Lines 1907 1907
Branches 568 568
=======================================
Hits 1285 1285
Misses 492 492
Partials 130 130
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds a first-party blog section to the Nuxt 4 dashboard app by integrating @nuxt/content (v3) with Valibot collection schemas and Nuxt UI blog/page components, migrating initial content and wiring navigation + prerender rules.
Changes:
- Added Nuxt Content + supporting dependencies and configured the
blogcontent collection schema (content.config.ts). - Implemented
/bloglisting and/blog/[slug]post pages using Nuxt UI blog/page components, TOC, and surround navigation. - Added a migrated blog post and updated header/footer navigation + route rules for prerendering.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
package.json |
Adds @nuxt/content, better-sqlite3, and Valibot JSON schema support dependency. |
nuxt.config.ts |
Registers @nuxt/content and prerender route rules for blog routes. |
content/blog/wolfstar-v7.md |
Adds migrated blog content with structured frontmatter. |
content.config.ts |
Defines the blog collection and frontmatter schema using Valibot. |
app/pages/blog/index.vue |
Implements blog listing with UBlogPosts/UBlogPost and SEO meta. |
app/pages/blog/[slug].vue |
Implements blog post page with renderer, TOC, surround, author chips, and OG image. |
app/composables/useHeader.ts |
Adds “Blog” to desktop + mobile navigation. |
app/composables/useFooter.ts |
Changes Blog footer link from external URL to internal /blog. |
| { | ||
| label: "Blog", | ||
| to: "/blog", | ||
| }, |
There was a problem hiding this comment.
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.
| class: "link-hover", | ||
| icon: "ph:newspaper-clipping-duotone", | ||
| label: "Blog", | ||
| to: "https://blog.wolfstar.rocks", | ||
| to: "/blog", | ||
| ui: { linkLeadingIcon: "bg-primary" }, |
There was a problem hiding this comment.
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.
| "@types/node": "24.12.0", | ||
| "@valibot/to-json-schema": "^1.6.0", |
There was a problem hiding this comment.
@valibot/to-json-schema is added under devDependencies, but pnpm-lock indicates it is a peer dependency of @nuxt/content. If the production environment installs with --prod (or otherwise omits devDependencies), the app/build can fail at runtime or build time. Move it to dependencies (or ensure it’s installed in production) to make the Nuxt Content setup reliable.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/composables/useFooter.ts (1)
21-27:⚠️ Potential issue | 🟡 MinorUpdate
app.config.tsBlog link to match the new internal route.The Blog link in
useFooter.tswas changed to/blog, butapp/app.config.tsstill references the external URLhttps://blog.wolfstar.rocks. SinceFooter.vueusesuseAppConfig()to fetch footer configuration fromapp.config.ts, the external URL will still be used in the rendered footer. Updateapp.config.tsline 26 to useto: "/blog"instead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/composables/useFooter.ts` around lines 21 - 27, The footer config in app.config.ts still points the Blog entry to the external URL "https://blog.wolfstar.rocks" while useFooter.ts and the app's internal route expect to use the internal route "/blog"; open app.config.ts and update the Blog item’s `to` property to `"/blog"` so it matches useFooter.ts and the footer rendered by Footer.vue via useAppConfig(); ensure the Blog entry's key/name matches the one in useFooter.ts (icon/label) so the correct item is replaced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/pages/blog/`[slug].vue:
- Around line 50-68: Replace the unstable index key on the author buttons by
using a stable unique identifier from each author (e.g., use author.id or
author.to instead of index) in the v-for on UButton (targeting the post.authors
array and the UButton component), and add rel="noopener noreferrer" to the
UButton props for external links (alongside target="_blank") to prevent
tabnabbing and meet security guidelines.
- Around line 75-78: UContentSurround is rendered unconditionally while
USeparator uses a guard; update the template so UContentSurround is only
rendered when surround has content (same condition as USeparator, e.g.
surround?.length). Locate the block with USeparator and UContentSurround and
wrap or add the same truthy guard around UContentSurround so both components are
rendered consistently when surround is non-empty.
In `@app/pages/blog/index.vue`:
- Around line 2-4: The blog listing query currently fetches all documents from
the "blog" collection via useAsyncData and queryCollection("blog") without
excluding drafts; update the query chain used in the useAsyncData call (the
anonymous loader passed to useAsyncData that calls
queryCollection("blog").order("date", "DESC").all()) to include a where filter
that excludes entries with draft: true (i.e., add a .where(...) before
.order/.all so only posts where draft is false or draft is not true are
returned).
---
Outside diff comments:
In `@app/composables/useFooter.ts`:
- Around line 21-27: The footer config in app.config.ts still points the Blog
entry to the external URL "https://blog.wolfstar.rocks" while useFooter.ts and
the app's internal route expect to use the internal route "/blog"; open
app.config.ts and update the Blog item’s `to` property to `"/blog"` so it
matches useFooter.ts and the footer rendered by Footer.vue via useAppConfig();
ensure the Blog entry's key/name matches the one in useFooter.ts (icon/label) so
the correct item is replaced.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 1b7311af-eb95-4728-997d-6340c2f763f3
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (8)
app/composables/useFooter.tsapp/composables/useHeader.tsapp/pages/blog/[slug].vueapp/pages/blog/index.vuecontent.config.tscontent/blog/wolfstar-v7.mdnuxt.config.tspackage.json
There was a problem hiding this comment.
Actionable comments posted: 13
♻️ Duplicate comments (2)
app/pages/blog/[slug].vue (2)
124-126:⚠️ Potential issue | 🟡 MinorGuard
UContentSurroundwith the same condition asUSeparator.Line 124 is conditional, but Line 126 renders unconditionally.
♻️ Proposed fix
- <USeparator v-if="surround?.length" /> - - <UContentSurround :surround="surround" /> + <template v-if="surround?.length"> + <USeparator /> + <UContentSurround :surround="surround" /> + </template>🤖 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 124 - 126, UContentSurround is rendered unconditionally while USeparator uses v-if="surround?.length", so guard UContentSurround with the same condition to avoid rendering when surround is empty/undefined; update the template around UContentSurround (the UContentSurround tag) to include v-if="surround?.length" or wrap both USeparator and UContentSurround in a parent element that has v-if="surround?.length" so both USeparator and UContentSurround render only when surround has content.
87-88:⚠️ Potential issue | 🟡 MinorReplace index key with a stable author key.
Using
indexas:keyis unstable for reordered lists.♻️ Proposed fix
- <UUser - v-for="(author, index) in article.authors" - :key="index" + <UUser + v-for="author in article.authors" + :key="author.to || author.name"Based on learnings: "Always use stable keys in lists to help virtual DOM diffing; avoid using array indices as keys unless the list is static."
🤖 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 87 - 88, The v-for loop uses the array index as the key (v-for="(author, index) in article.authors" :key="index"), which is unstable; replace the index with a stable unique identifier from each author object (e.g., :key="author.id" or :key="author.slug" or :key="author.email") and if author objects lack such a field, add/derive a stable unique property on each author before rendering (e.g., assign a UUID or use a deterministic slug) so that the virtual DOM diffing for the article.authors list uses a consistent key.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/composables/useBlog.ts`:
- 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).
- 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).
- Around line 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.
In `@app/composables/useClipboard.ts`:
- Around line 23-28: The copy function has a typo in the parameter name and does
not handle failures from _copy(); rename optionsOptions to a concise options (or
toastOptions) parameter, make copy async, await the Promise returned by
_copy(source), and wrap the await in a try/catch so clipboard errors (e.g.,
permission denied) are caught; on success keep calling toast.add(options) if
provided, and on failure call toast.add or processLogger.error with a clear
error message referencing the failure inside the catch. Ensure you update the
function signature and any callers of copy accordingly.
- Around line 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.
In `@app/pages/blog/`[slug].vue:
- Around line 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.
- Around line 45-47: The copied URL is malformed because the template literal in
copyLink() uses {article.value?.path || "/"} instead of a proper ${...}
expression; update the string construction in copyLink() to use
${article.value?.path || "/"} so the path is interpolated correctly (reference
the copyLink function and article.value?.path) and ensure the final string is a
valid URL before calling copy().
- Line 138: The template references an undefined prop `:links="links"` passed to
the UPageLinks component; declare and populate `links` in the component's script
or replace the reference with the existing `article.body.toc.links`. Update the
script (in the <script setup>) to define a reactive/const `links` (e.g., from
`article.body.toc.links`) or change the template to
`:links="article.body.toc.links"` so UPageLinks receives a valid value; ensure
the symbol names UPageLinks, links, and article.body.toc.links are used
consistently.
- Around line 38-42: The call to defineOgImageComponent uses a non-registered
component name and an invalid prop: change defineOgImageComponent("Docs", {...})
to use the registered component name "Default" and remove the unsupported
headline prop; pass only valid OGImageProps (colorMode, title, description,
icon, siteName, theme) so update the object to include only those keys (e.g.,
title and description) when invoking defineOgImageComponent.
In `@app/pages/blog/index.vue`:
- 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()".
In `@content.config.ts`:
- Line 27: The current schema defines the field named "date" using string(),
which allows malformed dates; update the schema entry for the "date" property in
the content config (the object that contains date: string()) to use Valibot's
stricter date validators — e.g., replace string() with pipe(string(), isoDate())
or use date() if you want a native Date object; reference the symbols string(),
pipe(), isoDate(), and date() when making the change so the "date" field
enforces ISO date validation.
- Around line 24-25: The schema in content.config.ts currently declares image as
optional(string()), but the frontmatter and consumers expect an Image object
with src and alt; update the schema to define image as an optional object
matching the Image shape (e.g., object({ src: string(), alt: optional(string())
})) so article.image.src and article.image.alt used in wolfstar-v7.md and
app/pages/blog/index.vue have the correct types and structure.
In `@content/blog/wolfstar-v7.md`:
- Line 34: Introduce "Artiel" before the sentence that mentions it: add a
section header like "Utility bots - Artiel" (matching the style of "Moderation
bots - WolfStar" and "Notification bots - Staryl") and a one- or two-sentence
intro describing what Artiel is and its purpose, then keep the existing line
"Artiel inherits all animal, fun, game, and meme commands from WolfStar" under
that section; ensure the new header and intro use the same formatting and tone
as the other bot sections.
---
Duplicate comments:
In `@app/pages/blog/`[slug].vue:
- Around line 124-126: UContentSurround is rendered unconditionally while
USeparator uses v-if="surround?.length", so guard UContentSurround with the same
condition to avoid rendering when surround is empty/undefined; update the
template around UContentSurround (the UContentSurround tag) to include
v-if="surround?.length" or wrap both USeparator and UContentSurround in a parent
element that has v-if="surround?.length" so both USeparator and UContentSurround
render only when surround has content.
- Around line 87-88: The v-for loop uses the array index as the key
(v-for="(author, index) in article.authors" :key="index"), which is unstable;
replace the index with a stable unique identifier from each author object (e.g.,
:key="author.id" or :key="author.slug" or :key="author.email") and if author
objects lack such a field, add/derive a stable unique property on each author
before rendering (e.g., assign a UUID or use a deterministic slug) so that the
virtual DOM diffing for the article.authors list uses a consistent key.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 315e7d5f-4386-4075-a48d-9b91a7f92d68
📒 Files selected for processing (9)
app/assets/css/main.cssapp/composables/useBlog.tsapp/composables/useClipboard.tsapp/pages/blog/[slug].vueapp/pages/blog/index.vueapp/types/blog.tscontent.config.tscontent/blog/wolfstar-v7.mdnuxt.config.ts
| 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")) | ||
| ); |
There was a problem hiding this comment.
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.
| 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.
| return ( | ||
| queryCollection("blog") | ||
| .where("extension", "=", "md") | ||
| /* .select('title', 'date', 'image', 'description', 'path', 'authors', 'category') */ |
There was a problem hiding this comment.
🧹 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).
|
|
||
| return { | ||
| articles, | ||
| // featuredArticle, |
There was a problem hiding this comment.
🧹 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).
| type toastOptions = { | ||
| title?: string; | ||
| description?: string; | ||
| icon?: string; | ||
| color?: | ||
| | "primary" | ||
| | "secondary" | ||
| | "info" | ||
| | "success" | ||
| | "warning" | ||
| | "error" | ||
| | "important" | ||
| | "neutral"; | ||
| }; |
There was a problem hiding this comment.
🧹 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.
| throw createError({ statusCode: 404, statusMessage: "Page not found", fatal: true }); | ||
| } | ||
|
|
||
| const { articles, fetchList } = useBlog(); |
There was a problem hiding this comment.
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.
| 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()".
content.config.ts
Outdated
| schema: object({ | ||
| image: optional(string()), | ||
| authors: array(Author), | ||
| date: string(), |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider using a date type for the date field.
The date field is validated as a plain string(). While this works, Valibot provides pipe(string(), isoDate()) or similar transforms for stricter date validation, which could catch malformed dates at build time.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@content.config.ts` at line 27, The current schema defines the field named
"date" using string(), which allows malformed dates; update the schema entry for
the "date" property in the content config (the object that contains date:
string()) to use Valibot's stricter date validators — e.g., replace string()
with pipe(string(), isoDate()) or use date() if you want a native Date object;
reference the symbols string(), pipe(), isoDate(), and date() when making the
change so the "date" field enforces ISO date validation.
| They are simply better, easier, and create a more scalable architecture. | ||
| :: | ||
|
|
||
| Artiel inherits all animal, fun, game, and meme commands from WolfStar. |
There was a problem hiding this comment.
"Artiel" is referenced without prior introduction.
This line mentions "Artiel inherits all animal, fun, game, and meme commands" but Artiel hasn't been introduced earlier in the post. Consider adding a section header or brief introduction for Artiel similar to the "Moderation bots - WolfStar" and "Notification bots - Staryl" sections.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@content/blog/wolfstar-v7.md` at line 34, Introduce "Artiel" before the
sentence that mentions it: add a section header like "Utility bots - Artiel"
(matching the style of "Moderation bots - WolfStar" and "Notification bots -
Staryl") and a one- or two-sentence intro describing what Artiel is and its
purpose, then keep the existing line "Artiel inherits all animal, fun, game, and
meme commands from WolfStar" under that section; ensure the new header and intro
use the same formatting and tone as the other bot sections.
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (2)
app/pages/blog/index.vue (1)
7-7:⚠️ Potential issue | 🟠 Major
useBlog()still exposes drafts on the public index.The current query in
app/composables/useBlog.tsorders posts but never excludesdraft: true, so the first draft added to the collection will show up on/blog.🔒 Proposed fix in
app/composables/useBlog.tsqueryCollection("blog") .where("extension", "=", "md") + .where("draft", "=", false) .order("date", "DESC")🤖 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 public blog index is showing draft posts because useBlog() (specifically the fetchList/articles query in the useBlog composable) does not filter out items with draft: true; update the query used by fetchList (and any related data-fetching functions inside useBlog) to explicitly exclude drafts (e.g., add a filter for draft === false or draft not true) so only non-draft posts are returned to the public /blog index. Ensure the change is applied where articles are derived/fetched so both initial load and subsequent fetchList calls honor the draft filter.content.config.ts (1)
26-27:⚠️ Potential issue | 🟠 MajorUse the shared
Imageschema for post cover images.Keeping
imageas a bare string means posts cannot provide alt text, so the UI has to invent a generic alternative for every cover image. ReusingImagehere lets frontmatter carry a meaningful text alternative instead.As per coding guidelines, "Informative images and graphics must have meaningful alternatives using alt text for img or role='img' and aria-label/aria-labelledby for SVG."♻️ Proposed fix
schema: object({ - image: optional(string()), + image: optional(Image), authors: array(Author),Update the blog frontmatter and consumers to read
image.src/image.altas part of the same change.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@content.config.ts` around lines 26 - 27, The frontmatter schema currently defines image as optional(string()), which prevents carrying alt text; replace that field with the shared Image schema (use the Image symbol) so frontmatter exposes image.src and image.alt, and then update all consumers that read the post frontmatter to access image.src and image.alt instead of the bare image string (adjust any parsing/usage code that referenced image as a string to handle image?.src and image?.alt).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/components/SocialLinks.vue`:
- Around line 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.
In `@app/pages/blog/`[slug].vue:
- Around line 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.
- Around line 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.
In `@content/blog/wolfstar-v7.md`:
- Around line 6-10: The post's frontmatter defines authors[].avatar with only a
src, but the Author schema in content.config.ts models avatar as an Image
requiring both src and alt; add a descriptive avatar.alt string for the author
entry (e.g., "RedStar avatar") so the authors[].avatar object includes both src
and alt and stays compliant with the Image/Author schema.
---
Duplicate comments:
In `@app/pages/blog/index.vue`:
- Line 7: The public blog index is showing draft posts because useBlog()
(specifically the fetchList/articles query in the useBlog composable) does not
filter out items with draft: true; update the query used by fetchList (and any
related data-fetching functions inside useBlog) to explicitly exclude drafts
(e.g., add a filter for draft === false or draft not true) so only non-draft
posts are returned to the public /blog index. Ensure the change is applied where
articles are derived/fetched so both initial load and subsequent fetchList calls
honor the draft filter.
In `@content.config.ts`:
- Around line 26-27: The frontmatter schema currently defines image as
optional(string()), which prevents carrying alt text; replace that field with
the shared Image schema (use the Image symbol) so frontmatter exposes image.src
and image.alt, and then update all consumers that read the post frontmatter to
access image.src and image.alt instead of the bare image string (adjust any
parsing/usage code that referenced image as a string to handle image?.src and
image?.alt).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: e1795aca-edd5-4875-bb0b-c70fd25c07ba
📒 Files selected for processing (5)
app/components/SocialLinks.vueapp/pages/blog/[slug].vueapp/pages/blog/index.vuecontent.config.tscontent/blog/wolfstar-v7.md
| <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> |
There was a problem hiding this comment.
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>🤖 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.
| if (isNullOrUndefined(article.value)) { | ||
| throw createError({ statusCode: 404, statusMessage: "Article not found", fatal: true }); | ||
| } |
There was a problem hiding this comment.
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.
| <span class="text-muted" | ||
| >· <time>{{ | ||
| new Date(article.date).toLocaleDateString("en", { | ||
| year: "numeric", | ||
| month: "short", | ||
| day: "numeric", | ||
| }) | ||
| }}</time></span |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd /tmp && find . -type f -name "[slug].vue" 2>/dev/null | head -5Repository: wolfstar-project/wolfstar.rocks
Length of output: 57
🏁 Script executed:
git ls-files app/pages/blog/ | head -20Repository: wolfstar-project/wolfstar.rocks
Length of output: 125
🏁 Script executed:
head -150 app/pages/blog/[slug].vue | tail -50Repository: 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 2Repository: 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.
| authors: | ||
| - name: RedStar | ||
| to: https://github.com/RedStar071 | ||
| avatar: | ||
| src: https://github.com/RedStar071.png |
There was a problem hiding this comment.
Add the missing avatar.alt field.
content.config.ts models authors[].avatar as an Image, so once avatar is present it needs both src and alt. This entry only provides src, which leaves the post data out of sync with the declared collection schema.
🛠️ Proposed fix
authors:
- name: RedStar
to: https://github.com/RedStar071
avatar:
src: https://github.com/RedStar071.png
+ alt: Avatar of RedStar📝 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.
| authors: | |
| - name: RedStar | |
| to: https://github.com/RedStar071 | |
| avatar: | |
| src: https://github.com/RedStar071.png | |
| authors: | |
| - name: RedStar | |
| to: https://github.com/RedStar071 | |
| avatar: | |
| src: https://github.com/RedStar071.png | |
| alt: Avatar of RedStar |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@content/blog/wolfstar-v7.md` around lines 6 - 10, The post's frontmatter
defines authors[].avatar with only a src, but the Author schema in
content.config.ts models avatar as an Image requiring both src and alt; add a
descriptive avatar.alt string for the author entry (e.g., "RedStar avatar") so
the authors[].avatar object includes both src and alt and stays compliant with
the Image/Author schema.
lorypelli
left a comment
There was a problem hiding this comment.
why did you decide to use Valibot instead of Zod or Arktype?
Valibot its faster and lighter than zod |
|
@copilot Verify each finding against the current code and only fix it if needed. In |
…tring(), isoDate()) Agent-Logs-Url: https://github.com/wolfstar-project/wolfstar.rocks/sessions/71837fb6-15bb-4970-ab21-4af436819dd9 Co-authored-by: RedStar071 <76824516+RedStar071@users.noreply.github.com>
Valibot was already a production dependency in
Verified — the Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|

content.config.tsdate field to usepipe(string(), isoDate())instead of barestring()📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.