Skip to content

Add uploaded-video block and animated home page thumbnails - #92

Merged
sidp merged 5 commits into
masterfrom
add-video-upload-block
Jul 3, 2026
Merged

Add uploaded-video block and animated home page thumbnails#92
sidp merged 5 commits into
masterfrom
add-video-upload-block

Conversation

@sidp

@sidp sidp commented Jul 3, 2026

Copy link
Copy Markdown
Owner

What this adds

Support for self-hosted videos uploaded to Sanity, in two places:

  • Video body block — a new video block 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.
  • Animated artwork thumbnails — a new thumbnailVideo file 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 the Video and ThumbnailVideo components.

The GROQ projections are extended so video blocks resolve file.asset->url and poster dimensions, and the home artwork query resolves thumbnailVideo.asset->url via a new homeArtworkListFields. Existing usages that don't project thumbnailVideoUrl fall back to the image, so the change is backward-compatible.

Also included

  • Data-fetching fixes for Next.js 16: webhook cache revalidation, tag/stega/draft-mode handling, and the updated generateStaticParams return shape.
  • Regenerated Sanity schema and types after the dependency upgrade.

Review notes

  • sanity.types.ts and apps/sanity/schema.json are generated.
  • The portable-text video handler in body.tsx uses an any-typed value, matching the existing image/code handlers; typing it is deferred.

sidp added 5 commits July 3, 2026 11:44
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.
@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
simonssoncom Ready Ready Preview, Comment Jul 3, 2026 10:04pm
website-sanity Ready Ready Preview, Comment Jul 3, 2026 10:04pm

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 post schema with a thumbnailVideo field and a video block 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}
/>
);
}
@sidp
sidp merged commit a97264d into master Jul 3, 2026
4 checks passed
@sidp
sidp deleted the add-video-upload-block branch July 3, 2026 22:24
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