diff --git a/.gitignore b/.gitignore index 9956edc083..830c05ef92 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,6 @@ out/ build dist robots.txt -sitemap*.xml analyze/ # misc diff --git a/apps/web/src/app/(cms)/academy/components/difficulty-label.tsx b/apps/web/src/app/(cms)/academy/components/difficulty-label.tsx index b0a29ba881..25c05e661a 100644 --- a/apps/web/src/app/(cms)/academy/components/difficulty-label.tsx +++ b/apps/web/src/app/(cms)/academy/components/difficulty-label.tsx @@ -10,6 +10,8 @@ interface DifficultyLabel { export function DifficultyLabel({ article, isCard }: DifficultyLabel) { const difficulty = article.difficulty + if (!difficulty) return <> + const slug = difficulty.slug as keyof typeof DIFFICULTY_ELEMENTS if (!slug) return <> diff --git a/apps/web/src/app/(cms)/academy/sitemap.ts b/apps/web/src/app/(cms)/academy/sitemap.ts new file mode 100644 index 0000000000..d750d933a7 --- /dev/null +++ b/apps/web/src/app/(cms)/academy/sitemap.ts @@ -0,0 +1,44 @@ +import { getAcademyArticles } from '@sushiswap/graph-client/strapi' +import type { MetadataRoute } from 'next' + +const products = ['bentobox', 'furo', 'onsen', 'sushixswap'] + +export default async function sitemap(): Promise { + try { + const { articles } = await getAcademyArticles({ + pagination: { pageSize: 10000 }, + }) + + return [ + { + url: 'https://sushi.com/academy', + lastModified: new Date(), + changeFrequency: 'yearly', + }, + { + url: 'https://sushi.com/academy/explore', + lastModified: new Date(), + changeFrequency: 'yearly', + }, + ...products.map( + (product) => + ({ + url: `https://sushi.com/academy/products/${product}`, + lastModified: new Date(), + changeFrequency: 'yearly', + }) as const, + ), + ...articles.map( + (article) => + ({ + url: `https://sushi.com/academy/${article.slug}`, + lastModified: new Date(article.updatedAt), + changeFrequency: 'weekly', + }) as const, + ), + ] + } catch { + console.error('sitemap: Error fetching academy articles') + return [] + } +} diff --git a/apps/web/src/app/(cms)/blog/sitemap.ts b/apps/web/src/app/(cms)/blog/sitemap.ts new file mode 100644 index 0000000000..eb10804c07 --- /dev/null +++ b/apps/web/src/app/(cms)/blog/sitemap.ts @@ -0,0 +1,29 @@ +import { getBlogArticles } from '@sushiswap/graph-client/strapi' +import type { MetadataRoute } from 'next' + +export default async function sitemap(): Promise { + try { + const { articles } = await getBlogArticles({ + pagination: { pageSize: 10000 }, + }) + + return [ + { + url: 'https://sushi.com/blog', + lastModified: new Date(), + changeFrequency: 'yearly', + }, + ...articles.map( + (article) => + ({ + url: `https://sushi.com/blog/${article.slug}`, + lastModified: new Date(article.updatedAt), + changeFrequency: 'weekly', + }) as const, + ), + ] + } catch { + console.error('sitemap: Error fetching blog articles') + return [] + } +} diff --git a/apps/web/src/app/(networks)/(non-evm)/aptos/sitemap.ts b/apps/web/src/app/(networks)/(non-evm)/aptos/sitemap.ts new file mode 100644 index 0000000000..26488d47eb --- /dev/null +++ b/apps/web/src/app/(networks)/(non-evm)/aptos/sitemap.ts @@ -0,0 +1,14 @@ +import type { MetadataRoute } from 'next' + +const aptosChainPaths = ['/pool', '/explore/pools', '/pool/add', '/swap'] + +export default function sitemap(): MetadataRoute.Sitemap { + return aptosChainPaths.map( + (path) => + ({ + url: `https://sushi.com/aptos/${path}`, + lastModified: new Date(), + changeFrequency: 'weekly', + }) as const, + ) +} diff --git a/apps/web/src/app/(networks)/(non-evm)/tron/sitemap.ts b/apps/web/src/app/(networks)/(non-evm)/tron/sitemap.ts new file mode 100644 index 0000000000..059983726d --- /dev/null +++ b/apps/web/src/app/(networks)/(non-evm)/tron/sitemap.ts @@ -0,0 +1,14 @@ +import type { MetadataRoute } from 'next' + +const tronChainPaths = ['/pool', '/explore/pools', '/pool/add', '/swap'] + +export default function sitemap(): MetadataRoute.Sitemap { + return tronChainPaths.map( + (path) => + ({ + url: `https://sushi.com/tron/${path}`, + lastModified: new Date(), + changeFrequency: 'weekly', + }) as const, + ) +} diff --git a/apps/web/src/app/legal/[slug]/page.tsx b/apps/web/src/app/legal/[slug]/page.tsx index a2c1737d5a..02c67a961e 100644 --- a/apps/web/src/app/legal/[slug]/page.tsx +++ b/apps/web/src/app/legal/[slug]/page.tsx @@ -2,27 +2,22 @@ import { Container, Separator } from '@sushiswap/ui' import { unstable_cache } from 'next/cache' import React from 'react' import { getGhostBody } from 'src/app/(cms)/lib/ghost/ghost' +import { legalPages } from '../_config' export const revalidate = 86400 export const dynamicParams = false -const pages = { - ['privacy-policy']: { title: 'Privacy Policy' }, - ['terms-of-service']: { title: 'Terms of Service' }, - ['cookie-policy']: { title: 'Cookie Policy' }, -} - export async function generateStaticParams() { - return Object.keys(pages).map((slug) => ({ slug: slug })) + return Object.keys(legalPages).map((slug) => ({ slug: slug })) } type Props = { - params: { slug: keyof typeof pages } + params: { slug: keyof typeof legalPages } } export async function generateMetadata({ params }: Props) { - const page = pages[params.slug] + const page = legalPages[params.slug] return { title: page.title, @@ -30,7 +25,7 @@ export async function generateMetadata({ params }: Props) { } export default async function Page({ params }: Props) { - const page = pages[params.slug] + const page = legalPages[params.slug] const { title, diff --git a/apps/web/src/app/legal/_config.ts b/apps/web/src/app/legal/_config.ts new file mode 100644 index 0000000000..a0502a6ab4 --- /dev/null +++ b/apps/web/src/app/legal/_config.ts @@ -0,0 +1,5 @@ +export const legalPages = { + ['privacy-policy']: { title: 'Privacy Policy' }, + ['terms-of-service']: { title: 'Terms of Service' }, + ['cookie-policy']: { title: 'Cookie Policy' }, +} diff --git a/apps/web/src/app/legal/sitemap.ts b/apps/web/src/app/legal/sitemap.ts new file mode 100644 index 0000000000..8e0ac07ad7 --- /dev/null +++ b/apps/web/src/app/legal/sitemap.ts @@ -0,0 +1,13 @@ +import type { MetadataRoute } from 'next' +import { legalPages } from './_config' + +export default function sitemap(): MetadataRoute.Sitemap { + return Object.keys(legalPages).map( + (page) => + ({ + url: `https://sushi.com/legal/${page}`, + lastModified: new Date(), + changeFrequency: 'weekly', + }) as const, + ) +} diff --git a/apps/web/src/app/robots.ts b/apps/web/src/app/robots.ts new file mode 100644 index 0000000000..df81e06832 --- /dev/null +++ b/apps/web/src/app/robots.ts @@ -0,0 +1,11 @@ +import { MetadataRoute } from 'next' + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: '*', + allow: ['/'], + }, + sitemap: 'https://sushi.com/sitemap-index.xml', + } +} diff --git a/apps/web/src/app/sitemap-index.xml/route.ts b/apps/web/src/app/sitemap-index.xml/route.ts new file mode 100644 index 0000000000..135a272848 --- /dev/null +++ b/apps/web/src/app/sitemap-index.xml/route.ts @@ -0,0 +1,25 @@ +import { CHAIN_IDS } from 'src/config' +import { getNetworkKey } from 'src/lib/network' + +const networkKeys = [...CHAIN_IDS.map(getNetworkKey), 'aptos', 'tron'] + +const sitemapFiles = [ + '/academy/sitemap.xml', + '/blog/sitemap.xml', + ...networkKeys.map((key) => `/sitemap.xml/${key}`), + '/legal/sitemap.xml', +] + +const generateSitemapLink = (url: string) => + `https://sushi.com${url}` + +export async function GET() { + const sitemapIndexXML = ` + + ${sitemapFiles.map((file) => generateSitemapLink(file)).join('')} + ` + + return new Response(sitemapIndexXML, { + headers: { 'Content-Type': 'text/xml' }, + }) +} diff --git a/apps/web/src/app/sitemap.ts b/apps/web/src/app/sitemap.ts new file mode 100644 index 0000000000..7d1a8509bc --- /dev/null +++ b/apps/web/src/app/sitemap.ts @@ -0,0 +1,36 @@ +import { MetadataRoute } from 'next' +import { CHAIN_IDS } from 'src/config' +import { getNetworkKey } from 'src/lib/network' + +const evmChainPaths = [ + '/migrate', + '/pool', + '/rewards', + '/cross-chain-swap', + '/dca', + '/limit', + '/swap', + '/explore/pools', + '/explore/smart-pools', + '/pool/incentivize', + '/pool/v2/add', + '/pool/v3/add', + '/pool/v3/fees', +] + +export async function generateSitemaps() { + return CHAIN_IDS.map((chainId) => ({ + id: getNetworkKey(chainId), + })) +} + +export default function sitemap({ id }: { id: string }): MetadataRoute.Sitemap { + return evmChainPaths.map( + (path) => + ({ + url: `https://sushi.com/${id}${path}`, + lastModified: new Date(), + changeFrequency: 'weekly', + }) as const, + ) +} diff --git a/packages/graph-client/src/subgraphs/strapi/queries/academy-articles.ts b/packages/graph-client/src/subgraphs/strapi/queries/academy-articles.ts index 9d655fbf46..cad288efa5 100644 --- a/packages/graph-client/src/subgraphs/strapi/queries/academy-articles.ts +++ b/packages/graph-client/src/subgraphs/strapi/queries/academy-articles.ts @@ -133,12 +133,14 @@ export async function getAcademyArticles( id: topic.id, name: topic.attributes.name, })), - difficulty: { - id: article.attributes.difficulty!.data!.id, - name: article.attributes.difficulty!.data!.attributes.name, - label: article.attributes.difficulty!.data!.attributes.label, - slug: article.attributes.difficulty!.data!.attributes.slug, - }, + difficulty: article.attributes.difficulty.data + ? { + id: article.attributes.difficulty.data.id, + name: article.attributes.difficulty.data.attributes.name, + label: article.attributes.difficulty.data.attributes.label, + slug: article.attributes.difficulty.data.attributes.slug, + } + : null, cover: transformImage(article.attributes.cover.data!), authors: article.attributes.authors!.data.map((author) => ({ name: author.attributes.name,