-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add seo system with social and AI support #51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds a centralized SEO module and applies it across multiple Vue pages; expands index.html head with canonical, Open Graph, Twitter, and JSON‑LD metadata; PostPage now updates SEO after fetching content. Also adds VITE_SITE_URL to .env.example and adjusts caddy/WebCaddyfile.internal and hero copy. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Router
participant Page as PageComponent
participant SEO as "seo (support/seo.ts)"
participant DOM as "Document Head"
participant API
User->>Router: Navigate
Router->>Page: mount / setup()
Note over Page,SEO: Static pages (Home/About/Projects/Resume/Subscribe)
Page->>SEO: seo.apply({title, description, image, url?})
SEO->>DOM: set title, meta, OG/Twitter, canonical, JSON‑LD
alt Post page
Page->>API: fetch post slug
API-->>Page: PostResponse
Page->>SEO: seo.applyFromPost(post)
SEO->>DOM: set article meta + JSON‑LD Article
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Summary of Changes
Hello @gocanto, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!
This pull request introduces a robust and centralized SEO system to enhance the website's discoverability and presentation across search engines and social media platforms. It establishes a new utility for managing meta tags, applies foundational SEO settings to the main index.html, and enables dynamic, context-aware metadata injection for individual pages and posts, including rich structured data.
Highlights
- New SEO Utility: A new utility file,
src/seo.ts, has been added to centralize the management of meta tags for browsers, social networks (Open Graph, Twitter), and JSON-LD structured data. This utility provides a flexibleapplySeofunction to dynamically set SEO properties. - Default SEO Configuration: The
index.htmlfile has been updated to include essential default SEO meta tags, such as description, robots directives, theme color, canonical link, Open Graph tags, Twitter card tags, and a basic JSON-LD script for the website. - Page-Specific SEO Integration: The
applySeoutility has been integrated into various Vue pages (AboutPage.vue,HomePage.vue,ProjectsPage.vue,ResumePage.vue,SubscribePage.vue) to allow for dynamic, page-specific SEO metadata to be set, improving content relevance for search engines and social media. - Dynamic Post SEO with Structured Data: For
PostPage.vue, the SEO is now dynamically generated based on the fetched post's content, including its title, excerpt, cover image, and a comprehensiveArticleschema in JSON-LD, which aids AI and other crawlers in understanding the content.
Using Gemini Code Assist
The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.
Invoking Gemini
You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.
| Feature | Command | Description |
|---|---|---|
| Code Review | /gemini review |
Performs a code review for the current pull request in its current state. |
| Pull Request Summary | /gemini summary |
Provides a summary of the current pull request in its current state. |
| Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments. |
| Help | /gemini help |
Displays a list of available commands. |
Customization
To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.
Limitations & Feedback
Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.
You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.
Footnotes
-
Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a comprehensive SEO system, which is a great addition. The new seo.ts utility provides a clean way to manage meta tags, and it's being used effectively across different pages to provide both static and dynamic SEO data.
I've left a few comments for improvement:
- In
index.html, there are hardcoded placeholder URLs that need to be replaced with the production domain to ensure correct indexing. - The default social sharing images in
index.htmlpoint to the favicon, which is not ideal for rich media previews. - In
src/seo.ts, there's an opportunity to refactor some helper functions to reduce code duplication.
Overall, this is a solid implementation of an important feature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (14)
index.html (2)
26-35: JSON-LD usesexample.com— update to real site and consider extrasReplace
https://example.com/with the real origin. Optionally addsameAswith social profiles and apotentialActionSearchAction if applicable.
37-41: Reduce dark-mode flashThe deferred script runs after paint, causing a theme flash. Inline a tiny, non-deferred snippet at the top of
<head>.- <script defer> + <script> if (localStorage.getItem('dark-mode') === 'true') { document.documentElement.classList.add('dark'); } </script>src/seo.ts (4)
80-99: Normalize canonical: strip fragments and tracking paramsCanonical should exclude
#...and common tracking query params.export function applySeo(options: SeoOptions): void { - const url = options.url ?? window.location.href; + if (typeof window === 'undefined' || typeof document === 'undefined') return; + const rawUrl = options.url ?? window.location.href; + const url = (() => { + try { + const u = new URL(rawUrl); + u.hash = ''; + ['utm_source','utm_medium','utm_campaign','utm_term','utm_content','gclid','fbclid'].forEach(p => u.searchParams.delete(p)); + return u.toString(); + } catch { + return rawUrl; + } + })(); @@ - setLink('canonical', url); + setLink('canonical', url);
81-86: Guard for non-browser/test environmentsAvoid errors when components import this in non-DOM contexts.
- const image = options.image + const image = options.image ? new URL(options.image, window.location.origin).toString() : undefined;This is fine after the early return above; the guard prevents
window/documentaccess in SSR/tests.
93-97: Theme color per schemeConsider emitting two
theme-colormetas withmediato match light/dark. Current single value can cause address-bar contrast issues.
117-119: Absolute URLs in JSON-LD images
setJsonLdinjects JSON as-is; ensureoptions.jsonLd.imageis absolute (some parsers require it). Either normalize here or pass an absolute from call sites.src/pages/SubscribePage.vue (1)
166-173: SEO hook applied correctlyTitle/description/image wired via
applySeo. Consider using a dedicated OG image rather than the About photo for brand consistency.src/pages/ResumePage.vue (1)
59-73: Good: page-level SEO metadata addedSame note as others: prefer a dedicated OG image (1200×630) rather than the About image.
src/pages/AboutPage.vue (2)
105-114: LGTM + page-level SEOTop-level
applySeois fine. Minor nit: the description mirrors the H1; consider a slightly different summary for SERP variety.
110-114: Use a consistent site-wide OG imageSwap
AboutPicturefor a site-wide OG default unless this page intentionally wants a portrait.src/pages/PostPage.vue (2)
200-219: Make JSON-LD image absolute and reuse it for metaSome parsers require absolute URLs in JSON-LD. Normalize once and reuse.
- if (post.value) { - applySeo({ - title: post.value.title, - description: post.value.excerpt, - image: post.value.cover_image_url, + if (post.value) { + const imageAbs = new URL(post.value.cover_image_url, window.location.origin).toString(); + applySeo({ + title: post.value.title, + description: post.value.excerpt, + image: imageAbs, type: 'article', url: fullURLFor(post.value), jsonLd: { '@context': 'https://schema.org', '@type': 'Article', headline: post.value.title, description: post.value.excerpt, - image: post.value.cover_image_url, + image: imageAbs, datePublished: post.value.published_at, author: { '@type': 'Person', name: post.value.author.display_name, }, }, }); }
200-219: Optional: enrich Article schemaAdd
mainEntityOfPage(the canonical URL) anddateModifiedif available.src/pages/ProjectsPage.vue (1)
76-82: SEO integration looks goodAs with other pages, consider a dedicated OG default image to avoid reusing the About picture.
src/pages/HomePage.vue (1)
62-68: Home SEO hooked up correctlyTitle/description/image set early. Same OG image note.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
index.html(1 hunks)src/pages/AboutPage.vue(2 hunks)src/pages/HomePage.vue(1 hunks)src/pages/PostPage.vue(2 hunks)src/pages/ProjectsPage.vue(1 hunks)src/pages/ResumePage.vue(1 hunks)src/pages/SubscribePage.vue(1 hunks)src/seo.ts(1 hunks)
🧰 Additional context used
🪛 GitHub Actions: Test Format Workflow
src/pages/SubscribePage.vue
[error] 1-1: git diff --exit-code failed (exit code 1). Uncommitted changes detected in src/pages/SubscribePage.vue.
src/pages/ResumePage.vue
[error] 1-1: git diff --exit-code failed (exit code 1). Uncommitted changes detected in src/pages/ResumePage.vue.
src/pages/HomePage.vue
[error] 1-1: git diff --exit-code failed (exit code 1). Uncommitted changes detected in src/pages/HomePage.vue.
src/pages/ProjectsPage.vue
[error] 1-1: git diff --exit-code failed (exit code 1). Uncommitted changes detected in src/pages/ProjectsPage.vue.
src/pages/PostPage.vue
[error] 1-1: git diff --exit-code failed (exit code 1). Uncommitted changes detected in src/pages/PostPage.vue.
index.html
[error] 1-1: git diff --exit-code failed (exit code 1). Uncommitted changes detected in index.html.
src/pages/AboutPage.vue
[error] 1-1: git diff --exit-code failed (exit code 1). Uncommitted changes detected in src/pages/AboutPage.vue.
src/seo.ts
[error] 1-1: git diff --exit-code failed (exit code 1). Uncommitted changes detected in src/seo.ts.
🔇 Additional comments (3)
src/seo.ts (1)
1-75: Overall utility looks solidHelpers avoid duplicate tags by updating-or-creating, and defaults are sensible.
index.html (1)
1-48: CI: "git diff --exit-code" failing due to formatting — formatter couldn't be executed hereAttempted
npm ci+npx prettier --write .but npm errored (EBADENGINE): package requires node ^22.12.0 while the environment has node v24.3.0. Run the repo formatter locally with Node 22 (e.g.,nvm use 22.12.0) or update package.jsonenginesto support Node 24, then commit the formatted changes.src/pages/PostPage.vue (1)
195-224: Confirmed — getPost returns PostResponse; no action required.src/stores/api/store.ts returns await this.client.get(url), and tests (tests/stores/api/store.test.ts) mock client.get to resolve to the raw post object (e.g. { slug: 'a' }) and assert getPost returns that shape.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (7)
src/support/seo.ts (7)
31-33: Normalize canonical/og:url and harden URL parsing
- Clean hash and tracking params in canonical/og:url.
- Wrap image URL resolution to avoid throws on bad inputs.
- const url = options.url ?? window.location.href; - const image = options.image ? new URL(options.image, window.location.origin).toString() : undefined; + const rawUrl = options.url ?? window.location.href; + let pageUrl = rawUrl; + try { + const u = new URL(rawUrl, window.location.origin); + u.hash = ''; + for (const p of ['utm_source','utm_medium','utm_campaign','utm_term','utm_content','gclid','fbclid']) { + u.searchParams.delete(p); + } + pageUrl = u.toString(); + } catch { /* keep rawUrl */ } + let image: string | undefined; + if (options.image) { + try { image = new URL(options.image, window.location.origin).toString(); } catch { image = undefined; } + } @@ - this.setLink('canonical', url); + this.setLink('canonical', pageUrl); @@ - this.setMetaByProperty('og:url', url); + this.setMetaByProperty('og:url', pageUrl);Also applies to: 47-47, 53-53
70-90: Make post URLs safe and JSON‑LD absolute/self‑contained
- Encode slug to avoid invalid URLs.
- Use absolute URL for JSON‑LD image and include url/mainEntityOfPage.
- this.apply({ - title: post.title, - description: post.excerpt, - image: post.cover_image_url, - type: 'article', - url: new URL(`/posts/${post.slug}`, window.location.origin).toString(), - jsonLd: { + const origin = window.location.origin; + const postUrl = new URL(`/posts/${encodeURIComponent(post.slug)}`, origin).toString(); + let jsonLdImage = post.cover_image_url; + if (jsonLdImage) { + try { jsonLdImage = new URL(jsonLdImage, origin).toString(); } catch { /* leave as is */ } + } + this.apply({ + title: post.title, + description: post.excerpt, + image: post.cover_image_url, + type: 'article', + url: postUrl, + jsonLd: { '@context': 'https://schema.org', '@type': 'Article', headline: post.title, description: post.excerpt, - image: post.cover_image_url, + image: jsonLdImage, + url: postUrl, + mainEntityOfPage: postUrl, datePublished: post.published_at, author: { '@type': 'Person', name: SITE_NAME, }, }, });
44-45: Use site name for app titles (more conventional for PWA)application-name and apple-mobile-web-app-title should generally be the app/site name, not the page title.
- this.setMetaByName('application-name', title); - this.setMetaByName('apple-mobile-web-app-title', title); + this.setMetaByName('application-name', SITE_NAME); + this.setMetaByName('apple-mobile-web-app-title', SITE_NAME);
15-15: Add alt text for social imagesExpose imageAlt to improve accessibility/previews and set og:image:alt and twitter:image:alt.
export interface SeoOptions { title?: string; description?: string; keywords?: string; image?: string; + imageAlt?: string; url?: string;this.setMetaByProperty('og:image', image); + this.setMetaByProperty('og:image:alt', options.imageAlt); this.setMetaByProperty('og:site_name', options.siteName ?? SITE_NAME);this.setMetaByName('twitter:description', description); this.setMetaByName('twitter:image', image); + this.setMetaByName('twitter:image:alt', options.imageAlt);Also applies to: 54-56, 64-65
7-7: Minor: drop “.ts” from type-only import for portabilityType-only imports are erased; avoiding the extension helps TS/Vite/ESM setups that disallow non-extension-mapped paths.
-import type { PostResponse } from '@api/response/posts-response.ts'; +import type { PostResponse } from '@api/response/posts-response';
40-40: Meta keywords are ignored by major search enginesKeep if you have downstream consumers; otherwise consider removing to reduce churn.
9-9: Config source for SITE_NAMEIf SITE_NAME varies across envs/sites, consider sourcing from a config/env module instead of a hardcoded constant.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
index.html(1 hunks)src/pages/AboutPage.vue(2 hunks)src/pages/HomePage.vue(1 hunks)src/pages/PostPage.vue(2 hunks)src/pages/ProjectsPage.vue(1 hunks)src/pages/ResumePage.vue(1 hunks)src/pages/SubscribePage.vue(1 hunks)src/support/seo.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
- src/pages/AboutPage.vue
- src/pages/ProjectsPage.vue
- src/pages/HomePage.vue
- src/pages/SubscribePage.vue
- src/pages/PostPage.vue
- index.html
- src/pages/ResumePage.vue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (5)
index.html (3)
18-18: Don’t use favicon as social imageUse a 1200×630 (1.91:1) asset; favicons render poorly in previews.
Apply:
-<meta property="og:image" content="/favicon.ico" /> +<meta property="og:image" content="/og-default.jpg" /> @@ -<meta name="twitter:image" content="/favicon.ico" /> +<meta name="twitter:image" content="/og-default.jpg" />Place og-default.jpg under public/.
Also applies to: 24-24
13-25: Static OG/Twitter tags won’t match routes for non-JS scrapersEnable SSR/SSG or prerender top pages/posts so correct tags are in initial HTML.
Options:
- Add a prerender step (vite-plugin-ssg/prerender) for key routes.
- Or migrate blog routes to SSR/SSG (e.g., Nuxt/vite-ssg) so seo.apply runs server-side.
10-10: Remove placeholder canonical or inject real origin at build time%VITE_SITE_URL% is not replaced by Vite by default; this will ship literally and harm SEO.
Apply one:
-<link rel="canonical" href="%VITE_SITE_URL%/" /> +<!-- Canonical will be set per-route by the SEO helper (SSR/prerender recommended) --> +<!-- <link rel="canonical" href="%VITE_SITE_URL%/" /> -->Or wire a transformIndexHtml/plugin to replace %VITE_SITE_URL% with the real origin at build time.
#!/bin/bash # Verify whether HTML env injection exists rg -nP -C2 'transformIndexHtml|vite-plugin-html|inject.*env|%VITE_SITE_URL%' -- package.json vite.config.* index.htmlsrc/support/seo.ts (2)
40-49: Guard for SSR/tests: window/document may be undefinedBoth methods will throw outside the browser.
Apply:
export class Seo { apply(options: SeoOptions): void { + if (!this.isBrowser) return; const currentPath = window.location.pathname + window.location.search; @@ applyFromPost(post: PostResponse): void { + if (!this.isBrowser) return; this.apply({ @@ } + + private get isBrowser(): boolean { + return typeof window !== 'undefined' && typeof document !== 'undefined' && !!document.head; + }Also applies to: 81-101
103-116: Prevent SPA‑stale tags: remove when value is empty and dedupeEarly returns leave old meta/link/canonical values after route changes.
Apply:
private setMetaByName(name: string, content?: string): void { - if (!content) return; - let element = document.head.querySelector<HTMLMetaElement>(`meta[name="${name}"]`); - if (!element) { - element = document.createElement('meta'); - element.setAttribute('name', name); - document.head.appendChild(element); - } - element.setAttribute('content', content); + const selector = `meta[name="${name}"]`; + const existing = document.head.querySelector<HTMLMetaElement>(selector); + if (!content) { if (existing) existing.remove(); return; } + const el = existing ?? document.createElement('meta'); + if (!existing) { el.setAttribute('name', name); document.head.appendChild(el); } + el.setAttribute('content', content); } private setMetaByProperty(property: string, content?: string): void { - if (!content) return; - let element = document.head.querySelector<HTMLMetaElement>(`meta[property="${property}"]`); - if (!element) { - element = document.createElement('meta'); - element.setAttribute('property', property); - document.head.appendChild(element); - } - element.setAttribute('content', content); + const selector = `meta[property="${property}"]`; + const existing = document.head.querySelector<HTMLMetaElement>(selector); + if (!content) { if (existing) existing.remove(); return; } + const el = existing ?? document.createElement('meta'); + if (!existing) { el.setAttribute('property', property); document.head.appendChild(el); } + el.setAttribute('content', content); } private setLink(rel: string, href?: string): void { - if (!href) return; - let element = document.head.querySelector<HTMLLinkElement>(`link[rel="${rel}"]`); - if (!element) { - element = document.createElement('link'); - element.setAttribute('rel', rel); - document.head.appendChild(element); - } - element.setAttribute('href', href); + const nodes = Array.from(document.head.querySelectorAll<HTMLLinkElement>(`link[rel="${rel}"]`)); + if (!href) { nodes.forEach(n => n.remove()); return; } + const el = nodes[0] ?? document.createElement('link'); + if (!nodes[0]) { el.setAttribute('rel', rel); document.head.appendChild(el); } + // remove duplicates + nodes.slice(1).forEach(n => n.remove()); + el.setAttribute('href', href); }Also applies to: 118-127, 129-138
🧹 Nitpick comments (3)
.env.example (1)
22-23: Add explicit production guidance for VITE_SITE_URLClarify https, no trailing slash, and that deploy must set this value; avoids bad canonicals/OG URLs in prod.
Apply:
-# --- Public site URL used for canonical links (no trailing slash) -VITE_SITE_URL=http://localhost:5173 +# --- Public site URL used for canonical links (no trailing slash) +# Dev example: +VITE_SITE_URL=http://localhost:5173 +# Production MUST override with your public origin, e.g.: +# VITE_SITE_URL=https://oullin.iosrc/partials/HeroPartial.vue (1)
12-12: Fix minor copy issues (“Leadership” noun; double “of”)Small UX polish.
Apply:
-<p class="text-lg text-slate-400 dark:text-slate-300 mb-5">Writer, Speaker, Developer, AI Architect, Founder, and Leadership.</p> +<p class="text-lg text-slate-400 dark:text-slate-300 mb-5">Writer, Speaker, Developer, AI Architect, Founder, and Leader.</p> @@ -I'm a full-stack Software Engineer leader with over two decades of of experience in building complex web systems and products, specialising in areas like e-commerce, banking, +I'm a full-stack Software Engineering leader with over two decades of experience building complex web systems and products, specialising in areas like e-commerce, banking,Also applies to: 14-15
index.html (1)
31-31: JSON‑LD url uses placeholder tokenSame concern as canonical; ensure real origin or remove until build injection is wired.
Apply:
-"url": "%VITE_SITE_URL%/", +/* "url": "%VITE_SITE_URL%/", set via SSR/prerender */
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
.env.example(1 hunks)caddy/WebCaddyfile.internal(0 hunks)index.html(1 hunks)src/pages/AboutPage.vue(2 hunks)src/pages/HomePage.vue(1 hunks)src/pages/PostPage.vue(2 hunks)src/pages/ProjectsPage.vue(2 hunks)src/pages/ResumePage.vue(2 hunks)src/pages/SubscribePage.vue(1 hunks)src/partials/HeroPartial.vue(1 hunks)src/support/seo.ts(1 hunks)
💤 Files with no reviewable changes (1)
- caddy/WebCaddyfile.internal
🚧 Files skipped from review as they are similar to previous changes (6)
- src/pages/PostPage.vue
- src/pages/HomePage.vue
- src/pages/SubscribePage.vue
- src/pages/ResumePage.vue
- src/pages/ProjectsPage.vue
- src/pages/AboutPage.vue
🧰 Additional context used
🪛 GitHub Actions: Test Format Workflow
src/support/seo.ts
[error] 1-1: Command failed: git diff --exit-code. Detected uncommitted/formatting changes in src/support/seo.ts (see diff showing formatting adjustments). Please run the formatter and commit the changes.
🔇 Additional comments (1)
src/support/seo.ts (1)
1-184: Fix formatting to satisfy CIPrettier formatted src/support/seo.ts in the verification run, but the commit didn’t complete and ESLint failed due to a missing config dependency (@eslint/js). Locally run the following and re-run CI:
- Install deps: npm ci
- Format: npm run format || npm run fmt || npx prettier --write src/support/seo.ts
- Lint fix (install missing plugin if needed): npm install -D @eslint/js && npx eslint --fix src/support/seo.ts
- Configure git identity and commit: git config user.email "you@example.com" && git config user.name "Your Name" && git add -A && git commit -m "chore: format seo module"
Summary
Summary by CodeRabbit