-
Notifications
You must be signed in to change notification settings - Fork 0
feat: improve media lazy loading and seo metadata #65
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
|
Caution Review failedThe pull request is closed. 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 new lazy-link Vue directive for prefetching routes and registers it app-wide. Broadly enhances SEO: expands head metadata, introduces keywords, imageAlt, locale, hreflang alternates, and Person JSON-LD. Updates multiple pages/partials to use v-lazy-link and image loading attributes; enriches page-level useSeo configs. Adds comprehensive directive tests. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant A as <rect fill="#eef7ff" stroke="#aac8f0">Anchor (v-lazy-link)</rect>
participant IO as <rect fill="#eef7ff" stroke="#aac8f0">IntersectionObserver</rect>
participant B as Browser Idle/Timers
participant R as Router
participant C as Route Components
User->>A: Render anchor (mounted)
A->>IO: Observe element (if supported)
A->>B: Schedule idle prefetch fallback
Note over A,IO: Also attaches pointerenter/focus/touchstart
IO-->>A: Element visible
A->>R: resolve(href)
R-->>A: Matched route records
A->>C: Trigger dynamic imports (prefetch)
C-->>A: Prefetched (cached)
User->>A: Hover/Focus (optional)
A->>B: One-time idle prefetch (if not yet done)
User->>A: Navigate click
A->>R: Normal navigation (unchanged)
A-->>A: Unmounted -> cleanup observers/listeners/timers
sequenceDiagram
autonumber
participant P as Page (useSeo)
participant SEO as <rect fill="#f6f4ff" stroke="#c9c0f5">Seo.apply</rect>
participant H as Document Head
participant L as <rect fill="#f6f4ff" stroke="#c9c0f5">hreflang builder</rect>
P->>SEO: options { title, description, image, imageAlt, keywords, locale, siteLanguage, jsonLd[] }
SEO->>H: Set meta tags (title/desc/og/twitter)
SEO->>H: Set link rel=canonical
SEO->>H: Set twitter:creator/site if provided
SEO->>H: Insert JSON-LD (Person + Page)
SEO->>H: Set html lang/siteLanguage
SEO->>L: Compute alternates
L-->>H: link rel="alternate" hreflang=*
Estimated code review effort🎯 4 (Complex) | ⏱️ ~70 minutes Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (14)
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 |
Summary of ChangesHello @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 focuses on optimizing the website's performance and search engine discoverability. It introduces a smart prefetching mechanism for navigation links and defers image loading, while simultaneously enriching the site's metadata with more detailed keywords and structured data. These changes collectively aim to provide a smoother, faster user experience and improve the site's ranking and presentation in search engine results. Highlights
Using Gemini Code AssistThe 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
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 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
|
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 significant improvements to lazy loading and SEO metadata. A new v-lazy-link directive is added to prefetch route components, and media assets are now lazy-loaded to improve performance. The SEO metadata has been greatly expanded with more keywords, structured data, and default tags.
My review focuses on a few key areas to ensure these performance and SEO improvements are implemented optimally:
- I've identified a regression in the SEO helper functions that could lead to stale meta tags.
- There are a couple of instances where new lazy-loading attributes might negatively impact the Largest Contentful Paint (LCP) for critical images.
Overall, this is a great set of enhancements. Addressing these few points will make it even better.
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: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/partials/SideNavPartial.vue (1)
97-105: Bug:Array.of(constraint).includes(fullPath)is incorrect
Array.of(constraint)wraps the array, soincludes(fullPath)always fails. Since you can rely onisExactActivefor Home, remove this helper to avoid future misuse.-function applyClassIf(constraint: string[]): boolean { - if (isHome.value) { - return true; - } - - const fullPath = currentRoute.value.fullPath; - - return Array.of(constraint).includes(fullPath); -}src/pages/PostPage.vue (2)
43-57: URL-encode share intent parameters
post.titleand the URL should be encoded to avoid malformed links for spaces/special chars.-const xURLFor = (post: PostResponse) => { - return `https://x.com/intent/tweet?url=${fullURLFor(post)}&text=${post.title}`; -}; +const xURLFor = (post: PostResponse) => { + const url = encodeURIComponent(fullURLFor(post)); + const text = encodeURIComponent(post.title); + return `https://x.com/intent/tweet?url=${url}&text=${text}`; +};
75-87: Use a<button>for the in-page share action (not an anchor)This is an action, not navigation. Also avoids any interference from
v-lazy-link.- <a - v-lazy-link - class="flex justify-center items-center text-slate-400 dark:text-slate-500 hover:text-fuchsia-500 dark:hover:text-teal-600 transition duration-150 ease-in-out" - href="#" - aria-label="Share" - @click.prevent="sharePost(post)" - > + <button + type="button" + class="flex justify-center items-center text-slate-400 dark:text-slate-500 hover:text-fuchsia-500 dark:hover:text-teal-600 transition duration-150 ease-in-out" + aria-label="Share" + @click="sharePost(post)" + > ... - </a> + </button>
🧹 Nitpick comments (16)
src/partials/AvatarPartial.vue (1)
2-9: Add intrinsic width/height to prevent CLS; confirm this avatar isn’t LCP-critical.
- The Tailwind size classes won’t reserve intrinsic space. Add width/height attributes to the img to avoid layout shifts.
- If this avatar can be above the fold, consider not using loading="lazy" (and fetchpriority="low") to avoid delaying LCP.
Example template change:
<img :class="className" :src="props.avatar" :alt="props.alt" + :width="avatarWidth" + :height="avatarHeight" loading="lazy" decoding="async" fetchpriority="low" />Add in <script setup>:
// map to pixels (defaults shown) const avatarWidth = 80; const avatarHeight = 80; // or promote numeric props for intrinsic size if variable per use-siteindex.html (4)
8-8: Meta keywords have negligible SEO value today.Safe to keep, but don’t rely on them for rankings; consider removing to reduce noise.
21-24: Use absolute URLs for Open Graph images and add site_name.Relative OG images may be ignored by crawlers; include og:site_name.
Apply:
-<meta property="og:url" content="%VITE_SITE_URL%/" /> -<meta property="og:image" content="/favicon.ico" /> +<meta property="og:url" content="%VITE_SITE_URL%/" /> +<meta property="og:image" content="%VITE_SITE_URL%/favicon.ico" /> +<meta property="og:site_name" content="Gustavo Ocanto" />
26-31: Use absolute Twitter image URL and include alt.Twitter summary cards expect an absolute image and benefit from image alt.
Apply:
-<meta name="twitter:image" content="/favicon.ico" /> +<meta name="twitter:image" content="%VITE_SITE_URL%/favicon.ico" /> +<meta name="twitter:image:alt" content="Gustavo Ocanto — Personal site" />
33-57: Enrich JSON-LD: add WebSite SearchAction and Person image.Improves SERP features and entity understanding.
Example within the existing array:
{ "@context": "https://schema.org", "@type": "WebSite", "url": "%VITE_SITE_URL%/", "name": "Gustavo Ocanto", "description": "Personal Website of Gustavo Ocanto, Engineering Leader, AI Architect, and Software Engineer." + ,"potentialAction": { + "@type": "SearchAction", + "target": "%VITE_SITE_URL%/search?q={search_term_string}", + "query-input": "required name=search_term_string" + } }, { "@context": "https://schema.org", "@type": "Person", "name": "Gustavo Ocanto", "url": "%VITE_SITE_URL%/", "jobTitle": "Engineering Leader, AI Architect, Software Engineer", "description": "Engineering leader and AI architect sharing expertise in software, leadership, and product building.", + "image": "%VITE_SITE_URL%/og-image.jpg", "sameAs": [ "https://x.com/gocanto", "https://www.linkedin.com/in/gocanto/", "https://github.com/gocanto" ] }src/partials/FeaturedProjectsPartial.vue (1)
7-14: Confirm directive behavior for external links.Ensure no prefetch for off-site URLs.
If the directive can’t detect externals reliably, restrict v-lazy-link usage to internal navigation only in this loop.
src/partials/ArticlesListPartial.vue (1)
8-15: Don’t use v-lazy-link on non-navigational controls; prefer a button.This anchor doesn’t navigate (href="#", click.prevent). Using a button improves semantics/a11y and avoids unnecessary directive work.
Apply:
-<a - v-lazy-link - href="#" +<button + type="button" :class=" filters.category === category.slug ? 'text-slate-800 border-fuchsia-500 dark:text-slate-200 dark:border-teal-500' : 'text-slate-500 border-transparent hover:border-slate-300 dark:text-slate-300 dark:hover:border-slate-700' " class="block py-3 font-medium border-b-2" @click.prevent="selectCategory(category.slug)" - >{{ category.name }}</a> + >{{ category.name }}</button>src/partials/ArticleItemPartial.vue (1)
31-36: Secondary arrow link is unfocusable; add aria-hidden to avoid duplicate announcementSince this duplicate link is
tabindex="-1", addaria-hidden="true"to prevent screen readers from announcing it twice.- tabindex="-1" + tabindex="-1" + aria-hidden="true"src/partials/TalksPartial.vue (1)
16-23: Tailwind arbitrary size values missing units
max-w-[336]/max-h-[189]won’t compile; add units.- class="absolute inset-0 w-full h-full object-cover opacity-40 max-w-[336] max-h-[189]" + class="absolute inset-0 w-full h-full object-cover opacity-40 max-w-[336px] max-h-[189px]"src/pages/AboutPage.vue (1)
63-73: Don’t applyv-lazy-linktomailto:linksPrefetching/parsing isn’t applicable to non-HTTP schemes; remove the directive here.
- v-lazy-link class="blog-link"src/pages/SubscribePage.vue (2)
123-128: Remove v-lazy-link on non-navigational anchors.href="#0" is ignored by the directive (normalized to null), so the directive adds no value here. Either remove the directive or convert these to buttons.
Apply this diff:
- <a - v-lazy-link + <a class="rounded-lg border border-slate-200 dark:border-slate-800 dark:bg-linear-to-t dark:from-slate-800 dark:to-slate-800/30 odd:-rotate-1 even:rotate-1 hover:rotate-0 transition-transform duration-700 hover:duration-100 ease-in-out p-5" href="#0" > ... - <a - v-lazy-link + <a class="rounded-lg border border-slate-200 dark:border-slate-800 dark:bg-linear-to-t dark:from-slate-800 dark:to-slate-800/30 odd:-rotate-1 even:rotate-1 hover:rotate-0 transition-transform duration-700 hover:duration-100 ease-in-out p-5" href="#0" >Also applies to: 149-154
130-139: Align testimonial images with public asset serving.Same note as above: prefer /images/... (public) or imported assets to ensure reliable bundling and caching.
Also applies to: 156-165
src/support/lazy-loading.ts (3)
59-60: Normalize without hash to avoid duplicate prefetch/cache entries.Hashes don’t change route components. Dropping them prevents redundant prefetches for the same path with different fragments.
Apply this diff:
- return `${parsed.pathname}${parsed.search}${parsed.hash}`; + return `${parsed.pathname}${parsed.search}`;
75-94: Debounce prefetch and avoid duplicate dynamic imports.
- Add to the prefetched set before resolving to prevent concurrent duplicate scheduling.
- Catch import rejections to avoid unhandled promise warnings.
Apply this diff:
function prefetchRoute(href: string): void { if (!isBrowser) { return; } - if (prefetchedRoutes.has(href)) { + if (prefetchedRoutes.has(href)) { return; } try { + // Mark as prefetched early to avoid duplicate concurrent work + prefetchedRoutes.add(href); + const resolved = router.resolve(href); resolved.matched.forEach((record) => { const components = record.components ?? (record.component ? { default: record.component } : undefined); if (!components) { return; } Object.values(components).forEach((component) => { if (typeof component === 'function') { // Trigger the dynamic import for the route component - void component(); + void component().catch((err: unknown) => { + // avoid unhandled promise rejections; keep noise minimal + console.warn('Lazy link prefetch failed for', href, err); + }); } }); }); - - prefetchedRoutes.add(href); + // already added above } catch (error) { console.error('Unable to prefetch route component for link', href, error); } }
112-132: Guard against repeated idle scheduling.Avoid stacking multiple idle callbacks when several triggers fire before the first callback runs.
Apply this diff:
const schedulePrefetch = (): void => { - if (!isBrowser || prefetchedRoutes.has(href)) { + if (!isBrowser || prefetchedRoutes.has(href)) { return; } const runPrefetch = () => { idleHandle = null; prefetchRoute(href); element.dataset.lazyLink = 'prefetched'; }; - if (idleCallback) { + if (idleCallback) { + if (idleHandle !== null) return; idleHandle = idleCallback(() => { runPrefetch(); }, { timeout: 1500 }); } else { + if (idleHandle !== null) return; idleHandle = window.setTimeout(() => { runPrefetch(); }, 120); } };src/support/seo.ts (1)
309-336: Avoid duplicating keyword normalization logic.normalizeKeywords and buildKeywords overlap. Consider reusing one to reduce maintenance.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (19)
index.html(1 hunks)src/main.ts(1 hunks)src/pages/AboutPage.vue(4 hunks)src/pages/HomePage.vue(1 hunks)src/pages/PostPage.vue(5 hunks)src/pages/ProjectsPage.vue(1 hunks)src/pages/ResumePage.vue(2 hunks)src/pages/SubscribePage.vue(4 hunks)src/partials/ArticleItemPartial.vue(1 hunks)src/partials/ArticlesListPartial.vue(1 hunks)src/partials/AvatarPartial.vue(1 hunks)src/partials/FeaturedProjectsPartial.vue(1 hunks)src/partials/ProjectCardPartial.vue(1 hunks)src/partials/RecommendationPartial.vue(1 hunks)src/partials/SideNavPartial.vue(5 hunks)src/partials/TalksPartial.vue(1 hunks)src/partials/WidgetSocialPartial.vue(1 hunks)src/support/lazy-loading.ts(1 hunks)src/support/seo.ts(4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/main.ts (1)
src/support/lazy-loading.ts (1)
lazyLinkDirective(204-242)
🔇 Additional comments (24)
src/main.ts (1)
15-16: Directive registration looks good.Global registration before mount is correct and matches directive’s browser guard.
src/partials/RecommendationPartial.vue (1)
11-20: LGTM: image lazy-loading with intrinsic size.Attributes are appropriate and accessibility preserved via meaningful alt.
src/partials/ProjectCardPartial.vue (2)
2-8: Check external-link handling in v-lazy-link.Same consideration as other external anchors; avoid unnecessary prefetch.
See the script in WidgetSocialPartial comment to verify guards apply here too.
13-21: LGTM: icon image lazy-loading.Appropriate attributes; alt derived from item.title.
index.html (1)
12-14: Ensure VITE_SITE_URL is defined in all deploy environments
NoVITE_SITE_URLentry found in any.env*—add it to your local.envfiles and CI/deploy settings so the HTML canonical and hreflang links interpolate correctly.src/partials/ArticleItemPartial.vue (1)
18-25: Using a directive on<router-link>: confirm directive handles component roots correctlyVue applies directives on a component’s root element. Ensure
v-lazy-linkcorrectly binds to<router-link>’s rendered<a>and doesn’t interfere with its click/keyboard handling.src/partials/SideNavPartial.vue (4)
6-8: LGTM on applyingv-lazy-linkto the avatar link
36-46: LGTM on lazy-link usage with custom router-link slots
51-59: LGTM
64-72: LGTMsrc/pages/PostPage.vue (2)
18-27: LGTM on back link with lazy prefetch
59-73: LGTM on external share link; ensure directive no-ops for externalsVerify
v-lazy-linksafely no-ops on external links (target="_blank") and doesn’t attempt route prefetch.src/pages/HomePage.vue (2)
56-56: LGTM on expanded SEO imports
62-77: Confirm array support inuseSeoforjsonLdandkeywords
Ensure the hook’s types and rendering logic accept arrays for both props (emitting multiple<script type="application/ld+json">tags and correctly processing keyword lists).src/partials/TalksPartial.vue (1)
7-15: External links: verifyv-lazy-linkno-opsEnsure the directive does not attempt SPA prefetching for external targets.
src/pages/ResumePage.vue (2)
58-58: LGTM on SEO imports
68-83: VerifyjsonLdarray handling inuseSeo
InspectsetJsonLdinsrc/support/seo.ts: ensure it accepts an array of JSON-LD objects (not just a single object), merges or replaces correctly, and deduplicates entries when multiple calls occur.src/pages/ProjectsPage.vue (2)
68-68: LGTM on SEO imports
76-91: SEO JSON-LD: verify CollectionPage type and properties
Confirm that schema.org @type “CollectionPage” is the correct choice for /projects and that your SEO renderer supports the properties name, url and description on it.src/pages/AboutPage.vue (1)
35-43: External tech links: confirm directive ignores non-internal URLsEnsure
v-lazy-linklimits itself to same-origin/router links to avoid unnecessary work on external anchors.src/pages/SubscribePage.vue (2)
201-220: SEO config upgrade looks solid.Good use of imageAlt, keywords via buildKeywords, and array-based JSON‑LD including PERSON_JSON_LD.
63-112: Verify asset path resolution for avatars.Relative paths like ../images/avatar-01.jpg may not resolve in Vite unless they’re in src and handled by the bundler. Prefer public assets via absolute paths (/images/...) or import the assets.
Would you confirm these files exist under public/images? If not, I can generate a quick codemod to switch to absolute paths.
src/support/lazy-loading.ts (1)
204-242: Directive lifecycle and cleanup logic look good.State tracking via WeakMap, idempotent update handling, and full cleanup on unmount are well done.
src/support/seo.ts (1)
7-36: Good SEO defaults and reusable PERSON_JSON_LD.Solid base description/keywords and shared Person schema for reuse across pages.
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 (16)
src/partials/SideNavPartial.vue (3)
5-15: Ensure directive actually binds to the anchor; consider adding an accessible label.Using v-lazy-link directly on generally applies to its root , but this isn’t guaranteed for multi-root components. For consistency with other items and to guarantee the directive attaches to the anchor, prefer the custom slot.
Also add an aria-label for screen readers.
-<router-link v-lazy-link to="/"> - <AvatarPartial - width="w-16" - height="h-16" - loading="lazy" - decoding="async" - fetchpriority="low" - /> -</router-link> +<router-link v-slot="{ href, navigate }" to="/" custom> + <a v-lazy-link :href="href" @click="navigate" aria-label="Home"> + <AvatarPartial + width="w-16" + height="h-16" + loading="lazy" + decoding="async" + fetchpriority="low" + /> + </a> +</router-link>
43-52: Add aria-current to all nav anchors for accessibility.Mirror the Home item a11y improvement for About/Projects/Resume.
-<a v-lazy-link class="h6 blog-side-nav-router-link-a" :class="bindIconClassFor(isExactActive)" :href="href" @click="navigate"> +<a v-lazy-link class="h6 blog-side-nav-router-link-a" :class="bindIconClassFor(isExactActive)" :href="href" @click="navigate" :aria-current="isExactActive ? 'page' : null">Also applies to: 58-66, 71-79
95-101: Usepathinstead offullPathto make home detection robust against query/hash.-return currentRoute.value.fullPath === '/'; +return currentRoute.value.path === '/';src/support/lazy-loading.ts (4)
28-73: Harden href normalization (protocol-relative and data/blob URIs).Add guards for protocol-relative URLs (//example.com) and data/blob URIs to avoid false-positive prefetches.
- if (trimmed === '' || trimmed.startsWith('#') || trimmed.startsWith('mailto:') || trimmed.startsWith('tel:') || trimmed.startsWith('javascript:')) { + if ( + trimmed === '' || + trimmed.startsWith('#') || + trimmed.startsWith('mailto:') || + trimmed.startsWith('tel:') || + trimmed.startsWith('javascript:') || + trimmed.startsWith('//') || // protocol-relative external URL + /^data:|^blob:/.test(trimmed) // non-navigational URIs + ) { return null; }
121-144: Prevent duplicate scheduling when multiple triggers fire before idle runs.If both intersection and a pointer/focus event fire quickly, you can enqueue multiple idle jobs. Track a per-element scheduled flag and skip re-scheduling.
- let idleHandle: number | null = null; + let idleHandle: number | null = null; + let scheduled = false; const schedulePrefetch = (): void => { - if (!isBrowser || prefetchedRoutes.has(href)) { + if (!isBrowser || scheduled || prefetchedRoutes.has(href)) { return; } + scheduled = true; const runPrefetch = () => { idleHandle = null; + scheduled = false; prefetchRoute(href); element.dataset.lazyLink = 'prefetched'; };And in cleanup:
cleanupFns.push(() => { if (idleHandle !== null) { if (cancelIdleCallback) { cancelIdleCallback(idleHandle); } else { window.clearTimeout(idleHandle); } idleHandle = null; } + scheduled = false; });
80-106: Cache by route path (not full href) to avoid duplicate prefetch across query/hash variants.The same route component is used regardless of query/hash; caching by resolved.path reduces redundant work.
Consider:
- Compute const key = resolved.path and use it for the Set.
- Optionally maintain a small in-flight Set to short-circuit concurrent scheduling.
148-169: Consider a shared IntersectionObserver to reduce per-link allocations.Creating one observer per link is OK at small scale; a shared observer with a registry can cut allocations and overhead on content-heavy pages.
src/partials/ProjectCardPartial.vue (2)
3-3: Unnecessary directive on external links.This anchor is target="_blank" to external URLs; the directive immediately ignores it. You can drop v-lazy-link here.
- <a - v-lazy-link + <a
13-13: Alt text clarity (nit).Consider a more descriptive alt like “{{ item.title }} icon” if appropriate.
tests/support/lazy-loading.test.ts (1)
139-176: Great coverage of intersection + idle flow. Add a race test if feasible.A quick follow-up test asserting no double scheduling when both intersection and pointerenter fire before idle runs would lock in the de-dupe change.
src/partials/ArticlesListPartial.vue (1)
9-19: Drop v-lazy-link for non-navigational filters.These anchors have href="#" and are handled via click.prevent; the directive will always ignore them. Remove to reduce template noise.
- <a - v-lazy-link + <a href="#"src/pages/ProjectsPage.vue (1)
68-68: Enrich CollectionPage JSON-LD with ItemList (optional)Consider adding an ItemList (itemListElement of ListItem with position/url/name) to the CollectionPage for stronger SERP hints.
Also applies to: 79-91
src/pages/AboutPage.vue (1)
28-35: Guard v-lazy-link on external/mailto anchorsIf the directive prefetches Vue routes, it should no-op for external/http(s) and mailto links to avoid unnecessary work or errors.
If you prefer to keep logic purely in the directive, ensure it checks el instanceof HTMLAnchorElement and skips when:
- href starts with http(s):// and host !== current host
- href starts with mailto:, tel:, or is just a hash
Example guard (in the directive):
const href = el.getAttribute('href') ?? ''; const isExternal = /^https?:\/\//i.test(href) && new URL(href, location.href).host !== location.host; const isSpecial = /^(mailto:|tel:|#)/i.test(href); if (isExternal || isSpecial) return; // skip prefetchAlso applies to: 56-58
src/pages/SubscribePage.vue (1)
124-139: Avoid v-lazy-link on non-navigational anchorsThese href="#0" anchors don’t navigate; using the directive here adds overhead. Prefer button elements or remove the directive.
-<a v-lazy-link ... href="#0"> +<button type="button" ...> ... -</a> +</button>Also applies to: 150-165
src/support/seo.ts (2)
85-86: Meta name="language" is nonstandard (optional)html[lang] already conveys language; consider dropping the meta to reduce noise.
- this.setMetaByName('language', language);
299-316: Schema.org Article.keywords should be TextPrefer a comma-separated string over an array for JSON-LD validity.
- keywords: tagKeywords, + keywords: tagKeywords.join(', '),
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (24)
.github/workflows/deploy.yml(1 hunks)docker-compose.yml(1 hunks)index.html(2 hunks)src/css/style.css(3 hunks)src/css/support/blog.css(3 hunks)src/css/support/theme.css(1 hunks)src/pages/AboutPage.vue(4 hunks)src/pages/HomePage.vue(1 hunks)src/pages/PostPage.vue(5 hunks)src/pages/ProjectsPage.vue(2 hunks)src/pages/ResumePage.vue(2 hunks)src/pages/SubscribePage.vue(4 hunks)src/partials/ArticleItemPartial.vue(2 hunks)src/partials/ArticlesListPartial.vue(1 hunks)src/partials/AvatarPartial.vue(1 hunks)src/partials/FeaturedProjectsPartial.vue(1 hunks)src/partials/ProjectCardPartial.vue(2 hunks)src/partials/RecommendationPartial.vue(1 hunks)src/partials/SideNavPartial.vue(5 hunks)src/partials/TalksPartial.vue(1 hunks)src/partials/WidgetSocialPartial.vue(1 hunks)src/support/lazy-loading.ts(1 hunks)src/support/seo.ts(9 hunks)tests/support/lazy-loading.test.ts(1 hunks)
✅ Files skipped from review due to trivial changes (4)
- .github/workflows/deploy.yml
- docker-compose.yml
- src/css/support/theme.css
- src/css/support/blog.css
🚧 Files skipped from review as they are similar to previous changes (8)
- src/partials/AvatarPartial.vue
- src/partials/RecommendationPartial.vue
- src/pages/ResumePage.vue
- index.html
- src/pages/PostPage.vue
- src/partials/TalksPartial.vue
- src/partials/WidgetSocialPartial.vue
- src/partials/FeaturedProjectsPartial.vue
🧰 Additional context used
🪛 ast-grep (0.39.5)
tests/support/lazy-loading.test.ts
[warning] 87-87: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: document.body.innerHTML = ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html
(dom-content-modification)
[warning] 88-88: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: document.head.innerHTML = ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html
(dom-content-modification)
🪛 Biome (2.1.2)
src/css/style.css
[error] 25-25: Unknown property is not allowed.
See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.
(lint/correctness/noUnknownProperty)
🔇 Additional comments (14)
src/css/style.css (3)
11-13: Consistent Tailwind import ordering looks goodThe switch to single-quoted imports keeps the Tailwind layers tidy and consistent with the surrounding style sheet.
25-25: Plugin configuration formatting is tidyIndenting the
strategydeclaration keeps the plugin block easy to scan alongside the rest of the file.
76-86: Color token formatting remains consistentThe updated spacing on the fuchsia palette maintains the existing values while matching the file’s indentation style.
src/partials/SideNavPartial.vue (2)
23-37: LGTM; using isExactActive is the right call. Add aria-current for a11y.This addresses the prior suggestion to rely on isExactActive. Add aria-current to indicate the active page to assistive tech.
- <a + <a v-lazy-link class="h6 blog-side-nav-router-link-a" :class="bindIconClassFor(isExactActive)" :href="href" @click="navigate" + :aria-current="isExactActive ? 'page' : null" >
103-105: LGTM; simple and clear class binding helper.tests/support/lazy-loading.test.ts (1)
80-90: innerHTML resets flagged by static analysis are acceptable in tests.These resets are safe here since content is not user-provided. No action needed.
src/partials/ArticleItemPartial.vue (1)
20-25: LGTM: lazy-link on RouterLinkUsage looks correct and non-invasive.
Also applies to: 32-40
src/pages/AboutPage.vue (2)
91-91: LGTM: SEO imports and richer metadataThe additions (imageAlt, keywords via buildKeywords, and PERSON_JSON_LD) are coherent.
Also applies to: 115-127
20-20: Hero image should be eager/high priority for LCPAbove-the-fold image should be loading="eager" and fetchpriority="high"; also add intrinsic width/height to prevent CLS.
Apply:
-<img class="rounded-lg w-full mb-5" :src="aboutPicture" :alt="`Portrait of: ${formattedNickname}`" loading="lazy" decoding="async" fetchpriority="low" /> +<img class="rounded-lg w-full mb-5" :src="aboutPicture" :alt="`Portrait of: ${formattedNickname}`" loading="eager" decoding="async" fetchpriority="high" width="1280" height="720" />Adjust width/height to the image’s actual intrinsic size.
src/pages/HomePage.vue (1)
56-77: LGTM: Expanded SEO configimageAlt, keywords, and multi-entry jsonLd are consistent with seo.ts changes.
src/pages/SubscribePage.vue (2)
69-72: LGTM: Lazy-loading small avatarsAppropriate use of loading/decoding/fetchpriority for non-critical imagery.
Also applies to: 79-82, 89-92, 99-102, 109-112
201-219: LGTM: SEO metadataImports and useSeo configuration align with new API.
src/support/seo.ts (2)
96-103: Good: prevent stale OG/Twitter image tagsConditional set/clear avoids leaking prior page images across SPA navigations.
Please sanity-check a route change from a page with image to one without, to confirm og:image/twitter:image are removed.
Also applies to: 114-120
187-204: LGTM: Single managed JSON-LD scriptScoped by id and fully replaced per page; good SPA hygiene.
Summary
Testing
https://chatgpt.com/codex/tasks/task_e_68d4f6fc91c883339df37e0b0528f12f
Summary by CodeRabbit