feat(web): merge DigitalOcean release announcement updates (#860)

* feat(web): announce DigitalOcean acquisition across sites

* fix(web): make blog routes resilient without Sanity config

* fix(web): add mobile arrow cue to announcement banner

* fix(web): point acquisition links to announcement post
This commit is contained in:
Musa 2026-04-02 09:03:52 -04:00 committed by GitHub
parent 0857cfafbf
commit 39b430d74b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 3156 additions and 701 deletions

View file

@ -12,6 +12,7 @@
"clean": "rm -rf .next"
},
"dependencies": {
"@heroicons/react": "^2.2.0",
"@katanemo/shared-styles": "*",
"@katanemo/ui": "*",
"next": "^16.1.6",

View file

@ -66,7 +66,9 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body className={`${ibmPlexSans.variable} antialiased text-white`}>
<body
className={`${ibmPlexSans.variable} overflow-hidden antialiased text-white`}
>
{/* Google tag (gtag.js) */}
<Script
src="https://www.googletagmanager.com/gtag/js?id=G-RLD5BDNW5N"
@ -80,7 +82,9 @@ export default function RootLayout({
gtag('config', 'G-RLD5BDNW5N');
`}
</Script>
<div className="min-h-screen">{children}</div>
<div className="h-screen overflow-hidden">
{children}
</div>
</body>
</html>
);

View file

@ -1,11 +1,23 @@
import Image from "next/image";
import Link from "next/link";
import LogoSlider from "../components/LogoSlider";
import { ArrowRightIcon } from "@heroicons/react/16/solid";
export default function HomePage() {
return (
<main className="relative flex min-h-screen items-center justify-center overflow-hidden px-6 pt-12 pb-16 font-sans sm:pt-20 lg:items-start lg:justify-start lg:pt-24">
<main className="relative flex h-full items-center justify-center overflow-hidden px-6 pt-12 pb-16 font-sans sm:pt-20 lg:items-start lg:justify-start lg:pt-24">
<div className="relative mx-auto w-full max-w-6xl flex flex-col items-center justify-center text-left lg:items-start lg:justify-start">
<Link
href="https://digitalocean.com/blog/digitalocean-acquires-katanemo-labs-inc"
target="_blank"
rel="noopener noreferrer"
className="mb-7 inline-flex max-w-[20rem] items-center gap-1 self-start rounded-full border border-[#22A875]/30 bg-[#22A875]/30 px-2.5 py-1 text-left text-[12px] leading-tight font-medium text-white transition-opacity hover:opacity-90 lg:hidden"
>
<span>
DigitalOcean acquires Katanemo Labs, Inc.
</span>
<ArrowRightIcon aria-hidden className="h-3 w-3 shrink-0 text-white/90" />
</Link>
<div className="pointer-events-none mb-6 w-full self-start lg:hidden">
<Image
src="/KatanemoLogo.svg"
@ -17,6 +29,20 @@ export default function HomePage() {
/>
</div>
<div className="relative z-10 max-w-xl sm:max-w-2xl lg:max-w-2xl xl:max-w-8xl lg:pr-[26vw] xl:pr-[2vw] sm:right-0 md:right-0 lg:right-0 xl:right-20 2xl:right-50 sm:mt-36 mt-0">
<Link
href="https://digitalocean.com/blog/digitalocean-acquires-katanemo-labs-inc"
target="_blank"
rel="noopener noreferrer"
className="mb-4 hidden max-w-full items-center gap-2 rounded-full border border-[#22A875]/70 bg-[#22A875]/50 px-4 py-1 text-left text-sm font-medium text-white transition-opacity hover:opacity-90 lg:inline-flex"
>
<span>
DigitalOcean acquires Katanemo Labs, Inc.
</span>
<ArrowRightIcon
aria-hidden
className="h-4 w-4 shrink-0 text-white/90"
/>
</Link>
<h1 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-sans font-medium leading-tight tracking-tight text-white">
Forward-deployed AI infrastructure engineers.
</h1>

View file

@ -16,6 +16,7 @@
"migrate:blogs": "tsx scripts/migrate-blogs.ts"
},
"dependencies": {
"@heroicons/react": "^2.2.0",
"@katanemo/shared-styles": "*",
"@katanemo/ui": "*",
"@portabletext/react": "^5.0.0",
@ -32,8 +33,8 @@
"next": "^16.1.6",
"next-sanity": "^11.6.9",
"papaparse": "^5.5.3",
"react": "19.2.0",
"react-dom": "19.2.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-markdown": "^10.1.0",
"react-syntax-highlighter": "^16.1.0",
"remark-gfm": "^4.0.1",

View file

@ -39,6 +39,10 @@ function loadFont(fileName: string, baseUrl: string) {
}
async function getBlogPost(slug: string) {
if (!client) {
return null;
}
const query = `*[_type == "blog" && slug.current == $slug && published == true][0] {
_id,
title,
@ -53,8 +57,13 @@ async function getBlogPost(slug: string) {
}
}`;
const post = await client.fetch(query, { slug });
return post;
try {
const post = await client.fetch(query, { slug });
return post;
} catch (error) {
console.error("Error fetching blog post for OG image:", error);
return null;
}
}
function formatDate(dateString: string): string {

View file

@ -17,6 +17,10 @@ interface BlogPost {
}
async function getBlogPost(slug: string): Promise<BlogPost | null> {
if (!client) {
return null;
}
const query = `*[_type == "blog" && slug.current == $slug && published == true][0] {
_id,
title,
@ -26,8 +30,13 @@ async function getBlogPost(slug: string): Promise<BlogPost | null> {
author
}`;
const post = await client.fetch(query, { slug });
return post || null;
try {
const post = await client.fetch(query, { slug });
return post || null;
} catch (error) {
console.error("Error fetching blog post metadata:", error);
return null;
}
}
export async function generateMetadata({

View file

@ -25,6 +25,10 @@ interface BlogPost {
}
async function getBlogPost(slug: string): Promise<BlogPost | null> {
if (!client) {
return null;
}
const query = `*[_type == "blog" && slug.current == $slug && published == true][0] {
_id,
title,
@ -51,17 +55,31 @@ async function getBlogPost(slug: string): Promise<BlogPost | null> {
author
}`;
const post = await client.fetch(query, { slug });
return post || null;
try {
const post = await client.fetch(query, { slug });
return post || null;
} catch (error) {
console.error("Error fetching blog post:", error);
return null;
}
}
async function getAllBlogSlugs(): Promise<string[]> {
if (!client) {
return [];
}
const query = `*[_type == "blog" && published == true] {
"slug": slug.current
}`;
const posts = await client.fetch(query);
return posts.map((post: { slug: string }) => post.slug);
try {
const posts = await client.fetch(query);
return posts.map((post: { slug: string }) => post.slug);
} catch (error) {
console.error("Error fetching blog slugs:", error);
return [];
}
}
export async function generateStaticParams() {

View file

@ -44,6 +44,10 @@ function formatDate(dateString: string): string {
}
async function getBlogPosts(): Promise<BlogPost[]> {
if (!client) {
return [];
}
const query = `*[_type == "blog" && published == true] | order(publishedAt desc) {
_id,
title,
@ -58,7 +62,12 @@ async function getBlogPosts(): Promise<BlogPost[]> {
featured
}`;
return await client.fetch(query);
try {
return await client.fetch(query);
} catch (error) {
console.error("Error fetching blog posts:", error);
return [];
}
}
export default async function BlogPage() {

View file

@ -1,4 +1,6 @@
import type { Metadata } from "next";
import { ArrowRightIcon } from "@heroicons/react/16/solid";
import Link from "next/link";
import Script from "next/script";
import "@katanemo/shared-styles/globals.css";
import { Analytics } from "@vercel/analytics/next";
@ -35,6 +37,27 @@ export default function RootLayout({
gtag('config', 'G-ML7B1X9HY2');
`}
</Script>
<Link
href="https://digitalocean.com/blog/digitalocean-acquires-katanemo-labs-inc"
target="_blank"
rel="noopener noreferrer"
className="block w-full bg-[#7780D9] py-3 text-white transition-opacity"
>
<div className="mx-auto flex max-w-[85rem] items-center justify-center gap-4 px-6 text-center md:justify-between md:text-left lg:px-8">
<span className="w-full text-xs font-medium leading-snug md:w-auto md:text-base flex items-center">
DigitalOcean acquires Katanemo Labs, Inc. to accelerate AI
development
<ArrowRightIcon
aria-hidden
className="ml-1 inline-block h-3 w-3 align-[-1px] text-white/90 md:hidden"
/>
</span>
<span className="hidden shrink-0 items-center gap-1 text-base font-medium tracking-[-0.989px] font-mono leading-snug opacity-70 transition-opacity hover:opacity-100 md:inline-flex">
Read the announcement
<ArrowRightIcon aria-hidden className="h-3.5 w-3.5 text-white/70" />
</span>
</div>
</Link>
<ConditionalLayout>{children}</ConditionalLayout>
<Analytics />
</body>

View file

@ -10,6 +10,10 @@ interface BlogPost {
}
async function getBlogPosts(): Promise<BlogPost[]> {
if (!client) {
return [];
}
const query = `*[_type == "blog" && published == true] | order(publishedAt desc) {
slug,
publishedAt,

View file

@ -6,15 +6,22 @@ const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID;
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET;
const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION;
export const client = createClient({
projectId,
dataset,
apiVersion,
useCdn: true, // Set to false if statically generating pages, using ISR or using the on-demand revalidation API
});
export const hasSanityConfig = Boolean(projectId && dataset && apiVersion);
const builder = imageUrlBuilder(client);
export const client = hasSanityConfig
? createClient({
projectId,
dataset,
apiVersion,
useCdn: true, // Set to false if statically generating pages, using ISR or using the on-demand revalidation API
})
: null;
const builder = client ? imageUrlBuilder(client) : null;
export function urlFor(source: SanityImageSource) {
if (!builder) {
throw new Error("Sanity client is not configured.");
}
return builder.image(source);
}