Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8d96f0d
adding builder blog and posts
mnelsonBT May 7, 2026
1b2d275
updating personas
mnelsonBT May 7, 2026
99988f8
fix(netlify): trace hub-route MD into the function bundle
pettinarip May 11, 2026
d251319
Merge pull request #18166 from ethereum/test-md-hub-isr-tracer-includes
wackerow May 11, 2026
bf2f036
Merge branch 'dev' of https://github.com/ethereum/ethereum-org-websit…
mnelsonBT May 12, 2026
d39c0ed
updating blog posts
mnelsonBT May 12, 2026
faf9ef5
Merge remote-tracking branch 'origin/dev' into builder-hub-blog
mnelsonBT May 27, 2026
9ba1454
Apply /latest/ URL changes and resolve branch conflicts
mnelsonBT May 27, 2026
f1eb815
Remove old placeholder blog posts
mnelsonBT May 27, 2026
b7c4577
fixing breadcrumb
mnelsonBT May 27, 2026
3d97e8c
changing hero image
mnelsonBT May 27, 2026
2b0eab6
updating hero images
mnelsonBT May 27, 2026
27f28e3
refactor: align /latest with /tutorials styling
myelinated-wackerow May 27, 2026
276d775
update(ui): developer blog cards
myelinated-wackerow May 27, 2026
f67ab2e
revert: MdxHero usage
myelinated-wackerow May 27, 2026
ef09385
refactor: use existing tutorial layout
myelinated-wackerow May 28, 2026
b6e34a8
feat(ui): add blog image to page conditionally
myelinated-wackerow May 28, 2026
1325c4a
patch: hide edit button for blog entries
myelinated-wackerow May 28, 2026
6c30651
patch(ui): apply text-body-medium to CardParagraph
myelinated-wackerow May 28, 2026
232c6ab
fix: nested p tags, scroll container spacing
myelinated-wackerow May 28, 2026
6bcfd1a
patch: full width CTA on mobile
myelinated-wackerow May 28, 2026
c69589b
patch(ui): use ghost variant, rm preview card bg
myelinated-wackerow May 28, 2026
671cd94
feat: add RSS feed for /latest/ posts
myelinated-wackerow May 28, 2026
a4ca50c
patch: rm redundant intl string
myelinated-wackerow May 28, 2026
931d891
chore: prettify
myelinated-wackerow May 28, 2026
ee43cba
patch: adjustments from code review
myelinated-wackerow May 28, 2026
077b22d
rename: IBlogPost to BlogPost
myelinated-wackerow May 28, 2026
fed5762
refactor(intl): page-latest namespace
myelinated-wackerow May 28, 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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ pnpm-lock.yaml.bak
next-env.d.ts

# rss feeds
feed.xml
/feed.xml

# Sitemaps
sitemap*.xml
Expand Down
87 changes: 86 additions & 1 deletion app/[locale]/developers/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,14 @@ import { VStack } from "@/components/ui/flex"
import Link from "@/components/ui/Link"
import InlineLink from "@/components/ui/Link"
import { Section } from "@/components/ui/section"
import { TagsInlineText } from "@/components/ui/tag"
import { TerminalTypewriter } from "@/components/ui/terminal-typewriter"

import { getBlogFallbackHero } from "@/lib/utils/blog"
import { cn } from "@/lib/utils/cn"
import { getAppPageContributorInfo } from "@/lib/utils/contributors"
import { formatDateRange } from "@/lib/utils/date"
import { formatDate, formatDateRange } from "@/lib/utils/date"
import { getBlogPostsData } from "@/lib/utils/md"
import { getMetadata } from "@/lib/utils/metadata"
import { screens } from "@/lib/utils/screen"

Expand All @@ -52,6 +55,7 @@ import tutorialTagsBanner from "@/public/images/developers/tutorial-tags-banner.
import dogeImage from "@/public/images/doge-computer.png"
import fallbackThumbnail from "@/public/images/eth-glyph-thumbnail.png"
import heroImage from "@/public/images/heroes/developers-hub-hero.png"

const H3 = (props: ChildOnlyProp) => <h3 className="mt-10 mb-8" {...props} />

