Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
a9b76cc
Add agent context schemas
mnelsonBT Feb 3, 2026
cc37857
video gallery feature
mnelsonBT Feb 16, 2026
0b9612e
adding real transcripts
mnelsonBT Feb 16, 2026
e9a852e
editing transcripts
mnelsonBT Feb 19, 2026
73c4ac2
editing video descriptions
mnelsonBT Feb 19, 2026
abfc865
editing page headings
mnelsonBT Feb 19, 2026
459ef36
fixing curly brackets
mnelsonBT Feb 19, 2026
89ebf00
embedding remaining videos
mnelsonBT Feb 19, 2026
f53a55f
reducing caption length
mnelsonBT Feb 19, 2026
df7dbde
updating accordian for SEO
mnelsonBT Feb 19, 2026
959d6de
transcript format tweak
mnelsonBT Feb 19, 2026
93da4d0
updating page subtitle
mnelsonBT Feb 19, 2026
b662eaa
remove context files and revert next.config.js
mnelsonBT Feb 19, 2026
ef36f3d
refactor: use breadcrumb molecule component
wackerow Mar 4, 2026
4ec6660
refactor: use ui/card component, server-side
wackerow Mar 4, 2026
47a7924
refactor: use ui/section, add sr-only h2
wackerow Mar 4, 2026
56acfc2
adding StartTime and pulling in site videos
mnelsonBT Mar 23, 2026
16c7225
adding transcripts
mnelsonBT Mar 26, 2026
7c51fb9
text to markdown
mnelsonBT Mar 26, 2026
56b7938
removing category shelves from landing
mnelsonBT Mar 26, 2026
820ad7d
adding new videos
mnelsonBT Mar 26, 2026
031bf41
refactor: add VideoFrontmatter type
wackerow Mar 27, 2026
ccb6dcb
refactor: migrate video metadata to frontmatter
wackerow Mar 27, 2026
4c8d3dd
refactor: filesystem-based video data layer
wackerow Mar 27, 2026
d4c2012
refactor: add preserveNewlines to stripMarkdown
wackerow Mar 27, 2026
103a66e
refactor: update VideoWatch, remove page-scoped components
wackerow Mar 27, 2026
83601a8
refactor: colocate video page components
wackerow Mar 27, 2026
8975f70
refactor: use frontmatter for video detail page
wackerow Mar 27, 2026
0913d74
chore: remove video unit tests
wackerow Mar 27, 2026
fed337b
chore: audit cleanup
wackerow Mar 27, 2026
6f1ad72
feat: add JSON-LD to videos listing page
wackerow Mar 27, 2026
03e4f76
Merge remote-tracking branch 'origin' into videos-refactor
wackerow Mar 27, 2026
1cec2a9
refactor: extract toIsoDuration to time.ts
wackerow Mar 27, 2026
06a11b9
perf: reduce build memory for video pages
wackerow Mar 27, 2026
9a51f76
Merge branch 'dev' into videos-refactor
wackerow Mar 27, 2026
f3d5668
chore: rm unnecessary comment and type union
wackerow Mar 27, 2026
ed9167c
feat(seo): add JSON-LD to VideoWatch embeds
wackerow Mar 27, 2026
f62978b
fix(seo): remove VideoObject from listing ItemList
wackerow Mar 27, 2026
5fdda75
revert(seo): remove JSON-LD from VideoWatch embeds
wackerow Mar 27, 2026
eced3d2
perf: statically generate all video pages
myelinated-wackerow Apr 1, 2026
cc098f7
refactor: audit cleanup for videos PR
myelinated-wackerow Apr 1, 2026
025a4fd
Merge branch 'dev' into pr-17870-videos-static
myelinated-wackerow Apr 1, 2026
468e014
adding video documentation
mnelsonBT Apr 6, 2026
098d5c1
fixing gallery category constants
mnelsonBT Apr 6, 2026
32d55c6
docs: sync video tag reference with constants
wackerow Apr 7, 2026
2ce29d0
docs: clarify category key tag requirement
wackerow Apr 7, 2026
9fde07f
adding trailing videos
mnelsonBT Apr 7, 2026
8302b72
transcript copyedits
mnelsonBT Apr 7, 2026
6959b0c
feat: add video counts to category filter tags
myelinated-wackerow Apr 7, 2026
d4f15f2
fixing transcripts
mnelsonBT Apr 7, 2026
cc04d48
patch: EthBoulder 2026 year
myelinated-wackerow Apr 8, 2026
0999214
Merge branch 'dev' into videos-refactor
myelinated-wackerow Apr 8, 2026
a819c09
feat: add remark-heading-id to simple MD renderer
myelinated-wackerow Apr 8, 2026
e09a41e
chore: apply custom header IDs to video transcripts
myelinated-wackerow Apr 8, 2026
ea18c34
fix: support heading IDs in simple MD renderer
myelinated-wackerow Apr 8, 2026
77075d1
fix(refactor): getTranslations string argument
myelinated-wackerow Apr 8, 2026
a1e2ef3
revert: crowdin-translation-guide video
myelinated-wackerow Apr 8, 2026
ecbb782
fix: use hqdefault (480x360) thumbnail
myelinated-wackerow Apr 8, 2026
5fc5f14
feat(ui): move h1 below image
myelinated-wackerow Apr 8, 2026
542e27c
feat(ui): add timer, align breadcrumb position
myelinated-wackerow Apr 8, 2026
9cd91e2
Merge branch 'dev' into videos-refactor
wackerow Apr 9, 2026
f6d09f9
feat(ui): semibold CardTitle variant
wackerow Apr 9, 2026
03885af
feat(ui): use ui/card for VideoWatch
wackerow Apr 9, 2026
e33ef78
feat: pass className to VideoWatch
wackerow Apr 9, 2026
c121629
Merge branch 'staging' into videos-refactor
wackerow Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions .github/ISSUE_TEMPLATE/suggest_video.yaml
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
3 changes: 2 additions & 1 deletion app/[locale]/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { GHIssue, SlugPageParams } from "@/lib/types"

import I18nProvider from "@/components/I18nProvider"
import mdComponents from "@/components/MdComponents"
import VideoWatch from "@/components/Videos/VideoWatch"

import { dateToString } from "@/lib/utils/date"
import { getLayoutFromSlug } from "@/lib/utils/layout"
Expand Down Expand Up @@ -57,7 +58,7 @@ export default async function Page(props: { params: Promise<SlugPageParams> }) {
} = await getPageData({
locale,
slug,
baseComponents: mdComponents,
baseComponents: { ...mdComponents, VideoWatch },
Copy link
Copy Markdown
Member

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

componentsMapping,
scope: {
gfissues,
Expand Down
57 changes: 57 additions & 0 deletions app/[locale]/videos/[slug]/page-jsonld.tsx
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,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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} />
}
145 changes: 145 additions & 0 deletions app/[locale]/videos/[slug]/page.tsx
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
Loading
Loading