Add uploaded-video block and animated home page thumbnails - #92
Merged
Conversation
The Next.js 16 / next-sanity 12 upgrade bumped the Sanity toolchain but did not regenerate the extracted schema and query types, leaving them out of sync with the current CLI output. Regenerating brings them back in step so later typegen runs produce no incidental diff.
Presenting 3D animation loops previously required a YouTube embed, whose player chrome and suggested-video overlays distract from the artwork. This adds a video block that streams a file uploaded to Sanity through a native <video> element, giving full control over the presentation. Autoplaying videos play muted and looping only while in view, gated by an IntersectionObserver to avoid loading and decoding offscreen clips. Turning autoplay off shows native controls unmuted, so videos with sound start from the beginning when the viewer presses play. When the viewer prefers reduced motion, autoplay is suppressed in favour of controls. A lazy/eager loading control mirrors the image block for the first video on a page. The aspect ratio is derived from the poster image to prevent layout shift, falling back to 16:9. A separate alt field provides an accessible name, and the block renders nothing until a file is uploaded so the Studio preview stays clean while editing.
The home page can now feature a single moving artwork among the still thumbnails to draw the eye, without pulling the visitor into a YouTube player. An optional thumbnail video on a post plays muted and looping in place of its still image, but only in the home page artwork list: the projection that fetches it is scoped to that query, so the same post keeps its still image on the artworks page and everywhere else. Playback is gated by an IntersectionObserver, and viewers who prefer reduced motion see the still image instead. The post's existing image serves as the poster and fallback, so no extra fields are needed.
The poster is only the still frame shown before playback, so fetching it at near-source width wasted bandwidth. Body posters drop to 1280px and the smaller thumbnail posters to 800px, matching how large each is actually shown.
The thumbnail defaulted to the fallback image on the server and swapped to video after hydration, so every motion user downloaded the full-resolution image only to replace it. Render the video on the server instead and fall back to the image on the client only when reduced motion is set, optimising for the common case. Share the reduced-motion detection and play-in-view behavior between the body video block and the thumbnail via two hooks instead of duplicating the effects in each component.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Adds support for self-hosted Sanity-uploaded videos both as a new Portable Text video body block and as animated post thumbnails on the home page (via a projected thumbnailVideoUrl). The change also regenerates Sanity schema/type artifacts to reflect the new fields.
Changes:
- Extend the Sanity
postschema with athumbnailVideofield and avideoblock type in post bodies. - Add frontend rendering for body videos and for home-page thumbnail videos, including reduced-motion and in-viewport playback hooks.
- Extend GROQ projections/types to include video URLs and related metadata, and update the home page query to request
thumbnailVideoUrl.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/sanity/schemaTypes/postType.ts | Adds thumbnailVideo field and video portable-text block schema. |
| apps/sanity/schema.json | Regenerated schema artifact reflecting new/updated types and references. |
| apps/frontend/src/utils/use-reduced-motion.ts | Adds reduced-motion detection hook used to gate video playback/rendering. |
| apps/frontend/src/utils/use-play-in-view.ts | Adds IntersectionObserver-based “play while in view” hook for videos. |
| apps/frontend/src/utils/sanity-data.ts | Extends GROQ projections for video blocks and home artwork list (thumbnailVideoUrl). |
| apps/frontend/src/types.ts | Introduces VideoFields type for the new video projection shape. |
| apps/frontend/src/components/video.tsx | New body video renderer component. |
| apps/frontend/src/components/posts-list/types.ts | Extends PostItem with thumbnailVideoUrl. |
| apps/frontend/src/components/posts-list/thumbnail-video.tsx | New animated thumbnail renderer (video + reduced-motion image fallback). |
| apps/frontend/src/components/posts-list/post-item.tsx | Renders ThumbnailVideo when thumbnailVideoUrl is available. |
| apps/frontend/src/components/body.tsx | Adds portable-text handler to render the video block. |
| apps/frontend/src/app/page.tsx | Updates home artworks query to use homeArtworkListFields. |
| apps/frontend/sanity.types.ts | Regenerated Sanity query/type artifact reflecting new schema/projections. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+1
to
+18
| import { useEffect, useState } from 'react'; | ||
|
|
||
| export function useReducedMotion() { | ||
| const [reducedMotion, setReducedMotion] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| const query = window.matchMedia('(prefers-reduced-motion: reduce)'); | ||
| setReducedMotion(query.matches); | ||
|
|
||
| const onChange = (event: MediaQueryListEvent) => | ||
| setReducedMotion(event.matches); | ||
| query.addEventListener('change', onChange); | ||
|
|
||
| return () => query.removeEventListener('change', onChange); | ||
| }, []); | ||
|
|
||
| return reducedMotion; | ||
| } |
Comment on lines
+24
to
+40
| video.muted = true; | ||
|
|
||
| const observer = new IntersectionObserver( | ||
| ([entry]) => { | ||
| if (!entry) { | ||
| return; | ||
| } | ||
|
|
||
| if (entry.isIntersecting) { | ||
| video.play().catch(() => {}); | ||
| } else { | ||
| video.pause(); | ||
| } | ||
| }, | ||
| { threshold: 0 }, | ||
| ); | ||
|
|
Comment on lines
+3
to
+11
| import { createImageUrlBuilder as imageUrlBuilder } from '@sanity/image-url'; | ||
| import { type FC, useRef } from 'react'; | ||
| import type { VideoFields } from '../types'; | ||
| import cx from '../utils/cx'; | ||
| import { client } from '../utils/sanity-client'; | ||
| import { usePlayInView } from '../utils/use-play-in-view'; | ||
| import { useReducedMotion } from '../utils/use-reduced-motion'; | ||
|
|
||
| const builder = imageUrlBuilder(client); |
Comment on lines
+3
to
+13
| import { createImageUrlBuilder as imageUrlBuilder } from '@sanity/image-url'; | ||
| import { type FC, useRef } from 'react'; | ||
| import type { ImageFields } from '../../types'; | ||
| import cx from '../../utils/cx'; | ||
| import { client } from '../../utils/sanity-client'; | ||
| import { usePlayInView } from '../../utils/use-play-in-view'; | ||
| import { useReducedMotion } from '../../utils/use-reduced-motion'; | ||
| import Image from '../image'; | ||
|
|
||
| const builder = imageUrlBuilder(client); | ||
|
|
Comment on lines
+34
to
+45
| if (reducedMotion) { | ||
| return ( | ||
| <Image | ||
| image={image} | ||
| loading={loading} | ||
| width={3200} | ||
| height={2400} | ||
| sizes={sizes} | ||
| className={className} | ||
| /> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
Support for self-hosted videos uploaded to Sanity, in two places:
videoblock in post bodies. Uploads a video file with an optional poster image (sets the aspect ratio, falls back to 16:9), caption, alt text, autoplay toggle, and full/text-width layout. When autoplay is on the video plays muted and looping while in view; when off it shows native controls and plays with sound.thumbnailVideofile field on posts. On the home page artwork list, the thumbnail animates from the uploaded video; the still image is used everywhere else and as the poster frame.Rendering approach
Both surfaces play only while in the viewport (IntersectionObserver) and respect
prefers-reduced-motion. The thumbnail renders the video on the server so motion users (the common case) don't download the full-resolution fallback image just to discard it after hydration; it swaps to the responsive image on the client only when reduced motion is set. The shared reduced-motion detection and play-in-view behavior live in two hooks (use-reduced-motion,use-play-in-view) rather than being duplicated across theVideoandThumbnailVideocomponents.The GROQ projections are extended so
videoblocks resolvefile.asset->urland poster dimensions, and the home artwork query resolvesthumbnailVideo.asset->urlvia a newhomeArtworkListFields. Existing usages that don't projectthumbnailVideoUrlfall back to the image, so the change is backward-compatible.Also included
generateStaticParamsreturn shape.Review notes
sanity.types.tsandapps/sanity/schema.jsonare generated.videohandler inbody.tsxuses anany-typedvalue, matching the existingimage/codehandlers; typing it is deferred.