From 2f8f4a22743187fee29863137f03b1854935fbb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gorzeli=C5=84ski?= Date: Sat, 5 Oct 2024 10:32:18 +0200 Subject: [PATCH 1/3] refactor: Use cookie for theme --- constants/cookies.ts | 3 +- constants/theme.ts | 3 +- .../meta-image/meta-image.types.tsx | 2 +- hooks/use-theme.tsx | 9 ++-- lib/__tests__/meta-image.test.ts | 53 ++++++++++++++++++- lib/__tests__/theme.test.ts | 30 +---------- lib/index.ts | 8 +-- lib/meta-image.ts | 5 +- lib/theme.ts | 8 --- package-lock.json | 23 ++++++++ package.json | 1 + 11 files changed, 90 insertions(+), 55 deletions(-) diff --git a/constants/cookies.ts b/constants/cookies.ts index 5bdd9ef..50bd867 100644 --- a/constants/cookies.ts +++ b/constants/cookies.ts @@ -1,3 +1,4 @@ export const COOKIES = { - locale: 'locale' + locale: 'locale', + theme: 'theme' } as const diff --git a/constants/theme.ts b/constants/theme.ts index ab53a3b..a3b25e2 100644 --- a/constants/theme.ts +++ b/constants/theme.ts @@ -1,5 +1,4 @@ export const THEME = { attribute: 'data-color-mode', - osMedia: '(prefers-color-scheme: light)', - lsKey: 'theme' + osMedia: '(prefers-color-scheme: light)' } as const diff --git a/design-system/components/meta-image/meta-image.types.tsx b/design-system/components/meta-image/meta-image.types.tsx index 579fed3..d8898b8 100644 --- a/design-system/components/meta-image/meta-image.types.tsx +++ b/design-system/components/meta-image/meta-image.types.tsx @@ -3,6 +3,6 @@ import { Theme } from '@/types' export type MetaImageProps = { title: string subtitle: string - theme: Theme + theme?: Theme backgroundURL?: string } diff --git a/hooks/use-theme.tsx b/hooks/use-theme.tsx index b12b24a..ad97e1a 100644 --- a/hooks/use-theme.tsx +++ b/hooks/use-theme.tsx @@ -1,7 +1,8 @@ import { useEffect, useState } from 'react' +import { setCookie, hasCookie } from 'cookies-next' import { Theme } from '@/types' -import { THEME } from '@/constants' -import { getThemeAttribute, setThemeAttribute, setThemeToLs } from '@/lib' +import { COOKIES, THEME } from '@/constants' +import { getThemeAttribute, setThemeAttribute } from '@/lib' export function useTheme() { const [theme, setTheme] = useState('light') @@ -9,7 +10,7 @@ export function useTheme() { const saveTheme = (theme: Theme) => { setTheme(theme) setThemeAttribute(theme) - setThemeToLs(theme) + setCookie(COOKIES.theme, theme) } const toggleTheme = (theme: Theme): void => @@ -19,6 +20,8 @@ export function useTheme() { const savedTheme = getThemeAttribute() setTheme(savedTheme) + if (!hasCookie(COOKIES.theme)) setCookie(COOKIES.theme, savedTheme) + const listenOsThemeChange = (event: MediaQueryListEvent) => { saveTheme(event.matches ? 'light' : 'dark') } diff --git a/lib/__tests__/meta-image.test.ts b/lib/__tests__/meta-image.test.ts index 9828735..1457647 100644 --- a/lib/__tests__/meta-image.test.ts +++ b/lib/__tests__/meta-image.test.ts @@ -11,7 +11,7 @@ describe('meta-image', () => { }) expect(metaImage).toEqual({ - url: '/en/api/og/?title=title&subtitle=subtitle&backgroundURL=backgroundURL', + url: '/en/api/og/?title=title&subtitle=subtitle&theme=light&backgroundURL=backgroundURL', alt: 'alt', type: 'image/png', width: 1200, @@ -27,12 +27,61 @@ describe('meta-image', () => { }) expect(metaImage).toEqual({ - url: '/pl/api/twitter/?title=title&subtitle=subtitle', + url: '/pl/api/twitter/?title=title&subtitle=subtitle&theme=light', alt: 'alt', type: 'image/png', width: 1200, height: 600 }) }) + + it('returns meta image with the default theme', () => { + const metaImage = getMetaImage('og', 'en', { + alt: 'alt', + title: 'title', + subtitle: 'subtitle' + }) + + expect(metaImage).toEqual({ + url: '/en/api/og/?title=title&subtitle=subtitle&theme=light', + alt: 'alt', + type: 'image/png', + width: 1200, + height: 630 + }) + }) + + it('returns meta image with the default theme', () => { + const metaImage = getMetaImage('og', 'en', { + alt: 'alt', + title: 'title', + subtitle: 'subtitle' + }) + + expect(metaImage).toEqual({ + url: '/en/api/og/?title=title&subtitle=subtitle&theme=light', + alt: 'alt', + type: 'image/png', + width: 1200, + height: 630 + }) + }) + + it('returns meta image with the dark theme', () => { + const metaImage = getMetaImage('og', 'en', { + theme: 'dark', + alt: 'alt', + title: 'title', + subtitle: 'subtitle' + }) + + expect(metaImage).toEqual({ + url: '/en/api/og/?title=title&subtitle=subtitle&theme=dark', + alt: 'alt', + type: 'image/png', + width: 1200, + height: 630 + }) + }) }) }) diff --git a/lib/__tests__/theme.test.ts b/lib/__tests__/theme.test.ts index 287f186..ab1caee 100644 --- a/lib/__tests__/theme.test.ts +++ b/lib/__tests__/theme.test.ts @@ -1,10 +1,4 @@ -import { - getThemeAttribute, - getThemeFromLs, - hslToRgb, - setThemeAttribute, - setThemeToLs -} from '../theme' +import { getThemeAttribute, hslToRgb, setThemeAttribute } from '../theme' describe('theme', () => { describe('getThemeAttribute()', () => { @@ -25,28 +19,6 @@ describe('theme', () => { }) }) - describe('getThemeFromLs()', () => { - it('returns theme from local storage', () => { - window.localStorage.setItem('theme', 'dark') - - expect(getThemeFromLs()).toBe('dark') - }) - - it('returns null if there is no theme in local storage', () => { - window.localStorage.removeItem('theme') - - expect(getThemeFromLs()).toBe(null) - }) - }) - - describe('setThemeToLs()', () => { - it('sets ', () => { - setThemeToLs('light') - - expect(window.localStorage.getItem('theme')).toBe('light') - }) - }) - describe('hslToRgb()', () => { it('converts an HSL string to an RGB string', () => { expect(hslToRgb('hsl(208, 7%, 100%)')).toBe('rgb(255, 255, 255)') diff --git a/lib/index.ts b/lib/index.ts index eb67715..99bccf7 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -11,10 +11,4 @@ export { isInternal, getAbsoluteURL, generateAlternateLinks } from './link' export { getMetaImage } from './meta-image' export { selectActiveClass } from './navigation' export { capitalize } from './string' -export { - hslToRgb, - getThemeAttribute, - getThemeFromLs, - setThemeAttribute, - setThemeToLs -} from './theme' +export { hslToRgb, getThemeAttribute, setThemeAttribute } from './theme' diff --git a/lib/meta-image.ts b/lib/meta-image.ts index 7cca1b9..a83bb8d 100644 --- a/lib/meta-image.ts +++ b/lib/meta-image.ts @@ -9,7 +9,7 @@ type MetaImage = type Params = Pick< MetaImageProps, - 'title' | 'subtitle' | Partial<'backgroundURL'> + 'title' | 'subtitle' | Partial<'theme'> | Partial<'backgroundURL'> > & { alt: string } @@ -19,12 +19,13 @@ export function getMetaImage( lang: Locale, params: Params ): MetaImage { - const { alt, title, subtitle, backgroundURL } = params + const { theme, title, subtitle, backgroundURL, alt } = params const searchParams = new URLSearchParams() searchParams.set('title', title) searchParams.set('subtitle', subtitle) + theme ? searchParams.set('theme', theme) : searchParams.set('theme', 'light') if (backgroundURL) searchParams.set('backgroundURL', backgroundURL) const sizes = { diff --git a/lib/theme.ts b/lib/theme.ts index 95ef3ce..fecfcde 100644 --- a/lib/theme.ts +++ b/lib/theme.ts @@ -9,14 +9,6 @@ export function setThemeAttribute(theme: Theme) { document.documentElement.setAttribute(THEME.attribute, theme) } -export function setThemeToLs(theme: Theme) { - window.localStorage.setItem(THEME.lsKey, theme) -} - -export function getThemeFromLs() { - return window.localStorage.getItem(THEME.lsKey) as Theme | null -} - export function hslToRgb(hsl: string) { let [h, s, l] = hsl .replace('hsl(', '') diff --git a/package-lock.json b/package-lock.json index 7588c8e..8b45cca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@formatjs/intl-localematcher": "^0.5.2", + "cookies-next": "^4.2.1", "eslint-config-prettier": "^9.0.0", "negotiator": "^0.6.3", "next": "14.2.5", @@ -2855,6 +2856,11 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -4257,6 +4263,23 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookies-next": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/cookies-next/-/cookies-next-4.2.1.tgz", + "integrity": "sha512-qsjtZ8TLlxCSX2JphMQNhkm3V3zIMQ05WrLkBKBwu50npBbBfiZWIdmSMzBGcdGKfMK19E0PIitTfRFAdMGHXg==", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^0.6.0" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", diff --git a/package.json b/package.json index 330e2e6..63be37c 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@formatjs/intl-localematcher": "^0.5.2", + "cookies-next": "^4.2.1", "eslint-config-prettier": "^9.0.0", "negotiator": "^0.6.3", "next": "14.2.5", From ce8128f5e6a27c7963572c5f46b717f2ca447de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gorzeli=C5=84ski?= Date: Sat, 5 Oct 2024 11:15:49 +0200 Subject: [PATCH 2/3] fix: Make theme checks more restrictive --- app/[lang]/about/page.tsx | 7 ++-- .../meta-image/meta-image.types.tsx | 2 +- lib/__tests__/meta-image.test.ts | 34 +++++++++++++++---- lib/__tests__/theme.test.ts | 22 +++++++++++- lib/index.ts | 7 +++- lib/meta-image.ts | 4 ++- lib/theme.ts | 4 +++ 7 files changed, 68 insertions(+), 12 deletions(-) diff --git a/app/[lang]/about/page.tsx b/app/[lang]/about/page.tsx index 9e2ed37..0a42eae 100644 --- a/app/[lang]/about/page.tsx +++ b/app/[lang]/about/page.tsx @@ -1,7 +1,9 @@ import { Metadata } from 'next' +import { cookies } from 'next/headers' +import { getCookie } from 'cookies-next' import { WebPage, WithContext } from 'schema-dts' -import { PageProps } from '@/types' -import { LINKS } from '@/constants' +import { PageProps, Theme } from '@/types' +import { COOKIES, LINKS } from '@/constants' import { getDictionary } from '@/scripts' import { generateAlternateLinks, getMetaImage, localizePath } from '@/lib' import { openGraph, twitter } from '@/app/shared-metadata' @@ -31,6 +33,7 @@ export async function generateMetadata({ const { layout, page } = await getDictionary(lang) const languages = generateAlternateLinks(LINKS.about) const metaImageParams = { + theme: getCookie(COOKIES.theme, { cookies }) as Theme, title: page.about.metadata.title, subtitle: layout.root.metadata.title, alt: page.about.metadata.image.alt diff --git a/design-system/components/meta-image/meta-image.types.tsx b/design-system/components/meta-image/meta-image.types.tsx index d8898b8..579fed3 100644 --- a/design-system/components/meta-image/meta-image.types.tsx +++ b/design-system/components/meta-image/meta-image.types.tsx @@ -3,6 +3,6 @@ import { Theme } from '@/types' export type MetaImageProps = { title: string subtitle: string - theme?: Theme + theme: Theme backgroundURL?: string } diff --git a/lib/__tests__/meta-image.test.ts b/lib/__tests__/meta-image.test.ts index 1457647..1b0dfb8 100644 --- a/lib/__tests__/meta-image.test.ts +++ b/lib/__tests__/meta-image.test.ts @@ -7,6 +7,7 @@ describe('meta-image', () => { alt: 'alt', title: 'title', subtitle: 'subtitle', + theme: 'light', backgroundURL: 'backgroundURL' }) @@ -23,7 +24,8 @@ describe('meta-image', () => { const metaImage = getMetaImage('twitter', 'pl', { alt: 'alt', title: 'title', - subtitle: 'subtitle' + subtitle: 'subtitle', + theme: 'light' }) expect(metaImage).toEqual({ @@ -39,7 +41,8 @@ describe('meta-image', () => { const metaImage = getMetaImage('og', 'en', { alt: 'alt', title: 'title', - subtitle: 'subtitle' + subtitle: 'subtitle', + theme: 'light' }) expect(metaImage).toEqual({ @@ -51,11 +54,12 @@ describe('meta-image', () => { }) }) - it('returns meta image with the default theme', () => { + it('returns meta image with the light theme', () => { const metaImage = getMetaImage('og', 'en', { alt: 'alt', title: 'title', - subtitle: 'subtitle' + subtitle: 'subtitle', + theme: 'light' }) expect(metaImage).toEqual({ @@ -69,10 +73,10 @@ describe('meta-image', () => { it('returns meta image with the dark theme', () => { const metaImage = getMetaImage('og', 'en', { - theme: 'dark', alt: 'alt', title: 'title', - subtitle: 'subtitle' + subtitle: 'subtitle', + theme: 'dark' }) expect(metaImage).toEqual({ @@ -83,5 +87,23 @@ describe('meta-image', () => { height: 630 }) }) + + it('returns meta image with the default theme if the passed theme is wrong', () => { + const metaImage = getMetaImage('og', 'en', { + alt: 'alt', + title: 'title', + subtitle: 'subtitle', + // @ts-expect-error + theme: 'wrong' + }) + + expect(metaImage).toEqual({ + url: '/en/api/og/?title=title&subtitle=subtitle&theme=light', + alt: 'alt', + type: 'image/png', + width: 1200, + height: 630 + }) + }) }) }) diff --git a/lib/__tests__/theme.test.ts b/lib/__tests__/theme.test.ts index ab1caee..39a2f6b 100644 --- a/lib/__tests__/theme.test.ts +++ b/lib/__tests__/theme.test.ts @@ -1,6 +1,26 @@ -import { getThemeAttribute, hslToRgb, setThemeAttribute } from '../theme' +import { + getCorrectTheme, + getThemeAttribute, + hslToRgb, + setThemeAttribute +} from '../theme' describe('theme', () => { + describe('getCorrectTheme()', () => { + it('returns the light theme', () => { + expect(getCorrectTheme('light')).toBe('light') + }) + + it('returns the dark theme', () => { + expect(getCorrectTheme('dark')).toBe('dark') + }) + + it('returns the default theme', () => { + expect(getCorrectTheme(null)).toBe('light') + expect(getCorrectTheme('wrong')).toBe('light') + }) + }) + describe('getThemeAttribute()', () => { it('returns the theme attribute from the html element', () => { document.documentElement.setAttribute('data-color-mode', 'dark') diff --git a/lib/index.ts b/lib/index.ts index 99bccf7..66ce416 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -11,4 +11,9 @@ export { isInternal, getAbsoluteURL, generateAlternateLinks } from './link' export { getMetaImage } from './meta-image' export { selectActiveClass } from './navigation' export { capitalize } from './string' -export { hslToRgb, getThemeAttribute, setThemeAttribute } from './theme' +export { + hslToRgb, + getCorrectTheme, + getThemeAttribute, + setThemeAttribute +} from './theme' diff --git a/lib/meta-image.ts b/lib/meta-image.ts index a83bb8d..7fe80bd 100644 --- a/lib/meta-image.ts +++ b/lib/meta-image.ts @@ -2,6 +2,7 @@ import { Metadata } from 'next' import { Locale } from '@/i18n.config' import { CONTENTTYPE, OPENGRAPH, TWITTER } from '@/constants' import { MetaImageProps } from '@/design-system/components/meta-image/meta-image.types' +import { getCorrectTheme } from './theme' type MetaImage = | NonNullable['images'] @@ -25,7 +26,8 @@ export function getMetaImage( searchParams.set('title', title) searchParams.set('subtitle', subtitle) - theme ? searchParams.set('theme', theme) : searchParams.set('theme', 'light') + searchParams.set('theme', getCorrectTheme(theme)) + if (backgroundURL) searchParams.set('backgroundURL', backgroundURL) const sizes = { diff --git a/lib/theme.ts b/lib/theme.ts index fecfcde..19f7b0a 100644 --- a/lib/theme.ts +++ b/lib/theme.ts @@ -1,6 +1,10 @@ import { Theme } from '@/types' import { THEME } from '@/constants' +export function getCorrectTheme(theme: string | null) { + return theme === 'light' || theme === 'dark' ? theme : 'light' +} + export function getThemeAttribute() { return document.documentElement.getAttribute(THEME.attribute) as Theme } From 5a20c4a0b65d4496d7a7e737177b46439cba9681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gorzeli=C5=84ski?= Date: Sat, 5 Oct 2024 11:26:31 +0200 Subject: [PATCH 3/3] feat: Use the theme cookie in generating meta images --- app/[lang]/api/og/route.tsx | 8 ++-- app/[lang]/api/twitter/route.tsx | 8 ++-- app/[lang]/blog/[slug]/page.tsx | 7 +++- app/[lang]/blog/page.tsx | 7 +++- app/[lang]/layout.tsx | 43 ++++++++++------------ app/[lang]/not-found.tsx | 3 +- app/[lang]/portfolio/[slug]/page.tsx | 7 +++- app/[lang]/portfolio/page.tsx | 7 +++- app/[lang]/subscription-confirmed/page.tsx | 7 +++- app/[lang]/uses/page.tsx | 7 +++- 10 files changed, 62 insertions(+), 42 deletions(-) diff --git a/app/[lang]/api/og/route.tsx b/app/[lang]/api/og/route.tsx index 30dcb7a..d927990 100644 --- a/app/[lang]/api/og/route.tsx +++ b/app/[lang]/api/og/route.tsx @@ -1,17 +1,19 @@ import React from 'react' import { ImageResponse } from 'next/og' -import { Theme } from '@/types' import { OPENGRAPH } from '@/constants' import { getMetaFont } from '@/scripts' +import { getCorrectTheme } from '@/lib' import { MetaImage } from '@/design-system' export async function GET(request: Request) { try { const { searchParams } = new URL(request.url) - const theme = 'light' + + const theme = getCorrectTheme(searchParams.get('theme')) const title = searchParams.get('title')! const subtitle = searchParams.get('subtitle')! const backgroundURL = searchParams.get('backgroundURL') || undefined + const fontMedium = await getMetaFont('Montserrat-Medium.ttf', { weight: 500, style: 'normal' @@ -24,7 +26,7 @@ export async function GET(request: Request) { return new ImageResponse( ( { const { layout, page } = await getDictionary(lang) const metaImageParams = { + theme: getCookie(COOKIES.theme, { cookies }) as Theme, title: page.home.metadata.title, subtitle: layout.root.metadata.title, alt: page.home.metadata.image.alt @@ -57,6 +60,7 @@ export default async function RootLayout({ params: { lang: Locale } }) { const { lang } = params + const theme = getCookie(COOKIES.theme, { cookies }) const dictionary = await getDictionary(lang) const jsonLd: WithContext = { '@context': 'https://schema.org', @@ -75,37 +79,30 @@ export default async function RootLayout({ suppressHydrationWarning className={`${montserrat.variable} ${lora.variable} ${firaCode.variable}`} lang={lang} + data-color-mode={theme} >