Skip to content

Conversation

@gocanto
Copy link
Contributor

@gocanto gocanto commented Sep 16, 2025

Summary

  • add Vue composition helpers that apply SEO metadata reactively and guard DOM access when rendering outside the browser
  • update the static page components to call the new helpers so their metadata stays in sync with Vue state
  • switch the post detail page to the reactive helper so post-specific metadata is written once the API response arrives

Summary by CodeRabbit

  • New Features
    • Automatic, reactive SEO updates across pages for titles, descriptions, images.
    • Enhanced article pages with structured data (JSON‑LD) to improve search visibility and rich results.
    • More consistent social sharing previews via improved meta and link handling.
  • Bug Fixes
    • Improved stability in server-side rendering by safely handling SEO in non-DOM environments, preventing potential runtime errors.
    • Hardened DOM operations for meta, link, and script tags to avoid intermittent failures.

@coderabbitai
Copy link

coderabbitai bot commented Sep 16, 2025

Warning

Rate limit exceeded

@gocanto has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 0 minutes and 29 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between d3f836a and 92e586e.

📒 Files selected for processing (7)
  • 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/support/seo.ts (8 hunks)

Note

Other AI code review bot(s) detected

CodeRabbit 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.

Walkthrough

Refactors page-level SEO initialization from a singleton (seo.apply/applyFromPost) to composable hooks (useSeo, useSeoFromPost). Updates multiple Vue pages to call the new hooks. Enhances src/support/seo.ts with SSR guards, DOM-safe helpers, reactive option resolution, and JSON-LD generation for posts.

Changes

Cohort / File(s) Summary of Changes
Pages using useSeo
src/pages/AboutPage.vue, src/pages/HomePage.vue, src/pages/ProjectsPage.vue, src/pages/ResumePage.vue, src/pages/SubscribePage.vue
Replace import { seo } with import { useSeo }. Change seo.apply({ title, description, image }) to useSeo({ title, description, image }). SITE_NAME imports unchanged where present.
Post page using useSeoFromPost
src/pages/PostPage.vue
Replace import { seo } with import { useSeoFromPost }. Remove seo.applyFromPost(post.value) call; invoke useSeoFromPost(post) during setup. No functional changes to fetch/markdown logic.
SEO core module
src/support/seo.ts
Add SSR guards (hasDocument, hasWindow) and protect DOM helpers. Introduce resolveValue helper. Add exported hooks: useSeo(options) and useSeoFromPost(post). useSeoFromPost builds Article JSON-LD and canonical URL. Existing Seo class/instance retained; operations now SSR-safe.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant V as Vue Component
  participant H as useSeo()
  participant S as Seo (internal)
  participant D as DOM (document/window)

  U->>V: Navigates to page
  activate V
  V->>H: useSeo({ title, description, image })
  activate H
  H->>H: Resolve reactive options (refs/getters)
  H->>S: apply(options)
  alt Browser environment
    S->>D: Set <title>, <meta>, <link rel=canonical>, JSON-LD
  else SSR/No DOM
    S-->>H: No-op (guarded)
  end
  deactivate H
  note over V,H: Hook re-applies on options change and cleans up on unmount
  deactivate V
Loading
sequenceDiagram
  autonumber
  participant V as PostPage.vue
  participant HP as useSeoFromPost(post)
  participant H as useSeo()
  participant S as Seo
  participant D as DOM

  V->>HP: useSeoFromPost(post)
  activate HP
  HP->>HP: Derive SeoOptions + Article JSON-LD from post
  HP->>H: useSeo(derivedOptions)
  H->>S: apply(derivedOptions)
  S->>D: Update title/meta/canonical + JSON-LD (guarded for SSR)
  deactivate HP
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

A bunny tweaks the meta breeze,
From singletons to hooks with ease.
Titles hop, descriptions flow,
Canonicals in gentle tow.
SSR-safe burrows made—
JSON-LD in shaded glade.
Thump! The SEO is good to go. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Integrate SEO utilities with Vue" succinctly and accurately summarizes the primary change in this PR — introducing Vue composition helpers for SEO and updating pages to use them. It is concise, specific to the main implementation change, and free of noisy or vague wording, making it suitable for PR history and quick scanning by teammates.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gocanto gocanto marked this pull request as draft September 16, 2025 04:27
Copy link

