fix: repair static seo output

This commit is contained in:
jinhojang6
2026-05-18 15:30:43 +09:00
parent 50f4fa9dd1
commit 8083bb987f
11 changed files with 119 additions and 26 deletions
+1
View File
@@ -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
@@ -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',
})
}
@@ -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() {
@@ -1,4 +1,5 @@
import { MetadataRoute } from 'next'
import type { MetadataRoute } from 'next'
import siteConfig from '@/constants/site-config'
export const dynamic = 'force-static'
+61 -12
View File
@@ -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<MetadataRoute.Sitemap> {
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))
}
@@ -33,9 +33,9 @@ export default function BlockchainHero({ data, backHref }: Props) {
<div className="relative hidden md:block md:h-13">
<div className="absolute top-0 left-0 flex items-center gap-3">
<LogosMark size={26} className="shrink-0 text-gray-03" />
<span className="text-h3-serif leading-none text-brand-dark-green">
<h1 className="text-h3-serif leading-none text-brand-dark-green">
{data.headline}
</span>
</h1>
</div>
{data.body ? (
@@ -33,9 +33,9 @@ export default function NetworkingHero({ data, backHref }: Props) {
<div className="relative hidden md:block md:h-29.75">
<div className="absolute top-0 left-0 flex items-center gap-3">
<LogosMark size={26} className="shrink-0 text-gray-03" />
<span className="text-h3-serif leading-none text-brand-dark-green">
<h1 className="text-h3-serif leading-none text-brand-dark-green">
{data.headline}
</span>
</h1>
</div>
{data.body ? (
+5 -3
View File
@@ -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',
}
+5 -3
View File
@@ -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<Metadata> {
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,
},
}
}
+37 -1
View File
@@ -73,6 +73,39 @@ const collectExpectedRoutes = async (): Promise<string[]> => {
].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 = `<loc>${toCanonicalUrl(route)}</loc>`
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<void> => {
const failures: string[] = []
const checkedHtmlFiles = new Set<string>()
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`)
@@ -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"