Skip to content

Conversation

@gocanto
Copy link
Contributor

@gocanto gocanto commented Sep 15, 2025

Summary

  • add SEO utility to manage meta tags for browsers, social networks, and JSON-LD structured data
  • seed index.html and pages with SEO defaults and per-page metadata
  • enrich post pages with dynamic Open Graph, Twitter, and Article schema

Summary by CodeRabbit

  • New Features
    • Site-wide SEO enhancements: updated page titles, descriptions, robots directives, theme color, and canonical URLs.
    • Added Open Graph and Twitter Card metadata and JSON-LD for richer link previews and search results.
    • Page-level SEO applied to Home, About, Projects, Resume, Subscribe; posts now auto-generate SEO from content.
  • Content
    • Hero roles updated to include "AI Architect".
  • Bug Fixes
    • Minor copy typo introduced in the bio ("of of" duplicated).

@coderabbitai
Copy link

coderabbitai bot commented Sep 15, 2025

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

Adds 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

Cohort / File(s) Summary of Changes
HTML head metadata expansion
index.html
Replaced title; added meta description, robots, theme-color; canonical link (placeholder %VITE_SITE_URL%/); Open Graph and Twitter tags; JSON‑LD WebSite block; preserved existing dark-mode deferred script.
SEO module introduction
src/support/seo.ts
New SEO helper: adds DEFAULT_SITE_URL, SITE_NAME, SITE_URL, SeoOptions interface, Seo class with apply and applyFromPost, and exports a singleton seo (export const seo = new Seo()). Manages document.title, meta tags, OG/Twitter, canonical link, and JSON‑LD; includes helpers for meta/link manipulation.
Pages: page-level SEO apply
src/pages/HomePage.vue, src/pages/AboutPage.vue, src/pages/ProjectsPage.vue, src/pages/ResumePage.vue, src/pages/SubscribePage.vue
Imported seo (and SITE_NAME/shared og image where used); invoked seo.apply during setup with page-specific title/description/image. No template or data-fetching changes.
Post page: SEO from content
src/pages/PostPage.vue
Imported seo; after fetching post in onMounted, calls seo.applyFromPost(post) to set article-specific meta and JSON‑LD.
Environment example
.env.example
Added VITE_SITE_URL=http://localhost:5173 (new public env variable) placed after ENV_API_LOCAL_DIR.
Server config changes
caddy/WebCaddyfile.internal
Removed global debug block; replaced with a :80 site block; narrowed @relay_get matcher to method GET so GET /relay/* responds 405 while non-GET /relay/* are proxied to /api{path}; other static/reverse_proxy/TLS settings unchanged.
UI copy update
src/partials/HeroPartial.vue
Updated short-role list to include "AI Architect"; inserted "of experience" after "two decades" (introduces duplicated "of"). No logic 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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Optimise production build #21 — Touches src/pages/PostPage.vue's onMounted flow (initializes highlighter before/around post fetch); likely to conflict or require coordination with seo.applyFromPost insertion.

Poem

I nibbled tags and meta leaves,
Sprinkled OG stardust on the eaves.
JSON‑LD tucked in my tiny paw,
Canonicals lined the SEO law.
Ears up high — the pages sing for sure. 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly summarizes the primary change—adding an SEO system with social metadata—which matches the new seo module, index.html meta updates, and per-page seo.apply calls in the changeset; however, the phrase "AI support" is not clearly reflected in the diff and may be ambiguous or misleading. Overall the title is concise, focused, and related to the main changes.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/add-seo-system-to-vue-app

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.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

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.

❤️ Share

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

@gocanto gocanto marked this pull request as draft September 15, 2025 07:23
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 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 flexible applySeo function to dynamically set SEO properties.
  • Default SEO Configuration: The index.html file 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 applySeo utility 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 comprehensive Article schema 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

  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.

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 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.html point 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.

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: 4

🧹 Nitpick comments (14)
index.html (2)

26-35: JSON-LD uses example.com — update to real site and consider extras

Replace https://example.com/ with the real origin. Optionally add sameAs with social profiles and a potentialAction SearchAction if applicable.


37-41: Reduce dark-mode flash

The 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 params

Canonical 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 environments

Avoid 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/document access in SSR/tests.


93-97: Theme color per scheme

Consider emitting two theme-color metas with media to match light/dark. Current single value can cause address-bar contrast issues.


117-119: Absolute URLs in JSON-LD images

setJsonLd injects JSON as-is; ensure options.jsonLd.image is 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 correctly

Title/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 added

Same 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 SEO

Top-level applySeo is fine. Minor nit: the description mirrors the H1; consider a slightly different summary for SERP variety.


110-114: Use a consistent site-wide OG image

Swap AboutPicture for 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 meta

Some 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 schema

Add mainEntityOfPage (the canonical URL) and dateModified if available.

src/pages/ProjectsPage.vue (1)

76-82: SEO integration looks good

As 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 correctly

Title/description/image set early. Same OG image note.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d105876 and 9cb6540.

📒 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 solid

Helpers 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 here

Attempted 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.json engines to 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.

@gocanto gocanto marked this pull request as ready for review September 15, 2025 08:24
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: 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 images

Expose 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 portability

Type-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 engines

Keep if you have downstream consumers; otherwise consider removing to reduce churn.


9-9: Config source for SITE_NAME

If 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9cb6540 and 31d2ec2.

📒 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

@gocanto gocanto marked this pull request as draft September 15, 2025 09:21
@gocanto gocanto marked this pull request as ready for review September 16, 2025 03:38
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

♻️ Duplicate comments (5)
index.html (3)

18-18: Don’t use favicon as social image

Use 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 scrapers

Enable 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.html
src/support/seo.ts (2)

40-49: Guard for SSR/tests: window/document may be undefined

Both 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 dedupe

Early 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_URL

Clarify 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.io
src/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 token

Same 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

📥 Commits

Reviewing files that changed from the base of the PR and between 31d2ec2 and 4ed1a0c.

📒 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 CI

Prettier 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"

@gocanto gocanto merged commit 846947b into main Sep 16, 2025
2 of 3 checks passed
@gocanto gocanto deleted the codex/add-seo-system-to-vue-app branch September 16, 2025 03:55
This was referenced Sep 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants