Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Pretty Preview OG Tag Data with Social Cards #41

Merged
merged 28 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
442d582
Stub out OG tag preview components
dthyresson Feb 23, 2024
fd0c0d9
Add subtitle description
dthyresson Feb 23, 2024
e1a6641
Refactor og preview with audit callouts
dthyresson Feb 25, 2024
4bd79d3
rework previewers
dthyresson Feb 27, 2024
fcd53c9
Validate og tag data with zod.
dthyresson Feb 27, 2024
eac571b
refactor validators
dthyresson Feb 28, 2024
33f11ed
Merge branch 'main' into dt-og-preview-components
dthyresson Feb 28, 2024
9f29a04
Rework error sections and preview component sections on page
dthyresson Feb 28, 2024
33bc173
pretty preview first
dthyresson Feb 28, 2024
3626c43
Draft of all preview providers implemented
dthyresson Feb 29, 2024
1df884c
Merge branch 'main' into dt-og-preview-components
dthyresson Feb 29, 2024
a2b4064
rename validation to audit
dthyresson Feb 29, 2024
462f7ba
restores connection watcher
dthyresson Feb 29, 2024
11e1690
Merge branch 'main' into dt-og-preview-components
dthyresson Feb 29, 2024
7cb552b
fix merge issues
dthyresson Feb 29, 2024
706da39
fix merge issues with ssr detection
dthyresson Feb 29, 2024
a2a74f9
Fix merge issue with info auth settings
dthyresson Feb 29, 2024
493a389
move ssr checks
dthyresson Feb 29, 2024
37e918b
ssr enabled or not rendering error message
dthyresson Mar 1, 2024
743bb9b
The default url to preview needs to be read from user project config
dthyresson Mar 1, 2024
cd0aadc
First pass at all provider validation audits
dthyresson Mar 1, 2024
2c73c96
Select user agent and in audit see if provider could be for agent
dthyresson Mar 1, 2024
dc7a4ae
pin zod version
Tobbe Mar 4, 2024
c54d758
config.sdl.ts: Consistent blank lines
Tobbe Mar 4, 2024
1f6a541
Split OGTagPreviewPage into several files for easier grocking
Tobbe Mar 4, 2024
02a42da
DiscordPreviewer: Destructure props
Tobbe Mar 4, 2024
254b552
Destructure props, and inline switch case
Tobbe Mar 4, 2024
1f11b4e
Remove unused component EmptyPreviewer
Tobbe Mar 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,27 @@ export const Success = ({
image:
'https://tailwindui.com/img/ecommerce-images/order-history-page-03-product-03.jpg',
title: `${blogPost.title} | RedwoodJS Blog`,
description: blogPost.body,
description: blogPost.body.substring(0, 10),
site_name: 'redwoodjs.com',
url: `https://redwoodjs.com/blog-posts/${blogPost.id}`,
}}
article={{
author: blogPost.author.fullName,
published_date: blogPost.createdAt,
published_time: blogPost.createdAt,
}}
profile={{
username: blogPost.author.fullName,
}}
twitter={{
card: 'summary_large_image',
site: '@redwoodjs',
site: 'redwoodjs.com',
url: `https://redwoodjs.com/blog-posts/${blogPost.id}`,
creator: `@${blogPost.author.fullName}`,
title: `${blogPost.title} | RedwoodJS Blog`,
description: blogPost.body.substring(0, 10),
image:
'https://tailwindui.com/img/ecommerce-images/order-history-page-03-product-04.jpg',
'https://tailwindui.com/img/ecommerce-images/order-history-page-03-product-03.jpg',
'image:alt': 'this is a description of image',
}}
/>
Expand Down
3 changes: 2 additions & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"open-graph-scraper": "6.2.2",
"prisma-json-schema-generator": "4.0.0",
"smtp-server": "3.13.0",
"uuid": "9.0.1"
"uuid": "9.0.1",
"zod": "3.22.4"
},
"devDependencies": {
"@types/better-sqlite3": "7.6.4",
Expand Down
9 changes: 9 additions & 0 deletions api/src/graphql/config.sdl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,19 @@ export const schema = gql`
jwtSecret: String
}

type WebConfig {
id: String!
host: String
port: Int
apiUrl: String
}

type StudioConfig {
id: String!
basePort: Int
graphiql: GraphiQLConfig
}

type EnabledStatus {
status: Boolean
message: String
Expand All @@ -33,6 +41,7 @@ export const schema = gql`
type UserProjectConfig {
id: String!
ssr: StreamingSsrConfig
web: WebConfig
}

type Query {
Expand Down
31 changes: 29 additions & 2 deletions api/src/graphql/ogTagPreview.sdl.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,39 @@
export const schema = gql`
type OGTagPreview {
enum OGPreviewSeverity {
WARNING
ERROR
OK
}

enum OGPreviewProvider {
GENERIC
FACEBOOK
TWITTER
LINKEDIN
DISCORD
SLACK
}

type OGTagPreviewProviderAudit {
provider: OGPreviewProvider!
audit: OGTagPreviewAudit!
}

type OGTagPreviewAudit {
messages: [String]!
severity: OGPreviewSeverity!
}

type OGTagPreviewResponse {
id: ID!
userAgent: String!
error: Boolean!
result: JSON
audits: [OGTagPreviewProviderAudit!]
}

type Query {
ogTagPreview(url: String!, customUserAgent: String): OGTagPreview! @skipAuth
ogTagPreview(url: String!, customUserAgent: String): OGTagPreviewResponse!
@skipAuth
}
`
7 changes: 7 additions & 0 deletions api/src/lib/og/auditors/discord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from 'zod'

import { GenericAuditor } from './generic'

export const DiscordAuditor = GenericAuditor.extend({
ogDescription: z.string(),
})
4 changes: 4 additions & 0 deletions api/src/lib/og/auditors/facebook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { GenericAuditor } from './generic'

// https://developers.facebook.com/docs/sharing/webmasters/crawler
export const FacebookAuditor = GenericAuditor.extend({})
27 changes: 27 additions & 0 deletions api/src/lib/og/auditors/generic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { z } from 'zod'
/**
* Basic Metdaata
* @see: https://ogp.me/#metadata
* The four required properties for every page are:
*
* og:title - The title of your object as it should appear within the graph, e.g., "The Rock".
* og:type - The type of your object, e.g., "video.movie". Depending on the type you specify, other properties may also be required.
* og:image - An image URL which should represent your object within the graph.
* og:url - The canonical URL of your object that will be used as its permanent ID in the graph, e.g., "https://www.imdb.com/title/tt0117500/".
*/
export const BasicMetadataAuditor = z.object({
ogTitle: z.string(),
ogType: z.string(),
ogUrl: z.string(),
ogDescription: z.string().optional(),
ogSiteName: z.string().optional(),
})

export const ArticleAuditor = BasicMetadataAuditor.extend({
articleAuthor: z.string().optional(),
articlePublisher: z.string().optional(),
articlePublishedDate: z.string().optional(),
articlePublishedTime: z.string().optional(),
})

export const GenericAuditor = ArticleAuditor.extend({})
3 changes: 3 additions & 0 deletions api/src/lib/og/auditors/linkedin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { GenericAuditor } from './generic'

export const LinkedInAuditor = GenericAuditor.extend({})
3 changes: 3 additions & 0 deletions api/src/lib/og/auditors/slack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { GenericAuditor } from './generic'

export const SlackAuditor = GenericAuditor.extend({})
18 changes: 18 additions & 0 deletions api/src/lib/og/auditors/twitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { z } from 'zod'

import { GenericAuditor } from './generic'

const TwitterImageSchema = z.object({
url: z.string(),
alt: z.string().optional(),
})

// https://dev.twitter.com/cards/getting-started
export const TwitterAuditor = GenericAuditor.extend({
twitterTitle: z.string(),
twitterCard: z.string(),
twitterImage: z.array(TwitterImageSchema),
twitterSite: z.string().optional(),
twitterUrl: z.string().optional(),
twitterDescription: z.string().optional(),
})
95 changes: 95 additions & 0 deletions api/src/lib/og/og.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import type {
OGTagPreviewResponse,
OGTagPreviewProviderAudit,
OGPreviewProvider,
OGTagPreviewAudit,
} from '../../../types/graphql'

import { DiscordAuditor } from './auditors/discord'
import { FacebookAuditor } from './auditors/facebook'
import { GenericAuditor } from './auditors/generic'
import { LinkedInAuditor } from './auditors/linkedin'
import { SlackAuditor } from './auditors/slack'
import { TwitterAuditor } from './auditors/twitter'

const PROVIDERS = [
'GENERIC',
'DISCORD',
'FACEBOOK',
'LINKEDIN',
'SLACK',
'TWITTER',
] as OGPreviewProvider[]

const formatMessages = (validationResult): OGTagPreviewAudit['messages'] => {
const messages = []
const formatted = validationResult['error'].format()

Object.entries(formatted).forEach(([key, value]) => {
if (key !== '_errors') {
const errors = value['_errors'] || []
messages.push(`${key}: ${errors.join(', ')}`)
}
})

return messages
}

const getProviderAuditor = (provider: OGPreviewProvider) => {
switch (provider) {
case 'DISCORD':
return DiscordAuditor
break
case 'FACEBOOK':
return FacebookAuditor
break
case 'LINKEDIN':
return LinkedInAuditor
break
case 'SLACK':
return SlackAuditor
break
break
case 'TWITTER':
return TwitterAuditor
break
default:
return GenericAuditor
}
}

const auditForProvider = (
result,
provider: OGPreviewProvider
): OGTagPreviewProviderAudit => {
const auditResult = getProviderAuditor(provider).safeParse(result)

if (!auditResult.success) {
return {
provider,
audit: {
severity: 'WARNING',
messages: formatMessages(auditResult),
},
}
}

return {
provider,
audit: { severity: 'OK', messages: ['All required tags provided.'] },
}
}

export const auditor = (
result: OGTagPreviewResponse['result'],
error: boolean
): OGTagPreviewProviderAudit[] => {
if (error) {
return PROVIDERS.map((provider) => ({
provider,
audit: { severity: 'ERROR', messages: ['Unable to preview'] },
}))
}

return PROVIDERS.map((provider) => auditForProvider(result, provider))
}
8 changes: 8 additions & 0 deletions api/src/services/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,18 @@ export const userProjectConfig = async (): Promise<
const status = config.experimental?.streamingSsr?.enabled
const message = status ? 'SSR is enabled' : 'SSR is not enabled'

const { host, port, apiUrl } = config.web

// TODO: initially SSR will be behind a feature flag that’s on by default in
// our new (yet to be created) Bighorn template
return {
id: 'user-project-config-id',
web: {
id: 'user-project-config-web-id',
host: host ?? 'localhost',
port,
apiUrl,
},
ssr: { id: 'user-project-config-ssr-id', enabled: { status, message } },
}
}
49 changes: 27 additions & 22 deletions api/src/services/ogTagPreview/ogTagPreview.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,49 @@
import openGraphScraper from 'open-graph-scraper'
import type { QueryResolvers } from 'types/graphql'

import { SyntaxError } from '@redwoodjs/graphql-server'

import { auditor } from 'src/lib/og/og'
import { getUserProjectConfig } from 'src/util/project'

export const ogTagPreview: QueryResolvers['ogTagPreview'] = async ({
url,
customUserAgent,
}) => {
const config = await getUserProjectConfig()

// Use an example of Google's user agent if none is provided
customUserAgent ??=
'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Chrome/W.X.Y.Z Safari/537.36'

const config = await getUserProjectConfig()

// short-cut in the case that SSR is disabled
if (!config.experimental?.streamingSsr?.enabled) {
return {
id: url,
userAgent: customUserAgent,
error: true,
result: {
message: 'No OG Tag Preview possible because SSR is disabled.',
},
}
throw new SyntaxError('SSR is not enabled')
}

const res = await fetch(url, {
headers: {
'User-Agent': customUserAgent,
},
})
try {
const response = await fetch(url, {
headers: {
'User-Agent': customUserAgent,
},
})

const html = await res.text()
const html = await response.text()
const customResult = await openGraphScraper({ html })

const customResult = await openGraphScraper({ html })
const { result, error } = customResult
const audits = auditor(result, error)

return {
id: url,
userAgent: customUserAgent,
error: customResult.error,
result: customResult.result,
return {
id: url,
userAgent: customUserAgent,
error,
result,
audits,
}
} catch {
throw new SyntaxError(
`Unable to preview ${url} with user agent ${customUserAgent}`
)
}
}
8 changes: 2 additions & 6 deletions api/src/util/graphqlProxy.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import httpProxy from '@fastify/http-proxy'
import type { FastifyInstance, HookHandlerDoneFunction } from 'fastify'

import {
getUserProjectWebHost,
getUserProjectWebPort,
getUserProjectGraphQlEndpoint,
} from './project'
import { getUserProjectConfig } from './project'

/**
* Graphql Proxy - Takes studio "/proxies/graphql" and forwards to the projects
Expand All @@ -26,7 +22,7 @@ export async function graphqlProxy(
upstream: `http://${webHost}:${webConfig.port}`,
prefix: '/proxies/graphql',
// Strip the initial scheme://host:port from the graphqlEndpoint
rewritePrefix,
rewritePrefix: '/' + graphqlEndpoint.split('/').slice(3).join('/'),
disableCache: true,
})

Expand Down
2 changes: 1 addition & 1 deletion redwood.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# projects using their own server file
port = 4319
[browser]
open = true
open = false
[notifications]
versionUpdates = ["latest"]
[generate]
Expand Down
Loading