From 3762f473d19593f6c7e24ae9d982c95de44acc15 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 20:49:02 +0000 Subject: [PATCH 01/13] feat(ui): add changelog page and dedicated OG images Add a /changelog page that lists the project's GitHub releases (fetched from ungh.cc) using Nuxt UI's ChangelogVersions/ChangelogVersion with MDC for release notes, revalidated hourly via ISR. Header nav gains a Changelog link and the footer link now points to the internal page. Introduce dedicated Takumi OG image components for blog posts and the changelog, keeping the WolfStar house style (themed radial gradient + branding) while adding the publish date and stacked author avatars. Blog posts without a frontmatter image now use the BlogPost OG template. Both new components are exempted from the hardcoded-color guardrail, matching the existing Page OG component. Claude-Session: https://claude.ai/code/session_01LJcAGG6gk6ZQFtsYTWL17C Co-authored-by: Codesmith --- app/components/OgImage/BlogPost.takumi.vue | 210 ++++++++++++++++++ app/components/OgImage/Changelog.takumi.vue | 110 +++++++++ app/composables/useFooter.ts | 2 +- app/composables/useHeader.ts | 8 + app/pages/(marketing)/blog/[slug].vue | 7 +- app/pages/(marketing)/changelog/index.vue | 102 +++++++++ nuxt.config.ts | 3 + .../design-tokens/no-hardcoded-colors.test.ts | 2 + 8 files changed, 442 insertions(+), 2 deletions(-) create mode 100644 app/components/OgImage/BlogPost.takumi.vue create mode 100644 app/components/OgImage/Changelog.takumi.vue create mode 100644 app/pages/(marketing)/changelog/index.vue diff --git a/app/components/OgImage/BlogPost.takumi.vue b/app/components/OgImage/BlogPost.takumi.vue new file mode 100644 index 000000000..a0357d0fe --- /dev/null +++ b/app/components/OgImage/BlogPost.takumi.vue @@ -0,0 +1,210 @@ + + + diff --git a/app/components/OgImage/Changelog.takumi.vue b/app/components/OgImage/Changelog.takumi.vue new file mode 100644 index 000000000..bbbcca2de --- /dev/null +++ b/app/components/OgImage/Changelog.takumi.vue @@ -0,0 +1,110 @@ + + + diff --git a/app/composables/useFooter.ts b/app/composables/useFooter.ts index 2a85cd527..7fc5503e7 100644 --- a/app/composables/useFooter.ts +++ b/app/composables/useFooter.ts @@ -17,7 +17,7 @@ export const useFooter = () => { { class: "link-hover", label: "Changelog", - to: "https://github.com/wolfstar-project/wolfstar.rocks/releases", + to: "/changelog", }, ], label: "Product", diff --git a/app/composables/useHeader.ts b/app/composables/useHeader.ts index a9d843736..3e2354cf6 100644 --- a/app/composables/useHeader.ts +++ b/app/composables/useHeader.ts @@ -55,6 +55,10 @@ export function useHeader() { label: "Blog", to: "/blog", }, + { + label: "Changelog", + to: "/changelog", + }, ]); const mobileLinks = computed(() => [ @@ -95,6 +99,10 @@ export function useHeader() { label: "Blog", to: "/blog", }, + { + label: "Changelog", + to: "/changelog", + }, { icon: "lucide:github", label: "GitHub", diff --git a/app/pages/(marketing)/blog/[slug].vue b/app/pages/(marketing)/blog/[slug].vue index e9f48e571..631a03bb3 100644 --- a/app/pages/(marketing)/blog/[slug].vue +++ b/app/pages/(marketing)/blog/[slug].vue @@ -132,9 +132,14 @@ useSeoMeta({ }); if (!post.image) { - defineOgImage("Page", { + defineOgImage("BlogPost", { title, description, + date: post.date, + authors: post.authors.map((author) => ({ + name: author.name, + avatar: author.avatar?.src, + })), }); } diff --git a/app/pages/(marketing)/changelog/index.vue b/app/pages/(marketing)/changelog/index.vue new file mode 100644 index 000000000..ee5ceb821 --- /dev/null +++ b/app/pages/(marketing)/changelog/index.vue @@ -0,0 +1,102 @@ + + + diff --git a/nuxt.config.ts b/nuxt.config.ts index c12998da5..6c15a74f7 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -263,6 +263,9 @@ export default defineNuxtConfig({ "/wolfstar": { appLayout: "default", prerender: true, robots: true }, "/blog": { appLayout: "default", prerender: true, robots: true }, "/blog/**": { appLayout: "default", prerender: true, robots: true }, + // Changelog pulls live GitHub releases from ungh.cc, so it revalidates via + // ISR (1 hour) rather than prerendering against the external API at build time. + "/changelog": { appLayout: "default", robots: true, ...getISRConfig(60 * 60) }, }, sourcemap: { diff --git a/test/unit/design-tokens/no-hardcoded-colors.test.ts b/test/unit/design-tokens/no-hardcoded-colors.test.ts index 0613595ae..27f195fb5 100644 --- a/test/unit/design-tokens/no-hardcoded-colors.test.ts +++ b/test/unit/design-tokens/no-hardcoded-colors.test.ts @@ -18,6 +18,8 @@ const ROOT = join(import.meta.dirname, "../../.."); const ALLOW_LIST = new Set([ // Satori/Takumi requires resolved static colors (no var() support) "app/components/OgImage/Page.takumi.vue", + "app/components/OgImage/BlogPost.takumi.vue", + "app/components/OgImage/Changelog.takumi.vue", // Discord-domain components maintain Discord brand fidelity with scoped vars "app/components/discord/message.vue", "app/components/discord/message-reply.vue", From a33a9ac941bb6a97876afb944d510601521e50bc Mon Sep 17 00:00:00 2001 From: RedStar071 Date: Thu, 16 Jul 2026 21:11:27 +0000 Subject: [PATCH 02/13] fix(ui): register changelog as marketing route and allow ungh.cc in CSP Co-authored-by: Codesmith --- app/utils/marketing-routes.ts | 1 + nuxt.config.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/app/utils/marketing-routes.ts b/app/utils/marketing-routes.ts index b5cfe9fbb..45ffe955b 100644 --- a/app/utils/marketing-routes.ts +++ b/app/utils/marketing-routes.ts @@ -8,6 +8,7 @@ export const MARKETING_PATHS = [ "/profile", "/account", "/blog", + "/changelog", ] as const; export function isMarketingPath(path: string): boolean { diff --git a/nuxt.config.ts b/nuxt.config.ts index 6c15a74f7..dccecec92 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -509,6 +509,7 @@ export default defineNuxtConfig({ "https://media.discordapp.net", "https://discord.com", "https://api.iconify.design", + "https://ungh.cc", // Changelog page fetches GitHub releases from ungh.cc on client-side navigation "https://*.netlify.com", "https://*.netlify.app", "https://wolfstar.rocks", // Better Auth's client calls the configured site URL directly (e.g. /api/auth/get-session), not just relative paths From ae69d610e52d5698d2ed9f737c7b71a16f1f9283 Mon Sep 17 00:00:00 2001 From: RedStar071 Date: Thu, 16 Jul 2026 21:27:16 +0000 Subject: [PATCH 03/13] fix(ui): scope changelog heading ids per release to avoid duplicate ids Co-authored-by: Codesmith --- app/pages/(marketing)/changelog/index.vue | 20 +++++ app/utils/changelog-heading-ids.ts | 72 +++++++++++++++ .../app/utils/changelog-heading-ids.test.ts | 87 +++++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 app/utils/changelog-heading-ids.ts create mode 100644 test/unit/app/utils/changelog-heading-ids.test.ts diff --git a/app/pages/(marketing)/changelog/index.vue b/app/pages/(marketing)/changelog/index.vue index ee5ceb821..650c1cc8c 100644 --- a/app/pages/(marketing)/changelog/index.vue +++ b/app/pages/(marketing)/changelog/index.vue @@ -36,6 +36,7 @@ v-if="version.markdown" :value="version.markdown" :cache-key="version.tag" + :parser-options="headingIdParserOptions(version.tag)" /> @@ -57,6 +58,25 @@ interface UnghRelease { const site = useSiteConfig(); +/** + * Release notes render one `` per version, each with a fresh slugger, so + * shared section headings ("Fixes", "Chore", ...) would emit duplicate ids + * across the page. Scope every release's heading ids by its tag to keep them + * unique and satisfy html-validate's `no-dup-id` rule during prerender. + */ +function headingIdParserOptions(tag: string) { + return { + rehype: { + plugins: { + "changelog-heading-ids": { + instance: rehypeChangelogHeadingIds, + options: { prefix: tag }, + }, + }, + }, + }; +} + const title = "Changelog"; const description = "Track every release, improvement, and fix across the WolfStar Project."; diff --git a/app/utils/changelog-heading-ids.ts b/app/utils/changelog-heading-ids.ts new file mode 100644 index 000000000..e1aa9c47f --- /dev/null +++ b/app/utils/changelog-heading-ids.ts @@ -0,0 +1,72 @@ +interface HastNode { + type: string; + tagName?: string; + value?: string; + properties?: Record; + children?: HastNode[]; +} + +const HEADING_TAG = /^h[1-6]$/; + +/** + * Slugifies heading text into an id-safe fragment (lowercase alphanumerics + * joined by single dashes, no leading/trailing dash). + */ +export function slugifyHeadingText(value: string): string { + return value + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); +} + +function extractText(node: HastNode): string { + if (node.type === "text") { + return node.value ?? ""; + } + + return (node.children ?? []).map(extractText).join(""); +} + +/** + * Rewrites heading ids in a parsed hast tree so they are prefixed with a + * per-release slug and de-duplicated within the tree. + * + * Each changelog release renders in its own `` instance with a fresh + * slugger, so without a prefix identical section headings ("Fixes", "Chore", + * ...) collide across releases and fail html-validate's `no-dup-id` rule when + * the page is prerendered. + */ +export function prefixHeadingIds(tree: HastNode, prefix: string): void { + const base = slugifyHeadingText(prefix); + const seen = new Map(); + + const walk = (node: HastNode): void => { + if (node.type === "element" && node.tagName && HEADING_TAG.test(node.tagName)) { + const slug = slugifyHeadingText(extractText(node)) || "section"; + const scoped = base ? `${base}-${slug}` : slug; + const count = seen.get(scoped) ?? 0; + seen.set(scoped, count + 1); + node.properties = node.properties ?? {}; + node.properties.id = count === 0 ? scoped : `${scoped}-${count}`; + } + + for (const child of node.children ?? []) { + walk(child); + } + }; + + walk(tree); +} + +/** + * MDC/rehype plugin factory. Registered as a `parserOptions.rehype.plugins` + * entry on the changelog `` so each release gets release-scoped heading + * ids. MDC's compiler keeps a pre-set `properties.id`, so this runs before its + * own slug generation. + */ +export function rehypeChangelogHeadingIds(options: { prefix: string }) { + return (tree: HastNode): void => { + prefixHeadingIds(tree, options.prefix); + }; +} diff --git a/test/unit/app/utils/changelog-heading-ids.test.ts b/test/unit/app/utils/changelog-heading-ids.test.ts new file mode 100644 index 000000000..a123c89e0 --- /dev/null +++ b/test/unit/app/utils/changelog-heading-ids.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from "vitest"; +import { + prefixHeadingIds, + rehypeChangelogHeadingIds, + slugifyHeadingText, +} from "~/utils/changelog-heading-ids"; + +interface HastNode { + type: string; + tagName?: string; + value?: string; + properties?: Record; + children?: HastNode[]; +} + +function heading(level: number, text: string): HastNode { + return { + type: "element", + tagName: `h${level}`, + properties: {}, + children: [{ type: "text", value: text }], + }; +} + +function tree(...children: HastNode[]): HastNode { + return { type: "root", children }; +} + +describe("slugifyHeadingText", () => { + it("lowercases and joins words with single dashes", () => { + expect(slugifyHeadingText("Bug Fixes")).toBe("bug-fixes"); + }); + + it("strips emoji, punctuation, and leading/trailing dashes", () => { + expect(slugifyHeadingText("❤️ Contributors")).toBe("contributors"); + }); +}); + +describe("prefixHeadingIds", () => { + it("scopes heading ids by the release prefix", () => { + const root = tree(heading(2, "Fixes"), heading(3, "Chore")); + + prefixHeadingIds(root, "v1.2.3"); + + expect(root.children?.[0]?.properties?.id).toBe("v1-2-3-fixes"); + expect(root.children?.[1]?.properties?.id).toBe("v1-2-3-chore"); + }); + + it("keeps ids unique across releases sharing section names", () => { + const releaseA = tree(heading(2, "Fixes")); + const releaseB = tree(heading(2, "Fixes")); + + prefixHeadingIds(releaseA, "v1.0.0"); + prefixHeadingIds(releaseB, "v2.0.0"); + + expect(releaseA.children?.[0]?.properties?.id).toBe("v1-0-0-fixes"); + expect(releaseB.children?.[0]?.properties?.id).toBe("v2-0-0-fixes"); + }); + + it("de-duplicates repeated headings within a single release", () => { + const root = tree(heading(2, "Fixes"), heading(2, "Fixes")); + + prefixHeadingIds(root, "v1.0.0"); + + expect(root.children?.[0]?.properties?.id).toBe("v1-0-0-fixes"); + expect(root.children?.[1]?.properties?.id).toBe("v1-0-0-fixes-1"); + }); + + it("falls back to a section slug when a heading has no text", () => { + const root = tree(heading(2, "")); + + prefixHeadingIds(root, "v1.0.0"); + + expect(root.children?.[0]?.properties?.id).toBe("v1-0-0-section"); + }); +}); + +describe("rehypeChangelogHeadingIds", () => { + it("returns a transformer that prefixes heading ids in place", () => { + const transform = rehypeChangelogHeadingIds({ prefix: "v1.0.0" }); + const root = tree(heading(2, "Enhancements")); + + transform(root); + + expect(root.children?.[0]?.properties?.id).toBe("v1-0-0-enhancements"); + }); +}); From 41591e0aaa6d9be4405e21f32d170503c795aafd Mon Sep 17 00:00:00 2001 From: RedStar Date: Sat, 18 Jul 2026 13:34:19 +0200 Subject: [PATCH 04/13] feat(config): add Nuxt Studio integration and update environment variables - Added Nuxt Studio module to nuxt.config.ts. - Updated .env.example to include GitHub OAuth credentials for Nuxt Studio. - Added nuxt-studio dependency to package.json and pnpm-lock.yaml, specifying version 1.7.0. --- .env.example | 11 ++- nuxt.config.ts | 1 + package.json | 1 + pnpm-lock.yaml | 213 ++++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 214 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index 1a834d29c..ce96d00fa 100644 --- a/.env.example +++ b/.env.example @@ -25,9 +25,14 @@ NUXT_BETTER_AUTH_SECRET= # Session cookie name — MUST differ per deployment to prevent cookie bleed across subdomains # Production (wolfstar.rocks): leave blank (defaults to wolfstar-session) # Beta/staging (beta.wolfstar.rocks): wolfstar-session-beta -NUXT_SESSION_COOKIE_NAME= - -# Secret for generating OG images (generate a secure random string) +NUXT_SESSION_COOKIE_NAME= + +# Nuxt Studio GitHub OAuth credentials +# Configure the callback URL as https://your-domain.example/__nuxt_studio/auth/github +NUXT_STUDIO_AUTH_GITHUB_CLIENT_ID= +NUXT_STUDIO_AUTH_GITHUB_CLIENT_SECRET= + +# Secret for generating OG images (generate a secure random string) # You can use: openssl rand -base64 32 NUXT_IMAGE_PROXY_SECRET= # =================================== diff --git a/nuxt.config.ts b/nuxt.config.ts index dccecec92..4d18bb688 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -16,6 +16,7 @@ export default defineNuxtConfig({ modules: [ "@nuxt/ui", "@nuxt/content", + "nuxt-studio", "@nuxt/image", "@nuxt/hints", "@nuxt/fonts", diff --git a/package.json b/package.json index 5117c1aab..c6d8097ea 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "nuxt-og-image": "6.5.1", "nuxt-security": "^2.5.1", "nuxt-skew-protection": "^1.3.0", + "nuxt-studio": "1.7.0", "nuxt-vitalizer": "^2.0.0", "postcss-nested": "^7.0.2", "simple-git": "^3.32.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ca2be832..0ad8b796a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -146,7 +146,7 @@ importers: version: 17.4.2 evlog: specifier: ^2.14.0 - version: 2.20.0(@nuxt/kit@4.4.8)(@voidzero-dev/vite-plus-core@0.1.19)(h3@1.15.11)(hono@4.12.28)(nitropack@2.13.4)(ofetch@1.5.1)(react@19.2.7) + version: 2.20.0(@nuxt/kit@4.4.8)(@voidzero-dev/vite-plus-core@0.1.19)(ai@6.0.228)(h3@1.15.11)(hono@4.12.28)(nitropack@2.13.4)(ofetch@1.5.1)(react@19.2.7) feed: specifier: ^5.2.1 version: 5.2.1 @@ -165,6 +165,9 @@ importers: nuxt-skew-protection: specifier: ^1.3.0 version: 1.3.0(@netlify/blobs@10.7.9)(@nuxt/schema@4.4.8)(@nuxtjs/robots@6.1.2)(@voidzero-dev/vite-plus-core@0.1.19)(db0@0.3.4)(ioredis@5.11.1)(magicast@0.5.3)(nuxt@4.4.8)(vue@3.5.38) + nuxt-studio: + specifier: 1.7.0 + version: 1.7.0(@netlify/blobs@10.7.9)(db0@0.3.4)(ioredis@5.11.1)(magicast@0.5.3)(vue@3.5.38) nuxt-vitalizer: specifier: ^2.0.0 version: 2.0.0(magicast@0.5.3) @@ -306,7 +309,7 @@ importers: version: 1.1.4 skilld: specifier: 1.7.4 - version: 1.7.4(@opentelemetry/api@1.9.1)(magicast@0.5.3)(pg@8.22.0)(playwright@1.61.1)(unstorage@1.17.5)(ws@8.21.0)(zod@4.4.3) + version: 1.7.4(@opentelemetry/api@1.9.1)(ai@6.0.228)(magicast@0.5.3)(pg@8.22.0)(playwright@1.61.1)(unstorage@1.17.5)(ws@8.21.0)(zod@4.4.3) storybook: specifier: catalog:storybook version: 10.4.6(@testing-library/dom@10.4.1)(@types/react@19.2.17)(prettier@3.9.4)(react@19.2.7)(vite-plus@0.1.19) @@ -334,6 +337,28 @@ packages: '@adobe/css-tools@4.5.0': resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + '@ai-sdk/gateway@3.0.151': + resolution: {integrity: sha512-gsKEm1LleR/xm1FiJjnNxf1ZUKZryj20STsKJB7TdMcaiRqQgeHrdYWv/DNSA8ZBkRahHC+wEYHaI0VR9/EOwQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@4.0.39': + resolution: {integrity: sha512-XPR6o7561RYUkfYlLYouWsvm6Gv2tYIQy5pttGRkvML98u138ClPThn5yiQg5rMQutTtZLB+GUC8qFFCzDKQQQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@3.0.14': + resolution: {integrity: sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA==} + engines: {node: '>=18'} + + '@ai-sdk/vue@3.0.228': + resolution: {integrity: sha512-8+t7DEJt97r7ZmYR641oxr0KLes27tnx3TybYhi/KQm/UUtuq66nhMKealZY6y6Tq3Yg9+gsOgcoCAu13YWtWA==} + engines: {node: '>=18'} + peerDependencies: + vue: 3.5.38 + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -3000,6 +3025,9 @@ packages: '@nuxtjs/html-validator@2.1.0': resolution: {integrity: sha512-ldo8ioSsH3OEumtgwDMokTxlhjgO9FxjJWViAxisq5l/wjvaVX8SYTQ02wjtQcQQPSvS6BwgypAp400RlyFHng==} + '@nuxtjs/mdc@0.21.1': + resolution: {integrity: sha512-DIeUD7IahWVUSoZExysxH9dX51Io6hcQYgGJODq0cMTGqaoDD32lRfHBJxYUmy+sUCV1+1hfa2ixspgJgEd2GA==} + '@nuxtjs/mdc@0.22.1': resolution: {integrity: sha512-+tkGhBNxapWZiadZaf9yTxMtnSshjPY2VVnZsy/YoOu+c1p2S6MQgAHB9QWk+FIPT0epJ/VAtGh6qW1tjyPXOg==} @@ -6344,6 +6372,10 @@ packages: engines: {node: '>=20'} hasBin: true + '@vercel/oidc@3.2.0': + resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} + engines: {node: '>= 20'} + '@vite-pwa/assets-generator@1.0.2': resolution: {integrity: sha512-MCbrb508JZHqe7bUibmZj/lyojdhLRnfkmyXnkrCM2zVrjTgL89U8UEfInpKTvPeTnxsw2hmyZxnhsdNR6yhwg==} engines: {node: '>=16.14.0'} @@ -6872,6 +6904,12 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ai@6.0.228: + resolution: {integrity: sha512-3TXPF+meV/B0ObVWqLZDfTo0UjT9eKQX+QO5B8n7TSyLxnU3U9FHKxRp7U9mGuTVh7fdo2OtU0J8uGFR4EStsg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + ajv-errors@3.0.0: resolution: {integrity: sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==} peerDependencies: @@ -8551,6 +8589,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} + engines: {node: '>=18.0.0'} + evlog@2.20.0: resolution: {integrity: sha512-S4r1+uHKRJg/1AH7LYHjuBAOJpe6Pdf3n9bb1lrIzeNNYzi4dvycrS+regpyriaalx69+IL6vB3OnPN7e7DVLw==} engines: {node: '>=18.0.0'} @@ -9804,6 +9846,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -10872,6 +10917,9 @@ packages: pusher-js: optional: true + nuxt-studio@1.7.0: + resolution: {integrity: sha512-gIRCLPHaj5EcLOX/D1e1ymdJL7dwkxVI5Jrylrv5vIhPWYjC+vggcp71xO4bsC1i9Y1jfbNRbtidVr5wdT0ckg==} + nuxt-vitalizer@2.0.0: resolution: {integrity: sha512-X2mabvcXMfKmWc46p8WmCbTlr8+wDniPHunQIRSGWmH0goqw6YTAe0+ttxRNISGxcobXDqqip7qPCE6+T60t1g==} @@ -12773,6 +12821,11 @@ packages: engines: {node: '>=16'} hasBin: true + swrv@1.2.0: + resolution: {integrity: sha512-lH/g4UcNyj+7lzK4eRGT4C68Q4EhQ6JtM9otPRIASfhhzfLWtbZPHcMuhuba7S9YVYuxkMUGImwMyGpfbkH07A==} + peerDependencies: + vue: 3.5.38 + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -14026,6 +14079,33 @@ snapshots: '@adobe/css-tools@4.5.0': {} + '@ai-sdk/gateway@3.0.151(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 3.0.14 + '@ai-sdk/provider-utils': 4.0.39(zod@4.4.3) + '@vercel/oidc': 3.2.0 + zod: 4.4.3 + + '@ai-sdk/provider-utils@4.0.39(zod@4.4.3)': + dependencies: + '@ai-sdk/provider': 3.0.14 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.1.0 + zod: 4.4.3 + + '@ai-sdk/provider@3.0.14': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/vue@3.0.228(vue@3.5.38)(zod@4.4.3)': + dependencies: + '@ai-sdk/provider-utils': 4.0.39(zod@4.4.3) + ai: 6.0.228(zod@4.4.3) + swrv: 1.2.0(vue@3.5.38) + vue: 3.5.38(typescript@6.0.3) + transitivePeerDependencies: + - zod + '@alloc/quick-lru@5.2.0': {} '@antfu/install-pkg@1.1.0': @@ -17794,6 +17874,56 @@ snapshots: - magicast - vitest + '@nuxtjs/mdc@0.21.1(magicast@0.5.3)': + dependencies: + '@nuxt/kit': 4.4.8(magicast@0.5.3) + '@shikijs/core': 4.3.1 + '@shikijs/engine-javascript': 4.3.1 + '@shikijs/langs': 4.3.1 + '@shikijs/themes': 4.3.1 + '@shikijs/transformers': 4.3.1 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@vue/compiler-core': 3.5.39 + consola: 3.4.2 + debug: 4.4.3 + defu: 6.1.7 + destr: 2.0.5 + detab: 3.0.2 + github-slugger: 2.0.0 + hast-util-format: 1.1.0 + hast-util-to-mdast: 10.1.2 + hast-util-to-string: 3.0.1 + mdast-util-to-hast: 13.2.1 + micromark-util-sanitize-uri: 2.0.1 + parse5: 8.0.1 + pathe: 2.0.3 + property-information: 7.2.0 + rehype-external-links: 3.0.0 + rehype-minify-whitespace: 6.0.2 + rehype-raw: 7.0.0 + rehype-remark: 10.0.1 + rehype-slug: 6.0.0 + rehype-sort-attribute-values: 5.0.1 + rehype-sort-attributes: 5.0.1 + remark-emoji: 5.0.2 + remark-gfm: 4.0.1 + remark-mdc: 3.11.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remark-stringify: 11.0.0 + scule: 1.3.0 + shiki: 4.3.1 + ufo: 1.6.4 + unified: 11.0.5 + unist-builder: 4.0.0 + unist-util-visit: 5.1.0 + unwasm: 0.5.3 + vfile: 6.0.3 + transitivePeerDependencies: + - magicast + - supports-color + '@nuxtjs/mdc@0.22.1(magicast@0.5.3)': dependencies: '@nuxt/kit': 4.4.8(magicast@0.5.3) @@ -18075,7 +18205,7 @@ snapshots: '@opentelemetry/api-logs@0.217.0': dependencies: - '@opentelemetry/api': 1.9.0 + '@opentelemetry/api': 1.9.1 '@opentelemetry/api@1.9.0': {} @@ -20558,6 +20688,8 @@ snapshots: - rollup - supports-color + '@vercel/oidc@3.2.0': {} + '@vite-pwa/assets-generator@1.0.2': dependencies: cac: 6.7.14 @@ -21175,6 +21307,14 @@ snapshots: agent-base@7.1.4: {} + ai@6.0.228(zod@4.4.3): + dependencies: + '@ai-sdk/gateway': 3.0.151(zod@4.4.3) + '@ai-sdk/provider': 3.0.14 + '@ai-sdk/provider-utils': 4.0.39(zod@4.4.3) + '@opentelemetry/api': 1.9.1 + zod: 4.4.3 + ajv-errors@3.0.0(ajv@8.20.0): dependencies: ajv: 8.20.0 @@ -22888,9 +23028,12 @@ snapshots: events@3.3.0: {} - evlog@2.20.0(@nuxt/kit@4.4.8)(@voidzero-dev/vite-plus-core@0.1.19)(h3@1.15.11)(hono@4.12.28)(nitropack@2.13.4)(ofetch@1.5.1)(react@19.2.7): + eventsource-parser@3.1.0: {} + + evlog@2.20.0(@nuxt/kit@4.4.8)(@voidzero-dev/vite-plus-core@0.1.19)(ai@6.0.228)(h3@1.15.11)(hono@4.12.28)(nitropack@2.13.4)(ofetch@1.5.1)(react@19.2.7): optionalDependencies: '@nuxt/kit': 4.4.8(magicast@0.5.3) + ai: 6.0.228(zod@4.4.3) h3: 1.15.11 hono: 4.12.28 nitropack: 2.13.4(@electric-sql/pglite@0.4.1)(@netlify/blobs@10.7.9)(@voidzero-dev/vite-plus-core@0.1.19)(drizzle-orm@0.41.0)(mysql2@3.15.3)(oxc-parser@0.132.0)(rolldown@1.1.4) @@ -24258,6 +24401,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json-stable-stringify@1.3.0: @@ -26169,6 +26314,50 @@ snapshots: - vite - vue + nuxt-studio@1.7.0(@netlify/blobs@10.7.9)(db0@0.3.4)(ioredis@5.11.1)(magicast@0.5.3)(vue@3.5.38): + dependencies: + '@ai-sdk/gateway': 3.0.151(zod@4.4.3) + '@ai-sdk/vue': 3.0.228(vue@3.5.38)(zod@4.4.3) + '@iconify-json/lucide': 1.2.116 + '@nuxtjs/mdc': 0.21.1(magicast@0.5.3) + '@vueuse/core': 14.3.0(vue@3.5.38) + ai: 6.0.228(zod@4.4.3) + defu: 6.1.7 + destr: 2.0.5 + js-yaml: 4.3.0 + minimatch: 10.2.5 + nuxt-component-meta: 0.17.2(magicast@0.5.3) + remark-mdc: 3.11.1 + shiki: 4.3.1 + unstorage: 1.17.5(@netlify/blobs@10.7.9)(db0@0.3.4)(ioredis@5.11.1) + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + optionalDependencies: + ipx: 3.1.1(@netlify/blobs@10.7.9)(db0@0.3.4)(ioredis@5.11.1) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - idb-keyval + - ioredis + - magicast + - supports-color + - uploadthing + - vue + nuxt-vitalizer@2.0.0(magicast@0.5.3): dependencies: '@nuxt/kit': 4.4.8(magicast@0.5.3) @@ -28174,9 +28363,10 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 - retriv@0.13.2(@huggingface/transformers@4.2.0)(pg@8.22.0)(sqlite-vec@0.1.9)(typescript@6.0.2): + retriv@0.13.2(@huggingface/transformers@4.2.0)(ai@6.0.228)(pg@8.22.0)(sqlite-vec@0.1.9)(typescript@6.0.2): optionalDependencies: '@huggingface/transformers': 4.2.0 + ai: 6.0.228(zod@4.4.3) pg: 8.22.0 sqlite-vec: 0.1.9 typescript: 6.0.2 @@ -28523,7 +28713,7 @@ snapshots: ufo: 1.6.4 vue: 3.5.38(typescript@6.0.3) - skilld@1.7.4(@opentelemetry/api@1.9.1)(magicast@0.5.3)(pg@8.22.0)(playwright@1.61.1)(unstorage@1.17.5)(ws@8.21.0)(zod@4.4.3): + skilld@1.7.4(@opentelemetry/api@1.9.1)(ai@6.0.228)(magicast@0.5.3)(pg@8.22.0)(playwright@1.61.1)(unstorage@1.17.5)(ws@8.21.0)(zod@4.4.3): dependencies: '@clack/prompts': 1.7.0 '@huggingface/transformers': 4.2.0 @@ -28544,14 +28734,14 @@ snapshots: oxc-parser: 0.129.0 p-limit: 7.3.0 pathe: 2.0.3 - retriv: 0.13.2(@huggingface/transformers@4.2.0)(pg@8.22.0)(sqlite-vec@0.1.9)(typescript@6.0.2) + retriv: 0.13.2(@huggingface/transformers@4.2.0)(ai@6.0.228)(pg@8.22.0)(sqlite-vec@0.1.9)(typescript@6.0.2) semver: 7.8.5 sqlite-vec: 0.1.9 std-env: 4.1.0 tinyglobby: 0.2.17 typebox: 1.3.4 typescript: 6.0.2 - unagent: 0.0.8(@huggingface/transformers@4.2.0)(pg@8.22.0)(sqlite-vec@0.1.9)(unstorage@1.17.5) + unagent: 0.0.8(@huggingface/transformers@4.2.0)(ai@6.0.228)(pg@8.22.0)(sqlite-vec@0.1.9)(unstorage@1.17.5) unist-util-visit: 5.1.0 transitivePeerDependencies: - '@ai-sdk/cohere' @@ -28929,6 +29119,10 @@ snapshots: picocolors: 1.1.1 sax: 1.6.0 + swrv@1.2.0(vue@3.5.38): + dependencies: + vue: 3.5.38(typescript@6.0.3) + symbol-tree@3.2.4: {} tagged-tag@1.0.0: {} @@ -29201,7 +29395,7 @@ snapshots: ultrahtml@1.6.0: {} - unagent@0.0.8(@huggingface/transformers@4.2.0)(pg@8.22.0)(sqlite-vec@0.1.9)(unstorage@1.17.5): + unagent@0.0.8(@huggingface/transformers@4.2.0)(ai@6.0.228)(pg@8.22.0)(sqlite-vec@0.1.9)(unstorage@1.17.5): dependencies: croner: 9.1.0 hookable: 6.1.1 @@ -29210,6 +29404,7 @@ snapshots: yaml: 2.9.0 optionalDependencies: '@huggingface/transformers': 4.2.0 + ai: 6.0.228(zod@4.4.3) pg: 8.22.0 sqlite-vec: 0.1.9 unstorage: 1.17.5(@netlify/blobs@10.7.9)(db0@0.3.4)(ioredis@5.11.1) From 48fb4cb07d349d3c56baca6000efb10542e47e02 Mon Sep 17 00:00:00 2001 From: RedStar071 Date: Sat, 18 Jul 2026 12:00:25 +0000 Subject: [PATCH 05/13] fix(config): load Nuxt Studio only in dev to unbreak build and vitest Co-authored-by: Codesmith --- nuxt.config.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nuxt.config.ts b/nuxt.config.ts index 4d18bb688..c86ee9998 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -1,7 +1,7 @@ import netlifyNuxt from "@netlify/nuxt"; import { auditRedactPreset } from "evlog"; import { createResolver } from "nuxt/kit"; -import { isCI, isTest, provider } from "std-env"; +import { isCI, isDevelopment, isTest, provider } from "std-env"; import { pwa } from "./config/pwa"; import { generateRuntimeConfig } from "./server/utils/runtimeConfig"; @@ -16,7 +16,11 @@ export default defineNuxtConfig({ modules: [ "@nuxt/ui", "@nuxt/content", - "nuxt-studio", + // Nuxt Studio is a local content-editing tool. It ships multi-MB editor + // bundles that overflow the PWA precache (breaking the build) and registers + // Vite plugins that break the rolldown-based Vitest environment, so only + // load it during local development. + ...(isDevelopment ? ["nuxt-studio"] : []), "@nuxt/image", "@nuxt/hints", "@nuxt/fonts", From da428b12b214da034e4ea2d0c865440881a04d80 Mon Sep 17 00:00:00 2001 From: RedStar071 Date: Sat, 18 Jul 2026 12:02:00 +0000 Subject: [PATCH 06/13] fix(ui): render release and article dates in UTC to avoid hydration mismatch Co-authored-by: Codesmith --- .env.example | 5 +++-- app/utils/format-date-by-locale.ts | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index ce96d00fa..a6cea7df4 100644 --- a/.env.example +++ b/.env.example @@ -27,8 +27,9 @@ NUXT_BETTER_AUTH_SECRET= # Beta/staging (beta.wolfstar.rocks): wolfstar-session-beta NUXT_SESSION_COOKIE_NAME= -# Nuxt Studio GitHub OAuth credentials -# Configure the callback URL as https://your-domain.example/__nuxt_studio/auth/github +# Nuxt Studio GitHub OAuth credentials (local development only) +# Nuxt Studio is registered only when running `nuxt dev`, so it is never exposed +# in production. Configure the callback URL as http://localhost:3000/__nuxt_studio/auth/github NUXT_STUDIO_AUTH_GITHUB_CLIENT_ID= NUXT_STUDIO_AUTH_GITHUB_CLIENT_SECRET= diff --git a/app/utils/format-date-by-locale.ts b/app/utils/format-date-by-locale.ts index faf7349bd..5b0805c91 100644 --- a/app/utils/format-date-by-locale.ts +++ b/app/utils/format-date-by-locale.ts @@ -1,7 +1,12 @@ export function formatDateByLocale(locale: string, date: string | number | Date) { + // Pin the calendar day to UTC so the server (UTC host) and the client + // (any local time zone) render the same date. Without this, a timestamp + // near UTC midnight formats as a different day during hydration in + // western time zones, causing a hydration mismatch. return new Date(date).toLocaleDateString(locale, { year: "numeric", month: "long", day: "numeric", + timeZone: "UTC", }); } From cbc760bd25a7fec9e4c0a965f5521294dd2fa104 Mon Sep 17 00:00:00 2001 From: RedStar071 Date: Sat, 18 Jul 2026 12:04:31 +0000 Subject: [PATCH 07/13] fix(config): ignore dev-only nuxt-studio dependency in knip Co-authored-by: Codesmith --- knip.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/knip.ts b/knip.ts index 5d24316c9..d02ce0e79 100644 --- a/knip.ts +++ b/knip.ts @@ -69,6 +69,9 @@ const config: KnipConfig = { "@nuxt/icon", "nuxt-security", + /** Registered as a Nuxt module only in dev via a conditional spread in nuxt.config.ts, so knip can't resolve it statically */ + "nuxt-studio", + /** Used in the app in guild/logs components */ "@tanstack/table-core", From b8524dae127d2494bf7dff27c4da16ea50775ace Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 14:47:18 +0000 Subject: [PATCH 08/13] feat(blog): always use generated OG image for blog posts Use the generated BlogPost OG image as og:image for every blog post, including posts that define a frontmatter image, so social shares get the branded card with title, date, and authors. The frontmatter image is still used for the blog listing thumbnails. Future posts inherit this automatically via the shared [slug] route. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01LJcAGG6gk6ZQFtsYTWL17C --- app/pages/(marketing)/blog/[slug].vue | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/app/pages/(marketing)/blog/[slug].vue b/app/pages/(marketing)/blog/[slug].vue index 631a03bb3..b67510b1f 100644 --- a/app/pages/(marketing)/blog/[slug].vue +++ b/app/pages/(marketing)/blog/[slug].vue @@ -128,20 +128,17 @@ useSeoMeta({ description, ogDescription: description, ogTitle: `${title} · WolfStar Blog`, - ...(post.image ? { ogImage: `${site.url}${post.image}` } : {}), }); -if (!post.image) { - defineOgImage("BlogPost", { - title, - description, - date: post.date, - authors: post.authors.map((author) => ({ - name: author.name, - avatar: author.avatar?.src, - })), - }); -} +defineOgImage("BlogPost", { + title, + description, + date: post.date, + authors: post.authors.map((author) => ({ + name: author.name, + avatar: author.avatar?.src, + })), +}); function formatSocialIntentQueryText(handle: string | undefined): string { const credit = handle ? ` by @${handle}` : ""; From 76a3ec04d5ea2171e3fd281dc37195df3b72ca48 Mon Sep 17 00:00:00 2001 From: RedStar071 Date: Sat, 18 Jul 2026 15:01:13 +0000 Subject: [PATCH 09/13] fix(blog): preserve custom frontmatter OG images Co-authored-by: Codesmith --- app/pages/(marketing)/blog/[slug].vue | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/app/pages/(marketing)/blog/[slug].vue b/app/pages/(marketing)/blog/[slug].vue index b67510b1f..631a03bb3 100644 --- a/app/pages/(marketing)/blog/[slug].vue +++ b/app/pages/(marketing)/blog/[slug].vue @@ -128,17 +128,20 @@ useSeoMeta({ description, ogDescription: description, ogTitle: `${title} · WolfStar Blog`, + ...(post.image ? { ogImage: `${site.url}${post.image}` } : {}), }); -defineOgImage("BlogPost", { - title, - description, - date: post.date, - authors: post.authors.map((author) => ({ - name: author.name, - avatar: author.avatar?.src, - })), -}); +if (!post.image) { + defineOgImage("BlogPost", { + title, + description, + date: post.date, + authors: post.authors.map((author) => ({ + name: author.name, + avatar: author.avatar?.src, + })), + }); +} function formatSocialIntentQueryText(handle: string | undefined): string { const credit = handle ? ` by @${handle}` : ""; From f8e4b402074652692b2eb3dd9365731550b37ecd Mon Sep 17 00:00:00 2001 From: RedStar Date: Sat, 18 Jul 2026 23:42:31 +0200 Subject: [PATCH 10/13] feat(changelog): add contributor mention and contributors components --- .../changelog/ContributorMention.vue | 69 +++++++++++++ app/components/changelog/Contributors.vue | 29 ++++++ app/pages/(marketing)/changelog/index.vue | 69 ++++++++++--- app/utils/parse-release-contributors.ts | 75 ++++++++++++++ test/nuxt/a11y.spec.ts | 39 ++++++++ .../changelog/ContributorMention.spec.ts | 84 ++++++++++++++++ .../utils/parse-release-contributors.test.ts | 97 +++++++++++++++++++ 7 files changed, 451 insertions(+), 11 deletions(-) create mode 100644 app/components/changelog/ContributorMention.vue create mode 100644 app/components/changelog/Contributors.vue create mode 100644 app/utils/parse-release-contributors.ts create mode 100644 test/nuxt/components/changelog/ContributorMention.spec.ts create mode 100644 test/unit/app/utils/parse-release-contributors.test.ts diff --git a/app/components/changelog/ContributorMention.vue b/app/components/changelog/ContributorMention.vue new file mode 100644 index 000000000..d278a8ff4 --- /dev/null +++ b/app/components/changelog/ContributorMention.vue @@ -0,0 +1,69 @@ + + + diff --git a/app/components/changelog/Contributors.vue b/app/components/changelog/Contributors.vue new file mode 100644 index 000000000..448aeca78 --- /dev/null +++ b/app/components/changelog/Contributors.vue @@ -0,0 +1,29 @@ + + + diff --git a/app/pages/(marketing)/changelog/index.vue b/app/pages/(marketing)/changelog/index.vue index 650c1cc8c..a1b9613af 100644 --- a/app/pages/(marketing)/changelog/index.vue +++ b/app/pages/(marketing)/changelog/index.vue @@ -38,6 +38,11 @@ :cache-key="version.tag" :parser-options="headingIdParserOptions(version.tag)" /> + @@ -48,6 +53,12 @@ From 07bb1b7df162df291c2936fe5321014d0e7f58b0 Mon Sep 17 00:00:00 2001 From: RedStar071 Date: Sat, 18 Jul 2026 21:58:48 +0000 Subject: [PATCH 12/13] fix(ui): keep unique contributor heading id alongside region label Co-authored-by: Codesmith --- app/components/changelog/Contributors.vue | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/components/changelog/Contributors.vue b/app/components/changelog/Contributors.vue index a897afeb3..a53386c91 100644 --- a/app/components/changelog/Contributors.vue +++ b/app/components/changelog/Contributors.vue @@ -1,6 +1,6 @@