From 9d46d91f1d6252e1461fdbcdb6243ec9d5a46afc Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Wed, 7 Oct 2026 12:16:13 +1100 Subject: [PATCH 1/2] fix(skill-card): report an avatar that failed before hydration A row that hydrates late misses its avatar's error event, so the board kept a gone owner's broken image. The mount now reads the image. --- .../skill-card/_SkillCardIdentity.vue | 12 ++++++ .../unit/skill-card-avatar-error.nuxt.test.ts | 42 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 test/unit/skill-card-avatar-error.nuxt.test.ts diff --git a/app/components/skill-card/_SkillCardIdentity.vue b/app/components/skill-card/_SkillCardIdentity.vue index 2d4aa287..8023351f 100644 --- a/app/components/skill-card/_SkillCardIdentity.vue +++ b/app/components/skill-card/_SkillCardIdentity.vue @@ -23,6 +23,17 @@ const emit = defineEmits<{ avatarError: [] }>() +const avatar = useTemplateRef('avatar') + +// An avatar can fail before this card hydrates, such as in a row that +// hydrates only once it scrolls into view. Its error event then fired with no +// listener, so the mount reads the image instead. +onMounted(() => { + const image = avatar.value + if (image?.complete && image.naturalWidth === 0) + emit('avatarError') +}) + /** The author names the owner, so the byline needs only the Repository after it. */ const where = computed(() => view.author ? view.repo : view.source) @@ -31,6 +42,7 @@ const where = computed(() => view.author ? view.repo : view.source)
true }) + Object.defineProperty(prototype, 'naturalWidth', { configurable: true, get: () => width }) +} + +afterEach(() => { + for (const [key, descriptor] of Object.entries(original)) { + if (descriptor) + Object.defineProperty(prototype, key, descriptor) + else + delete (prototype as unknown as Record)[key] + } +}) + +describe('skill card avatar', () => { + it('reports an avatar that failed before the card hydrated', async () => { + settleImages(0) + const wrapper = await mountSuspended(SkillCard, { props: { skill, layout: 'row' } }) + expect(wrapper.emitted('avatarError')).toEqual([['gone-owner']]) + wrapper.unmount() + }) + + it('stays quiet for an avatar that loaded', async () => { + settleImages(36) + const wrapper = await mountSuspended(SkillCard, { props: { skill, layout: 'row' } }) + expect(wrapper.emitted('avatarError')).toBeUndefined() + wrapper.unmount() + }) +}) From 7dce249462b896976d0e4e3ea52f6efe46450406 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Wed, 7 Oct 2026 12:16:14 +1100 Subject: [PATCH 2/2] perf(trending): hydrate board rows past the head on view The board hydrated all thirty rows, every post included, before it responded. Rows past the first five now render on the server and hydrate once they scroll into view. --- .../marketing/app/components/BoardRankedList.vue | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/layers/marketing/app/components/BoardRankedList.vue b/layers/marketing/app/components/BoardRankedList.vue index 9d53a503..ceffa270 100644 --- a/layers/marketing/app/components/BoardRankedList.vue +++ b/layers/marketing/app/components/BoardRankedList.vue @@ -55,11 +55,25 @@ const chunks = computed(() => [
  1. + +