diff --git a/.gitignore b/.gitignore index 89327b30..38c0a04f 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ yarn-error.log* *.tfvars /terraform/.terraform /terraform/*.plan + +# ephemeral build artifacts +/lib/honeybadger/config.vars.js diff --git a/components/Footer/Footer.tsx b/components/Footer/Footer.tsx index 8b78bbb2..5f64bb2a 100644 --- a/components/Footer/Footer.tsx +++ b/components/Footer/Footer.tsx @@ -7,7 +7,7 @@ export default function Footer() { return ( - + ); diff --git a/components/Header/Super.tsx b/components/Header/Super.tsx index f1fb2ed1..28e7b1ed 100644 --- a/components/Header/Super.tsx +++ b/components/Header/Super.tsx @@ -38,7 +38,6 @@ export default function HeaderSuper() { }, []); const userAuthContext = React.useContext(UserContext); - const handleMenu = () => setIsExpanded(!isExpanded); return ( diff --git a/components/Search/Search.styled.ts b/components/Search/Search.styled.ts index d4e1b384..da585c48 100644 --- a/components/Search/Search.styled.ts +++ b/components/Search/Search.styled.ts @@ -78,14 +78,53 @@ const Clear = styled("button", { }, }); -const Results = styled("p", { +const ResultsMessage = styled("span", { color: "$black50", - padding: "0 $gr4", + padding: "0 $gr4 $gr4", fontSize: "$gr3", - "@md": { - padding: "0", + "@lg": { + padding: "0 0 $gr3", }, }); -export { Button, Clear, Input, Results, SearchStyled }; +const NoResultsMessage = styled("span", { + display: "flex", + flexDirection: "column", + justifyContent: "center", + height: "100%", + alignItems: "center", + alignSelf: "center", + color: "$black50", + padding: "0 0 $gr8", + margin: "0 auto", + fontSize: "$gr3", + fontFamily: "$northwesternSansLight", + textAlign: "center", + flexGrow: "1", + + strong: { + color: "$black", + fontFamily: "$northwesternSansBold", + fontWeight: "400", + display: "block", + margin: "0 0 $gr2", + fontSize: "$gr4", + }, +}); + +const ResultsWrapper = styled("div", { + display: "flex", + flexDirection: "column", + minHeight: "80vh", +}); + +export { + Button, + Clear, + Input, + NoResultsMessage, + ResultsMessage, + ResultsWrapper, + SearchStyled, +}; diff --git a/context/user-context.tsx b/context/user-context.tsx index d77dac74..2d3adb5e 100644 --- a/context/user-context.tsx +++ b/context/user-context.tsx @@ -1,7 +1,6 @@ import React, { ReactNode, createContext, useState } from "react"; import { type User, type UserContext } from "@/types/context/user"; -import { DCAPI_ENDPOINT } from "@/lib/constants/endpoints"; -import axios from "axios"; +import { getUser } from "@/lib/user-helpers"; const UserContext = createContext({ user: null }); @@ -10,31 +9,25 @@ const UserProvider = ({ children }: { children: ReactNode }) => { React.useEffect(() => { /* Determine if user is authenticated via cookie */ - axios - .get(`${DCAPI_ENDPOINT}/auth/whoami`, { - withCredentials: true, - }) - .then((result) => { - if (!result.data) return; - const { - email, - isLoggedIn = false, - isReadingRoom = false, - name, - sub, - } = result.data; - setUser({ - email, - isLoggedIn, - isReadingRoom, - name, - sub, - }); + getUser().then((result) => { + if (!result) return; + const { + email, + isLoggedIn = false, + isReadingRoom = false, + name, + sub, + } = result; + setUser({ + email, + isLoggedIn, + isReadingRoom, + name, + sub, }); + }); }, []); - // 165.124.167.1 - return ( {children} ); diff --git a/lib/constants/bucket.ts b/lib/constants/bucket.ts new file mode 100644 index 00000000..32b3561c --- /dev/null +++ b/lib/constants/bucket.ts @@ -0,0 +1,3 @@ +const DC_SITEMAP_BUCKET = process.env.NEXT_PUBLIC_DC_SITEMAP_BUCKET; + +export { DC_SITEMAP_BUCKET }; diff --git a/lib/dc-api.ts b/lib/dc-api.ts index 3b1739d7..48c0b8c4 100644 --- a/lib/dc-api.ts +++ b/lib/dc-api.ts @@ -47,7 +47,10 @@ async function apiPostRequest( async function getIIIFResource(uri: string | null): Promise { if (!uri) return Promise.resolve(undefined); try { - const response = await axios(uri); + const response = await axios({ + url: uri, + withCredentials: true, + }); return response.data; } catch (err) { handleError(err); @@ -56,7 +59,6 @@ async function getIIIFResource(uri: string | null): Promise { function handleError(err: unknown) { const error = err as AxiosError; - //const error = err as AxiosError; if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx @@ -73,4 +75,4 @@ function handleError(err: unknown) { console.log("Error", error.message); } } -export { apiGetRequest, apiPostRequest, getIIIFResource }; +export { apiGetRequest, apiPostRequest, getIIIFResource, handleError }; diff --git a/lib/ga/data-layer.ts b/lib/ga/data-layer.ts index f98ab55a..f4a70d72 100644 --- a/lib/ga/data-layer.ts +++ b/lib/ga/data-layer.ts @@ -52,7 +52,6 @@ export function buildWorkDataLayer(work: Work): DataLayer { adminset: work?.library_unit || "", collections: work?.collection?.title ? work.collection.title : null, creatorsContributors, - isLoggedIn: false, pageTitle: work?.title || "", rightsStatement: work?.rights_statement?.label ? work.rights_statement.label diff --git a/lib/honeybadger/config.js b/lib/honeybadger/config.js index d25b23a3..2fb9691d 100644 --- a/lib/honeybadger/config.js +++ b/lib/honeybadger/config.js @@ -1,18 +1,23 @@ +import { + HONEYBADGER_API_KEY, + HONEYBADGER_ENV, + HONEYBADGER_REVISION, +} from "./config.vars.js"; import Honeybadger from "@honeybadger-io/js"; const setupHoneyBadger = () => { // https://docs.honeybadger.io/lib/javascript/reference/configuration.html const sharedHoneybadgerConfig = { - apiKey: process.env.HONEYBADGER_API_KEY, - environment: process.env.HONEYBADGER_ENV || process.env.NODE_ENV, + apiKey: HONEYBADGER_API_KEY, + environment: HONEYBADGER_ENV || process.env.NODE_ENV, projectRoot: "webpack://_N_E/./", // Uncomment to report errors in development: reportData: true, - revision: process.env.AWS_COMMIT_ID, + revision: HONEYBADGER_REVISION, }; - + if (typeof window === "undefined") { // Node config const projectRoot = process.cwd(); @@ -20,11 +25,11 @@ const setupHoneyBadger = () => { ...sharedHoneybadgerConfig, projectRoot: "webpack:///./", }).beforeNotify((notice) => { - notice.backtrace.forEach((line) => { + notice.backtrace = notice.backtrace.map((line) => { if (line.file) { line.file = line.file.replace( `${projectRoot}/.next/server`, - `${process.env.HONEYBADGER_ASSETS_URL}/..` + `${process.env.NEXT_PUBLIC_DC_URL}/_next/..` ); } return line; @@ -35,6 +40,16 @@ const setupHoneyBadger = () => { Honeybadger.configure({ ...sharedHoneybadgerConfig, projectRoot: "webpack://_N_E/./", + }).beforeNotify((notice) => { + notice.backtrace = notice.backtrace.map((line) => { + if (line.file) { + line.file = line.file.replace( + /^.+\/_next\//, + `${process.env.NEXT_PUBLIC_DC_URL}/_next/` + ); + } + return line; + }); }); } diff --git a/lib/user-helpers.ts b/lib/user-helpers.ts new file mode 100644 index 00000000..6be6183b --- /dev/null +++ b/lib/user-helpers.ts @@ -0,0 +1,14 @@ +import { DCAPI_ENDPOINT } from "@/lib/constants/endpoints"; +import axios from "axios"; +import { handleError } from "./dc-api"; + +export async function getUser() { + try { + const response = await axios.get(`${DCAPI_ENDPOINT}/auth/whoami`, { + withCredentials: true, + }); + return response.data; + } catch (err) { + handleError(err); + } +} diff --git a/next.config.js b/next.config.js index fa1834ac..f90e8905 100644 --- a/next.config.js +++ b/next.config.js @@ -1,14 +1,27 @@ +const fs = require("fs"); const HoneybadgerSourceMapPlugin = require("@honeybadger-io/webpack"); // Use the HoneybadgerSourceMapPlugin to upload the source maps during build step const { HONEYBADGER_API_KEY, HONEYBADGER_ENV, - HONEYBADGER_ASSETS_URL, HONEYBADGER_REPORT_DATA, + NEXT_PUBLIC_DC_URL, } = process.env; const NODE_ENV = process.env.HONEYBADGER_ENV || process.env.NODE_ENV; -const HONEYBADGER_REVISION = process.env.AWS_COMMIT_ID; +const HONEYBADGER_REVISION = process.env.HONEYBADGER_REVISION || process.env.AWS_COMMIT_ID; + +const HoneybadgerConfig = JSON.stringify({ + HONEYBADGER_API_KEY, + HONEYBADGER_ENV, + HONEYBADGER_REPORT_DATA, + HONEYBADGER_REVISION, +}, null, 2); + +fs.writeFileSync( + "lib/honeybadger/config.vars.js", + `module.exports = ${HoneybadgerConfig};` +); /** @type {import('next').NextConfig} */ module.exports = { @@ -28,7 +41,7 @@ module.exports = { ], }, reactStrictMode: true, - swcMinify: false, + swcMinify: true, webpack: (config) => { // When all the Honeybadger configuration env variables are // available/configured The Honeybadger webpack plugin gets pushed to the @@ -36,10 +49,10 @@ module.exports = { // This is an alternative to manually uploading the source maps. // See https://docs.honeybadger.io/lib/javascript/guides/using-source-maps.html // Note: This is disabled in development mode. + if ( HONEYBADGER_API_KEY && - HONEYBADGER_ASSETS_URL && - NODE_ENV === "production" + (NODE_ENV === "production" || NODE_ENV === "staging") ) { // `config.devtool` must be 'hidden-source-map' or 'source-map' to properly pass sourcemaps. // Next.js uses regular `source-map` which doesnt pass its sourcemaps to Webpack. @@ -47,11 +60,10 @@ module.exports = { // Use the hidden-source-map option when you don't want the source maps to be // publicly available on the servers, only to the error reporting config.devtool = "hidden-source-map"; - config.plugins.push( new HoneybadgerSourceMapPlugin({ apiKey: HONEYBADGER_API_KEY, - assetsUrl: HONEYBADGER_ASSETS_URL, + assetsUrl: `${NEXT_PUBLIC_DC_URL}/_next`, revision: HONEYBADGER_REVISION, }) ); diff --git a/package-lock.json b/package-lock.json index aa980c60..ddd9961a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "@honeybadger-io/webpack": "^4.8.2", "@iiif/parser": "^1.0.10", "@nulib/dcapi-types": "^2.0.0-rc.4", - "@nulib/design-system": "^1.6.1", + "@nulib/design-system": "^1.6.2", "@radix-ui/colors": "^0.1.8", "@radix-ui/react-accordion": "^1.0.1", "@radix-ui/react-aspect-ratio": "^1.0.1", @@ -2004,9 +2004,9 @@ "integrity": "sha512-vA1gAHusP9xE8NWEnPdKM0t8UsN8bcgw5a1j7/8fGekl2mgb/6H2F10J0bJ9MslfvLVsA1YFoW9yaV7OxbOzMg==" }, "node_modules/@nulib/design-system": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@nulib/design-system/-/design-system-1.6.1.tgz", - "integrity": "sha512-U8gKtb+Pu64Knw4P518LK1hDVeyb67+8iSDC/1+WqNHK7KsO+UoOfkiPoDNQSsyNn8/3D5msDiMl01rXbDt46Q==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@nulib/design-system/-/design-system-1.6.2.tgz", + "integrity": "sha512-tY3lxsfvh9fcNiEa/+o3C9gatRwA38UBMsfWIG2g2NeD4IzOnUmKgNr1FE1O7xxsHMAtIJuQ/A2dtMbWAJwNbw==", "dependencies": { "@radix-ui/react-popover": "^0.1.7-rc.43", "@stitches/react": "^1.2.7", @@ -15064,9 +15064,9 @@ "integrity": "sha512-vA1gAHusP9xE8NWEnPdKM0t8UsN8bcgw5a1j7/8fGekl2mgb/6H2F10J0bJ9MslfvLVsA1YFoW9yaV7OxbOzMg==" }, "@nulib/design-system": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@nulib/design-system/-/design-system-1.6.1.tgz", - "integrity": "sha512-U8gKtb+Pu64Knw4P518LK1hDVeyb67+8iSDC/1+WqNHK7KsO+UoOfkiPoDNQSsyNn8/3D5msDiMl01rXbDt46Q==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@nulib/design-system/-/design-system-1.6.2.tgz", + "integrity": "sha512-tY3lxsfvh9fcNiEa/+o3C9gatRwA38UBMsfWIG2g2NeD4IzOnUmKgNr1FE1O7xxsHMAtIJuQ/A2dtMbWAJwNbw==", "requires": { "@radix-ui/react-popover": "^0.1.7-rc.43", "@stitches/react": "^1.2.7", diff --git a/package.json b/package.json index 0e193b69..4fc95f7a 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@honeybadger-io/webpack": "^4.8.2", "@iiif/parser": "^1.0.10", "@nulib/dcapi-types": "^2.0.0-rc.4", - "@nulib/design-system": "^1.6.1", + "@nulib/design-system": "^1.6.2", "@radix-ui/colors": "^0.1.8", "@radix-ui/react-accordion": "^1.0.1", "@radix-ui/react-aspect-ratio": "^1.0.1", diff --git a/pages/_app.tsx b/pages/_app.tsx index ed6debda..102667d6 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -5,8 +5,10 @@ import React from "react"; import Script from "next/script"; import { SearchProvider } from "@/context/search-context"; import Transition from "@/components/Transition"; +import { User } from "@/types/context/user"; import { UserProvider } from "@/context/user-context"; import { defaultOpenGraphData } from "@/lib/open-graph"; +import { getUser } from "@/lib/user-helpers"; import globalStyles from "@/styles/global"; import setupHoneyBadger from "@/lib/honeybadger/config"; @@ -22,21 +24,32 @@ function MyApp({ Component, pageProps }: MyAppProps) { const { openGraphData = {} } = pageProps; const ogData = { ...defaultOpenGraphData, ...openGraphData }; const [mounted, setMounted] = React.useState(false); - React.useEffect(() => setMounted(true), []); + const [user, setUser] = React.useState(); + + React.useEffect(() => { + async function getData() { + const userResponse = await getUser(); + setUser(userResponse); + setMounted(true); + } + getData(); + }, []); { /** Add GTM (Google Tag Manager) data */ } React.useEffect(() => { - if (typeof window !== "undefined") { - // @ts-ignore - window.dataLayer?.push({ + if (typeof window !== "undefined" && mounted) { + const payload = { event: "VirtualPageView", // @ts-ignore ...pageProps.dataLayer, - }); + isLoggedIn: user?.isLoggedIn, + }; + // @ts-ignore + window.dataLayer?.push(payload); } - }, [pageProps]); + }, [mounted, pageProps, user]); return ( <> diff --git a/pages/api/sitemap/[filename].tsx b/pages/api/sitemap/[filename].tsx new file mode 100644 index 00000000..811ef755 --- /dev/null +++ b/pages/api/sitemap/[filename].tsx @@ -0,0 +1,47 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import axios, { AxiosError, AxiosResponse } from "axios"; +import { DC_SITEMAP_BUCKET } from "@/lib/constants/bucket"; + +class NotFound extends Error { + constructor() { + super("Not Found"); + } +} + +const getObject = async (filename: string): Promise => { + if (!DC_SITEMAP_BUCKET || !filename) throw new NotFound(); + try { + return await axios(`http://${DC_SITEMAP_BUCKET}/${filename}`, { responseType: "stream" }); + } catch (err) { + console.warn('caught in getObject', err); + if (err instanceof AxiosError) throw new NotFound(); + throw err; + } +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise { + const { filename } = req.query; + try { + const response: AxiosResponse = await getObject(filename as string); + + res + .status(200) + .setHeader("Content-Type", response.headers["content-type"] as string) + .setHeader("Content-Length", response.headers["content-length"] as string) + .setHeader("ETag", response.headers["etag"] as string) + .setHeader("Last-Modified", response.headers["last-modified"] as string); + response.data.pipe(res); + } catch (err) { + if (err instanceof NotFound) { + res.status(404).end("Not Found"); + } else { + throw err; + } + } +} + +export const config = { + api: { + responseLimit: false, + }, +}; diff --git a/pages/search.tsx b/pages/search.tsx index a73ce7a5..4077071a 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -1,3 +1,8 @@ +import { + NoResultsMessage, + ResultsMessage, + ResultsWrapper, +} from "@/components/Search/Search.styled"; import React, { useEffect, useState } from "react"; import { ApiSearchRequestBody } from "@/types/api/request"; import { ApiSearchResponse } from "@/types/api/response"; @@ -11,7 +16,6 @@ import Layout from "@/components/layout"; import { NextPage } from "next"; import { PRODUCTION_URL } from "@/lib/constants/endpoints"; import PaginationAltCounts from "@/components/Search/PaginationAltCounts"; -import { Results } from "@/components/Search/Search.styled"; import { apiPostRequest } from "@/lib/dc-api"; import axios from "axios"; import { buildDataLayer } from "@/lib/ga/data-layer"; @@ -130,19 +134,32 @@ const SearchPage: NextPage = () => { -
+ {loading && <>} {error &&

{error}

} {apiData && ( <> - - {totalResults && pluralize("result", totalResults)} - + {totalResults ? ( + + {" "} + {pluralize("result", totalResults)} + + ) : ( + + Your search did not match any results.{" "} + Please try broadening your search terms or adjusting your + filters. + + )} - + {totalResults ? ( + + ) : ( + <> + )} )} -
+
diff --git a/pages/shared/[id].tsx b/pages/shared/[id].tsx index 09d53257..10446a6c 100644 --- a/pages/shared/[id].tsx +++ b/pages/shared/[id].tsx @@ -32,8 +32,8 @@ const SharedPage: NextPage = () => { } useEffect(() => { - getWorkAndManifest(router.query?.id as string); - }, [router]); + !!router.query.id && getWorkAndManifest(router.query.id as string); + }, [router.query.id]); if (!(work && manifest)) return <>; diff --git a/public/sitemap.xml b/public/sitemap.xml deleted file mode 100644 index 0eb99f7d..00000000 --- a/public/sitemap.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - https://dc.library.northwestern.edu/sitemap.xml.gz - 2023-01-19 - - - \ No newline at end of file diff --git a/terraform/main.tf b/terraform/main.tf index 2f3fa4e7..3be34afb 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -69,22 +69,39 @@ resource "aws_amplify_app" "dc-next" { lifecycle { ignore_changes = [ - custom_rule, basic_auth_credentials ] } environment_variables = { - ENV = var.environment_name - HONEYBADGER_API_KEY = var.honeybadger_api_key - HONEYBADGER_ENV = var.environment_name - NEXT_PUBLIC_DCAPI_ENDPOINT = var.next_public_dcapi_endpoint - NEXT_PUBLIC_DC_URL = var.next_public_dc_url + ENV = var.environment_name + HONEYBADGER_API_KEY = var.honeybadger_api_key + HONEYBADGER_ENV = var.environment_name + NEXT_PUBLIC_DCAPI_ENDPOINT = var.next_public_dcapi_endpoint + NEXT_PUBLIC_DC_URL = var.next_public_dc_url + NEXT_PUBLIC_DC_SITEMAP_BUCKET = aws_s3_bucket_website_configuration.sitemap_website.website_endpoint + } + + custom_rule { + source = "/sitemap.xml" + target = "/api/sitemap/sitemap.xml" + status = 200 + } + + custom_rule { + source = "/sitemap.xml.gz" + target = "/api/sitemap/sitemap.xml.gz" + status = 200 + } + + custom_rule { + source = "/sitemap-<*>" + target = "/api/sitemap/sitemap-<*>" + status = 200 } } resource "aws_iam_role" "dc_next_amplify_role" { - name = "${var.project}-role" assume_role_policy = join("", data.aws_iam_policy_document.assume_role.*.json) managed_policy_arns = ["arn:aws:iam::aws:policy/AdministratorAccess-Amplify"] diff --git a/terraform/sitemap_bucket.tf b/terraform/sitemap_bucket.tf new file mode 100644 index 00000000..7689f4ae --- /dev/null +++ b/terraform/sitemap_bucket.tf @@ -0,0 +1,28 @@ +data "aws_iam_policy_document" "allow_sitemap_bucket_access_from_cloudfront" { + statement { + actions = ["s3:GetObject"] + resources = ["${aws_s3_bucket.sitemap_bucket.arn}/*"] + + principals { + type = "AWS" + identifiers = ["*"] + } + } +} + +resource "aws_s3_bucket_policy" "sitemap_bucket" { + bucket = aws_s3_bucket.sitemap_bucket.id + policy = data.aws_iam_policy_document.allow_sitemap_bucket_access_from_cloudfront.json +} + +resource "aws_s3_bucket" "sitemap_bucket" { + bucket = "${var.project}-${var.environment_name}-sitemaps" +} + +resource "aws_s3_bucket_website_configuration" "sitemap_website" { + bucket = aws_s3_bucket.sitemap_bucket.bucket + + index_document { + suffix = "sitemap.xml" + } +}