From 8083bb987fa44b3d47e99b64159ba3ffeaaa82eb Mon Sep 17 00:00:00 2001 From: jinhojang6 Date: Mon, 18 May 2026 15:30:43 +0900 Subject: [PATCH] fix: repair static seo output --- .github/workflows/test.yml | 1 + apps/web/app/[locale]/design-systems/page.tsx | 1 + .../[locale]/farewell-to-westphalia/page.tsx | 3 +- apps/web/app/{[locale] => }/robots.ts | 3 +- apps/web/app/sitemap.ts | 73 ++++++++++++++++--- .../sections/blockchain/blockchain-hero.tsx | 4 +- .../sections/networking/networking-hero.tsx | 4 +- apps/web/constants/site-config.ts | 8 +- apps/web/lib/metadata.ts | 8 +- apps/web/scripts/static-export-smoke.ts | 38 +++++++++- .../scripts/strip-default-locale-prefix.sh | 2 +- 11 files changed, 119 insertions(+), 26 deletions(-) rename apps/web/app/{[locale] => }/robots.ts (87%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1ecf116547..1572c0e64e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,6 +26,7 @@ jobs: - 5432:5432 env: DATABASE_URL: postgres://postgres:postgres@localhost:5432/logos_test + NEXT_PUBLIC_API_MODE: production NEXT_PUBLIC_SERVER_URL: http://localhost:3001 NEXT_PUBLIC_WEB_URL: http://localhost:3000 PAYLOAD_SECRET: test-secret-at-least-32-characters diff --git a/apps/web/app/[locale]/design-systems/page.tsx b/apps/web/app/[locale]/design-systems/page.tsx index 4248782adf..861c6cbe98 100644 --- a/apps/web/app/[locale]/design-systems/page.tsx +++ b/apps/web/app/[locale]/design-systems/page.tsx @@ -32,6 +32,7 @@ export async function generateMetadata({ description: 'Logos design tokens — Color Palette and Type Styles mirrored from Figma.', locale, + noindex: true, path: '/design-systems', }) } diff --git a/apps/web/app/[locale]/farewell-to-westphalia/page.tsx b/apps/web/app/[locale]/farewell-to-westphalia/page.tsx index 15e1ff76f5..1d81ea29dc 100644 --- a/apps/web/app/[locale]/farewell-to-westphalia/page.tsx +++ b/apps/web/app/[locale]/farewell-to-westphalia/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next' import { ROUTES } from '@/constants/routes' +import { absoluteUrl } from '@/lib/metadata' /** * Static-export-friendly redirect from /farewell-to-westphalia → /book. @@ -19,7 +20,7 @@ const TARGET = ROUTES.book export const metadata: Metadata = { title: 'Redirecting…', robots: { index: false, follow: false }, - alternates: { canonical: TARGET }, + alternates: { canonical: absoluteUrl(TARGET) }, } export default function FarewellToWestphaliaPage() { diff --git a/apps/web/app/[locale]/robots.ts b/apps/web/app/robots.ts similarity index 87% rename from apps/web/app/[locale]/robots.ts rename to apps/web/app/robots.ts index 9e61eb9b22..def4bd05b1 100644 --- a/apps/web/app/[locale]/robots.ts +++ b/apps/web/app/robots.ts @@ -1,4 +1,5 @@ -import { MetadataRoute } from 'next' +import type { MetadataRoute } from 'next' + import siteConfig from '@/constants/site-config' export const dynamic = 'force-static' diff --git a/apps/web/app/sitemap.ts b/apps/web/app/sitemap.ts index d9705afe97..87a4def486 100644 --- a/apps/web/app/sitemap.ts +++ b/apps/web/app/sitemap.ts @@ -1,21 +1,70 @@ -import { MetadataRoute } from 'next' +import type { MetadataRoute } from 'next' + +import { getAllIdeas, getAllRfps, getCircles } from '@repo/content/loaders' + import siteConfig from '@/constants/site-config' import { ROUTES } from '@/constants/routes' export const dynamic = 'force-static' -export default function sitemap(): MetadataRoute.Sitemap { - const siteUrl = siteConfig.url +const staticIndexableRoutes = [ + ROUTES.home, + ROUTES.about, + ROUTES.activeCircles, + ROUTES.blog, + ROUTES.book, + ROUTES.brandKit, + ROUTES.buildersHub, + ROUTES.ideas, + ROUTES.rfps, + ROUTES.circles, + ROUTES.faq, + ROUTES.lambdaPrize, + ROUTES.links, + ROUTES.logosBroadcastNetwork, + ROUTES.movement, + ROUTES.nodeProgram, + ROUTES.podcast, + ROUTES.press, + ROUTES.privacy, + ROUTES.security, + ROUTES.technologyStack, + ROUTES.blockchain, + ROUTES.messaging, + ROUTES.networking, + ROUTES.storage, + ROUTES.terms, + ROUTES.workWithUs, +] as const + +const buildSitemapEntry = ( + route: string, + lastModified: string +): MetadataRoute.Sitemap[number] => { + const normalizedSiteUrl = siteConfig.url.replace(/\/+$/, '') + return { + url: + route === '/' ? `${normalizedSiteUrl}/` : `${normalizedSiteUrl}${route}`, + lastModified, + } +} + +export default async function sitemap(): Promise { + const lastModified = new Date().toISOString().split('T')[0]! + const [rfps, ideas, circles] = await Promise.all([ + getAllRfps({ locale: 'en', status: 'published' }), + getAllIdeas({ locale: 'en', status: 'published' }), + getCircles({ locale: 'en', status: 'published' }), + ]) const routes = [ - ROUTES.home, - ROUTES.activeCircles, - ROUTES.lambdaPrize, - ROUTES.logosBroadcastNetwork, - ].map((route) => ({ - url: `${siteUrl}${route}`, - lastModified: new Date().toISOString().split('T')[0], - })) + ...staticIndexableRoutes, + ...rfps.map((rfp) => `${ROUTES.rfps}/${rfp.slug}`), + ...ideas.map((idea) => `${ROUTES.ideas}/${idea.slug}`), + ...circles.map((circle) => ROUTES.circle(circle.slug)), + ] - return [...routes] + return [...new Set(routes)] + .sort((a, b) => a.localeCompare(b)) + .map((route) => buildSitemapEntry(route, lastModified)) } diff --git a/apps/web/components/sections/blockchain/blockchain-hero.tsx b/apps/web/components/sections/blockchain/blockchain-hero.tsx index 27446cc581..813075bff1 100644 --- a/apps/web/components/sections/blockchain/blockchain-hero.tsx +++ b/apps/web/components/sections/blockchain/blockchain-hero.tsx @@ -33,9 +33,9 @@ export default function BlockchainHero({ data, backHref }: Props) {
- +

{data.headline} - +

{data.body ? ( diff --git a/apps/web/components/sections/networking/networking-hero.tsx b/apps/web/components/sections/networking/networking-hero.tsx index 8a37bdd30a..3083b1c6de 100644 --- a/apps/web/components/sections/networking/networking-hero.tsx +++ b/apps/web/components/sections/networking/networking-hero.tsx @@ -33,9 +33,9 @@ export default function NetworkingHero({ data, backHref }: Props) {
- +

{data.headline} - +

{data.body ? ( diff --git a/apps/web/constants/site-config.ts b/apps/web/constants/site-config.ts index cc769d87f6..25a982bdfa 100644 --- a/apps/web/constants/site-config.ts +++ b/apps/web/constants/site-config.ts @@ -1,5 +1,7 @@ import { env } from '@/lib/env' +import siteSettings from '../../../content/site/en/settings.json' + type SiteConfig = { name: string title: string @@ -11,10 +13,10 @@ type SiteConfig = { const siteConfig: SiteConfig = { name: 'Logos', - title: 'Logos Next Tailwind Template', - description: 'Template for Next.js, Tailwind CSS, and Acid Info LSD', + title: siteSettings.siteTitle, + description: siteSettings.siteDescription, url: env.NEXT_PUBLIC_SITE_URL ?? 'https://logos.co', - keywords: ['Logos', 'Web3'], + keywords: siteSettings.keywords, defaultLocale: 'en', } diff --git a/apps/web/lib/metadata.ts b/apps/web/lib/metadata.ts index 529f683216..3ef068a7fb 100644 --- a/apps/web/lib/metadata.ts +++ b/apps/web/lib/metadata.ts @@ -1,9 +1,10 @@ import siteConfig from '@/constants/site-config' import { env } from '@/lib/env' -import { Metadata } from 'next' +import type { Metadata } from 'next' type DefaultMetadataProps = { locale: string + noindex?: boolean title?: string description?: string path?: string @@ -24,6 +25,7 @@ export async function createDefaultMetadata({ title = '', description = '', locale, + noindex = false, path = '', }: DefaultMetadataProps): Promise { const _title = title || siteConfig.title @@ -68,8 +70,8 @@ export async function createDefaultMetadata({ creator: siteConfig.name, keywords: siteConfig.keywords, robots: { - index: env.NEXT_PUBLIC_API_MODE === 'production', - follow: true, + index: !noindex && env.NEXT_PUBLIC_API_MODE === 'production', + follow: !noindex, }, } } diff --git a/apps/web/scripts/static-export-smoke.ts b/apps/web/scripts/static-export-smoke.ts index e26d7b7f47..0c86ea4c54 100644 --- a/apps/web/scripts/static-export-smoke.ts +++ b/apps/web/scripts/static-export-smoke.ts @@ -73,6 +73,39 @@ const collectExpectedRoutes = async (): Promise => { ].sort((a, b) => a.localeCompare(b)) } +const toCanonicalUrl = (route: string): string => { + return route === '/' ? 'https://logos.co/' : `https://logos.co${route}` +} + +const assertSeoFiles = (expectedRoutes: readonly string[]): string[] => { + const failures: string[] = [] + const robotsPath = join(outDir, 'robots.txt') + const sitemapPath = join(outDir, 'sitemap.xml') + + if (!existsSync(robotsPath)) { + failures.push('robots.txt is missing from the static export root') + } else { + const robots = readFileSync(robotsPath, 'utf8') + if (!robots.includes('Sitemap: https://logos.co/sitemap.xml')) { + failures.push('robots.txt is missing the production sitemap URL') + } + } + + if (!existsSync(sitemapPath)) { + failures.push('sitemap.xml is missing from the static export root') + } else { + const sitemap = readFileSync(sitemapPath, 'utf8') + for (const route of expectedRoutes) { + const loc = `${toCanonicalUrl(route)}` + if (!sitemap.includes(loc)) { + failures.push(`sitemap.xml is missing ${toCanonicalUrl(route)}`) + } + } + } + + return failures +} + const isLocalAssetHref = (href: string): boolean => { if (!href.startsWith('/')) return false if (href.startsWith('//')) return false @@ -127,7 +160,10 @@ const main = async (): Promise => { const failures: string[] = [] const checkedHtmlFiles = new Set() - for (const route of await collectExpectedRoutes()) { + const expectedRoutes = await collectExpectedRoutes() + failures.push(...assertSeoFiles(expectedRoutes)) + + for (const route of expectedRoutes) { const htmlFile = findHtmlFile(route) if (!htmlFile) { failures.push(`${route} did not export an HTML file`) diff --git a/apps/web/scripts/strip-default-locale-prefix.sh b/apps/web/scripts/strip-default-locale-prefix.sh index fb1ada8612..cfad032705 100755 --- a/apps/web/scripts/strip-default-locale-prefix.sh +++ b/apps/web/scripts/strip-default-locale-prefix.sh @@ -69,7 +69,7 @@ find "$OUT_DIR" -type f -name "*.html" ! -name "index.html" ! -name "404.html" ! mv "$html_file" "$route_dir/index.html" done -find "$OUT_DIR" -type f -name "*.txt" ! -name "index.txt" ! -name "404.txt" ! -name "_not-found.txt" | while read -r txt_file; do +find "$OUT_DIR" -type f -name "*.txt" ! -name "index.txt" ! -name "404.txt" ! -name "_not-found.txt" ! -name "robots.txt" | while read -r txt_file; do route_dir="${txt_file%.txt}" mkdir -p "$route_dir" mv "$txt_file" "$route_dir/index.txt"