mirror of
https://github.com/logos-co/logos-web.git
synced 2026-08-27 12:11:14 +00:00
fix: repair static seo output
This commit is contained in:
@@ -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
@@ -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 ? (
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user