diff --git a/astro.config.mjs b/astro.config.mjs index 9f653ff8be..6b9db93a73 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -92,38 +92,13 @@ export default defineConfig({ rss: `${site}/rss`, }, components: { + Head: 'src/components/overrides/Head.astro', Header: './src/components/overrides/Header.astro', Footer: 'src/components/overrides/Footer.astro', ThemeSelect: 'src/components/overrides/ThemeSelect.astro', PageFrame: 'src/components/overrides/PageFrame.astro', }, - head: [ - { - tag: 'meta', - attrs: { property: 'og:image', content: site + '/og.png?v=1' }, - }, - { - tag: 'meta', - attrs: { property: 'twitter:image', content: site + '/og.png?v=1' }, - }, - { - tag: 'script', - attrs: { - src: '/navigate.js', - }, - }, - { - tag: 'link', - attrs: { - rel: 'manifest', - href: '/manifest.json', - }, - }, - { - tag: 'meta', - attrs: { name: 'theme-color', content: '#181818' }, - }, - ], + // head: moved to Head.astro override editLink: { baseUrl: process.env.NODE_ENV === 'development' diff --git a/package.json b/package.json index 94772ed416..d383f2d640 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "preview": "astro preview" }, "dependencies": { + "@fontsource-variable/inter": "^5.0.16", + "@fontsource/noto-sans-sc": "^5.0.17", "@astrojs/markdown-remark": "^5.0.0", "@astrojs/rss": "^4.0.5", "@astrojs/starlight": "^0.24.0", @@ -29,6 +31,8 @@ "astro": "^4.4.4", "astro-d2": "^0.3.0", "astro-feelback": "^0.3.4", + "astro-og-canvas": "^0.4.2", + "canvaskit-wasm": "^0.39.1", "astrojs-service-worker": "^2.0.0", "jsdom": "^24.0.0", "prettier": "^3.2.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc9e5bbe7f..4a63564ec8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,12 @@ importers: '@astrojs/starlight': specifier: ^0.24.0 version: 0.24.5(patch_hash=kv7yo3q5mkk7b2of2xzi4btao4)(astro@4.11.5(@types/node@20.14.10)(sass@1.77.7)(terser@5.31.0)(typescript@5.5.3)) + '@fontsource-variable/inter': + specifier: ^5.0.16 + version: 5.0.19 + '@fontsource/noto-sans-sc': + specifier: ^5.0.17 + version: 5.0.19 '@lorenzo_lewis/starlight-utils': specifier: ^0.1.0 version: 0.1.2(@astrojs/starlight@0.24.5(patch_hash=kv7yo3q5mkk7b2of2xzi4btao4)(astro@4.11.5(@types/node@20.14.10)(sass@1.77.7)(terser@5.31.0)(typescript@5.5.3)))(astro@4.11.5(@types/node@20.14.10)(sass@1.77.7)(terser@5.31.0)(typescript@5.5.3)) @@ -37,9 +43,15 @@ importers: astro-feelback: specifier: ^0.3.4 version: 0.3.4 + astro-og-canvas: + specifier: ^0.4.2 + version: 0.4.2(astro@4.11.5(@types/node@20.14.10)(sass@1.77.7)(terser@5.31.0)(typescript@5.5.3)) astrojs-service-worker: specifier: ^2.0.0 version: 2.0.0(@types/babel__core@7.20.5)(astro@4.11.5(@types/node@20.14.10)(sass@1.77.7)(terser@5.31.0)(typescript@5.5.3)) + canvaskit-wasm: + specifier: ^0.39.1 + version: 0.39.1 jsdom: specifier: ^24.0.0 version: 24.1.0 @@ -955,6 +967,12 @@ packages: '@feelback/js@0.3.4': resolution: {integrity: sha512-xr7gTqSJcVUYQlELs1TntYovCBjMcYUr/hGKTnDoF64/lig5CbX4bOmqLoF50IImCy5q3oIwg9w+TSFvtBwsIA==} + '@fontsource-variable/inter@5.0.19': + resolution: {integrity: sha512-V5KPpF5o0sI1uNWAdFArC87NDOb/ZJDPXLomEiKmDCYMlDUCTn2flkuAZkyME2rtGOKO7vzCuDJAND0m/5PhDA==} + + '@fontsource/noto-sans-sc@5.0.19': + resolution: {integrity: sha512-WI9j4N3sz9RQElaBpkIE/gnaX6VhIO4ajOq1FVEVKSrxyfAhXtFBGZuG7KBqhWbWrx/DYrxOjK2O9BPxIWKORA==} + '@img/sharp-darwin-arm64@0.33.4': resolution: {integrity: sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==} engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} @@ -1336,6 +1354,9 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + '@webgpu/types@0.1.21': + resolution: {integrity: sha512-pUrWq3V5PiSGFLeLxoGqReTZmiiXwY3jRkIG5sLLKjyqNxrwm/04b4nw7LSmGWJcKk59XOM/YRTUwOzo4MMlow==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1442,6 +1463,12 @@ packages: '@astrojs/db': optional: true + astro-og-canvas@0.4.2: + resolution: {integrity: sha512-OQsH6Gr2HX9ZRHdVy2OcXVBIPI65WvEtLG/60krnphh8d3ldhuAFunymYaNGcrdSZcYgXkHWejbPt//3qaRidA==} + engines: {node: '>=18.14.1'} + peerDependencies: + astro: ^3.0.0 || ^4.0.0 + astro-remote@0.3.2: resolution: {integrity: sha512-Xwm6Y+ldQEnDB2l1WwVqeUs3QvUX8LtJWnovpXlf8xhpicPu159jXOhDbHZS9wilGO/+/nR67A1qskF8pDvdGQ==} engines: {node: '>=18.14.1'} @@ -1551,6 +1578,12 @@ packages: caniuse-lite@1.0.30001640: resolution: {integrity: sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==} + canvaskit-wasm@0.37.2: + resolution: {integrity: sha512-212imazRF98gLOTiU4JAXM7xDvaknI7jaPtAg4ETXGW5rLQs6pomgIvVPUSfoKnQVTdGgzj+B4e+/u0Da20aGg==} + + canvaskit-wasm@0.39.1: + resolution: {integrity: sha512-Gy3lCmhUdKq+8bvDrs9t8+qf7RvcjuQn+we7vTVVyqgOVO1UVfHpsnBxkTZw+R4ApEJ3D5fKySl9TU11hmjl/A==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -4787,6 +4820,10 @@ snapshots: '@feelback/js@0.3.4': {} + '@fontsource-variable/inter@5.0.19': {} + + '@fontsource/noto-sans-sc@5.0.19': {} + '@img/sharp-darwin-arm64@0.33.4': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.0.2 @@ -5133,6 +5170,8 @@ snapshots: '@ungap/structured-clone@1.2.0': {} + '@webgpu/types@0.1.21': {} + acorn-jsx@5.3.2(acorn@8.12.0): dependencies: acorn: 8.12.0 @@ -5235,6 +5274,13 @@ snapshots: pathe: 1.1.2 recast: 0.23.9 + astro-og-canvas@0.4.2(astro@4.11.5(@types/node@20.14.10)(sass@1.77.7)(terser@5.31.0)(typescript@5.5.3)): + dependencies: + astro: 4.11.5(@types/node@20.14.10)(sass@1.77.7)(terser@5.31.0)(typescript@5.5.3) + canvaskit-wasm: 0.37.2 + deterministic-object-hash: 2.0.2 + entities: 4.5.0 + astro-remote@0.3.2: dependencies: entities: 4.5.0 @@ -5435,6 +5481,12 @@ snapshots: caniuse-lite@1.0.30001640: {} + canvaskit-wasm@0.37.2: {} + + canvaskit-wasm@0.39.1: + dependencies: + '@webgpu/types': 0.1.21 + ccount@2.0.1: {} chalk@2.4.2: diff --git a/src/assets/og-bg.png b/src/assets/og-bg.png new file mode 100644 index 0000000000..9dce9dfadb Binary files /dev/null and b/src/assets/og-bg.png differ diff --git a/src/assets/og-logo.png b/src/assets/og-logo.png new file mode 100644 index 0000000000..cf840341a8 Binary files /dev/null and b/src/assets/og-logo.png differ diff --git a/src/components/overrides/Head.astro b/src/components/overrides/Head.astro new file mode 100644 index 0000000000..aeed8578b1 --- /dev/null +++ b/src/components/overrides/Head.astro @@ -0,0 +1,46 @@ +--- +/** TODO: Check how to override the current metatag 'content' instead + * so that the settings on @file astro.config can be utilized along + * without conflicts. + * + * Tested with these (from Starlight source @file head.astro) but couldn't get around getting the + * default head into a format to pass into the @function createHead + * (virtual needs /// on @file env.ts) + * + * --- + * import { createHead } from 'node_modules/@astrojs/starlight/utils/head'; + * import config from 'virtual:starlight/user-config'; + * const head = createHead(headDefaults, config.head, data.head); + * --- + * {head.map(({ tag: Tag, attrs, content }) => )} + * + */ + +import type { Props } from '@astrojs/starlight/props'; +import Default from '@astrojs/starlight/components/Head.astro'; + +import { getOgImageUrl } from 'src/utils/getOgImageUrl'; + +const { isFallback } = Astro.props; +const ogImageUrl = getOgImageUrl(Astro.url.pathname, !!isFallback); +const imageSrc = ogImageUrl ?? 'og.png'; + +let canonicalImageSrc = new URL(imageSrc, Astro.site); + +// Override URL for development environment +const isDev = import.meta.env.DEV; +if (isDev) { + canonicalImageSrc = new URL(imageSrc, Astro.url); +} +--- + + + + + + + + + + + diff --git a/src/pages/0/0.ts b/src/pages/0/0.ts new file mode 100644 index 0000000000..8e52bbd88e --- /dev/null +++ b/src/pages/0/0.ts @@ -0,0 +1,9 @@ +import { allPages } from '../../utils/content'; + +// https://github.com/withastro/docs/blob/main/src/pages/0/0.ts +// https://www.github.com/withastro/docs/pull/4266/commits/030073f32d6dfe586c6e1da8d48d6b5485541ba2 + +export function GET() { + allPages[0]; + return new Response(''); +} diff --git a/src/pages/open-graph/[...docs].ts b/src/pages/open-graph/[...docs].ts new file mode 100644 index 0000000000..a282384237 --- /dev/null +++ b/src/pages/open-graph/[...docs].ts @@ -0,0 +1,110 @@ +import type { CollectionEntry } from 'astro:content'; +import { OGImageRoute } from 'astro-og-canvas'; +import { allPages } from 'src/utils/content'; + +// setup rtlLanguages here +const rtlLanguages = new Set(['']); +const getLangFromSlug = (slug: CollectionEntry<'docs'>['slug']) => slug.split('/')[0]; + +/** Paths for all of our Markdown content we want to generate OG images for. */ +const paths = process.env.CONTEXT == 'production' || import.meta.env.DEV ? allPages : []; +// const paths = allPages // use this to build locally + +/** An object mapping file paths to file metadata. */ +const pages = Object.fromEntries(paths.map(({ id, slug, data }) => [id, { data, slug }])); + +/** + * TODO: This can be improved + * Helper function to clamp a string + * @returns A string that fits in two lines with "..." at the end if text is longer than MAX_LEN + */ +function clampTwoLines(txt: string, fontSize: number): string { + // those numbers are what more or less fit to description and title size, not precisely + // it can vary based on font, as of now it matches Inter. + // Maybe this can help? https://github.com/adambisek/string-pixel-width/blob/master/src/pixelWidthCalculator.html + // or this https://github.com/Evgenus/js-server-text-width + let MAX_LEN = 73; + // title: + if (fontSize > 60) { + MAX_LEN = 48; + } + if (txt.length > MAX_LEN) { + txt = txt.trim().substring(0, MAX_LEN).trim(); + txt[txt.length - 1] === '.' ? (txt += '..') : (txt += '...'); + } + const arTxt = breakText(txt, 2, 80 / 2); + return arTxt.join(''); +} + +function breakText(str: string, maxLines: number, maxLineLen: number) { + const segmenterTitle = new Intl.Segmenter('en-US', { granularity: 'word' }); + const segments = segmenterTitle.segment(str); + + let linesOut = ['']; + let lineNo = 0; + let offsetInLine = 0; + for (const word of Array.from(segments)) { + if (offsetInLine + word.segment.length >= maxLineLen) { + lineNo++; + offsetInLine = 0; + linesOut.push(''); + } + + if (lineNo >= maxLines) { + return linesOut.slice(0, maxLines); + } + + linesOut[lineNo] += word.segment; + offsetInLine += word.segment.length; + } + + return linesOut; +} + +// REFERENCE: +// https://github.com/delucis/astro-og-canvas/tree/latest/packages/astro-og-canvas +export const { getStaticPaths, GET } = OGImageRoute({ + param: 'docs', + pages, + getImageOptions: async (_, { data, slug }: (typeof pages)[string]) => { + /** titleSize and descSize are coupled with @function clampTwoLines() */ + let [titleSize, descSize] = [72, 48]; + let description = ''; + let title = clampTwoLines(data.title, titleSize); + if (data.description) { + description = clampTwoLines(data.description, descSize); + } + if (slug.startsWith('blog/') && data.date && data.excerpt) { + description = clampTwoLines(data.excerpt, descSize); + } + return { + title, + description, + dir: rtlLanguages.has(getLangFromSlug(slug)) ? 'rtl' : 'ltr', + padding: 66, + bgImage: { path: './src/assets/og-bg.png' }, + logo: { path: './src/assets/og-logo.png' }, + font: { + title: { + /** Size is coupled with @function clampTwoLines() */ + size: titleSize, + lineHeight: 1.25, + weight: 'Normal', + families: ['Inter', 'Noto Sans SC Thin'], + }, + description: { + /** Size is coupled with @function clampTwoLines() */ + size: descSize, + lineHeight: 1.25, + weight: 'Normal', + families: ['Inter', 'Noto Sans SC Thin'], + }, + }, + fonts: [ + './node_modules/@fontsource-variable/inter/files/inter-latin-standard-normal.woff2', + // simplified chinese + './node_modules/@fontsource/noto-sans-sc/files/noto-sans-sc-chinese-simplified-400-normal.woff2', + ], + }; + }, +}); diff --git a/src/pages/open-graph/_open-graph-files.md b/src/pages/open-graph/_open-graph-files.md new file mode 100644 index 0000000000..3ba4db22e9 --- /dev/null +++ b/src/pages/open-graph/_open-graph-files.md @@ -0,0 +1,11 @@ +# Open Graph files: + +```sh +src/assets/og-bg.png - Background image +src/assets/og-logo.png - Logo image +src/components/overrides/Head.astro - Head override to inject social metatags +src/pages/0/0.ts - Chris\'s "00 hack" to make it work in Starlight in this specific setup +src/utils/content.ts - Export docs collection +src/utils/getOgImageUrl.ts - Used in the head override to get the url for current page +src/pages/open-graph/[...docs].ts - Customize and generate each image and rtlLanguages +``` diff --git a/src/pages/open-graph/preview.astro b/src/pages/open-graph/preview.astro new file mode 100644 index 0000000000..d373b68dd9 --- /dev/null +++ b/src/pages/open-graph/preview.astro @@ -0,0 +1,39 @@ +--- +import { Image } from 'astro:assets'; + +let base = Astro.url.origin; +if (process.env.CONTEXT !== 'production') { + base = process.env.DEPLOY_URL ?? ''; +} + +const a = `${base}/open-graph/blog/partnership-crabnebula.png`; +const b = `${base}/open-graph/plugin/cli.png`; +const c = `${base}/open-graph/zh-cn/plugin/notification.png`; +const d = `${base}/open-graph/zh-cn/plugin.png`; +const e = `${base}/open-graph/fr/plugin.png`; +const f = `${base}/open-graph/blog/tauri-board-elections-and-governance-updates.png`; +const g = `${base}/open-graph/blog/tauri-2-0-0-alpha-4.png`; +const h = `${base}/open-graph/reference/environment-variables.png`; +const i = `${base}/open-graph/blog/tauri-community-growth-and-feedback.png`; +--- + + + + + + + + Preview + + + _ + _ + _ + _ + _ + _ + _ + _ + _ + + diff --git a/src/utils/content.ts b/src/utils/content.ts new file mode 100644 index 0000000000..4d4edc88f2 --- /dev/null +++ b/src/utils/content.ts @@ -0,0 +1,2 @@ +import { getCollection } from 'astro:content'; +export const allPages = await getCollection('docs'); diff --git a/src/utils/getOgImageUrl.ts b/src/utils/getOgImageUrl.ts new file mode 100644 index 0000000000..93db702728 --- /dev/null +++ b/src/utils/getOgImageUrl.ts @@ -0,0 +1,24 @@ +//99% from https://github.com/withastro/docs/blob/096295e306cd7b4973fb2be154e2bdbcba1df8fd/src/util/getOgImageUrl.ts + +import { getStaticPaths } from '../pages/open-graph/[...docs]'; +import type { GetStaticPathsOptions, GetStaticPathsResult } from 'astro'; +const routes = (await getStaticPaths({} as GetStaticPathsOptions)) as GetStaticPathsResult; + +/** All the OpenGraph image paths as generated by our `getStaticPaths`. */ +const paths = new Set(routes.map(({ params }) => params.docs)); + +/** + * Get the path to the OpenGraph image for a page + * @param path Pathname of the page URL. + * @param isFallback Whether or not this page is displaying fallback content. + * @returns Path to the OpenGraph image if found. Otherwise, `undefined`. + */ +export function getOgImageUrl(path: string, isFallback: boolean): string | undefined { + let imagePath = path.replace(/^\//, '').replace(/\/$/, '') + '.png'; + + if (isFallback) { + // Remove the language segment for fallback pages. + imagePath = imagePath.slice(imagePath.indexOf('/') + 1); + } + if (paths.has(imagePath)) return '/open-graph/' + imagePath; +}