diff --git a/netlify.toml b/netlify.toml index 3c2c90b..21a96a9 100644 --- a/netlify.toml +++ b/netlify.toml @@ -9,7 +9,7 @@ [[redirects]] from = "/tmir" to = "https://tmir.reactiflux.com/" - status = 302 + status = 301 force = true [[plugins]] diff --git a/next-sitemap.config.js b/next-sitemap.config.js index 1895c8d..fdd6093 100644 --- a/next-sitemap.config.js +++ b/next-sitemap.config.js @@ -9,6 +9,9 @@ module.exports = { generateRobotsTxt: true, // (optional) priority: 1, autoLastmod: false, + // The OAuth callback and the unfinished podcast player carry noindex. They + // stay crawlable in robots.txt: Google has to fetch a page to see noindex. + exclude: ["/auth/*", "/podcasts/*", "/404", "/blog/1"], transform: async (config, url) => { if (url.includes("/blog/post/")) { return await transformBlogPost(url); diff --git a/public/_redirects b/public/_redirects index 3e14c12..a402a5c 100644 --- a/public/_redirects +++ b/public/_redirects @@ -1 +1,48 @@ -/transcripts/tmir-204-08 /transcripts/tmir-2024-08 +# This Month in React transcripts moved to tmir.reactiflux.com. Each old +# transcript URL redirects permanently to its episode page there; the +# heading anchors are generated with the same slug rule, so #fragments +# still land on the same section. +/transcripts/tmir-2024-01 https://tmir.reactiflux.com/episodes/2024-01 301! +/transcripts/tmir-2024-02 https://tmir.reactiflux.com/episodes/2024-02 301! +/transcripts/tmir-2024-03 https://tmir.reactiflux.com/episodes/2024-03 301! +/transcripts/tmir-2024-04 https://tmir.reactiflux.com/episodes/2024-04 301! +/transcripts/tmir-2024-05 https://tmir.reactiflux.com/episodes/2024-05 301! +/transcripts/tmir-2024-06 https://tmir.reactiflux.com/episodes/2024-06 301! +/transcripts/tmir-2024-07 https://tmir.reactiflux.com/episodes/2024-07 301! +/transcripts/tmir-2024-08 https://tmir.reactiflux.com/episodes/2024-08 301! +/transcripts/tmir-2024-09 https://tmir.reactiflux.com/episodes/2024-09 301! +/transcripts/tmir-2024-10 https://tmir.reactiflux.com/episodes/2024-10 301! +/transcripts/tmir-2024-11 https://tmir.reactiflux.com/episodes/2024-11 301! +/transcripts/tmir-2024-12 https://tmir.reactiflux.com/episodes/2024-12 301! +/transcripts/tmir-2025-01 https://tmir.reactiflux.com/episodes/2025-01 301! +/transcripts/tmir-2025-02 https://tmir.reactiflux.com/episodes/2025-02 301! +/transcripts/tmir-2025-03 https://tmir.reactiflux.com/episodes/2025-03 301! +/transcripts/tmir-2025-04 https://tmir.reactiflux.com/episodes/2025-04 301! +/transcripts/tmir-2025-05 https://tmir.reactiflux.com/episodes/2025-05 301! +/transcripts/tmir-2025-06 https://tmir.reactiflux.com/episodes/2025-06 301! +/transcripts/tmir-2025-07 https://tmir.reactiflux.com/episodes/2025-07 301! +/transcripts/tmir-2025-08 https://tmir.reactiflux.com/episodes/2025-08 301! +/transcripts/tmir-2025-09 https://tmir.reactiflux.com/episodes/2025-09 301! +/transcripts/tmir-2025-10 https://tmir.reactiflux.com/episodes/2025-10 301! +/transcripts/tmir-2025-11 https://tmir.reactiflux.com/episodes/2025-11 301! +/transcripts/tmir-2025-12 https://tmir.reactiflux.com/episodes/2025-12 301! +/transcripts/tmir-2026-01 https://tmir.reactiflux.com/episodes/2026-01 301! +/transcripts/tmir-2026-02 https://tmir.reactiflux.com/episodes/2026-02 301! +/transcripts/tmir-2026-04 https://tmir.reactiflux.com/episodes/2026-04 301! +/transcripts/tmir-2026-05 https://tmir.reactiflux.com/episodes/2026-05 301! +/transcripts/tmir-2026-06 https://tmir.reactiflux.com/episodes/2026-06 301! +/transcripts/tmir-2026-07 https://tmir.reactiflux.com/episodes/2026-07 301! +/transcripts/tmir-2026-08 https://tmir.reactiflux.com/episodes/2026-08 301! +/transcripts/tmir-april-2023 https://tmir.reactiflux.com/episodes/2023-04 301! +/transcripts/tmir-august-2023 https://tmir.reactiflux.com/episodes/2023-08 301! +/transcripts/tmir-dec-2023 https://tmir.reactiflux.com/episodes/2023-12 301! +/transcripts/tmir-june-2023 https://tmir.reactiflux.com/episodes/2023-06 301! +/transcripts/tmir-march-2023 https://tmir.reactiflux.com/episodes/2023-03 301! +/transcripts/tmir-may-2023 https://tmir.reactiflux.com/episodes/2023-05 301! +/transcripts/tmir-nov-2023 https://tmir.reactiflux.com/episodes/2023-11 301! +/transcripts/tmir-october-2023 https://tmir.reactiflux.com/episodes/2023-10 301! +/transcripts/tmir-september-2023 https://tmir.reactiflux.com/episodes/2023-09 301! +/transcripts/tmir-204-08 https://tmir.reactiflux.com/episodes/2024-08 301! + +# Page 1 of the blog index is /blog; /blog/1 is built too but never linked. +/blog/1 /blog 301! diff --git a/src/components/Layout/Layout.js b/src/components/Layout/Layout.js index 0683c5f..564152d 100644 --- a/src/components/Layout/Layout.js +++ b/src/components/Layout/Layout.js @@ -18,6 +18,8 @@ export function Layout({ children, description, title, + noindex = false, + structuredData = undefined, sidebar = false, ...props }) { @@ -55,7 +57,12 @@ export function Layout({
- + {typeof children === "function" ? children(setIsOpen) : children} {sidebar ? ( diff --git a/src/components/Layout/SEO.js b/src/components/Layout/SEO.js index 6b7030a..d8982ad 100644 --- a/src/components/Layout/SEO.js +++ b/src/components/Layout/SEO.js @@ -1,45 +1,89 @@ import React from "react"; import Head from "next/head"; +import { useRouter } from "next/router"; import Favicon from "assets/favicon.png"; -// This will be broken on previews cuz fuck you it's easy -const banner = "https://reactiflux.com/logo-banner.png"; +export const SITE_URL = "https://www.reactiflux.com"; +export const SITE_NAME = "Reactiflux"; +export const SITE_DESCRIPTION = + "Reactiflux is a Discord community of React and JavaScript developers. Ask questions, get help, talk shop, and join live Q&As and the This Month in React podcast."; + +// Absolute and on www, the host the site is served from. Deploy previews still +// point here, which is fine: crawlers only ever see production. +const BANNER = `${SITE_URL}/logo-banner.png`; + +/** The URL this page is canonically served at: no query, hash or trailing slash. */ +function canonicalUrl(asPath) { + const path = asPath.split(/[?#]/)[0].replace(/(.)\/+$/, "$1"); + return `${SITE_URL}${path}`; +} + +export function SEO({ + description, + image, + meta, + keywords, + title, + noindex, + structuredData, +}) { + const { asPath } = useRouter(); + const url = canonicalUrl(asPath); + // Bare titles like "Jobs" or "Transcripts" say nothing in a search result + // or a browser tab full of them, so name the site unless the title already does. + const fullTitle = !title + ? SITE_NAME + : title.includes(SITE_NAME) + ? title + : `${title} · ${SITE_NAME}`; + const desc = description || SITE_DESCRIPTION; + const img = image || BANNER; -export function SEO({ description, image, lang, meta, keywords, title }) { return ( {[ - { name: `description`, content: description }, - { property: `og:title`, content: title }, - { property: `og:image`, content: banner }, - { property: `og:description`, content: description }, + { name: `description`, content: desc }, + { property: `og:title`, content: fullTitle }, + { property: `og:description`, content: desc }, + { property: `og:url`, content: url }, + { property: `og:site_name`, content: SITE_NAME }, { property: `og:type`, content: `website` }, - { name: `twitter:card`, content: `summary` }, - { name: `twitter:title`, content: title }, - { name: `twitter:image`, content: banner }, - { name: `twitter:description`, content: description }, + { property: `og:image`, content: img }, + { name: `twitter:card`, content: `summary_large_image` }, + { name: `twitter:site`, content: `@reactiflux` }, + { name: `twitter:title`, content: fullTitle }, + { name: `twitter:description`, content: desc }, + { name: `twitter:image`, content: img }, ] + .concat(noindex ? { name: `robots`, content: `noindex` } : []) .concat( keywords.length > 0 ? { name: `keywords`, content: keywords.join(`, `) } : [], ) - .concat(image ? { property: `og:image`, content: image } : []) .concat(meta) - .map((m, index) => ( - + .map((m) => ( + ))} - {[{ rel: "icon", href: Favicon.src, type: "image/png" }].map((x, i) => ( - - ))} - {title} + {/* A noindexed page asking to be canonical sends Google mixed signals. */} + {noindex ? null : } + + {fullTitle} + {structuredData ? ( +