@gemini-code-assist gemini-code-assist bot left a 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 significantly refactors the application's SEO management by integrating new Vue Composition API helpers. The primary goal is to enable reactive updates of SEO metadata and ensure robust behavior in server-side rendering (SSR) contexts by guarding direct DOM manipulations. This change streamlines how SEO information is applied across various pages, making it more dynamic and resilient.

Highlights

  • New Vue Composition Helpers: Introduced useSeo and useSeoFromPost for managing SEO metadata in Vue components, leveraging the Composition API.
  • Reactive SEO Metadata Application: SEO metadata is now applied reactively, ensuring it stays in sync with Vue component state changes.
  • Server-Side Rendering (SSR) Safety: Added checks to guard direct DOM access, preventing errors when components are rendered outside a browser environment, improving SSR compatibility.
  • Refactored Page Components: All static pages (AboutPage, HomePage, ProjectsPage, ResumePage, SubscribePage) have been updated to utilize the new useSeo helper for consistent SEO application.
  • Dynamic Post SEO: The PostPage now uses useSeoFromPost to dynamically apply SEO metadata based on fetched post data, removing manual seo.applyFromPost calls.
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 by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

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 pull request 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

  1. 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.

@gocanto gocanto removed the codex label Sep 16, 2025
Copy link

@gemini-code-assist gemini-code-assist bot left a 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 Vue composition helpers for managing SEO metadata reactively, which is a great improvement. The implementation is solid, with good practices like SSR guards and automatic cleanup of watchers. My feedback focuses on improving code style consistency, specifically regarding indentation, and a small refactoring suggestion to enhance readability and maintainability in the new SEO utility file.

Copy link

@coderabbitai coderabbitai bot left a 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

🧹 Nitpick comments (9)
src/support/seo.ts (4)

61-63: Use site name for application-name titles (avoid per-route labels).

application-name and apple-mobile-web-app-title should stay constant (the app/site name), not the per-page title.

Apply this diff:

- 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);

109-147: Prevent stale meta/link tags when content becomes undefined.

Current helpers no-op on falsy content, which can leave outdated tags after reactive updates (e.g., image removed). Prefer removing the tag when content is empty.

Apply this diff:

- private setMetaByName(name: string, content?: string): void {
-   if (!hasDocument) return;
-   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);
- }
+ private setMetaByName(name: string, content?: string): void {
+   if (!hasDocument) return;
+   const selector = `meta[name="${name}"]`;
+   let element = document.head.querySelector<HTMLMetaElement>(selector);
+   if (!content) {
+     if (element) element.remove();
+     return;
+   }
+   if (!element) {
+     element = document.createElement('meta');
+     element.setAttribute('name', name);
+     document.head.appendChild(element);
+   }
+   element.setAttribute('content', content);
+ }

- private setMetaByProperty(property: string, content?: string): void {
-   if (!hasDocument) return;
-   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);
- }
+ private setMetaByProperty(property: string, content?: string): void {
+   if (!hasDocument) return;
+   const selector = `meta[property="${property}"]`;
+   let element = document.head.querySelector<HTMLMetaElement>(selector);
+   if (!content) {
+     if (element) element.remove();
+     return;
+   }
+   if (!element) {
+     element = document.createElement('meta');
+     element.setAttribute('property', property);
+     document.head.appendChild(element);
+   }
+   element.setAttribute('content', content);
+ }

- private setLink(rel: string, href?: string): void {
-   if (!hasDocument) return;
-   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);
- }
+ private setLink(rel: string, href?: string): void {
+   if (!hasDocument) return;
+   const selector = `link[rel="${rel}"]`;
+   let element = document.head.querySelector<HTMLLinkElement>(selector);
+   if (!href) {
+     if (element) element.remove();
+     return;
+   }
+   if (!element) {
+     element = document.createElement('link');
+     element.setAttribute('rel', rel);
+     document.head.appendChild(element);
+   }
+   element.setAttribute('href', href);
+ }

149-165: Consider multiple JSON‑LD blocks support.

Using a single fixed id overwrites other structured data. If you anticipate multiple entities, accept an array or allow keyed scripts (e.g., data-seo-key).


202-209: Run SEO writes after render.

Use flush: 'post' to avoid DOM writes before paint/hydration.

Apply this diff:

- const stop = watchEffect(() => {
+ const stop = watchEffect(() => {
     const resolved = resolveValue(options);
     if (!resolved) return;
     seo.apply(resolved);
- });
+ }, { flush: 'post' });
src/pages/ProjectsPage.vue (1)

76-80: Copy tweak for grammar/clarity.

Possessive missing on site name; minor polish.