const Text = (props: ChildOnlyProp) => <p className="mb-6" {...props} />
Expand Down Expand Up @@ -140,6 +144,8 @@ const DevelopersPage = async (props: { params: Promise<PageParams> }) => {

const hackathons = (await getHackathons()).slice(0, 5)

const recentPosts = (await getBlogPostsData(locale)).slice(0, 3)

const { contributors } = await getAppPageContributorInfo(
"developers",
locale as Lang
Expand Down Expand Up @@ -459,6 +465,85 @@ const DevelopersPage = async (props: { params: Promise<PageParams> }) => {
</div>
</Section>

{recentPosts.length > 0 && (
<Section id="blog" className="space-y-4 py-10 md:py-12">
<h2>{t("page-developers-blog-title")}</h2>
<p>{t("page-developers-blog-desc")}</p>

<EdgeScrollContainer className="[--edge-spacing:2rem]">
{recentPosts.map((post) => (
<EdgeScrollItem
key={post.href}
asChild
className="ms-6 w-[calc(100%-4rem)] max-w-md md:min-w-96 md:flex-1 lg:max-w-[33%]"
>
<Card
href={post.href}
customEventOptions={{
eventCategory: "builder-blog",
eventAction: "click",
eventName: post.title,
}}
variant="ghost"
size="sm"
>
<CardHeader>
<CardBanner size="sm">
{post.image ? (
<Image
src={post.image}
alt=""
width={1200}
height={630}
sizes="448px"
/>
) : (
<Image
src={getBlogFallbackHero(post.href)}
alt=""
sizes="448px"
/>
)}
</CardBanner>
</CardHeader>
<CardContent>
<CardTitle className="line-clamp-2">
{post.title}
</CardTitle>
<TagsInlineText
list={[post.author, post.team]}
variant="light"
className="italic"
/>
<CardParagraph size="sm" className="line-clamp-3">
{post.description}
</CardParagraph>
</CardContent>
<CardFooter>
<CardParagraph size="sm">
{formatDate(post.published, locale)}
</CardParagraph>
</CardFooter>
</Card>
</EdgeScrollItem>
))}
</EdgeScrollContainer>

<div className="flex justify-center max-sm:*:w-full">
<ButtonLink
href="/latest/"
customEventOptions={{
eventCategory: "builder-blog",
eventAction: "click",
eventName: "view-all-updates",
}}
>
{t("page-developers-blog-view-all")}
</ButtonLink>
</div>
</Section>
)}

<Section
id="docs"
className={cn(
Expand Down
85 changes: 85 additions & 0 deletions app/[locale]/latest/feed.xml/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { NextRequest } from "next/server"
import { getTranslations } from "next-intl/server"

import { getBlogPostsData } from "@/lib/utils/md"
import { getFullUrl } from "@/lib/utils/url"

export const dynamic = "force-static"

const XML_ESCAPE: Record<string, string> = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&apos;",
}

const escapeXml = (value: string): string =>
value.replace(/[&<>"']/g, (c) => XML_ESCAPE[c])

export async function GET(
_: NextRequest,
{ params }: { params: Promise<{ locale: string }> }
) {
const { locale } = await params

const t = await getTranslations({
locale,
namespace: "page-latest",
})

const posts = await getBlogPostsData(locale)

// Strip trailing slash for the .xml file URL (getFullUrl appends one for
// directory-style URLs, but feed.xml is a file).
const feedUrl = getFullUrl(locale, "/latest/feed.xml").replace(/\/$/, "")
const channelLink = getFullUrl(locale, "/latest/")
const channelTitle = t("page-latest-title")
const channelDescription = t("page-latest-subtitle")

const items = posts
.map((post) => {
const link = getFullUrl(locale, post.href)
const pubDate = new Date(post.published).toUTCString()
const creator = post.author
? `<dc:creator>${escapeXml(post.author)}</dc:creator>`
: ""
const categories = (post.tags ?? [])
.map((tag) => `<category>${escapeXml(tag)}</category>`)
.join("")
return [
"<item>",
`<title>${escapeXml(post.title)}</title>`,
`<link>${link}</link>`,
`<guid isPermaLink="true">${link}</guid>`,
`<description>${escapeXml(post.description)}</description>`,
creator,
`<pubDate>${pubDate}</pubDate>`,
categories,
"</item>",
].join("")
})
.join("")

const lastBuildDate = new Date().toUTCString()
const xml = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">',
"<channel>",
`<title>${escapeXml(channelTitle)}</title>`,
`<link>${channelLink}</link>`,
`<atom:link href="${feedUrl}" rel="self" type="application/rss+xml" />`,
`<description>${escapeXml(channelDescription)}</description>`,
`<language>${locale}</language>`,
`<lastBuildDate>${lastBuildDate}</lastBuildDate>`,
items,
"</channel>",
"</rss>",
].join("")

return new Response(xml, {
headers: {
"Content-Type": "application/rss+xml; charset=utf-8",
},
})
}
82 changes: 82 additions & 0 deletions app/[locale]/latest/page-jsonld.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { BlogPost, FileContributor } from "@/lib/types"

import PageJsonLD from "@/components/PageJsonLD"

import { normalizeUrlForJsonLd } from "@/lib/utils/url"

import { BASE_GRAPH_NODES } from "@/lib/jsonld/constants"
import { REFERENCE } from "@/lib/jsonld/references"

export default async function BlogPageJsonLD({
locale,
blogPosts,
contributors,
}: {
locale: string
blogPosts: BlogPost[]
contributors: FileContributor[]
}) {
const url = normalizeUrlForJsonLd(locale, "/latest")

const contributorList = contributors.map((contributor) => ({
"@type": "Person",
name: contributor.login,
url: contributor.html_url,
}))

const blogPostItems = blogPosts.map((post, index) => ({
"@type": "ListItem",
position: index + 1,
url: normalizeUrlForJsonLd(locale, post.href),
name: post.title,
}))

const jsonLd = {
"@context": "https://schema.org",
"@graph": [
...BASE_GRAPH_NODES,
{
"@type": "CollectionPage",
"@id": url,
name: "Builder updates",
description:
"Builder resources, tools, and developments from the Ethereum ecosystem.",
url,
inLanguage: locale,
isPartOf: REFERENCE.ETHEREUM_ORG_WEBSITE,
publisher: REFERENCE.ETHEREUM_FOUNDATION,
contributor: contributorList,
breadcrumb: {
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Home",
item: normalizeUrlForJsonLd(locale, "/"),
},
{
"@type": "ListItem",
position: 2,
name: "Developers",
item: normalizeUrlForJsonLd(locale, "/developers"),
},
{
"@type": "ListItem",
position: 3,
name: "Builder updates",
item: url,
},
],
},
mainEntity: {
"@type": "ItemList",
numberOfItems: blogPosts.length,
itemListElement: blogPostItems,
},
},
],
}

return <PageJsonLD structuredData={jsonLd} />
}
Loading
Loading