diff --git a/.env.example b/.env.example index 7eaf8973c..e1755d467 100644 --- a/.env.example +++ b/.env.example @@ -2,3 +2,4 @@ ZENDESK_KEY= GTM_ID= BRANCH=optional (sets the edit path) +BASE_URL=optional (subpath the site is served from, no trailing slash; used by PR previews) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index f302efd0b..0783150c6 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -2,6 +2,9 @@ name: PR build on: pull_request: + # `closed` is included so the preview deployment is torn down when the PR + # closes; the build/link-check job skips that event + types: [opened, reopened, synchronize, closed] workflow_dispatch: # Allows manual triggering from the GitHub UI permissions: @@ -11,6 +14,7 @@ permissions: jobs: build: + if: github.event.action != 'closed' runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -116,3 +120,60 @@ jobs: if (filtered.length > 0) { core.setFailed("There are broken links in the documentation."); } + + # Deploys the built site to GitHub Pages under pr-preview/pr-/ and + # posts a sticky comment on the PR with the preview URL; the preview is + # removed when the PR closes. Builds separately from the job above because + # the preview needs a different baseUrl than the link check. Fork PRs are + # skipped: they get neither secrets nor a writable GITHUB_TOKEN. + deploy-preview: + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + timeout-minutes: 30 + # Concurrent runs for the same PR would race on pushing to gh-pages + concurrency: pr-preview-${{ github.ref }} + permissions: + contents: write + pull-requests: write + steps: + - uses: actions/checkout@v7 + with: + # Images are Git LFS-tracked; skip fetching them on teardown, where + # the checkout is only needed so the action can push to gh-pages + lfs: ${{ github.event.action != 'closed' }} + + - uses: actions/setup-node@v6 + if: github.event.action != 'closed' + with: + node-version: 24 + cache: npm + + # The ADO registry requires auth even to install; the repo .npmrc is + # credential-less so creds go into ~/.npmrc here + - name: Authenticate to codat-npm feed + if: github.event.action != 'closed' + run: | + { + echo "//pkgs.dev.azure.com/codat/Codat/_packaging/codat-npm/npm/registry/:username=codat" + echo "//pkgs.dev.azure.com/codat/Codat/_packaging/codat-npm/npm/registry/:_password=${ADO_NPM_FEED_TOKEN}" + echo "//pkgs.dev.azure.com/codat/Codat/_packaging/codat-npm/npm/registry/:email=npm-requires-email@example.com" + } >> ~/.npmrc + env: + ADO_NPM_FEED_TOKEN: ${{ secrets.ADO_NPM_FEED_TOKEN }} + + - name: Install dependencies + if: github.event.action != 'closed' + run: npm ci + + - name: Build site for preview + if: github.event.action != 'closed' + run: npm run build + env: + GTM_ID: ${{ vars.GTM_ID }} + # GitHub Pages serves the preview from a subpath (no trailing slash) + BASE_URL: /codat-docs/pr-preview/pr-${{ github.event.number }} + + - name: Deploy preview + uses: rossjrw/pr-preview-action@v1 + with: + source-dir: ./build diff --git a/docusaurus.config.js b/docusaurus.config.js index 0ff1d2317..0a6070e59 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -80,8 +80,11 @@ import navbar from "./nav.config"; import redirects from "./redirects.config"; import { generateAPISitemaps } from "./src/utils/oas-sitemap.js"; +import rehypeImageBaseUrl from "./src/utils/rehype-image-base-url.js"; -const BASE_URL = ""; +// PR preview deploys serve the site from a subpath on GitHub Pages, so the +// preview workflow overrides this. No trailing slash — baseUrl appends one. +const BASE_URL = process.env.BASE_URL ?? ""; require("dotenv").config(); @@ -129,6 +132,7 @@ const config = { routeBasePath: "/", sidebarPath: "./sidebars.js", editUrl: `https://github.com/codatio/codat-docs/edit/${process.env?.BRANCH || "main"}/`, + rehypePlugins: [[rehypeImageBaseUrl, { baseUrl: `${BASE_URL}/` }]], exclude: ["README.md"], lastVersion: "current", versions: { @@ -140,6 +144,7 @@ const config = { }, blog: { showReadingTime: true, + rehypePlugins: [[rehypeImageBaseUrl, { baseUrl: `${BASE_URL}/` }]], blogTitle: "Codat updates", blogDescription: "Engineering and product updates from Codat.", postsPerPage: 10, diff --git a/src/components/Api/index.tsx b/src/components/Api/index.tsx index e4b359d21..ca2a2f636 100644 --- a/src/components/Api/index.tsx +++ b/src/components/Api/index.tsx @@ -2,6 +2,7 @@ import React, { useState, Suspense } from "react"; import { Helmet } from "react-helmet"; import BrowserOnly from "@docusaurus/BrowserOnly"; +import useBaseUrl from "@docusaurus/useBaseUrl"; import Layout from "@theme/Layout"; import Navbar from "@theme/Navbar"; @@ -21,6 +22,9 @@ const Api = ({ socialBanner = "https://docs.codat.io/img/meta/codat-bg.png", }) => { const [menuOpen, setMenuOpen] = useState(false); + // The OAS specs live in static/, so the fetch URL must respect baseUrl for + // deploys served from a subpath (e.g. PR previews) + const specUrl = useBaseUrl(url); return ( @@ -48,7 +52,7 @@ const Api = ({ {() => ( - + )} diff --git a/src/components/Cards/index.js b/src/components/Cards/index.js index 0cf3c575b..7dc534081 100644 --- a/src/components/Cards/index.js +++ b/src/components/Cards/index.js @@ -1,10 +1,20 @@ +import { useBaseUrlUtils } from "@docusaurus/useBaseUrl"; + +// Callers pass absolute image/link paths (e.g. /img/...), which bypass +// baseUrl and break deploys served from a subpath (e.g. PR previews); +// external URLs pass through withBaseUrl untouched const Card = (props) => { const { image, icon: Icon, title, children, className } = props; + const { withBaseUrl } = useBaseUrlUtils(); return (
  • - {Icon ? : } + {Icon ? ( + + ) : ( + + )}

    {title}

    @@ -16,11 +26,12 @@ const Card = (props) => { const CardTwo = (props) => { const { image, title, link, linkText, children, className } = props; + const { withBaseUrl } = useBaseUrlUtils(); return (
  • - +

    {title}

    @@ -28,7 +39,7 @@ const CardTwo = (props) => { {children}

    - {linkText} → + {linkText} →

  • ); @@ -36,20 +47,21 @@ const CardTwo = (props) => { const MiniCard = (props) => { const { image, title, subtitle, link, children, className } = props; + const { withBaseUrl } = useBaseUrlUtils(); return (
  • diff --git a/src/components/ClientLibraries/index.tsx b/src/components/ClientLibraries/index.tsx index a756b1086..cd4731a12 100644 --- a/src/components/ClientLibraries/index.tsx +++ b/src/components/ClientLibraries/index.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useBaseUrlUtils } from "@docusaurus/useBaseUrl"; const repoBaseUrl = "https://github.com/codatio/client-sdk-"; const repoProductTMP = "/tree/main/bank-feeds"; @@ -93,6 +94,9 @@ const getShieldUrl = (productName, language) => { const ClientLibraries = ({ productName }) => { const productUrl = !productName ? "" : "/tree/main/" + productName; + // The language icons are absolute /img/ paths, which bypass baseUrl and + // break deploys served from a subpath (e.g. PR previews) + const { withBaseUrl } = useBaseUrlUtils(); return (
      {languages.map((language, i) => { @@ -104,7 +108,10 @@ const ClientLibraries = ({ productName }) => { href={repoBaseUrl + language.name + productUrl} target="_blank" > - + diff --git a/src/components/Clients/index.js b/src/components/Clients/index.js index 24f9f48f8..3badc007d 100644 --- a/src/components/Clients/index.js +++ b/src/components/Clients/index.js @@ -1,14 +1,18 @@ import React from "react"; +import { useBaseUrlUtils } from "@docusaurus/useBaseUrl"; import styles from "./styles.module.scss"; const Client = (props) => { const { path, name, scale } = props; + // Callers pass absolute /img/ paths, which bypass baseUrl and break + // deploys served from a subpath (e.g. PR previews) + const { withBaseUrl } = useBaseUrlUtils(); return (
      {`${name} { const { colorMode } = useColorMode(); - const resolvedIcon = iconDark && colorMode === "dark" ? iconDark : icon; + // icon/img arrive as absolute paths from frontmatter (banner_image etc.), + // which bypass baseUrl and break deploys served from a subpath + const { withBaseUrl } = useBaseUrlUtils(); + const resolvedIcon = withBaseUrl( + iconDark && colorMode === "dark" ? iconDark : icon, + ); return (
      @@ -62,7 +68,7 @@ const PageHeader = ({ {videoUrl && }
      - {img && } + {img && }
      ); }; diff --git a/src/components/Products/index.js b/src/components/Products/index.js index 96d0647d6..7c31e2f7e 100644 --- a/src/components/Products/index.js +++ b/src/components/Products/index.js @@ -1,5 +1,6 @@ import React from "react"; import { useColorMode } from "@docusaurus/theme-common"; +import { useBaseUrlUtils } from "@docusaurus/useBaseUrl"; const other = [ { @@ -104,6 +105,9 @@ const allProducts = [ const Products = ({ mini, products, verbose }) => { const { colorMode } = useColorMode(); + // The logo/link paths above are absolute, which bypasses baseUrl and + // breaks deploys served from a subpath (e.g. PR previews) + const { withBaseUrl } = useBaseUrlUtils(); const validProducts = !products ? allProducts : products @@ -130,15 +134,15 @@ const Products = ({ mini, products, verbose }) => {
      @@ -147,7 +151,7 @@ const Products = ({ mini, products, verbose }) => {
      @@ -167,15 +171,15 @@ const Products = ({ mini, products, verbose }) => {
    • @@ -183,7 +187,7 @@ const Products = ({ mini, products, verbose }) => {

      {product.name}

      {product.description}

      - {product.linkText} + {product.linkText}

    • ); diff --git a/src/pages/support/index.tsx b/src/pages/support/index.tsx index 2961d9673..812bc3285 100644 --- a/src/pages/support/index.tsx +++ b/src/pages/support/index.tsx @@ -1,4 +1,5 @@ import React from "react"; +import Link from "@docusaurus/Link"; import GearFinanceDownIcon from "@components/GearFinanceDownIcon"; import FeatureBullet from "@components/FeatureBullet"; import Layout from "@theme/Layout"; @@ -24,7 +25,7 @@ const RaiseSupportTicket = () => {

      Raise a technical issue with our support team{" "} - here. + here.

    • diff --git a/src/theme/BlogSidebar/Desktop/index.tsx b/src/theme/BlogSidebar/Desktop/index.tsx index 115a4e0e8..ee40ef758 100644 --- a/src/theme/BlogSidebar/Desktop/index.tsx +++ b/src/theme/BlogSidebar/Desktop/index.tsx @@ -59,9 +59,9 @@ export default function BlogSidebarDesktop({ sidebar }: Props): JSX.Element { )) : "No deprecations"}
    - + See all... - +

    diff --git a/src/theme/MDXComponents/Img/index.js b/src/theme/MDXComponents/Img/index.js index 37a70166c..d29a7d32f 100644 --- a/src/theme/MDXComponents/Img/index.js +++ b/src/theme/MDXComponents/Img/index.js @@ -1,16 +1,22 @@ import React from "react"; import clsx from "clsx"; +import useBaseUrl from "@docusaurus/useBaseUrl"; import styles from "./styles.module.css"; function transformImgClassName(className) { return clsx(className, styles.img); } export default function MDXImg(props) { + // Raw in MDX bypasses baseUrl, breaking any deploy + // served from a subpath (e.g. PR previews); bundled/external srcs pass + // through useBaseUrl untouched + const src = useBaseUrl(props.src); return ( // eslint-disable-next-line jsx-a11y/alt-text ); diff --git a/src/utils/rehype-image-base-url.js b/src/utils/rehype-image-base-url.js new file mode 100644 index 000000000..7060f200d --- /dev/null +++ b/src/utils/rehype-image-base-url.js @@ -0,0 +1,40 @@ +// Literal JSX written inside Markdown/MDX content bypasses the +// MDXComponents mapping (and with it useBaseUrl), so absolute src paths +// break when the site is served from a subpath (e.g. PR previews, where +// baseUrl is /codat-docs/pr-preview/pr-/). Rewrites those srcs at +// compile time instead; a no-op for production builds, where baseUrl is "/". +const rehypeImageBaseUrl = ({ baseUrl }) => { + const prefix = (src) => + typeof src === "string" && src.startsWith("/") && !src.startsWith("//") + ? baseUrl + src.slice(1) + : src; + + const visit = (node) => { + if (node.type === "element" && node.tagName === "img" && node.properties) { + node.properties.src = prefix(node.properties.src); + } + + if ( + (node.type === "mdxJsxFlowElement" || + node.type === "mdxJsxTextElement") && + node.name === "img" + ) { + for (const attr of node.attributes ?? []) { + if (attr.type === "mdxJsxAttribute" && attr.name === "src") { + attr.value = prefix(attr.value); + } + } + } + + (node.children ?? []).forEach(visit); + }; + + return (tree) => { + if (!baseUrl || baseUrl === "/") { + return; + } + visit(tree); + }; +}; + +export default rehypeImageBaseUrl;