diff --git a/apps/web/content/media/20-06-2024-04-51-47.json b/apps/web/content/media/20-06-2024-04-51-47.json new file mode 100644 index 000000000..0f9f361fc --- /dev/null +++ b/apps/web/content/media/20-06-2024-04-51-47.json @@ -0,0 +1,21 @@ +{ + "_id": "2i9FuEJofr5yFe9DuleODHjjWkm", + "_type": "MediaFile", + "_index": "a0", + "_i18nId": "2i9FuEJofr5yFe9DuleODHjjWkm", + "_root": "media", + "title": "20-06-2024-04-51-47", + "location": "/20-06-2024-04-51-47.2i9FuEJofr5yFe9DuleODHjjWkm.png", + "extension": ".png", + "size": 105986, + "hash": "3869032d", + "width": 640, + "height": 300, + "averageColor": "#c8c4eb", + "focus": { + "x": 0.4828125, + "y": 0.5 + }, + "thumbHash": "9AYGC4DWeWh3iPc4gHr5rNU=", + "preview": "data:image/webp;base64,UklGRp4EAABXRUJQVlA4WAoAAAAgAAAAnwAASgAASUNDUMgBAAAAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADZWUDggsAIAAPAQAJ0BKqAASwA+bTaYSKQjIqEimAk4gA2JaQDPBaY24jix+AHZT/YOUuPXPxGcJxllQI/tJ6M/+p5FPnL2Df5H/Veqr6Jv7SCpLwLkx9NA9AAV+SPw33PyO5t9DR7EG4lRCpRKtK5aU6l6+2ca+1wiIEFEMNeBfTeR3ItE/aIIPIZrlXiX5Fj/OIL6fhDcAAD+9vnv7ec2e3mKaKbuXv5xgI6RuLccrqOeTYW+5tvPW6awKuhPTJjw6qtKCuv58MAnfvhJpAdN96Z8B1vVuwpbud5eC7WYMgTPR56xXgPwHYhbQqr7xxCsJ4FAx6+NvWpj3lpj8h7fJ69HZqAyj9/IiW5mDr9gwuED/5N/HJX4r/hZ5tM9QZ9i0ZM00giAwhJ8ywtO4+1uLOjXd/aLNOJgVGZPW8kJofj43dH4f76eaZq861FLJ6lx6n06H2Yf79jt5HOEencQT7wn7UFOWJ8TEQpOFb95fuawwXbFWO+WGCbEFIshhkV0DLL06jory78ywAgnluoxskiTrca7lqPKMbQ8A/ZZehgsQfevg56GsaGe7RKlgvJsxdCXkkj050fF9qN6OUTL/9TjJ5+VdymuAZ3kpDyoPfx3Dg2+n1qOBAd1JoaqLP3l+uvu0j5v0qbqBTq6fwCd1/kPfECFZ52J8kitYHnioB+sBL9KuTeC2cpiBjnvQVAFk1dBn4oZzO7E86iuFzRykXdzq7rHr+M4g/CPHrXyYeBEeqy3y3QF8eXw2WfXdErf2QkViYMXW+dpzaMZW4WwMT6aW7rzaZg5Knds4fNicBcmPR4JoOcHyNIsZ1AQOcmKLQaPoZQLIdmkaCOXlaOazwvjdCjneIrzkuoELrNuSNz42wtW9B1y2HwgcfwFhySixQTkF6pkwsgSC5SZqOOKgNtpmCMAAAA=" +} \ No newline at end of file diff --git a/apps/web/content/pages/blog/alinea-0-4-0.json b/apps/web/content/pages/blog/alinea-0-4-0.json index 8b4b6d659..4da214085 100644 --- a/apps/web/content/pages/blog/alinea-0-4-0.json +++ b/apps/web/content/pages/blog/alinea-0-4-0.json @@ -3,20 +3,19 @@ "_type": "BlogPost", "_index": "Zy", "_i18nId": "2YlP39QVFZOzf94YMwDVrzd4gNz", - "_root": "pages", "title": "Alinea 0.4.0 ⚡", "publishDate": "2023-12-01", "author": { "name": "Ben Merckx", "url": { - "_type": "url", "_id": "2YlPCEdeGLXkqwQQq4r482BrNmj", + "_type": "url", "_url": "https://github.com/benmerckx", "_target": "_blank" }, "avatar": { - "_type": "url", "_id": "2YlPFxJGo8JMh6ZDWt7NDu7MPSN", + "_type": "url", "_url": "https://avatars.githubusercontent.com/u/10584189?v=4&s=48", "_target": "_self" } @@ -220,8 +219,8 @@ "_type": "ImageBlock", "_id": "2YlUKcEMi7vbNDku6d1QlCoO5oA", "image": { - "_type": "image", "_id": "2YlUafLKDxzdvHmJP6Kpyd6y9xJ", + "_type": "image", "_entry": "2YlUadqGteQCGAVaJ6QuSiwsdHr" } }, @@ -251,7 +250,11 @@ "description": "", "openGraph": { "title": "", - "image": {}, + "image": { + "_id": "2i9FuSoeYtbbK9Oq7YNBJuygCit", + "_type": "image", + "_entry": "2i9FuEJofr5yFe9DuleODHjjWkm" + }, "description": "" } } diff --git a/apps/web/content/pages/index.json b/apps/web/content/pages/index.json index a818757ff..99cf54caf 100644 --- a/apps/web/content/pages/index.json +++ b/apps/web/content/pages/index.json @@ -9,8 +9,8 @@ "headline": "Shape content\nfor the modern web.", "byline": "Open source headless CMS with minimal setup.\nFully typed content right in your repository.", "action": { - "_type": "entry", "_id": "2YcxykzomCJyGwRFMXARgTxib4b", + "_type": "entry", "_entry": "2YWqKUTrANlgEFMyBUEYdih2HpP", "label": "Get started" }, @@ -81,25 +81,25 @@ }, "links": [ { - "_type": "entry", "_id": "2a7ftYTVOui6duXcA4Js0xG8QC6", "_index": "a0", + "_type": "entry", "_entry": "docs", "label": "Docs", "active": "" }, { - "_type": "entry", "_id": "2a7ftRO1unpBDnfvXtgJUBIBwNr", "_index": "a1", + "_type": "entry", "_entry": "CC8aJ6-2U3s31micAzTLU", "label": "Roadmap", "active": "" }, { - "_type": "entry", "_id": "2a7ftUxPD7w8EMFYWjpYoJVF6GP", "_index": "a2", + "_type": "entry", "_entry": "gx3H52whBKO_DdfcMNI3I", "label": "Blog", "active": "" @@ -107,15 +107,15 @@ ], "footer": [ { - "_type": "Section", "_id": "2ANVIUDmjNddF0QO8aB0gwXcRq8", "_index": "a0", + "_type": "Section", "label": "Developer", "links": [ { - "_type": "entry", "_id": "2a7fvVi9M7zcX2wx3IJfYBrCqeC", "_index": "a0", + "_type": "entry", "_entry": "docs", "label": "" } @@ -124,10 +124,15 @@ ], "metadata": { "title": "Alinea - Open source headless CMS", - "description": "", + "description": "Alinea is a open source headless CMS with minimal setup. Fully typed content right in your repository.", "openGraph": { + "siteName": "Alinea", + "image": { + "_id": "2i9HvjoJjgs3bDP5bMyQyekUfGu", + "_type": "image", + "_entry": "2i9FuEJofr5yFe9DuleODHjjWkm" + }, "title": "", - "image": {}, "description": "" } } diff --git a/apps/web/public/20-06-2024-04-51-47.2i9FuEJofr5yFe9DuleODHjjWkm.png b/apps/web/public/20-06-2024-04-51-47.2i9FuEJofr5yFe9DuleODHjjWkm.png new file mode 100644 index 000000000..f4d78a24a Binary files /dev/null and b/apps/web/public/20-06-2024-04-51-47.2i9FuEJofr5yFe9DuleODHjjWkm.png differ diff --git a/apps/web/src/page/BlogPostPage.tsx b/apps/web/src/page/BlogPostPage.tsx index 24f3c2dfd..030807984 100644 --- a/apps/web/src/page/BlogPostPage.tsx +++ b/apps/web/src/page/BlogPostPage.tsx @@ -5,7 +5,7 @@ import {TextFieldView} from '@/page/blocks/TextFieldView' import {BlogPost} from '@/schema/BlogPost' import {Query} from 'alinea' import {fromModule} from 'alinea/ui' -import {MetadataRoute} from 'next' +import {Metadata, MetadataRoute} from 'next' import {Breadcrumbs} from '../layout/Breadcrumbs' import css from './BlogPostPage.module.scss' import {BlogPostMeta} from './blog/BlogPostMeta' @@ -22,9 +22,26 @@ export async function generateStaticParams() { return slugs.map(slug => ({slug})) } -export async function generateMetadata({params}: BlogPostPageProps) { +export async function generateMetadata({ + params +}: BlogPostPageProps): Promise { const page = await cms.get(Query(BlogPost).whereUrl(`/blog/${params.slug}`)) - return {title: page.metadata?.title || page.title} + const openGraphImage = page.metadata?.openGraph.image + return { + title: page.metadata?.title || page.title, + description: page.metadata?.description || page.introduction, + openGraph: { + images: openGraphImage + ? [ + { + url: openGraphImage.src, + width: openGraphImage.width, + height: openGraphImage.height + } + ] + : [] + } + } } export default async function BlogPostPage({params}: BlogPostPageProps) { diff --git a/apps/web/src/page/HomePage.tsx b/apps/web/src/page/HomePage.tsx index 4ef2d70b8..9306c3fb4 100644 --- a/apps/web/src/page/HomePage.tsx +++ b/apps/web/src/page/HomePage.tsx @@ -14,6 +14,7 @@ import {PageContainer} from '@/layout/Page' import WebLayout from '@/layout/WebLayout' import {WebTypo} from '@/layout/WebTypo' import {Home} from '@/schema/Home' +import {Query} from 'alinea' import {HStack, VStack} from 'alinea/ui/Stack' import {IcRoundInsertDriveFile} from 'alinea/ui/icons/IcRoundInsertDriveFile' import {IcRoundPublish} from 'alinea/ui/icons/IcRoundPublish' @@ -21,16 +22,44 @@ import {PhGlobe} from 'alinea/ui/icons/PhGlobe' import {RiFlashlightFill} from 'alinea/ui/icons/RiFlashlightFill' import {fromModule} from 'alinea/ui/util/Styler' import {px} from 'alinea/ui/util/Units' -import type {MetadataRoute} from 'next' +import type {Metadata, MetadataRoute} from 'next' import {ComponentType, PropsWithChildren} from 'react' import {Link} from '../layout/nav/Link' import css from './HomePage.module.scss' const styles = fromModule(css) -export async function generateMetadata() { - const home = await cms.get(Home()) - return {title: home.metadata?.title || home.title} +export async function generateMetadata(): Promise { + const page = await cms.get( + Query(Home).select({ + url: (Query as any).url, + title: (Query as any).title, + metadata: (Home as any).metadata + }) + ) + const appUrl = 'https://alinea.sh' + const title = page.metadata?.title || page.title + const ogTitle = page.metadata?.openGraph?.title || title + const ogDescription = + page.metadata?.openGraph?.description || page.metadata?.description + const openGraphImage = page.metadata?.openGraph.image + + return { + metadataBase: new URL(appUrl), + title, + description: page.metadata?.description, + openGraph: { + url: appUrl + page.url, + siteName: page.metadata?.openGraph?.siteName, + title: ogTitle, + description: ogDescription, + images: openGraphImage?.src && { + url: openGraphImage.src, + width: openGraphImage.width, + height: openGraphImage.height + } + } + } } interface HighlightProps { diff --git a/src/core/Resolver.ts b/src/core/Resolver.ts index 96b793759..ee6eeab70 100644 --- a/src/core/Resolver.ts +++ b/src/core/Resolver.ts @@ -10,6 +10,26 @@ export interface PreviewUpdate { update: string } +export interface PreviewMetadata { + title: string + description?: string + language?: string + robots?: string + canonical?: string + 'og:url'?: string + 'og:site_name'?: string + 'og:title'?: string + 'og:description'?: string + 'og:image'?: string + 'og:image:width'?: string + 'og:image:height'?: string + 'twitter:card'?: string + 'twitter:title'?: string + 'twitter:image'?: string + 'twitter:image:width'?: string + 'twitter:image:height'?: string +} + export interface ResolveRequest { selection: Selection location?: Array diff --git a/src/dashboard/view/EntryEdit.tsx b/src/dashboard/view/EntryEdit.tsx index ca580624f..6acd6be9c 100644 --- a/src/dashboard/view/EntryEdit.tsx +++ b/src/dashboard/view/EntryEdit.tsx @@ -33,6 +33,7 @@ import {EntryNotice} from './entry/EntryNotice.js' import {EntryPreview} from './entry/EntryPreview.js' import {EntryTitle} from './entry/EntryTitle.js' import {FieldToolbar} from './entry/FieldToolbar.js' +import {BrowserPreviewMetaProvider} from './preview/BrowserPreview.js' const styles = fromModule(css) @@ -115,7 +116,7 @@ export function EntryEdit({editor}: EntryEditProps) { if (isBlocking && !isNavigationChange) confirm?.() }, [isBlocking, isNavigationChange, confirm]) return ( - <> + {alineaDev && ( <> @@ -266,6 +267,6 @@ export function EntryEdit({editor}: EntryEditProps) { )} - + ) } diff --git a/src/dashboard/view/preview/BrowserPreview.tsx b/src/dashboard/view/preview/BrowserPreview.tsx index cac902887..211d333bf 100644 --- a/src/dashboard/view/preview/BrowserPreview.tsx +++ b/src/dashboard/view/preview/BrowserPreview.tsx @@ -1,3 +1,4 @@ +import {PreviewMetadata} from 'alinea/core/Resolver' import {PreviewAction, PreviewMessage} from 'alinea/preview/PreviewMessage' import {HStack, Loader, Typo, fromModule, px} from 'alinea/ui' import {AppBar} from 'alinea/ui/AppBar' @@ -6,7 +7,14 @@ import {IcRoundArrowForward} from 'alinea/ui/icons/IcRoundArrowForward' import {IcRoundLock} from 'alinea/ui/icons/IcRoundLock' import {IcRoundOpenInNew} from 'alinea/ui/icons/IcRoundOpenInNew' import {IcRoundRefresh} from 'alinea/ui/icons/IcRoundRefresh' -import {useEffect, useRef, useState} from 'react' +import { + PropsWithChildren, + createContext, + useContext, + useEffect, + useRef, + useState +} from 'react' import {LivePreview} from '../entry/EntryPreview.js' import css from './BrowserPreview.module.scss' @@ -17,10 +25,42 @@ export interface BrowserPreviewProps { registerLivePreview(api: LivePreview): void } +const BrowserPreviewMetaContext = createContext<{ + setMetadata: (msg: PreviewMetadata) => void + metadata?: PreviewMetadata +}>({ + setMetadata: () => {} +}) + +export const BrowserPreviewMetaProvider: React.FC< + PropsWithChildren<{ + entryId: string + }> +> = ({entryId, children}) => { + const [metdata, setMetadata] = useState() + + useEffect(() => { + setMetadata(undefined) + }, [entryId]) + + return ( + + {children} + + ) +} + +export function usePreviewMetadata() { + return useContext(BrowserPreviewMetaContext)?.metadata +} + export function BrowserPreview({ url, registerLivePreview }: BrowserPreviewProps) { + const metaContext = useContext(BrowserPreviewMetaContext) const iframe = useRef(null) const [loading, setLoading] = useState(true) const hasPreviewListener = useRef(false) @@ -41,6 +81,9 @@ export function BrowserPreview({ } }) } + if (event.data.action === PreviewAction.Meta) { + metaContext.setMetadata(event.data) + } } addEventListener('message', handleMessage) diff --git a/src/field/metadata/MetadataField.browser.tsx b/src/field/metadata/MetadataField.browser.tsx index 3676bc8fa..0c6c89b9b 100644 --- a/src/field/metadata/MetadataField.browser.tsx +++ b/src/field/metadata/MetadataField.browser.tsx @@ -1,10 +1,16 @@ import {Field} from 'alinea/core' +import {PreviewMetadata} from 'alinea/core/Resolver' import {FormRow} from 'alinea/dashboard/atoms/FormAtoms' import {InputForm} from 'alinea/dashboard/editor/InputForm' import {useFieldOptions} from 'alinea/dashboard/editor/UseField' +import {useEntryEditor} from 'alinea/dashboard/hook/UseEntryEditor' +import {usePreviewMetadata} from 'alinea/dashboard/view/preview/BrowserPreview' +import {fromModule} from 'alinea/ui' import {MetadataField, metadata as createMetadata} from './MetadataField.js' +import css from './MetadataField.module.scss' export * from './MetadataField.js' +const styles = fromModule(css) export const metadata = Field.provideView(MetadataInput, createMetadata) @@ -14,9 +20,92 @@ interface MetadataInputProps { function MetadataInput({field}: MetadataInputProps) { const options = useFieldOptions(field) + const editor = useEntryEditor() + + return ( + <> + + + +
{editor?.preview && }
+ + ) +} + +function MetadataPreview() { + const metadata = usePreviewMetadata() + if (!metadata) return null + return ( +
+

Share preview

+ + +
+ ) +} + +const SearchEnginePreview = ({metaTags}: {metaTags: PreviewMetadata}) => { + return ( + <> +

Search engine

+
+
+
+ + + + + +
+
+ {metaTags['og:site_name'] && ( +

+ {metaTags['og:site_name']} +

+ )} +

+ {metaTags['og:url']} +

+
+
+

{metaTags['title']}

+ {metaTags['description'] && ( +

+ {metaTags['description']?.substring(0, 160)} +

+ )} +
+ + ) +} + +const OpenGraphPreview = ({metaTags}: {metaTags: PreviewMetadata}) => { return ( - - - + <> +

Social share

+
+ Open Graph image +
+

+ {metaTags['og:url']?.replace(/^https?:\/\//, '')} +

+
+

+ {metaTags['og:title'] || metaTags['title']} +

+

+ {metaTags['og:description']} +

+
+
+
+ ) } diff --git a/src/field/metadata/MetadataField.module.scss b/src/field/metadata/MetadataField.module.scss new file mode 100644 index 000000000..d029e9d0f --- /dev/null +++ b/src/field/metadata/MetadataField.module.scss @@ -0,0 +1,121 @@ +.preview { + margin-top: 1em; + + &-subtitle { + color: var(--alinea-fields-foreground); + margin-top: 16px; + margin-bottom: 8px; + } +} + +.searchengine { + padding: 10px; + max-width: 600px; + border: 1px solid var(--alinea-outline); + border-radius: var(--alinea-border-radius); + + &-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &-intro { + display: flex; + align-items: center; + + &-favicon { + flex-shrink: 0; + width: 26px; + height: 26px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 100%; + border: 1px solid var(--alinea-outline); + background-color: var(--alinea-selected); + margin-right: 12px; + + &-icon { + color: var(--alinea-selected-foreground); + width: 18px; + height: 18px; + line-height: 1; + } + } + + &-url { + font-size: 12px; + line-height: 18px; + } + + &-url, + &-sitename { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + &-title { + margin-block: 4px; + color: var(--alinea-selected-foreground); + font-size: 20px; + line-height: 1.3; + font-weight: 400; + } + + &-description { + font-size: 14px; + line-height: 1.5; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + } +} + +.opengraph { + max-width: 527px; + border: 1px solid var(--alinea-outline); + border-radius: var(--alinea-border-radius); + + &-img { + height: auto; + display: block; + max-width: 100%; + } + + &-body { + padding: 10px 12px; + + &-url { + font-size: 12px; + line-height: 16px; + text-transform: uppercase; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &-content { + height: 46px; + overflow: hidden; + + &-title { + margin-block: 4px; + font-size: 16px; + font-weight: 600; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + } + + &-description { + font-size: 14px; + line-height: 20px; + } + } + } +} diff --git a/src/field/metadata/MetadataField.tsx b/src/field/metadata/MetadataField.tsx index e6bad894d..a230e05ad 100644 --- a/src/field/metadata/MetadataField.tsx +++ b/src/field/metadata/MetadataField.tsx @@ -16,8 +16,9 @@ export interface MetadataFields { title: TextField description: TextField openGraph: ObjectField<{ - title: TextField + siteName: TextField image: ImageField + title: TextField description: TextField }> } @@ -37,8 +38,12 @@ export function metadata( description: text('Description', {multiline: true}), openGraph: object('Open graph', { fields: { - title: text('Title', {width: 0.5}), - image: image('Image', {width: 0.5}), + siteName: text('Site name', {width: 0.25}), + image: image('Image', { + width: 0.75, + help: 'Recommended size: 1200 x 630 pixels' + }), + title: text('Title'), description: text('Description', {multiline: true}) } }) diff --git a/src/preview/PreviewMessage.ts b/src/preview/PreviewMessage.ts index ec1c8b7c0..f21635020 100644 --- a/src/preview/PreviewMessage.ts +++ b/src/preview/PreviewMessage.ts @@ -1,4 +1,4 @@ -import type {PreviewUpdate} from 'alinea/core/Resolver' +import type {PreviewMetadata, PreviewUpdate} from 'alinea/core/Resolver' export enum PreviewAction { Ping = '[alinea-ping]', @@ -7,7 +7,8 @@ export enum PreviewAction { Refetch = '[alinea-refetch]', Previous = '[alinea-previous]', Next = '[alinea-next]', - Preview = '[alinea-preview]' + Preview = '[alinea-preview]', + Meta = '[alinea-meta]' } export type PreviewMessage = @@ -18,3 +19,4 @@ export type PreviewMessage = | {action: PreviewAction.Previous} | {action: PreviewAction.Next} | ({action: PreviewAction.Preview} & PreviewUpdate) + | ({action: PreviewAction.Meta} & PreviewMetadata) diff --git a/src/preview/RegisterPreview.ts b/src/preview/RegisterPreview.ts index 100c2d8e9..5dccbd6fb 100644 --- a/src/preview/RegisterPreview.ts +++ b/src/preview/RegisterPreview.ts @@ -1,4 +1,4 @@ -import type {PreviewUpdate} from 'alinea/core/Resolver' +import type {PreviewMetadata, PreviewUpdate} from 'alinea/core/Resolver' import {PreviewAction, PreviewMessage} from 'alinea/preview/PreviewMessage' export interface PreviewApi { @@ -34,14 +34,67 @@ export function registerPreview(api: PreviewApi) { ) } } + let observer: MutationObserver | null = null if (window.location != window.parent.location) { // On first load send a pong because we might have missed ping, // this can warn in the console but it seems we cannot catch it window.parent.postMessage({action: PreviewAction.Pong}, document.referrer) addEventListener('message', handleMessage) console.log('[Alinea preview listener attached]') + + function fetchAndSendMetadata() { + const meta = fetchMetadataFromDocument() + window.parent.postMessage( + {action: PreviewAction.Meta, ...meta}, + document.referrer + ) + } + try { + fetchAndSendMetadata() + observer = new MutationObserver(mutationList => { + console.log('mutationList', mutationList) + fetchAndSendMetadata() + }) + observer.observe(document.head, {childList: true}) + console.log('[Alinea meta data send to parent]') + } catch (e) { + console.error('[Alinea meta data send to parent failed]') + } } return () => { + if (observer) observer.disconnect() removeEventListener('message', handleMessage) } } + +function fetchMetadataFromDocument(): PreviewMetadata { + return { + title: document.title, + description: fetchData('meta[name="description"]'), + + language: fetchData('meta[name="language"]'), + robots: fetchData('meta[name="robots"]'), + canonical: fetchData('link[rel="canonical"]', 'href'), + + 'og:url': fetchData('meta[property="og:url"]'), + 'og:site_name': fetchData('meta[property="og:site_name"]'), + 'og:title': fetchData('meta[property="og:title"]'), + 'og:description': fetchData('meta[property="og:description"]'), + 'og:image': fetchData('meta[property="og:image"]'), + 'og:image:width': fetchData('meta[property="og:image:width"]'), + 'og:image:height': fetchData('meta[property="og:image:height"]'), + + 'twitter:card': fetchData('meta[property="twitter:card"]'), + 'twitter:title': fetchData('meta[property="twitter:title"]'), + 'twitter:image': fetchData('meta[property="twitter:image"]'), + 'twitter:image:width': fetchData('meta[property="twitter:image:width"]'), + 'twitter:image:height': fetchData('meta[property="twitter:image:height"]') + } +} + +function fetchData( + selector: string, + attribute = 'content' +): string | undefined { + return document.querySelector(selector)?.getAttribute(attribute) || undefined +}