From cdc110a0accce73c048615e602536628d7d1be42 Mon Sep 17 00:00:00 2001 From: Jinho Jang <41753422+jinhojang6@users.noreply.github.com> Date: Wed, 27 May 2026 13:26:53 +0900 Subject: [PATCH] UI updates (#15) * Refactor technology stack integration and update documentation links - Updated the technology stack links in various components to point to the new GitHub documentation repository. - Refactored the Lambda Prize page to utilize a shared TechStackSection instead of local tech cards. - Removed redundant tech stack message copies from the storage and technology stack pages. - Enhanced the home feature cards section to use Link components for navigation. - Added tests to ensure the integrity of the new tech stack structure and documentation links. * feat: integrate social proof stats into home page and update related components --- .../design-systems/_sections/components.tsx | 166 ++---------------- apps/web/app/[locale]/design-systems/page.tsx | 15 +- apps/web/app/[locale]/get-started/page.tsx | 8 +- .../_sections/lambda-prize-page.tsx | 146 +-------------- apps/web/app/[locale]/lambda-prize/page.tsx | 51 +----- apps/web/app/[locale]/page.tsx | 13 +- .../technology-stack/messaging/page.tsx | 6 +- .../technology-stack/storage/page.tsx | 7 +- .../broadcast-network-page-contract.test.ts | 13 ++ .../home-feature-cards-contract.test.ts | 24 +++ .../lambda-prize-tech-stack-contract.test.ts | 54 ++++++ .../sections/home/feature-cards-section.tsx | 24 ++- .../sections/home/social-proof-section.tsx | 42 +++-- .../sections/home/start-building-section.tsx | 4 +- .../site-header/site-header-client.tsx | 6 +- apps/web/constants/routes.ts | 2 +- apps/web/lib/__tests__/link-policy.test.ts | 50 +++++- .../lib/__tests__/social-proof-stats.test.ts | 77 ++++++++ apps/web/lib/active-circles.ts | 10 +- apps/web/lib/circle-events.ts | 5 +- apps/web/lib/logos-data-api.ts | 12 ++ apps/web/lib/social-proof-stats.ts | 131 ++++++++++++++ apps/web/lib/tech-stack-overview.ts | 20 +++ apps/web/messages/en.json | 85 +-------- content/builders-hub/resources/en.json | 2 +- content/builders-hub/settings/en.json | 6 +- content/pages/en/basecamp.json | 4 +- content/pages/en/home.json | 9 +- .../pages/en/technology-stack-blockchain.json | 21 ++- .../pages/en/technology-stack-messaging.json | 9 +- .../pages/en/technology-stack-networking.json | 9 +- .../pages/en/technology-stack-storage.json | 9 +- content/pages/en/technology-stack.json | 20 ++- 33 files changed, 569 insertions(+), 491 deletions(-) create mode 100644 apps/web/app/__tests__/home-feature-cards-contract.test.ts create mode 100644 apps/web/app/__tests__/lambda-prize-tech-stack-contract.test.ts create mode 100644 apps/web/lib/__tests__/social-proof-stats.test.ts create mode 100644 apps/web/lib/logos-data-api.ts create mode 100644 apps/web/lib/social-proof-stats.ts create mode 100644 apps/web/lib/tech-stack-overview.ts diff --git a/apps/web/app/[locale]/design-systems/_sections/components.tsx b/apps/web/app/[locale]/design-systems/_sections/components.tsx index 32026376cd..e137e2429d 100644 --- a/apps/web/app/[locale]/design-systems/_sections/components.tsx +++ b/apps/web/app/[locale]/design-systems/_sections/components.tsx @@ -7,7 +7,6 @@ import Image from 'next/image' import { getTranslations } from 'next-intl/server' import { - CardInfo, Footer, GiantSwitch, GiantSwitchTag, @@ -17,155 +16,28 @@ import { TableRow, ViewToggle, } from '@acid-info/logos-ui' -import { Button, Card } from '@/components/ui' +import type { Language } from '@repo/content/schemas' + +import { TechStackDiagram } from '@/components/sections/shared/tech-stack-diagram' +import { Button } from '@/components/ui' import { EXTERNAL_URLS, ROUTES } from '@/constants/routes' - -const cardImages = { - storage: '/design-systems/storage.png', - messaging: '/design-systems/messaging.png', - blockchain: '/design-systems/blockchain.png', - userModules: '/design-systems/user-modules.png', - networking: '/design-systems/networking.png', - kernel: '/design-systems/kernel.png', -} as const - -function Thumb({ src, alt }: { src: string; alt: string }) { - return {alt} -} +import { getHomeTechStackOverview } from '@/lib/tech-stack-overview' // --- Cards -------------------------------------------------------------- -function CardGrid({ state }: { state: 'default' | 'hover' }) { - const isHover = state === 'hover' - const networkingTitle = ( - <> - The Networking Stack: - Discovery, Peering, and Mix-Net - - ) +export async function Cards({ locale }: { locale: Language }) { + const techStack = await getHomeTechStackOverview(locale) return ( -
- {/* Row 1: four small cards */} -
- } - title="Storage" - description={ - isHover - ? 'Decentralised file storage and retrieval, using content-addressed (CID-based) data' - : undefined - } - ctaHref={isHover ? ROUTES.storage : undefined} - /> - } - title="Messaging" - description={ - isHover - ? 'Private, censorship-resistant communication between parties.' - : undefined - } - ctaHref={isHover ? ROUTES.messaging : undefined} - /> - } - title="Blockchain" - description={ - isHover ? 'Decentralised compute and consensus.' : undefined - } - ctaHref={isHover ? ROUTES.blockchain : undefined} - > - {isHover && ( - <> - - - - )} - - } - title="User Modules" - description={ - isHover - ? 'Anyone can build modules that plug into the same IPC infrastructure.' - : undefined - } - ctaHref={isHover ? ROUTES.technologyStack : undefined} - /> -
- - {/* Row 2: wide band — Networking Stack (no Lambda icon glyph in Figma) */} - } - title={networkingTitle} - description={ - isHover - ? 'This layer handles how Logos nodes find each other, establish connections, and communicate.' - : undefined - } - ctaHref={isHover ? ROUTES.networking : undefined} +
+

+ Cards +