Apply this diff:

 useSeo({
   title: 'Projects',
-  description: `Explore some of ${SITE_NAME} open source and client projects built to solve real engineering challenges.`,
+  description: `Explore some of ${SITE_NAME}'s open‑source and client projects built to solve real engineering challenges.`,
   image: ogImage,
 });
src/pages/SubscribePage.vue (1)

169-173: Copy tweak for grammar.

Reads more naturally and avoids gendered pronouns.

Apply this diff:

 useSeo({
   title: 'Subscribe',
-  description: `Subscribe to ${SITE_NAME}'s newsletter to updates of articles and cool things he is working on.`,
+  description: `Subscribe to ${SITE_NAME}'s newsletter for updates on new articles and projects.`,
   image: ogImage,
 });
src/pages/ResumePage.vue (1)

68-72: Optional copy tweak.

Slightly more concise.

Apply this diff:

 useSeo({
   title: 'Resume',
-  description: `Explore the experience, education, and recommendations of ${SITE_NAME}.`,
+  description: `Explore ${SITE_NAME}'s experience, education, and recommendations.`,
   image: ogImage,
 });
src/pages/AboutPage.vue (1)

110-114: Make SEO reactive and include brand in the title.
Leverage MaybeRef/Getters so metadata updates when profile loads; also add site name to title.

-useSeo({
-        title: 'About',
-        description: `${SITE_NAME} is an engineering leader who’s passionate about building reliable and smooth software.`,
-        image: AboutPicture,
-});
+useSeo({
+  title: () => `About — ${SITE_NAME}`,
+  description: () => `${formattedNickname.value} is an engineering leader passionate about building reliable, smooth software.`,
+  image: () => aboutPicture.value,
+});
src/pages/PostPage.vue (1)

139-140: Confirm undefined-safety and consider typing post as nullable.
Assuming the composable watches the ref and guards undefined. If not, add a null check; also prefer a nullable ref type for clarity.

Outside the changed lines, consider:

-const post = ref<PostResponse>();
+const post = ref<PostResponse | null>(null);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b562784 and d3f836a.

📒 Files selected for processing (7)
  • src/pages/AboutPage.vue (2 hunks)
  • src/pages/HomePage.vue (1 hunks)
  • src/pages/PostPage.vue (3 hunks)
  • src/pages/ProjectsPage.vue (2 hunks)
  • src/pages/ResumePage.vue (2 hunks)
  • src/pages/SubscribePage.vue (1 hunks)
  • src/support/seo.ts (5 hunks)
🔇 Additional comments (11)
src/support/seo.ts (2)

40-46: Good SSR guard.

Early-returning when DOM isn’t available prevents crashes during SSR/SSG. Looks solid.


215-243: LGTM on post-derived SEO.

Computed options + hook composition reads well. This will update on post changes and cleanly handles SSR no-ops.

Double-check that cover_image_url can be null; with the helper fixes above, og:image will be removed when absent rather than lingering.

src/pages/ProjectsPage.vue (1)

61-61: Migration to useSeo is correct.

src/pages/SubscribePage.vue (1)

162-162: Migration to useSeo is correct.

src/pages/HomePage.vue (2)

56-56: Migration to useSeo is correct.


62-66: LGTM.

Home metadata looks fine and will resolve absolute image via helper.

src/pages/ResumePage.vue (1)

57-57: Migration to useSeo is correct.

src/pages/AboutPage.vue (1)

84-84: LGTM: migrated to composable SEO API.
Import looks correct and aligns with the new API.

src/pages/PostPage.vue (3)

117-117: LGTM: switch to useSeoFromPost.
Import matches the new public API.


142-144: LGTM: marked config.
GFM + breaks are reasonable defaults.


200-201: Ignore — getPost returns PostResponse (no .data wrapper).

getPost is declared async getPost(slug: string): Promise<PostResponse> (src/stores/api/store.ts:123) and the unit test (tests/stores/api/store.test.ts:83–86) asserts it returns the raw post object; getProfile is the method that returns an { data: ... } wrapper. The suggested change is unnecessary.

Likely an incorrect or invalid review comment.

@gocanto gocanto marked this pull request as ready for review September 16, 2025 04:37
@gocanto gocanto merged commit de2db44 into main Sep 16, 2025
5 checks passed
@gocanto gocanto deleted the codex/modify-seo-implementation-for-vue branch September 16, 2025 04:37
@coderabbitai coderabbitai bot mentioned this pull request Sep 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants