From 96ed81265db667ce5d505c763386f9a94e2f51bf Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Fri, 22 Mar 2024 10:26:30 +0000 Subject: [PATCH 1/5] fix: pull frontmatter from proper key --- themes/book/app/routes/$.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/themes/book/app/routes/$.tsx b/themes/book/app/routes/$.tsx index acf377a96..e13622d7e 100644 --- a/themes/book/app/routes/$.tsx +++ b/themes/book/app/routes/$.tsx @@ -27,14 +27,16 @@ import { ComputeOptionsProvider, ThebeLoaderAndServer } from '@myst-theme/jupyte export const meta: MetaFunction = (args) => { const config = args.parentsData?.root?.config as SiteManifest | undefined; - const data = args.data as PageLoader | undefined; - if (!config || !data || !data.frontmatter) return {}; + const data = args.data; + if (!config || !data) return {}; + const frontmatter = (data.page as PageLoader).frontmatter; + if (!frontmatter) return {}; return getMetaTagsForArticle({ origin: '', url: args.location.pathname, - title: `${data.frontmatter.title} - ${config?.title}`, - description: data.frontmatter.description, - image: (data.frontmatter.thumbnailOptimized || data.frontmatter.thumbnail) ?? undefined, + title: `${frontmatter.title} - ${config?.title}`, + description: frontmatter.description, + image: (frontmatter.thumbnailOptimized || frontmatter.thumbnail) ?? undefined, }); }; From 212f97017faf124daf0bcc68bd3bf6fc1a01c91f Mon Sep 17 00:00:00 2001 From: stevejpurves Date: Mon, 1 Apr 2024 16:47:38 +0100 Subject: [PATCH 2/5] =?UTF-8?q?=E2=8F=AB=20V2=20meta=20functions=20set=20f?= =?UTF-8?q?ilds=20based=20on=20single=20project=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/common/src/utils.ts | 5 ++- packages/site/src/seo/meta.ts | 66 +++++++++++++++++++++++++++++-- themes/book/app/root.tsx | 9 +---- themes/book/app/routes/$.tsx | 44 ++++++++++++++------- themes/book/app/routes/_index.tsx | 36 ++++++++++++++--- themes/book/remix.config.dev.js | 1 + themes/book/remix.config.prod.js | 1 + 7 files changed, 129 insertions(+), 33 deletions(-) diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index 73579e02e..b7e8652db 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -17,8 +17,9 @@ export function getProject( projectSlug?: string, ): ManifestProject | undefined { if (!config) return undefined; - if (!projectSlug) return config.projects?.[0]; - const project = config.projects?.find((p) => p.slug === projectSlug); + if (!config.projects || config.projects.length === 0) return undefined; + if (!projectSlug) return config.projects[0]; + const project = config.projects?.find((p) => p.slug === projectSlug) ?? config.projects[0]; return project; } diff --git a/packages/site/src/seo/meta.ts b/packages/site/src/seo/meta.ts index 324a1a2bc..0b4625b6b 100644 --- a/packages/site/src/seo/meta.ts +++ b/packages/site/src/seo/meta.ts @@ -1,7 +1,8 @@ -import type { HtmlMetaDescriptor } from '@remix-run/react'; +import type { HtmlMetaDescriptor, V2_MetaDescriptor } from '@remix-run/react'; type SocialSite = { title: string; + description?: string; twitter?: string; }; @@ -20,15 +21,38 @@ function allDefined(meta: Record): HtmlMetaDe return Object.fromEntries(Object.entries(meta).filter(([, v]) => v)) as HtmlMetaDescriptor; } -export function getMetaTagsForSite({ title, twitter }: SocialSite): HtmlMetaDescriptor { +export function getMetaTagsForSite_V1({ + title, + description, + twitter, +}: SocialSite): HtmlMetaDescriptor { const meta = { title, + description, 'twitter:site': twitter ? `@${twitter.replace('@', '')}` : undefined, }; return allDefined(meta); } -export function getMetaTagsForArticle({ +export function getMetaTagsForSite({ + title, + description, + twitter, +}: SocialSite): V2_MetaDescriptor[] { + const meta: V2_MetaDescriptor[] = [ + { title }, + { property: 'og:title', content: title }, + { name: 'generator', content: 'mystmd' }, + ]; + if (description) { + meta.push({ name: 'description', content: description }); + meta.push({ property: 'og:description', content: description }); + } + if (twitter) meta.push({ name: 'twitter:site', content: `@${twitter.replace('@', '')}` }); + return meta; +} + +export function getMetaTagsForArticle_V1({ origin, url, title, @@ -55,3 +79,39 @@ export function getMetaTagsForArticle({ }; return allDefined(meta); } + +export function getMetaTagsForArticle({ + origin, + url, + title, + description, + image, + twitter, + keywords, +}: SocialArticle): V2_MetaDescriptor[] { + const meta: V2_MetaDescriptor[] = [ + { title }, + { property: 'og:title', content: title }, + { name: 'generator', content: 'mystmd' }, + ]; + if (description) { + meta.push({ name: 'description', content: description }); + meta.push({ property: 'og:description', content: description }); + } + if (keywords) meta.push({ name: 'keywords', content: keywords.join(', ') }); + if (origin && url) meta.push({ property: 'og:url', content: `${origin}${url}` }); + if (image) { + meta.push({ name: 'image', content: image }); + meta.push({ property: 'og:image', content: image }); + } + if (twitter) { + meta.push({ name: 'twitter:card', content: image ? 'summary_large_image' : 'summary' }); + meta.push({ name: 'twitter:creator', content: `@${twitter.replace('@', '')}` }); + meta.push({ name: 'twitter:title', content: title }); + if (description) meta.push({ name: 'twitter:description', content: description }); + if (image) meta.push({ name: 'twitter:image', content: image }); + meta.push({ name: 'twitter:alt', content: title }); + } + + return meta; +} diff --git a/themes/book/app/root.tsx b/themes/book/app/root.tsx index e10275469..f9a8447a8 100644 --- a/themes/book/app/root.tsx +++ b/themes/book/app/root.tsx @@ -1,4 +1,4 @@ -import type { LinksFunction, MetaFunction, LoaderFunction } from '@remix-run/node'; +import type { LinksFunction, V2_MetaFunction, LoaderFunction } from '@remix-run/node'; import tailwind from '~/styles/app.css'; import thebeCoreCss from 'thebe-core/dist/lib/thebe-core.css'; import { getConfig } from '~/utils/loaders.server'; @@ -14,13 +14,6 @@ import { import { Outlet, useLoaderData } from '@remix-run/react'; export { AppCatchBoundary as CatchBoundary } from '@myst-theme/site'; -export const meta: MetaFunction = ({ data }) => { - return getMetaTagsForSite({ - title: data?.config?.title, - twitter: data?.config?.options?.twitter, - }); -}; - export const links: LinksFunction = () => { return [ { rel: 'stylesheet', href: tailwind }, diff --git a/themes/book/app/routes/$.tsx b/themes/book/app/routes/$.tsx index e13622d7e..bd66de56c 100644 --- a/themes/book/app/routes/$.tsx +++ b/themes/book/app/routes/$.tsx @@ -1,7 +1,11 @@ -import { json, type LinksFunction, type LoaderFunction, type MetaFunction } from '@remix-run/node'; +import { + json, + type V2_MetaFunction, + type LinksFunction, + type LoaderFunction, +} from '@remix-run/node'; import { getProject, isFlatSite, type PageLoader } from '@myst-theme/common'; import { - getMetaTagsForArticle, KatexCSS, ArticlePage, useOutlineHeight, @@ -10,6 +14,7 @@ import { Navigation, TopNav, ArticlePageCatchBoundary, + getMetaTagsForArticle, } from '@myst-theme/site'; import { getConfig, getPage } from '~/utils/loaders.server'; import { useLoaderData } from '@remix-run/react'; @@ -25,18 +30,28 @@ import { import { MadeWithMyst } from '@myst-theme/icons'; import { ComputeOptionsProvider, ThebeLoaderAndServer } from '@myst-theme/jupyter'; -export const meta: MetaFunction = (args) => { - const config = args.parentsData?.root?.config as SiteManifest | undefined; - const data = args.data; - if (!config || !data) return {}; - const frontmatter = (data.page as PageLoader).frontmatter; - if (!frontmatter) return {}; +type ManifestProject = Required['projects'][0]; + +export const meta: V2_MetaFunction = ({ data, matches, location }) => { + if (!data) return []; + + const config: SiteManifest = data.config; + const project: ManifestProject = data.project; + const page: PageLoader['frontmatter'] = data.page.frontmatter; + return getMetaTagsForArticle({ origin: '', - url: args.location.pathname, - title: `${frontmatter.title} - ${config?.title}`, - description: frontmatter.description, - image: (frontmatter.thumbnailOptimized || frontmatter.thumbnail) ?? undefined, + url: location.pathname, + title: page.title + ? `${page.title} - ${config?.title ?? project.title}` + : `${config?.title ?? project.title}`, + description: page.description ?? project.description ?? config.description ?? undefined, + image: + (page.thumbnailOptimized || page.thumbnail) ?? + (project.thumbnailOptimized || project.thumbnail) ?? + undefined, + twitter: config?.options?.twitter, + keywords: page.keywords ?? project.keywords ?? config.keywords ?? [], }); }; @@ -54,11 +69,10 @@ export const loader: LoaderFunction = async ({ params, request }) => { slug: flat ? slug : projectName ? slug : undefined, redirect: process.env.MODE === 'static' ? false : true, }); - return json({ page, project }); + console.log('isflat', flat, projectName, slug, page, project); + return json({ config, page, project }); }; -type ManifestProject = Required['projects'][0]; - export function ArticlePageAndNavigation({ children, hide_toc, diff --git a/themes/book/app/routes/_index.tsx b/themes/book/app/routes/_index.tsx index 45542b754..fea6626a5 100644 --- a/themes/book/app/routes/_index.tsx +++ b/themes/book/app/routes/_index.tsx @@ -1,19 +1,45 @@ -import { KatexCSS, responseNoArticle, responseNoSite } from '@myst-theme/site'; -import type { LinksFunction, LoaderFunction } from '@remix-run/node'; -import { redirect } from '@remix-run/node'; +import { + KatexCSS, + getMetaTagsForArticle, + responseNoArticle, + responseNoSite, +} from '@myst-theme/site'; +import type { LinksFunction, LoaderFunction, V2_MetaFunction } from '@remix-run/node'; +import { json, redirect } from '@remix-run/node'; import { getConfig, getPage } from '~/utils/loaders.server'; import Page from './$'; +import { SiteManifest } from 'myst-config'; +import { getProject } from '@myst-theme/common'; + +type ManifestProject = Required['projects'][0]; + +export const meta: V2_MetaFunction = ({ data, location }) => { + if (!data) return []; + + const config: SiteManifest = data.config; + const project: ManifestProject = data.project; + + return getMetaTagsForArticle({ + origin: '', + url: location.pathname, + title: config?.title ?? project.title, + description: config.description ?? project.description ?? undefined, + image: (project.thumbnailOptimized || project.thumbnail) ?? undefined, + keywords: config.keywords ?? project.keywords ?? [], + twitter: config?.options?.twitter, + }); +}; export const links: LinksFunction = () => [KatexCSS]; export const loader: LoaderFunction = async ({ params, request }) => { const config = await getConfig(); if (!config) throw responseNoSite(); - const project = config?.projects?.[0]; + const project = getProject(config); if (!project) throw responseNoArticle(); if (project.slug) return redirect(`/${project.slug}`); const page = await getPage(request, { slug: project.index }); - return { page, project }; + return json({ config, page, project }); }; export default Page; diff --git a/themes/book/remix.config.dev.js b/themes/book/remix.config.dev.js index 9056f2534..59d52e82b 100644 --- a/themes/book/remix.config.dev.js +++ b/themes/book/remix.config.dev.js @@ -63,5 +63,6 @@ module.exports = { v2_routeConvention: true, v2_normalizeFormMethod: true, v2_headers: true, + v2_meta: true, }, }; diff --git a/themes/book/remix.config.prod.js b/themes/book/remix.config.prod.js index 5ded9fa74..4851a1a7d 100644 --- a/themes/book/remix.config.prod.js +++ b/themes/book/remix.config.prod.js @@ -11,5 +11,6 @@ module.exports = { v2_routeConvention: true, v2_normalizeFormMethod: true, v2_headers: true, + v2_meta: true, }, }; From 142f6c189a288cccce8e0e277e12460e95dac0d3 Mon Sep 17 00:00:00 2001 From: stevejpurves Date: Mon, 1 Apr 2024 16:56:43 +0100 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=8D=81=20leave=20fallback=20root=20me?= =?UTF-8?q?ta=20function=20in=20place?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- themes/book/app/root.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/themes/book/app/root.tsx b/themes/book/app/root.tsx index f9a8447a8..b5731d413 100644 --- a/themes/book/app/root.tsx +++ b/themes/book/app/root.tsx @@ -14,6 +14,14 @@ import { import { Outlet, useLoaderData } from '@remix-run/react'; export { AppCatchBoundary as CatchBoundary } from '@myst-theme/site'; +export const meta: V2_MetaFunction = ({ data }) => { + return getMetaTagsForSite({ + title: data?.config?.title, + description: data?.config?.description, + twitter: data?.config?.options?.twitter, + }); +}; + export const links: LinksFunction = () => { return [ { rel: 'stylesheet', href: tailwind }, From b59ddf0b5b48316595dc7e4bd7f325d6e45c6f68 Mon Sep 17 00:00:00 2001 From: stevejpurves Date: Mon, 1 Apr 2024 16:58:57 +0100 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=93=98changeset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/modern-weeks-repeat.md | 5 +++++ .changeset/ninety-planes-pump.md | 6 ++++++ 2 files changed, 11 insertions(+) create mode 100644 .changeset/modern-weeks-repeat.md create mode 100644 .changeset/ninety-planes-pump.md diff --git a/.changeset/modern-weeks-repeat.md b/.changeset/modern-weeks-repeat.md new file mode 100644 index 000000000..df3e7f56a --- /dev/null +++ b/.changeset/modern-weeks-repeat.md @@ -0,0 +1,5 @@ +--- +'@myst-theme/book': patch +--- + +Migrated to V2 Remix meta function pattern diff --git a/.changeset/ninety-planes-pump.md b/.changeset/ninety-planes-pump.md new file mode 100644 index 000000000..606a3cfc1 --- /dev/null +++ b/.changeset/ninety-planes-pump.md @@ -0,0 +1,6 @@ +--- +'@myst-theme/common': minor +'@myst-theme/site': minor +--- + +Migrated to meta helper functions to the Remix V2 pattern, old meta helpers functions are still available with a `_v1` suffix. From 41d0171011744c593476924384cdb499b4dd76c9 Mon Sep 17 00:00:00 2001 From: stevejpurves Date: Mon, 1 Apr 2024 17:14:09 +0100 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=93=90migrate=20artcle=20theme=20meta?= =?UTF-8?q?=20to=20V2=20and=20align=20with=20book?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- themes/article/app/root.tsx | 5 ++-- themes/article/app/routes/$.tsx | 42 ++++++++++++++++++++-------- themes/article/app/routes/_index.tsx | 34 +++++++++++++++++++--- themes/article/remix.config.dev.js | 1 + themes/article/remix.config.prod.js | 1 + 5 files changed, 65 insertions(+), 18 deletions(-) diff --git a/themes/article/app/root.tsx b/themes/article/app/root.tsx index 207072cfd..6f9f95d0a 100644 --- a/themes/article/app/root.tsx +++ b/themes/article/app/root.tsx @@ -1,4 +1,4 @@ -import type { LinksFunction, MetaFunction, LoaderFunction } from '@remix-run/node'; +import type { LinksFunction, MetaFunction, LoaderFunction, V2_MetaFunction } from '@remix-run/node'; import tailwind from '~/styles/app.css'; import thebeCoreCss from 'thebe-core/dist/lib/thebe-core.css'; import { getConfig } from '~/utils/loaders.server'; @@ -14,9 +14,10 @@ import { import { Outlet, useLoaderData } from '@remix-run/react'; export { AppCatchBoundary as CatchBoundary } from '@myst-theme/site'; -export const meta: MetaFunction = ({ data }) => { +export const meta: V2_MetaFunction = ({ data }) => { return getMetaTagsForSite({ title: data?.config?.title, + description: data?.config?.description, twitter: data?.config?.options?.twitter, }); }; diff --git a/themes/article/app/routes/$.tsx b/themes/article/app/routes/$.tsx index b30927c30..2bf97bce4 100644 --- a/themes/article/app/routes/$.tsx +++ b/themes/article/app/routes/$.tsx @@ -1,5 +1,10 @@ -import { isFlatSite, type PageLoader } from '@myst-theme/common'; -import type { LinksFunction, LoaderFunction, MetaFunction } from '@remix-run/node'; +import { getProject, isFlatSite, type PageLoader } from '@myst-theme/common'; +import { + json, + type LinksFunction, + type LoaderFunction, + type V2_MetaFunction, +} from '@remix-run/node'; import { getMetaTagsForArticle, KatexCSS, ArticlePageCatchBoundary } from '@myst-theme/site'; import { getConfig, getPage } from '~/utils/loaders.server'; import { useLoaderData } from '@remix-run/react'; @@ -10,16 +15,28 @@ import { ComputeOptionsProvider } from '@myst-theme/jupyter'; import { ProjectProvider, useBaseurl } from '@myst-theme/providers'; import { ThebeLoaderAndServer } from '@myst-theme/jupyter'; -export const meta: MetaFunction = (args) => { - const config = args.parentsData?.root?.config as SiteManifest | undefined; - const data = args.data as PageLoader | undefined; - if (!config || !data || !data.frontmatter) return {}; +type ManifestProject = Required['projects'][0]; + +export const meta: V2_MetaFunction = ({ data, matches, location }) => { + if (!data) return []; + + const config: SiteManifest = data.config; + const project: ManifestProject = data.project; + const page: PageLoader['frontmatter'] = data.page.frontmatter; + return getMetaTagsForArticle({ origin: '', - url: args.location.pathname, - title: `${data.frontmatter.title} - ${config?.title}`, - description: data.frontmatter.description, - image: (data.frontmatter.thumbnailOptimized || data.frontmatter.thumbnail) ?? undefined, + url: location.pathname, + title: page.title + ? `${page.title} - ${config?.title ?? project.title}` + : `${config?.title ?? project.title}`, + description: page.description ?? project.description ?? config.description ?? undefined, + image: + (page.thumbnailOptimized || page.thumbnail) ?? + (project.thumbnailOptimized || project.thumbnail) ?? + undefined, + twitter: config?.options?.twitter, + keywords: page.keywords ?? project.keywords ?? config.keywords ?? [], }); }; @@ -30,13 +47,14 @@ export const loader: LoaderFunction = async ({ params, request }) => { const projectName = second ? first : undefined; const slug = second || first; const config = await getConfig(); + const project = getProject(config, projectName ?? slug); const flat = isFlatSite(config); const page = await getPage(request, { project: flat ? projectName : projectName ?? slug, slug: flat ? slug : projectName ? slug : undefined, redirect: process.env.MODE === 'static' ? false : true, }); - return page; + return json({ config, project, page }); }; export default function Page() { @@ -44,7 +62,7 @@ export default function Page() { // const { container, outline } = useOutlineHeight(); // const { hide_outline } = (article.frontmatter as any)?.options ?? {}; const baseurl = useBaseurl(); - const article = useLoaderData() as PageLoader; + const { page: article } = useLoaderData() as { page: PageLoader }; return ( diff --git a/themes/article/app/routes/_index.tsx b/themes/article/app/routes/_index.tsx index 231072225..2c5446466 100644 --- a/themes/article/app/routes/_index.tsx +++ b/themes/article/app/routes/_index.tsx @@ -1,19 +1,45 @@ -import { ProjectPageCatchBoundary, responseNoArticle, responseNoSite } from '@myst-theme/site'; +import { + ProjectPageCatchBoundary, + getMetaTagsForArticle, + responseNoArticle, + responseNoSite, +} from '@myst-theme/site'; import Page from './$'; import { ArticlePageAndNavigation } from '../components/ArticlePageAndNavigation'; import { getConfig, getPage } from '../utils/loaders.server'; -import type { LoaderFunction } from '@remix-run/node'; +import type { LoaderFunction, V2_MetaFunction } from '@remix-run/node'; import { redirect } from '@remix-run/node'; +import { SiteManifest } from 'myst-config'; +import { getProject } from '@myst-theme/common'; export { links } from './$'; +type ManifestProject = Required['projects'][0]; + +export const meta: V2_MetaFunction = ({ data, location }) => { + if (!data) return []; + + const config: SiteManifest = data.config; + const project: ManifestProject = data.project; + + return getMetaTagsForArticle({ + origin: '', + url: location.pathname, + title: config?.title ?? project.title, + description: config.description ?? project.description ?? undefined, + image: (project.thumbnailOptimized || project.thumbnail) ?? undefined, + keywords: config.keywords ?? project.keywords ?? [], + twitter: config?.options?.twitter, + }); +}; + export const loader: LoaderFunction = async ({ params, request }) => { const config = await getConfig(); if (!config) throw responseNoSite(); - const project = config?.projects?.[0]; + const project = getProject(config); if (!project) throw responseNoArticle(); if (project.slug) return redirect(`/${project.slug}`); const page = await getPage(request, { slug: project.index }); - return page; + return { config, project, page }; }; export default Page; diff --git a/themes/article/remix.config.dev.js b/themes/article/remix.config.dev.js index 9056f2534..59d52e82b 100644 --- a/themes/article/remix.config.dev.js +++ b/themes/article/remix.config.dev.js @@ -63,5 +63,6 @@ module.exports = { v2_routeConvention: true, v2_normalizeFormMethod: true, v2_headers: true, + v2_meta: true, }, }; diff --git a/themes/article/remix.config.prod.js b/themes/article/remix.config.prod.js index 5ded9fa74..4851a1a7d 100644 --- a/themes/article/remix.config.prod.js +++ b/themes/article/remix.config.prod.js @@ -11,5 +11,6 @@ module.exports = { v2_routeConvention: true, v2_normalizeFormMethod: true, v2_headers: true, + v2_meta: true, }, };