+ - - {/* Row 3: wide band — Foundation Kernel (no Lambda icon glyph in Figma) */} - } - title="The Foundation: Logos Kernel" - description={ - isHover - ? 'A microkernel that handles the essential primitives every decentralised application needs.' - : undefined - } - ctaHref={isHover ? ROUTES.technologyStack : undefined} - /> -
- ) -} - -export function Cards() { - return ( -
-
-

- Default -

- -
- -
-

- Hover -

- -
) } @@ -183,7 +55,7 @@ export function Buttons() {

Primary

-
@@ -191,7 +63,7 @@ export function Buttons() {

Secondary

- @@ -199,7 +71,7 @@ export function Buttons() {

Tertiary

- @@ -207,7 +79,7 @@ export function Buttons() {

Link

- diff --git a/apps/web/app/[locale]/design-systems/page.tsx b/apps/web/app/[locale]/design-systems/page.tsx index 861c6cbe98..33166d0390 100644 --- a/apps/web/app/[locale]/design-systems/page.tsx +++ b/apps/web/app/[locale]/design-systems/page.tsx @@ -7,6 +7,8 @@ * - components.tsx → Cards / Buttons / Tables / GiantSwitches / * ViewToggles / Paginations / Footers */ +import { isActiveLocale } from '@repo/content/locales' + import { createDefaultMetadata } from '@/lib/metadata' import { ColorPalette } from './_sections/color-palette' @@ -37,12 +39,21 @@ export async function generateMetadata({ }) } -export default function DesignSystemsPage() { +export default async function DesignSystemsPage({ + params, +}: { + params: Promise<{ locale: string }> +}) { + const { locale } = await params + if (!isActiveLocale(locale)) { + throw new Error(`DesignSystemsPage received non-active locale "${locale}"`) + } + return (
- + diff --git a/apps/web/app/[locale]/get-started/page.tsx b/apps/web/app/[locale]/get-started/page.tsx index 90a52e4847..377c81a4e0 100644 --- a/apps/web/app/[locale]/get-started/page.tsx +++ b/apps/web/app/[locale]/get-started/page.tsx @@ -11,10 +11,10 @@ const NAMESPACE = 'pages.getStarted' const docs = [ { key: 'architecture', href: EXTERNAL_URLS.docs }, - { key: 'blockchain', href: `${EXTERNAL_URLS.docs}/logos-stack/blockchain` }, - { key: 'messaging', href: `${EXTERNAL_URLS.docs}/logos-stack/messaging` }, - { key: 'storage', href: `${EXTERNAL_URLS.docs}/logos-stack/storage` }, - { key: 'api', href: `${EXTERNAL_URLS.docs}/api` }, + { key: 'blockchain', href: EXTERNAL_URLS.docs }, + { key: 'messaging', href: EXTERNAL_URLS.docs }, + { key: 'storage', href: EXTERNAL_URLS.docs }, + { key: 'api', href: EXTERNAL_URLS.docs }, ] as const const communityLinks = [ diff --git a/apps/web/app/[locale]/lambda-prize/_sections/lambda-prize-page.tsx b/apps/web/app/[locale]/lambda-prize/_sections/lambda-prize-page.tsx index 6fe1c7b773..7d4e4f4987 100644 --- a/apps/web/app/[locale]/lambda-prize/_sections/lambda-prize-page.tsx +++ b/apps/web/app/[locale]/lambda-prize/_sections/lambda-prize-page.tsx @@ -1,10 +1,11 @@ import Image from 'next/image' import { LogosMark } from '@acid-info/logos-ui' +import type { TechStackOverviewSection } from '@repo/content/schemas' +import TechStackSection from '@/components/sections/home/tech-stack-section' import { Button, ButtonArrowIcon } from '@/components/ui' import { ROUTES } from '@/constants/routes' import { Link } from '@/i18n/navigation' -import { DownloadIcon } from '@/components/sections/shared/builder-cta-card' interface RowCopy { label: string @@ -18,17 +19,6 @@ interface PrizeCopy { status: string } -interface TechCardCopy { - title: string - body: string - badges?: string[] -} - -interface TechWideCopy { - title: string - body: string -} - interface SupportRowCopy extends RowCopy { action: string } @@ -62,22 +52,6 @@ interface LambdaPrizePageCopy { secondaryCta: string rows: RowCopy[] } - techStack: { - disclaimer: string - heading: string - body: string - cta: string - basecamp: { - title: string - body: string - cta: string - } - cards: TechCardCopy[] - networking: TechWideCopy - foundation: TechWideCopy - primaryCta: string - secondaryCta: string - } support: { heading: string body: string @@ -88,6 +62,7 @@ interface LambdaPrizePageCopy { interface LambdaPrizePageProps { copy: LambdaPrizePageCopy + techStack: TechStackOverviewSection } const prizeImages = [ @@ -327,113 +302,6 @@ function AboutProgramme({ copy }: { copy: LambdaPrizePageCopy['about'] }) { ) } -function TechCard({ card }: { card: TechCardCopy }) { - return ( -
-
- -

{card.title}

-

{card.body}

- {card.badges ? ( -
- {card.badges.map((badge) => ( -
- {badge} -
- ))} -
- ) : null} -
-
- ) -} - -function WideTechCard({ - title, - body, - className = '', -}: TechWideCopy & { className?: string }) { - return ( -
-
-

- {title} -

-

{body}

-
-
- ) -} - -function TechStack({ copy }: { copy: LambdaPrizePageCopy['techStack'] }) { - const headingLines = copy.heading.split('\n') - - return ( -
-
-

{copy.disclaimer}

-
- - -
-
-
-

- {headingLines.map((line) => ( - - {line} - - ))} -

-

{copy.body}

-
- {copy.cta} -
-
-
-
-
-
- -

{copy.basecamp.title}

-
-

- {copy.basecamp.body} -

-
- -
-
- {copy.cards.map((card) => ( - - ))} -
- - -
-
- ) -} - function Support({ copy }: { copy: LambdaPrizePageCopy['support'] }) { return (
@@ -469,14 +337,18 @@ function Support({ copy }: { copy: LambdaPrizePageCopy['support'] }) { ) } -export function LambdaPrizePage({ copy }: LambdaPrizePageProps) { +export function LambdaPrizePage({ copy, techStack }: LambdaPrizePageProps) { return (
- +
) diff --git a/apps/web/app/[locale]/lambda-prize/page.tsx b/apps/web/app/[locale]/lambda-prize/page.tsx index 3f97396706..c1fa0a6b5a 100644 --- a/apps/web/app/[locale]/lambda-prize/page.tsx +++ b/apps/web/app/[locale]/lambda-prize/page.tsx @@ -1,6 +1,9 @@ import { getTranslations } from 'next-intl/server' +import { isActiveLocale } from '@repo/content/locales' + import { ROUTES } from '@/constants/routes' +import { getHomeTechStackOverview } from '@/lib/tech-stack-overview' import { createTranslatedPageMetadata } from '@/lib/translated-page-metadata' import { LambdaPrizePage } from './_sections/lambda-prize-page' @@ -18,10 +21,15 @@ export default async function Page({ params: Promise<{ locale: string }> }) { const { locale } = await params + if (!isActiveLocale(locale)) { + throw new Error(`LambdaPrizePage received non-active locale "${locale}"`) + } const t = await getTranslations({ locale, namespace: NAMESPACE }) + const techStack = await getHomeTechStackOverview(locale) return ( (page.sections, 'hero', 'home.atf') + const techStack = findSection( page.sections, 'techStackOverview', 'home.techStack' ) + const parallelSocietyHeadline = findSection( page.sections, 'featuredText', 'home.parallelSocietyHeadline' ) + const parallelSocietyGallery = findSection( page.sections, 'gallery', 'home.parallelSociety' ) + const press = findSection( page.sections, 'relatedArticles', 'home.press' ) + const circlesCta = findSection( page.sections, 'featuredText', 'home.circlesCta' ) - const articles = await getLatestPressArticles(press.visibleCount ?? 4) + const [articles, socialProofStats] = await Promise.all([ + getLatestPressArticles(press.visibleCount ?? 4), + getSocialProofStats(), + ]) return ( <> - + diff --git a/apps/web/app/[locale]/technology-stack/messaging/page.tsx b/apps/web/app/[locale]/technology-stack/messaging/page.tsx index e43bfafc91..678971ea25 100644 --- a/apps/web/app/[locale]/technology-stack/messaging/page.tsx +++ b/apps/web/app/[locale]/technology-stack/messaging/page.tsx @@ -33,10 +33,8 @@ const findSection = createSectionFinder('messaging') * - messaging.builderCta → MessagingBuilderCta * - messaging.relatedArticles → MessagingRelatedArticles * - * `MessagingTechStack` is deferred (same rationale as `StorageTechStack`): - * it composes the global techStackOverview with messaging-specific intro - * copy. Migration awaits either a cross-page shared partial or extra fields - * on `techStackOverview`. + * `MessagingTechStack` composes the shared tech stack explorer so the card + * grid stays aligned with `/technology-stack`. */ export default async function MessagingPage({ params, diff --git a/apps/web/app/[locale]/technology-stack/storage/page.tsx b/apps/web/app/[locale]/technology-stack/storage/page.tsx index 78f661a4d7..9e9c69a228 100644 --- a/apps/web/app/[locale]/technology-stack/storage/page.tsx +++ b/apps/web/app/[locale]/technology-stack/storage/page.tsx @@ -34,11 +34,8 @@ const findSection = createSectionFinder('storage') * - storage.builderCta → StorageBuilderCta * - storage.relatedArticles → StorageRelatedArticles * - * `StorageTechStack` is deferred: it composes the global techStackOverview - * (shared with `/technology-stack`) with a storage-specific intro and per-card - * body text (`pages.storage.techStack.*`). Migrating cleanly needs either a - * cross-page shared partial or extra fields on `techStackOverview`. Until then - * the component continues to read from `messages.en.json`. + * `StorageTechStack` composes the shared tech stack explorer so the card grid + * stays aligned with `/technology-stack`. */ export default async function StoragePage({ params, diff --git a/apps/web/app/__tests__/broadcast-network-page-contract.test.ts b/apps/web/app/__tests__/broadcast-network-page-contract.test.ts index 91194157b5..721a3a77f6 100644 --- a/apps/web/app/__tests__/broadcast-network-page-contract.test.ts +++ b/apps/web/app/__tests__/broadcast-network-page-contract.test.ts @@ -19,4 +19,17 @@ describe('broadcast network page contract', () => { expect(source).toContain('h-[107px] w-[190px]') expect(source).not.toContain('size-[107px]') }) + + test('header background matches the press audio page tone', () => { + const source = readComponent( + '../components/site-header/site-header-client.tsx' + ) + + expect(source).toContain('usesAccentTanHeaderTone') + expect(source).toContain('ROUTES.logosBroadcastNetwork') + expect(source).toContain('ROUTES.podcast') + expect(source).toContain( + "usesAccentTanHeaderTone && 'bg-accent-tan'" + ) + }) }) diff --git a/apps/web/app/__tests__/home-feature-cards-contract.test.ts b/apps/web/app/__tests__/home-feature-cards-contract.test.ts new file mode 100644 index 0000000000..9915662526 --- /dev/null +++ b/apps/web/app/__tests__/home-feature-cards-contract.test.ts @@ -0,0 +1,24 @@ +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' + +import { describe, expect, test } from 'vitest' + +function readAppFile(path: string) { + return readFileSync(fileURLToPath(new URL(`../${path}`, import.meta.url)), { + encoding: 'utf8', + }) +} + +describe('home feature cards contract', () => { + test('uses the whole path card as the link target', () => { + const source = readAppFile( + '../components/sections/home/feature-cards-section.tsx' + ) + + expect(source).toContain("import { Link } from '@/i18n/navigation'") + expect(source).toContain(' { + test('reuses the home technology stack section instead of local cards', () => { + const page = readAppFile( + '../app/[locale]/lambda-prize/_sections/lambda-prize-page.tsx' + ) + + expect(page).toContain( + "import TechStackSection from '@/components/sections/home/tech-stack-section'" + ) + expect(page).not.toContain('function TechCard') + expect(page).not.toContain('function WideTechCard') + expect(page).not.toContain('interface TechCardCopy') + }) + + test('keeps the design systems card sample on the shared diagram', () => { + const page = readAppFile( + '../app/[locale]/design-systems/_sections/components.tsx' + ) + + expect(page).toContain( + "import { TechStackDiagram } from '@/components/sections/shared/tech-stack-diagram'" + ) + expect(page).not.toContain('function CardGrid') + expect(page).not.toContain('const cardImages') + expect(page).not.toContain('The Foundation: Logos Kernel') + }) + + test('does not keep page-local technology stack message copies', () => { + const messages = JSON.parse(readAppFile('../messages/en.json')) as { + pages: { + storage?: { techStack?: unknown } + technologyStack: { stack: Record } + } + } + + expect(messages.pages.storage?.techStack).toBeUndefined() + expect(Object.keys(messages.pages.technologyStack.stack).sort()).toEqual([ + 'body', + 'titleLine1', + 'titleLine2', + ]) + }) +}) diff --git a/apps/web/components/sections/home/feature-cards-section.tsx b/apps/web/components/sections/home/feature-cards-section.tsx index 0496639662..90d3c6c631 100644 --- a/apps/web/components/sections/home/feature-cards-section.tsx +++ b/apps/web/components/sections/home/feature-cards-section.tsx @@ -5,8 +5,9 @@ import { useTranslations } from 'next-intl' import { LogosMark } from '@acid-info/logos-ui' -import { Button } from '@/components/ui' +import { ButtonArrowIcon } from '@/components/ui' import { ROUTES } from '@/constants/routes' +import { Link } from '@/i18n/navigation' interface PathCard { key: 'build' | 'operate' | 'activism' @@ -21,8 +22,9 @@ interface PathCard { function PathCardView({ card }: { card: PathCard }) { return ( -
{card.title}
- + + + {card.cta} + + + +

{card.body}

- + ) } diff --git a/apps/web/components/sections/home/social-proof-section.tsx b/apps/web/components/sections/home/social-proof-section.tsx index 7acf527701..9275367bb5 100644 --- a/apps/web/components/sections/home/social-proof-section.tsx +++ b/apps/web/components/sections/home/social-proof-section.tsx @@ -3,8 +3,10 @@ import Image from 'next/image' import { useTranslations } from 'next-intl' +import type { SocialProofStats } from '@/lib/social-proof-stats' + interface SocialProofCard { - key: 'contributions' | 'nodes' | 'circles' | 'issues' + key: 'contributions' | 'contributors' | 'repositories' | 'circles' value: string label: string body: string @@ -49,13 +51,17 @@ function SocialProofCardView({ ) } -export default function SocialProofSection() { +interface SocialProofSectionProps { + stats: SocialProofStats +} + +export default function SocialProofSection({ stats }: SocialProofSectionProps) { const t = useTranslations('home.socialProof') const cards: SocialProofCard[] = [ { key: 'contributions', - value: t('contributions.value'), + value: stats.contributions, label: t('contributions.label'), body: t('contributions.body'), image: '/images/home/figma-refresh/social-contributions.webp', @@ -63,31 +69,31 @@ export default function SocialProofSection() { imageClassName: 'object-[47%_52%]', }, { - key: 'nodes', - value: t('nodes.value'), - label: t('nodes.label'), - body: t('nodes.body'), + key: 'contributors', + value: stats.contributors, + label: t('contributors.label'), + body: t('contributors.body'), image: '/images/home/figma-refresh/social-node.webp', mobileClassName: 'h-[432px]', imageClassName: 'object-[50%_52%]', }, + { + key: 'repositories', + value: stats.repositories, + label: t('repositories.label'), + body: t('repositories.body'), + image: '/images/home/figma-refresh/social-issues.webp', + mobileClassName: 'h-[411px]', + imageClassName: 'object-[47%_51%]', + }, { key: 'circles', - value: t('circles.value'), + value: stats.circles, label: t('circles.label'), body: t('circles.body'), image: '/images/home/figma-refresh/social-circles.webp', - mobileClassName: 'h-[411px]', - imageClassName: 'object-[50%_50%]', - }, - { - key: 'issues', - value: t('issues.value'), - label: t('issues.label'), - body: t('issues.body'), - image: '/images/home/figma-refresh/social-issues.webp', mobileClassName: 'h-[432px]', - imageClassName: 'object-[47%_51%]', + imageClassName: 'object-[50%_50%]', }, ] diff --git a/apps/web/components/sections/home/start-building-section.tsx b/apps/web/components/sections/home/start-building-section.tsx index a3061a50ab..8be33abbf5 100644 --- a/apps/web/components/sections/home/start-building-section.tsx +++ b/apps/web/components/sections/home/start-building-section.tsx @@ -2,7 +2,7 @@ import { getTranslations } from 'next-intl/server' import Image from 'next/image' import { Button } from '@/components/ui' -import { ROUTES } from '@/constants/routes' +import { EXTERNAL_URLS, ROUTES } from '@/constants/routes' interface SupportCard { title: string @@ -38,7 +38,7 @@ export default async function StartBuildingSection({ { title: t('lambdaPrize'), href: ROUTES.lambdaPrize, cta: t('cardCta') }, { title: t('rfps'), href: ROUTES.rfps, cta: t('cardCta') }, { title: t('ideas'), href: ROUTES.ideas, cta: t('cardCta') }, - { title: t('docs'), href: ROUTES.technologyStack, cta: t('cardCta') }, + { title: t('docs'), href: EXTERNAL_URLS.docs, cta: t('cardCta') }, ] return ( diff --git a/apps/web/components/site-header/site-header-client.tsx b/apps/web/components/site-header/site-header-client.tsx index 62997727a6..fa979edcbd 100644 --- a/apps/web/components/site-header/site-header-client.tsx +++ b/apps/web/components/site-header/site-header-client.tsx @@ -81,6 +81,9 @@ export default function SiteHeaderClient({ normalizedPathname.endsWith(ROUTES.lambdaPrize) const usesTransparentHeader = normalizedPathname.endsWith(ROUTES.press) const usesOverlayHeader = usesHeroHeaderTone || usesTransparentHeader + const usesAccentTanHeaderTone = + normalizedPathname.endsWith(ROUTES.logosBroadcastNetwork) || + normalizedPathname.endsWith(ROUTES.podcast) const open = () => { setInitialPanelLabel(null) setIsOpen(true) @@ -135,7 +138,8 @@ export default function SiteHeaderClient({
join(repoRoot, path)) const jobsHref = 'https://free.technology/jobs' const onboardingCalendarHref = 'https://cal.com/team/logos-onboarding/intro' +const logosDocsHref = 'https://github.com/logos-co/logos-docs' +const docsLabels = new Set(['docs', 'documentation', 'view the docs']) const routeUsageAllowlist = new Set([ 'apps/web/app/[locale]/work-with-us/page.tsx', 'apps/web/app/sitemap.ts', @@ -40,6 +43,31 @@ const routeUsageAllowlist = new Set([ 'apps/web/lib/__tests__/link-policy.test.ts', ]) +const collectDocsLinks = (value: unknown): Array<{ label: string; href: string }> => { + if (Array.isArray(value)) { + return value.flatMap(collectDocsLinks) + } + + if (!value || typeof value !== 'object') return [] + + const record = value as Record + const docsText = + typeof record.label === 'string' && docsLabels.has(record.label.toLowerCase()) + ? record.label + : typeof record.title === 'string' && + docsLabels.has(record.title.toLowerCase()) + ? record.title + : null + const ownLink = + docsText && typeof record.href === 'string' + ? [{ label: docsText, href: record.href }] + : [] + + return ownLink.concat( + Object.values(record).flatMap((child) => collectDocsLinks(child)) + ) +} + const collectTextFiles = (dir: string): string[] => { return readdirSync(dir).flatMap((entry) => { const path = join(dir, entry) @@ -148,6 +176,26 @@ describe('link policy', () => { ) }) + it('routes Docs and Documentation links to the Logos docs repository', () => { + const contentDocsLinks = [ + footer, + navigation, + buildersHubResources, + buildersHubSettings, + ...readdirSync(join(repoRoot, 'content/pages/en')) + .filter((entry) => entry.endsWith('.json')) + .map((entry) => + JSON.parse(readFileSync(join(repoRoot, 'content/pages/en', entry), 'utf8')) + ), + ].flatMap(collectDocsLinks) + + expect(EXTERNAL_URLS.docs).toBe(logosDocsHref) + expect(contentDocsLinks.length).toBeGreaterThan(0) + expect(contentDocsLinks).toEqual( + contentDocsLinks.map((link) => ({ ...link, href: logosDocsHref })) + ) + }) + it('does not use the retired Work With Us route for public CTAs', () => { const offenders = scannedRoots.flatMap(collectTextFiles).flatMap((file) => { const relativePath = relative(repoRoot, file) diff --git a/apps/web/lib/__tests__/social-proof-stats.test.ts b/apps/web/lib/__tests__/social-proof-stats.test.ts new file mode 100644 index 0000000000..b65fcc9f78 --- /dev/null +++ b/apps/web/lib/__tests__/social-proof-stats.test.ts @@ -0,0 +1,77 @@ +import { afterEach, describe, expect, test, vi } from 'vitest' + +import { getSocialProofStats } from '../social-proof-stats' + +const jsonResponse = (payload: unknown): Response => + ({ + ok: true, + status: 200, + json: async () => payload, + }) as unknown as Response + +afterEach(() => { + vi.restoreAllMocks() +}) + +describe('getSocialProofStats', () => { + test('maps contribution and circle API totals for the home social proof cards', async () => { + const fetchMock = vi + .spyOn(globalThis, 'fetch') + .mockResolvedValueOnce( + jsonResponse({ + stg_external_contributors_agg_total_ext_contributions: [ + { + total_commits: 3441, + total_external_collaborators: 218, + total_repositories: 341, + }, + ], + }) + ) + .mockResolvedValueOnce( + jsonResponse({ + data: { + stg_external_circle_circle_event_aggregate: { + aggregate: { + count: 34, + }, + }, + }, + }) + ) + + await expect(getSocialProofStats()).resolves.toEqual({ + contributions: '3,441', + contributors: '218', + repositories: '341', + circles: '34', + }) + + expect(fetchMock).toHaveBeenCalledTimes(2) + expect(fetchMock).toHaveBeenNthCalledWith( + 1, + 'https://hasura.bi.status.im/api/rest/contributions/count_all', + { cache: 'force-cache' } + ) + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'https://hasura.bi.status.im/v1/graphql', + expect.objectContaining({ + method: 'POST', + cache: 'force-cache', + headers: { 'Content-Type': 'application/json' }, + }) + ) + }) + + test('keeps the requested stats as build fallback when the APIs fail', async () => { + vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('network down')) + + await expect(getSocialProofStats()).resolves.toEqual({ + contributions: '3,441', + contributors: '218', + repositories: '341', + circles: '34', + }) + }) +}) diff --git a/apps/web/lib/active-circles.ts b/apps/web/lib/active-circles.ts index 7367717d27..2aba5d4215 100644 --- a/apps/web/lib/active-circles.ts +++ b/apps/web/lib/active-circles.ts @@ -1,11 +1,11 @@ import { getLatestEventsByCity } from './circle-events' +import { + BI_GRAPHQL_API_URL, + CIRCLES_GRAPHQL_RESPONSE_KEY, + LOGOS_GRAPHQL_API_URL, +} from './logos-data-api' import { logger } from './logger' -const LOGOS_GRAPHQL_API_URL = 'https://api.logos.co/v1/graphql' -const BI_GRAPHQL_API_URL = 'https://hasura.bi.status.im/v1/graphql' -const CIRCLES_GRAPHQL_RESPONSE_KEY = - 'stg_external_circle_circle_event_aggregate' - export const ACTIVE_CIRCLES_DAYS = 120 type GraphqlResponse = { diff --git a/apps/web/lib/circle-events.ts b/apps/web/lib/circle-events.ts index d3df2f22eb..db8e41fc81 100644 --- a/apps/web/lib/circle-events.ts +++ b/apps/web/lib/circle-events.ts @@ -1,7 +1,6 @@ +import { LOGOS_GRAPHQL_API_URL } from './logos-data-api' import { logger } from './logger' -const HASURA_URL = 'https://api.logos.co/v1/graphql' - const CIRCLE_EVENTS_QUERY = `query CircleEvents { stg_external_circle_circle_event { event_id @@ -41,7 +40,7 @@ interface HasuraResponse { */ export async function fetchCircleEvents(): Promise { try { - const response = await fetch(HASURA_URL, { + const response = await fetch(LOGOS_GRAPHQL_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: CIRCLE_EVENTS_QUERY }), diff --git a/apps/web/lib/logos-data-api.ts b/apps/web/lib/logos-data-api.ts new file mode 100644 index 0000000000..db5483a166 --- /dev/null +++ b/apps/web/lib/logos-data-api.ts @@ -0,0 +1,12 @@ +export const LOGOS_GRAPHQL_API_URL = 'https://api.logos.co/v1/graphql' + +export const BI_GRAPHQL_API_URL = 'https://hasura.bi.status.im/v1/graphql' + +export const CONTRIBUTIONS_API_URL = + 'https://hasura.bi.status.im/api/rest/contributions/count_all' + +export const CONTRIBUTIONS_API_RESPONSE_KEY = + 'stg_external_contributors_agg_total_ext_contributions' + +export const CIRCLES_GRAPHQL_RESPONSE_KEY = + 'stg_external_circle_circle_event_aggregate' diff --git a/apps/web/lib/social-proof-stats.ts b/apps/web/lib/social-proof-stats.ts new file mode 100644 index 0000000000..33b89d4a3a --- /dev/null +++ b/apps/web/lib/social-proof-stats.ts @@ -0,0 +1,131 @@ +import { + BI_GRAPHQL_API_URL, + CIRCLES_GRAPHQL_RESPONSE_KEY, + CONTRIBUTIONS_API_RESPONSE_KEY, + CONTRIBUTIONS_API_URL, +} from './logos-data-api' +import { logger } from './logger' + +const ACTIVE_CIRCLES_DAYS = 120 + +const FALLBACK_SOCIAL_PROOF_STATS = { + contributions: 3441, + contributors: 218, + repositories: 341, + circles: 34, +} as const + +type ContributionsApiData = { + total_commits?: number | null + total_external_collaborators?: number | null + total_repositories?: number | null +} + +type ContributionsApiResponse = { + [CONTRIBUTIONS_API_RESPONSE_KEY]?: ContributionsApiData[] +} + +type CirclesApiResponse = { + data?: { + [CIRCLES_GRAPHQL_RESPONSE_KEY]?: { + aggregate?: { + count?: number | null + } + } + } +} + +export interface SocialProofStats { + contributions: string + contributors: string + repositories: string + circles: string +} + +function formatStat( + value: number | null | undefined, + fallback: number +): string { + return (value ?? fallback).toLocaleString('en-US') +} + +function getTodayISODateDaysAgo(days: number): string { + const date = new Date() + date.setDate(date.getDate() - days) + return date.toISOString().split('T')[0] ?? '' +} + +function getCirclesGraphqlQuery(dateString: string): string { + return `query CountDistinctCities { + ${CIRCLES_GRAPHQL_RESPONSE_KEY}(where: { end_at: { _gte: "${dateString}" } }) { + aggregate { + count(distinct: true, columns: location_city) + } + } +}` +} + +async function fetchContributionsData(): Promise { + try { + const response = await fetch(CONTRIBUTIONS_API_URL, { + cache: 'force-cache', + }) + + if (!response.ok) { + throw new Error(`Contributions request failed: ${response.status}`) + } + + const data = (await response.json()) as ContributionsApiResponse + return data[CONTRIBUTIONS_API_RESPONSE_KEY]?.[0] ?? null + } catch (error) { + logger.error('Failed to fetch social proof contribution stats', { error }) + return null + } +} + +async function fetchTotalCircles(): Promise { + try { + const activeSinceDate = getTodayISODateDaysAgo(ACTIVE_CIRCLES_DAYS) + const response = await fetch(BI_GRAPHQL_API_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: getCirclesGraphqlQuery(activeSinceDate), + }), + cache: 'force-cache', + }) + + if (!response.ok) { + throw new Error(`Circles request failed: ${response.status}`) + } + + const data = (await response.json()) as CirclesApiResponse + return data.data?.[CIRCLES_GRAPHQL_RESPONSE_KEY]?.aggregate?.count ?? null + } catch (error) { + logger.error('Failed to fetch social proof circle stats', { error }) + return null + } +} + +export async function getSocialProofStats(): Promise { + const [contributionsData, totalCircles] = await Promise.all([ + fetchContributionsData(), + fetchTotalCircles(), + ]) + + return { + contributions: formatStat( + contributionsData?.total_commits, + FALLBACK_SOCIAL_PROOF_STATS.contributions + ), + contributors: formatStat( + contributionsData?.total_external_collaborators, + FALLBACK_SOCIAL_PROOF_STATS.contributors + ), + repositories: formatStat( + contributionsData?.total_repositories, + FALLBACK_SOCIAL_PROOF_STATS.repositories + ), + circles: formatStat(totalCircles, FALLBACK_SOCIAL_PROOF_STATS.circles), + } +} diff --git a/apps/web/lib/tech-stack-overview.ts b/apps/web/lib/tech-stack-overview.ts new file mode 100644 index 0000000000..bb268e8acf --- /dev/null +++ b/apps/web/lib/tech-stack-overview.ts @@ -0,0 +1,20 @@ +import { getPageCopy } from '@repo/content/loaders' +import type { Language, TechStackOverviewSection } from '@repo/content/schemas' + +import { ROUTES } from '@/constants/routes' + +import { createSectionFinder } from './page-sections' + +const findHomeSection = createSectionFinder('home') + +export async function getHomeTechStackOverview( + locale: Language +): Promise { + const homePage = await getPageCopy(ROUTES.home, locale) + + return findHomeSection( + homePage.sections, + 'techStackOverview', + 'home.techStack' + ) +} diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index bd06a6e3d4..5990cc14ba 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -38,24 +38,20 @@ }, "socialProof": { "contributions": { - "value": "2262", - "label": "Contributions", + "label": "Total contributions", "body": "An open source community of builders, researchers, and technologists." }, - "nodes": { - "value": "205", - "label": "Node Operators", + "contributors": { + "label": "Active contributors", + "body": "An open source community of builders, researchers, and technologists." + }, + "repositories": { + "label": "Repositories", "body": "Open source tools, libraries, and infrastructure to inspect, fork, and build on." }, "circles": { - "value": "26", "label": "Circles", "body": "Local chapters of activists and change seekers solving real world issues." - }, - "issues": { - "value": "26", - "label": "Winnable Issues", - "body": "Local issues that Circles identify and solve – from privacy tech to community funding." } }, "build": { @@ -305,29 +301,7 @@ "stack": { "titleLine1": "Explore the Logos", "titleLine2": "Technology Stack.", - "body": "The Logos technology stack is a unified ecosystem of distinct modules. The entire stack is modular so developers can assemble their own Logos-based platform with a different selection of modules.", - "storage": "Storage", - "messaging": "Messaging", - "blockchain": "Blockchain", - "userModules": "User Modules", - "basecamp": "Basecamp", - "basecampBody": "Lorem ipsum dolor sit amet consectetur lorem ipsum dolor sit amet consectetur lorem.", - "basecampCta": "Install", - "networkingLine1": "The Networking Stack:", - "networkingLine2": "Discovery, Peering, and Mix-Net", - "foundation": "The Foundation: Logos Kernel" - } - }, - "storage": { - "techStack": { - "storageBody": "Decentralised file storage and retrieval, using content-addressed (CID-based) data", - "messagingBody": "Private, censorship-resistant communication between parties.", - "blockchainBody": "Decentralised compute and consensus.", - "userModulesBody": "Anyone can build modules that plug into the same IPC infrastructure.", - "networkingBody": "This layer handles how Logos nodes find each other, establish connections, and communicate.", - "foundationBody": "A microkernel that handles the essential primitives every decentralised application needs.", - "lez": "LOGOS Execution zone (LEZ)", - "cryptarchia": "Data Availability and Consensus: Cryptarchia" + "body": "The Logos technology stack is a unified ecosystem of distinct modules. The entire stack is modular so developers can assemble their own Logos-based platform with a different selection of modules." } }, "ideas": { @@ -447,49 +421,6 @@ } } }, - "techStack": { - "disclaimer": "Disclaimer: This diagram oversimplifies the stack.", - "heading": "The Logos\nTechnology Stack", - "body": "Logos provides developers with a modular, plugin-based runtime for building decentralised applications.", - "cta": "See the stack", - "primaryCta": "Documentation", - "secondaryCta": "Builder Hub", - "basecamp": { - "title": "Basecamp", - "body": "Basecamp bundles the Logos stack into a practical starting point for builders.", - "cta": "Install" - }, - "cards": { - "storage": { - "title": "Storage", - "body": "Decentralised file storage and retrieval, using content-addressed (CID-based) data" - }, - "messaging": { - "title": "Messaging", - "body": "Private, censorship-resistant communication between parties." - }, - "blockchain": { - "title": "Blockchain", - "body": "Decentralised compute and consensus.", - "badges": { - "1": "LOGOS Execution zone (LEZ)", - "2": "Data Availability and Consensus: Cryptarchia" - } - }, - "userModules": { - "title": "User Modules", - "body": "Anyone can build modules that plug into the same IPC infrastructure." - } - }, - "networking": { - "title": "The Networking Stack:\nDiscovery, Peering, and Mix-Net", - "body": "This layer handles how Logos nodes find each other, establish connections, and communicate." - }, - "foundation": { - "title": "The Foundation: Logos Kernel", - "body": "A microkernel that handles the essential primitives every decentralised application needs." - } - }, "support": { "heading": "Get Support", "body": "Find examples, tutorials, and community channels for building on Logos.", diff --git a/content/builders-hub/resources/en.json b/content/builders-hub/resources/en.json index 0faf852e4f..2c3ef3278a 100644 --- a/content/builders-hub/resources/en.json +++ b/content/builders-hub/resources/en.json @@ -15,7 +15,7 @@ "title": "Documentation", "description": "Reference for the kernel, modules, and UI packages.", "ctaLabel": "Browse", - "href": "/technology-stack", + "href": "https://github.com/logos-co/logos-docs", "status": "published" }, { diff --git a/content/builders-hub/settings/en.json b/content/builders-hub/settings/en.json index 222e99db8e..e32ab02b6c 100644 --- a/content/builders-hub/settings/en.json +++ b/content/builders-hub/settings/en.json @@ -15,7 +15,11 @@ "href": "https://discord.gg/logosnetwork", "external": true }, - { "label": "View the Docs", "href": "/technology-stack" } + { + "label": "View the Docs", + "href": "https://github.com/logos-co/logos-docs", + "external": true + } ] }, "journey": { diff --git a/content/pages/en/basecamp.json b/content/pages/en/basecamp.json index a6b3c0c98c..4cba0bef9e 100644 --- a/content/pages/en/basecamp.json +++ b/content/pages/en/basecamp.json @@ -54,7 +54,7 @@ "description": "Basecamp is built around a modular architecture. At its core is a lightweight runtime that manages self-contained modules, each providing a distinct capability such as private peer-to-peer messaging or distributed storage.", "cta": { "label": "Documentation", - "href": "https://docs.logos.co", + "href": "https://github.com/logos-co/logos-docs", "external": true, "variant": "primary" }, @@ -176,7 +176,7 @@ "description": "Basecamp is an open-source application. If you want to explore the codebase, build from source, or develop your own modules, everything lives on GitHub.", "cta": { "label": "View the docs", - "href": "https://docs.logos.co", + "href": "https://github.com/logos-co/logos-docs", "external": true, "variant": "primary" } diff --git a/content/pages/en/home.json b/content/pages/en/home.json index d8b959d0bb..0cd1bbb9f9 100644 --- a/content/pages/en/home.json +++ b/content/pages/en/home.json @@ -73,7 +73,11 @@ "key": "home.techStack", "eyebrow": "A unified ecosystem. Private-by-default. Built for real life.", "title": "The Logos Technology Stack", - "cta": { "label": "See the Stack", "href": "/technology-stack" }, + "cta": { + "label": "See the Stack", + "href": "https://github.com/logos-co/logos-docs", + "external": true + }, "basecamp": { "title": "Basecamp", "body": "Lorem ipsum dolor sit amet consectetur lorem ipsum dolor sit amet consectetur lorem.", @@ -132,7 +136,8 @@ "subheadingExtra": "Privacy-preserving blockchain for sovereign order and decentralised governance.", "cta": { "label": "View the Docs", - "href": "/technology-stack" + "href": "https://github.com/logos-co/logos-docs", + "external": true }, "cards": [ { diff --git a/content/pages/en/technology-stack-blockchain.json b/content/pages/en/technology-stack-blockchain.json index a6bdda07b8..41703c219f 100644 --- a/content/pages/en/technology-stack-blockchain.json +++ b/content/pages/en/technology-stack-blockchain.json @@ -55,7 +55,8 @@ }, { "label": "Docs", - "href": "/technology-stack/blockchain", + "href": "https://github.com/logos-co/logos-docs", + "external": true, "variant": "secondary" } ] @@ -68,7 +69,8 @@ "description": "Logos Blockchain is a proof-of-stake layer-1 with privacy built into the consensus mechanism itself — not bolted on at the application layer. Block proposers are anonymous. Transactions are private by default. The chain supports the deployment of application-specific execution environments called Sovereign Zones, connected through Mantle Channels for interoperability.", "cta": { "label": "View the docs", - "href": "/technology-stack/blockchain" + "href": "https://github.com/logos-co/logos-docs", + "external": true } }, { @@ -79,7 +81,8 @@ "subheading": "A private proof-of-stake consensus mechanism where validator identities and stake amounts remain hidden.", "cta": { "label": "View the docs", - "href": "/technology-stack/blockchain" + "href": "https://github.com/logos-co/logos-docs", + "external": true }, "cards": [ { @@ -98,7 +101,8 @@ }, "secondaryCta": { "label": "Docs", - "href": "/technology-stack/blockchain", + "href": "https://github.com/logos-co/logos-docs", + "external": true, "variant": "secondary" } }, @@ -118,7 +122,8 @@ }, "secondaryCta": { "label": "Docs", - "href": "/technology-stack/blockchain", + "href": "https://github.com/logos-co/logos-docs", + "external": true, "variant": "secondary" } }, @@ -138,7 +143,8 @@ }, "secondaryCta": { "label": "Docs", - "href": "/technology-stack/blockchain", + "href": "https://github.com/logos-co/logos-docs", + "external": true, "variant": "secondary" } } @@ -153,7 +159,8 @@ "description": "Lorem ipsum dolor sit amet consectetur", "cta": { "label": "View the docs", - "href": "/technology-stack" + "href": "https://github.com/logos-co/logos-docs", + "external": true } }, { diff --git a/content/pages/en/technology-stack-messaging.json b/content/pages/en/technology-stack-messaging.json index f180a8c54a..669f446dba 100644 --- a/content/pages/en/technology-stack-messaging.json +++ b/content/pages/en/technology-stack-messaging.json @@ -47,7 +47,8 @@ }, { "label": "DOCS", - "href": "/technology-stack/messaging", + "href": "https://github.com/logos-co/logos-docs", + "external": true, "variant": "secondary" } ] @@ -60,7 +61,8 @@ "description": "Logos Messaging provides the messaging layer for the Logos stack, letting applications send messages, relay data, and coordinate without relying on centralised servers or exposing metadata to surveillance.\n\nIt uses a peer-to-peer relay network in which messages propagate across nodes with no central broker, and it supports private one-to-one messaging, group messaging, and public broadcast channels.\n\nMessages can be encrypted end-to-end, and the relay network is designed to resist traffic analysis, making it difficult for observers to determine who is communicating with whom.", "cta": { "label": "View the docs", - "href": "/technology-stack/messaging" + "href": "https://github.com/logos-co/logos-docs", + "external": true } }, { @@ -125,7 +127,8 @@ "description": "Lorem ipsum dolor sit amet consectetur", "cta": { "label": "View the docs", - "href": "/technology-stack" + "href": "https://github.com/logos-co/logos-docs", + "external": true } }, { diff --git a/content/pages/en/technology-stack-networking.json b/content/pages/en/technology-stack-networking.json index 7ffc3ecfbc..e7fac9a5ba 100644 --- a/content/pages/en/technology-stack-networking.json +++ b/content/pages/en/technology-stack-networking.json @@ -30,7 +30,8 @@ }, { "label": "Docs", - "href": "/technology-stack/networking", + "href": "https://github.com/logos-co/logos-docs", + "external": true, "variant": "secondary" } ] @@ -42,7 +43,8 @@ "description": "The networking layer handles how Logos nodes find each other, establish connections, and communicate. Private-by-default, this layer doesn't care whether modules above it are storing files, sending chat messages, or processing transactions. It provides private, reliable communication as a shared foundation.", "cta": { "label": "View the docs", - "href": "/technology-stack/networking" + "href": "https://github.com/logos-co/logos-docs", + "external": true } }, { @@ -90,7 +92,8 @@ "description": "Reference for the kernel, modules, and UI packages.", "cta": { "label": "View the docs", - "href": "/technology-stack" + "href": "https://github.com/logos-co/logos-docs", + "external": true } }, { diff --git a/content/pages/en/technology-stack-storage.json b/content/pages/en/technology-stack-storage.json index 9e60f9d01b..78779978e5 100644 --- a/content/pages/en/technology-stack-storage.json +++ b/content/pages/en/technology-stack-storage.json @@ -50,7 +50,8 @@ }, { "label": "DOCS", - "href": "/technology-stack/storage", + "href": "https://github.com/logos-co/logos-docs", + "external": true, "variant": "secondary" } ] @@ -63,7 +64,8 @@ "description": "Files are addressed by content rather than by location. When you store a file, the network returns a CID — a content identifier derived from the file itself — and anyone with that CID can retrieve the file from whichever nodes hold it. There is no central server to depend on and no single host to fail. The usage is straightforward: store a file and get back a content identifier; provide a content identifier and get back the file. Storage works both inside the Logos App and from your own applications, through either the high-level module API or the lower-level libstorage library.", "cta": { "label": "View the docs", - "href": "/technology-stack/storage" + "href": "https://github.com/logos-co/logos-docs", + "external": true } }, { @@ -140,7 +142,8 @@ "description": "Lorem ipsum dolor sit amet consectetur", "cta": { "label": "View the docs", - "href": "/technology-stack" + "href": "https://github.com/logos-co/logos-docs", + "external": true } }, { diff --git a/content/pages/en/technology-stack.json b/content/pages/en/technology-stack.json index b45b89c9b1..689b858be2 100644 --- a/content/pages/en/technology-stack.json +++ b/content/pages/en/technology-stack.json @@ -42,7 +42,8 @@ }, { "label": "Documentation", - "href": "/technology-stack#open-source", + "href": "https://github.com/logos-co/logos-docs", + "external": true, "variant": "secondary" } ], @@ -70,7 +71,8 @@ "eyebrow": "Disclaimer: Abstract representation of the stack.", "cta": { "label": "Docs", - "href": "/technology-stack#open-source" + "href": "https://github.com/logos-co/logos-docs", + "external": true }, "architecture": { "eyebrow": "Technology", @@ -81,7 +83,8 @@ ], "cta": { "label": "View the docs", - "href": "/technology-stack#stack" + "href": "https://github.com/logos-co/logos-docs", + "external": true }, "image": { "src": "/images/technology-stack/architecture-overview.jpg", @@ -209,7 +212,7 @@ }, "secondaryCta": { "label": "Docs", - "href": "https://docs.logos.co", + "href": "https://github.com/logos-co/logos-docs", "external": true } }, @@ -224,7 +227,7 @@ }, "secondaryCta": { "label": "Docs", - "href": "https://docs.waku.org", + "href": "https://github.com/logos-co/logos-docs", "external": true } }, @@ -239,7 +242,7 @@ }, "secondaryCta": { "label": "Docs", - "href": "https://docs.codex.storage", + "href": "https://github.com/logos-co/logos-docs", "external": true } }, @@ -254,7 +257,7 @@ }, "secondaryCta": { "label": "Docs", - "href": "https://vac.dev", + "href": "https://github.com/logos-co/logos-docs", "external": true } } @@ -268,7 +271,8 @@ "subheadingExtra": "Privacy-preserving blockchain for sovereign order and decentralised governance.", "cta": { "label": "View the Docs", - "href": "/technology-stack" + "href": "https://github.com/logos-co/logos-docs", + "external": true }, "cards": [ {