-
Notifications
You must be signed in to change notification settings - Fork 5.4k
refactor: videos page revamp #17870
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
Merged
Merged
refactor: videos page revamp #17870
Changes from 64 commits
Commits
Show all changes
65 commits
Select commit
Hold shift + click to select a range
a9b76cc
Add agent context schemas
mnelsonBT cc37857
video gallery feature
mnelsonBT 0b9612e
adding real transcripts
mnelsonBT e9a852e
editing transcripts
mnelsonBT 73c4ac2
editing video descriptions
mnelsonBT abfc865
editing page headings
mnelsonBT 459ef36
fixing curly brackets
mnelsonBT 89ebf00
embedding remaining videos
mnelsonBT f53a55f
reducing caption length
mnelsonBT df7dbde
updating accordian for SEO
mnelsonBT 959d6de
transcript format tweak
mnelsonBT 93da4d0
updating page subtitle
mnelsonBT b662eaa
remove context files and revert next.config.js
mnelsonBT ef36f3d
refactor: use breadcrumb molecule component
wackerow 4ec6660
refactor: use ui/card component, server-side
wackerow 47a7924
refactor: use ui/section, add sr-only h2
wackerow 56acfc2
adding StartTime and pulling in site videos
mnelsonBT 16c7225
adding transcripts
mnelsonBT 7c51fb9
text to markdown
mnelsonBT 56b7938
removing category shelves from landing
mnelsonBT 820ad7d
adding new videos
mnelsonBT 031bf41
refactor: add VideoFrontmatter type
wackerow ccb6dcb
refactor: migrate video metadata to frontmatter
wackerow 4c8d3dd
refactor: filesystem-based video data layer
wackerow d4c2012
refactor: add preserveNewlines to stripMarkdown
wackerow 103a66e
refactor: update VideoWatch, remove page-scoped components
wackerow 83601a8
refactor: colocate video page components
wackerow 8975f70
refactor: use frontmatter for video detail page
wackerow 0913d74
chore: remove video unit tests
wackerow fed337b
chore: audit cleanup
wackerow 6f1ad72
feat: add JSON-LD to videos listing page
wackerow 03e4f76
Merge remote-tracking branch 'origin' into videos-refactor
wackerow 1cec2a9
refactor: extract toIsoDuration to time.ts
wackerow 06a11b9
perf: reduce build memory for video pages
wackerow 9a51f76
Merge branch 'dev' into videos-refactor
wackerow f3d5668
chore: rm unnecessary comment and type union
wackerow ed9167c
feat(seo): add JSON-LD to VideoWatch embeds
wackerow f62978b
fix(seo): remove VideoObject from listing ItemList
wackerow 5fdda75
revert(seo): remove JSON-LD from VideoWatch embeds
wackerow eced3d2
perf: statically generate all video pages
myelinated-wackerow cc098f7
refactor: audit cleanup for videos PR
myelinated-wackerow 025a4fd
Merge branch 'dev' into pr-17870-videos-static
myelinated-wackerow 468e014
adding video documentation
mnelsonBT 098d5c1
fixing gallery category constants
mnelsonBT 32d55c6
docs: sync video tag reference with constants
wackerow 2ce29d0
docs: clarify category key tag requirement
wackerow 9fde07f
adding trailing videos
mnelsonBT 8302b72
transcript copyedits
mnelsonBT 6959b0c
feat: add video counts to category filter tags
myelinated-wackerow d4f15f2
fixing transcripts
mnelsonBT cc04d48
patch: EthBoulder 2026 year
myelinated-wackerow 0999214
Merge branch 'dev' into videos-refactor
myelinated-wackerow a819c09
feat: add remark-heading-id to simple MD renderer
myelinated-wackerow e09a41e
chore: apply custom header IDs to video transcripts
myelinated-wackerow ea18c34
fix: support heading IDs in simple MD renderer
myelinated-wackerow 77075d1
fix(refactor): getTranslations string argument
myelinated-wackerow a1e2ef3
revert: crowdin-translation-guide video
myelinated-wackerow ecbb782
fix: use hqdefault (480x360) thumbnail
myelinated-wackerow 5fc5f14
feat(ui): move h1 below image
myelinated-wackerow 542e27c
feat(ui): add timer, align breadcrumb position
myelinated-wackerow 9cd91e2
Merge branch 'dev' into videos-refactor
wackerow f6d09f9
feat(ui): semibold CardTitle variant
wackerow 03885af
feat(ui): use ui/card for VideoWatch
wackerow e33ef78
feat: pass className to VideoWatch
wackerow c121629
Merge branch 'staging' into videos-refactor
wackerow File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| name: Suggest a video | ||
| description: Suggest a video to add to the ethereum.org video gallery | ||
| title: Suggest a video | ||
| labels: ["feature ✨", "content 🖋️"] | ||
| body: | ||
| - type: markdown | ||
| attributes: | ||
| value: | | ||
| Before suggesting a video, please review [our video listing policy](https://ethereum.org/contributing/adding-videos/) to ensure your suggestion meets the criteria. | ||
| - type: markdown | ||
| id: video_info | ||
| attributes: | ||
| value: "## Video info" | ||
| - type: input | ||
| id: video_title | ||
| attributes: | ||
| label: Video title | ||
| description: What is the title of the video? | ||
| validations: | ||
| required: true | ||
| - type: input | ||
| id: video_url | ||
| attributes: | ||
| label: YouTube URL | ||
| description: Please provide the full YouTube URL (e.g. https://www.youtube.com/watch?v=...) | ||
| validations: | ||
| required: true | ||
| - type: textarea | ||
| id: video_description | ||
| attributes: | ||
| label: Video description | ||
| description: Provide a brief 1–3 sentence summary of what the video covers | ||
| validations: | ||
| required: true | ||
| - type: input | ||
| id: video_author | ||
| attributes: | ||
| label: Creator / Channel | ||
| description: Who created the video? (e.g. "Ethereum Foundation", "Finematics") | ||
| validations: | ||
| required: true | ||
| - type: input | ||
| id: video_duration | ||
| attributes: | ||
| label: Duration | ||
| description: "Video length in H:MM:SS or M:SS format (e.g. 1:08:42 or 12:30)" | ||
| validations: | ||
| required: true | ||
| - type: dropdown | ||
| id: video_education_level | ||
| attributes: | ||
| label: Education level | ||
| description: What level of Ethereum knowledge does this video assume? | ||
| options: | ||
| - "Beginner" | ||
| - "Intermediate" | ||
| - "Advanced" | ||
| validations: | ||
| required: true | ||
| - type: dropdown | ||
| id: video_format | ||
| attributes: | ||
| label: Format | ||
| description: What best describes the format of this video? | ||
| options: | ||
| - "Explainer" | ||
| - "Presentation" | ||
| - "Interview" | ||
| - "Tutorial" | ||
| - "Panel" | ||
| validations: | ||
| required: true | ||
| - type: input | ||
| id: video_topics | ||
| attributes: | ||
| label: Topics | ||
| description: "Comma-separated topic tags (e.g. scaling, layer-2, rollups). See existing tags at https://ethereum.org/videos/" | ||
| validations: | ||
| required: true | ||
| - type: input | ||
| id: video_upload_date | ||
| attributes: | ||
| label: Original upload date | ||
| description: "When was the video originally published? (YYYY-MM-DD format)" | ||
| validations: | ||
| required: true | ||
| - type: textarea | ||
| id: video_transcript | ||
| attributes: | ||
| label: Transcript (optional) | ||
| description: | | ||
| If you have a transcript, paste it here. Otherwise the team will generate one. | ||
| Transcripts should use markdown formatting with section headings and timestamps. | ||
| - type: textarea | ||
| id: video_justification | ||
| attributes: | ||
| label: Why should this video be listed? | ||
| description: Briefly explain how this video helps ethereum.org users learn about Ethereum | ||
| - type: checkboxes | ||
| id: video_work_on | ||
| attributes: | ||
| label: Would you like to work on this issue? | ||
| description: If yes, you can open a PR to add the video yourself following our contributing guide | ||
| options: | ||
| - label: "Yes" | ||
| required: false | ||
| - label: "No" | ||
| required: false | ||
| validations: | ||
| required: true |
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import type { VideoFrontmatter } from "@/lib/interfaces" | ||
|
|
||
| import PageJsonLD from "@/components/PageJsonLD" | ||
|
|
||
| import { ethereumFoundationOrganization } from "@/lib/utils/jsonld" | ||
| import { stripMarkdown } from "@/lib/utils/md" | ||
| import { toIsoDuration } from "@/lib/utils/time" | ||
| import { normalizeUrlForJsonLd } from "@/lib/utils/url" | ||
| import { getDefaultThumbnailUrl } from "@/lib/utils/videos" | ||
|
|
||
| export default function VideoPageJsonLD({ | ||
| locale, | ||
| slug, | ||
| frontmatter, | ||
| transcript, | ||
| }: { | ||
| locale: string | ||
| slug: string | ||
| frontmatter: VideoFrontmatter | ||
| transcript: string | null | ||
| }) { | ||
| const url = normalizeUrlForJsonLd(locale, `/videos/${slug}/`) | ||
|
|
||
| const jsonLd: Record<string, unknown> = { | ||
| "@context": "https://schema.org", | ||
| "@type": "VideoObject", | ||
| "@id": `${url}#video`, | ||
| name: frontmatter.title, | ||
| description: frontmatter.description, | ||
| uploadDate: `${frontmatter.uploadDate}T00:00:00+00:00`, | ||
| duration: toIsoDuration(frontmatter.duration), | ||
| thumbnailUrl: | ||
| frontmatter.customThumbnailUrl || | ||
| getDefaultThumbnailUrl(frontmatter.youtubeId), | ||
| embedUrl: `https://www.youtube.com/embed/${frontmatter.youtubeId}`, | ||
| contentUrl: `https://www.youtube.com/watch?v=${frontmatter.youtubeId}`, | ||
| educationalLevel: frontmatter.educationLevel, | ||
| inLanguage: frontmatter.lang, | ||
| creator: { | ||
| "@type": "Person", | ||
| name: frontmatter.author, | ||
| }, | ||
| publisher: ethereumFoundationOrganization, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should use the reference. Check #17955 |
||
| isAccessibleForFree: true, | ||
| isFamilyFriendly: true, | ||
| } | ||
|
|
||
| // Add transcript as plain text if available | ||
| if (transcript) { | ||
| // Escape < and / to prevent script injection (XSS protection) | ||
| jsonLd.transcript = stripMarkdown(transcript, true) | ||
| .replace(/</g, "\\u003c") | ||
| .replace(/\//g, "\\u002f") | ||
| } | ||
|
|
||
| return <PageJsonLD structuredData={jsonLd} /> | ||
| } | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| import { pick } from "lodash" | ||
| import type { Metadata } from "next" | ||
| import { notFound } from "next/navigation" | ||
| import { | ||
| getMessages, | ||
| getTranslations, | ||
| setRequestLocale, | ||
| } from "next-intl/server" | ||
|
|
||
| import type { VideoData } from "@/lib/types" | ||
|
|
||
| import Breadcrumbs from "@/components/Breadcrumbs" | ||
| import FeedbackCard from "@/components/FeedbackCard" | ||
| import I18nProvider from "@/components/I18nProvider" | ||
| import MainArticle from "@/components/MainArticle" | ||
| import { htmlElements } from "@/components/MdComponents" | ||
| import { | ||
| Accordion, | ||
| AccordionContent, | ||
| AccordionItem, | ||
| AccordionTrigger, | ||
| } from "@/components/ui/accordion" | ||
| import YouTube from "@/components/YouTube" | ||
|
|
||
| import { formatDate } from "@/lib/utils/date" | ||
| import { getMetadata } from "@/lib/utils/metadata" | ||
| import { getRequiredNamespacesForPage } from "@/lib/utils/translations" | ||
| import { getVideoData, getVideoSlugs } from "@/lib/utils/videos" | ||
|
|
||
| import VideoPageJsonLD from "./page-jsonld" | ||
|
|
||
| import { renderSimpleMarkdown } from "@/lib/md/renderSimple" | ||
|
|
||
| const VideoLandingPage = async (props: { | ||
| params: Promise<{ locale: string; slug: string }> | ||
| }) => { | ||
| const { locale, slug } = await props.params | ||
|
|
||
| const t = await getTranslations("page-videos") | ||
| setRequestLocale(locale) | ||
|
|
||
| let data: VideoData | undefined | ||
| try { | ||
| data = await getVideoData(slug, locale) | ||
| } catch { | ||
| notFound() | ||
| } | ||
|
|
||
| const { frontmatter } = data | ||
| const transcriptMdx = data.content.trim() || null | ||
|
|
||
| // Get i18n messages | ||
| const allMessages = await getMessages({ locale }) | ||
| const requiredNamespaces = getRequiredNamespacesForPage("/videos/") | ||
| const messages = pick(allMessages, requiredNamespaces) | ||
|
|
||
| const breadcrumbSlug = | ||
| "/videos/" + (frontmatter.breadcrumb || slug.replaceAll("-", " ")) | ||
|
|
||
| return ( | ||
| <I18nProvider locale={locale} messages={messages}> | ||
| <VideoPageJsonLD | ||
| locale={locale} | ||
| slug={slug} | ||
| frontmatter={frontmatter} | ||
| transcript={transcriptMdx} | ||
| /> | ||
|
|
||
| <MainArticle className="max-w-4xl space-y-8 px-4 md:px-8"> | ||
| <Breadcrumbs slug={breadcrumbSlug} startDepth={1} className="mt-11" /> | ||
|
|
||
| <div className="sticky top-24 z-10 md:static"> | ||
| <YouTube | ||
| id={frontmatter.youtubeId} | ||
| title={frontmatter.title} | ||
| className="max-w-full" | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="space-y-4"> | ||
| <h1>{frontmatter.title}</h1> | ||
|
|
||
| <p className="text-lg text-body-medium">{frontmatter.description}</p> | ||
|
|
||
| <p className="text-body-medium"> | ||
| {t("page-videos-date-published")}:{" "} | ||
| {formatDate(frontmatter.uploadDate, locale, { timeZone: "UTC" })} | ||
| </p> | ||
| </div> | ||
|
|
||
| {transcriptMdx && ( | ||
| <Accordion type="single" collapsible> | ||
| <AccordionItem value="transcript"> | ||
| <AccordionTrigger className="py-4"> | ||
| <h2 className="text-xl">{t("page-videos-view-transcript")}</h2> | ||
| </AccordionTrigger> | ||
| {/* forceMount keeps transcript in DOM for SEO crawlers */} | ||
| <AccordionContent | ||
| className="text-base [[data-state=closed]_&]:invisible [[data-state=closed]_&]:h-0" | ||
| forceMount | ||
| > | ||
| {await renderSimpleMarkdown(transcriptMdx, { | ||
| h1: htmlElements.h2, | ||
| })} | ||
| </AccordionContent> | ||
| </AccordionItem> | ||
| </Accordion> | ||
| )} | ||
|
|
||
| <FeedbackCard /> | ||
| </MainArticle> | ||
| </I18nProvider> | ||
| ) | ||
| } | ||
|
|
||
| export async function generateStaticParams() { | ||
| const slugs = await getVideoSlugs() | ||
| return slugs.map((slug) => ({ slug })) | ||
| } | ||
|
|
||
| export async function generateMetadata(props: { | ||
| params: Promise<{ locale: string; slug: string }> | ||
| }): Promise<Metadata> { | ||
| const { locale, slug } = await props.params | ||
|
|
||
| let data | ||
| try { | ||
| data = await getVideoData(slug, locale) | ||
| } catch { | ||
| const t = await getTranslations("common") | ||
| return { | ||
| title: t("page-not-found"), | ||
| description: t("page-not-found-description"), | ||
| } | ||
| } | ||
|
|
||
| return await getMetadata({ | ||
| locale, | ||
| slug: ["videos", slug], | ||
| title: `${data.frontmatter.title} | ethereum.org`, | ||
| description: data.frontmatter.description, | ||
| }) | ||
| } | ||
|
|
||
| export default VideoLandingPage |
Oops, something went wrong.
Oops, something went wrong.
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.
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.
can we put this into mdComponents intead? just to not create the precedent to add more here in the future