From 64be261ef39d76d4555bcd0af3cbaaed9280eb4c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 12:49:01 -0400 Subject: [PATCH] SEO: move TMiR transcripts to tmir.reactiflux.com, fix site metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 301 every /transcripts/tmir-* page to its episode on tmir.reactiflux.com and delete the 40 duplicate copies (and the shadowed tmir.md page). The heading anchors use the same slug rule there, so #fragments still work. - /tmir redirect is now a 301. - Every page gets a canonical URL, og:url, og:site_name, a www banner image and a large Twitter card. Titles get a " · Reactiflux" suffix unless they already name the site; pages without a description fall back to one. - Homepage: real title and description, Organization JSON-LD. - /transcripts gets its own H1, intro and dated list instead of duplicating the newest transcript. Transcript pages get ": Q&A transcript ()" titles (no more duplicate Jared Palmer / Nader Dabit / React Conf titles) and a one-line meta description. - noindex the Discord OAuth callback and the unfinished podcast player, and drop them and /blog/1 from the sitemap (/blog/1 redirects to /blog). - Descriptions for blog posts, /jobs and /tips. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011ZUEzbgHTThFQF8TL9bLzf --- netlify.toml | 2 +- next-sitemap.config.js | 3 + public/_redirects | 49 +- src/components/Layout/Layout.js | 9 +- src/components/Layout/SEO.js | 82 +- src/md-pages/tips.md | 1 + src/md-pages/tmir.md | 24 - src/pages/auth/discordcb.tsx | 8 +- src/pages/blog/post/[slug].tsx | 4 +- src/pages/index.js | 24 +- src/pages/jobs.js | 7 +- src/pages/podcasts/[slug].tsx | 14 + src/pages/transcripts.tsx | 71 +- src/pages/transcripts/[slug].tsx | 22 +- src/transcripts/tmir-2024-01.md | 549 ------------- src/transcripts/tmir-2024-02.md | 446 ----------- src/transcripts/tmir-2024-03.md | 495 ------------ src/transcripts/tmir-2024-04.md | 535 ------------- src/transcripts/tmir-2024-05.md | 640 --------------- src/transcripts/tmir-2024-06.md | 703 ---------------- src/transcripts/tmir-2024-07.md | 642 --------------- src/transcripts/tmir-2024-08.md | 646 --------------- src/transcripts/tmir-2024-09.md | 517 ------------ src/transcripts/tmir-2024-10.md | 751 ----------------- src/transcripts/tmir-2024-11.md | 723 ----------------- src/transcripts/tmir-2024-12.md | 592 -------------- src/transcripts/tmir-2025-01.md | 542 ------------- src/transcripts/tmir-2025-02.md | 494 ------------ src/transcripts/tmir-2025-03.md | 563 ------------- src/transcripts/tmir-2025-04.md | 504 ------------ src/transcripts/tmir-2025-05.md | 526 ------------ src/transcripts/tmir-2025-06.md | 361 --------- src/transcripts/tmir-2025-07.md | 669 ---------------- src/transcripts/tmir-2025-08.md | 498 ------------ src/transcripts/tmir-2025-09.md | 728 ----------------- src/transcripts/tmir-2025-10.md | 759 ------------------ src/transcripts/tmir-2025-11.md | 589 -------------- src/transcripts/tmir-2025-12.md | 1022 ------------------------ src/transcripts/tmir-2026-01.md | 840 ------------------- src/transcripts/tmir-2026-02.md | 691 ---------------- src/transcripts/tmir-2026-04.md | 596 -------------- src/transcripts/tmir-2026-05.md | 645 --------------- src/transcripts/tmir-2026-06.md | 541 ------------- src/transcripts/tmir-2026-07.md | 667 ---------------- src/transcripts/tmir-2026-08.md | 584 -------------- src/transcripts/tmir-april-2023.md | 461 ----------- src/transcripts/tmir-august-2023.md | 423 ---------- src/transcripts/tmir-dec-2023.md | 442 ---------- src/transcripts/tmir-june-2023.md | 391 --------- src/transcripts/tmir-march-2023.md | 514 ------------ src/transcripts/tmir-may-2023.md | 457 ----------- src/transcripts/tmir-nov-2023.md | 442 ---------- src/transcripts/tmir-october-2023.md | 379 --------- src/transcripts/tmir-september-2023.md | 513 ------------ 54 files changed, 242 insertions(+), 23158 deletions(-) delete mode 100644 src/md-pages/tmir.md delete mode 100644 src/transcripts/tmir-2024-01.md delete mode 100644 src/transcripts/tmir-2024-02.md delete mode 100644 src/transcripts/tmir-2024-03.md delete mode 100644 src/transcripts/tmir-2024-04.md delete mode 100644 src/transcripts/tmir-2024-05.md delete mode 100644 src/transcripts/tmir-2024-06.md delete mode 100644 src/transcripts/tmir-2024-07.md delete mode 100644 src/transcripts/tmir-2024-08.md delete mode 100644 src/transcripts/tmir-2024-09.md delete mode 100644 src/transcripts/tmir-2024-10.md delete mode 100644 src/transcripts/tmir-2024-11.md delete mode 100644 src/transcripts/tmir-2024-12.md delete mode 100644 src/transcripts/tmir-2025-01.md delete mode 100644 src/transcripts/tmir-2025-02.md delete mode 100644 src/transcripts/tmir-2025-03.md delete mode 100644 src/transcripts/tmir-2025-04.md delete mode 100644 src/transcripts/tmir-2025-05.md delete mode 100644 src/transcripts/tmir-2025-06.md delete mode 100644 src/transcripts/tmir-2025-07.md delete mode 100644 src/transcripts/tmir-2025-08.md delete mode 100644 src/transcripts/tmir-2025-09.md delete mode 100644 src/transcripts/tmir-2025-10.md delete mode 100644 src/transcripts/tmir-2025-11.md delete mode 100644 src/transcripts/tmir-2025-12.md delete mode 100644 src/transcripts/tmir-2026-01.md delete mode 100644 src/transcripts/tmir-2026-02.md delete mode 100644 src/transcripts/tmir-2026-04.md delete mode 100644 src/transcripts/tmir-2026-05.md delete mode 100644 src/transcripts/tmir-2026-06.md delete mode 100644 src/transcripts/tmir-2026-07.md delete mode 100644 src/transcripts/tmir-2026-08.md delete mode 100644 src/transcripts/tmir-april-2023.md delete mode 100644 src/transcripts/tmir-august-2023.md delete mode 100644 src/transcripts/tmir-dec-2023.md delete mode 100644 src/transcripts/tmir-june-2023.md delete mode 100644 src/transcripts/tmir-march-2023.md delete mode 100644 src/transcripts/tmir-may-2023.md delete mode 100644 src/transcripts/tmir-nov-2023.md delete mode 100644 src/transcripts/tmir-october-2023.md delete mode 100644 src/transcripts/tmir-september-2023.md diff --git a/netlify.toml b/netlify.toml index 3c2c90be..21a96a9c 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 1895c8d7..fdd60932 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 3e14c124..a402a5c5 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 0683c5fd..564152dd 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 6b7030aa..d8982ad0 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 ? ( +