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

Open Graph and social cards #56

Merged
merged 4 commits into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: typst-community/setup-typst@v3
- uses: actions/setup-node@v4
with:
node-version: "22.2.0"
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/pr_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: typst-community/setup-typst@v3
- uses: actions/setup-node@v4
with:
node-version: "22.2.0"
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@astrojs/mdx": "^0.19.7",
"@astrojs/mdx": "^3.1.8",
"@types/jsdom": "^21.1.7",
"@types/luxon": "^3.4.2",
"astro": "^2.10.15",
"astro": "^4.16.7",
"prettier": "^3.3.3",
"prettier-plugin-astro": "^0.11.1",
"rollup": "^3.29.4",
"sass": "^1.77.8",
"typescript": "^5.5.4"
},
"dependencies": {
"@astrojs/markdown-remark": "^2.2.1",
"@astrojs/markdown-remark": "^5.3.0",
"execa": "^9.4.1",
"jsdom": "^22.1.0",
"luxon": "^3.4.4",
"reset-css": "^5.0.2",
Expand Down
4,114 changes: 2,071 additions & 2,043 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

15 changes: 12 additions & 3 deletions src/layouts/Base.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ import Footer from "../components/Footer.astro"
export type Props = {
title?: string
navigation?: boolean
ogImagePath?: string
}
const { title, navigation } = Object.assign(
const { title, navigation, ogImagePath } = Object.assign(
{},
{ navigation: true },
Astro.props,
) as Props
const fullTitle = title ? `${title}—PyCon AU 2024` : "PyCon AU 2024"

const ogAbsoluteUrl = new URL(og, "https://2023.pycon.org.au/")
const ogAbsoluteUrl = new URL(
ogImagePath ?? (og as any as string),
"https://2023.pycon.org.au/",
)
---

<!DOCTYPE html>
Expand All @@ -27,7 +31,12 @@ const ogAbsoluteUrl = new URL(og, "https://2023.pycon.org.au/")
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:title" content={fullTitle} />
<link rel="icon" type="image/png" sizes="16x16" href="/static/common/img/icons/favicon.ico">
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/static/common/img/icons/favicon.ico"
/>
<script>
// Record where the visitor came from.
// We just store this in local storage, because we only use it
Expand Down
72 changes: 72 additions & 0 deletions src/pages/program/[sessionId]-[variant].png.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { getCollection, getEntry } from "astro:content"
import { mkdtemp, open } from "node:fs/promises"
import { join } from "node:path"
import { tmpdir } from "node:os"
import { execa } from "execa"
import { DateTime } from "luxon"
import { CONFERENCE_TZ, ROOMS_BY_SLUG } from "../../main_config"

type Params = {
sessionId: string
variant: "og" | "social"
}

export async function GET({
params,
request,
}: {
params: Params
request: Request
}) {
let session = (await getEntry("sessions", params.sessionId))!
let speakers = await Promise.all(
session?.data.speakers.map(async (speakerId) => {
let speaker = (await getEntry("people", speakerId))!
return {
name: speaker.data.name,
image: speaker.data.has_pic
? `../../public/people/${speaker.id}.jpg`
: "",
}
}),
)
let inputData = {
title: session.data.title,
speakers,
room: ROOMS_BY_SLUG[session.data.room!]?.name,
time: DateTime.fromJSDate(session.data.start!, {
zone: CONFERENCE_TZ,
}).toLocaleString({
weekday: "long",
hour: "numeric",
minute: "numeric",
}),
showPhotos: speakers.every((speaker) => !!speaker.image),
}
let dir = await mkdtemp(join(tmpdir(), "pyconau-2023-typst-"))
let inputPath = join(dir, "input.typ")
let inputFile = await open(inputPath, "wx")
let typstFile = (
{ og: "speakerCard.typ", social: "socialCard.typ" } as const
)[params.variant]
await inputFile.close()
const { stdout } = await execa({
encoding: "buffer",
})`typst compile src/typst/${typstFile} - -f png --font-path src/typst/fonts/ --input data=${JSON.stringify(inputData)} --root ../..`
let nonResizableArrayBuffer = new ArrayBuffer(stdout.byteLength)
let nonResizableArrayBufferView = new Uint8Array(nonResizableArrayBuffer)
nonResizableArrayBufferView.set(stdout)
return new Response(nonResizableArrayBuffer)
}

export async function getStaticPaths(): Promise<{ params: Params }[]> {
const pages = await getCollection("sessions")
return pages.flatMap((entry) =>
["og", "social"].map((variant) => ({
params: {
sessionId: entry.id,
variant,
},
})),
)
}
2 changes: 1 addition & 1 deletion src/pages/program/[sessionId].astro
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const daySlug = DateTime.fromJSDate(entry.data.start, {
}
</style>

<Base title={entry.data.title}>
<Base title={entry.data.title} ogImagePath={`/program/${entry.id}-og.png`}>
{
/*
<div class="time-bar">
Expand Down
Binary file added src/typst/fonts/Catamaran-Bold.ttf
Binary file not shown.
Binary file added src/typst/fonts/Catamaran-Regular.ttf
Binary file not shown.
Binary file added src/typst/social-card-background.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
134 changes: 134 additions & 0 deletions src/typst/socialCard.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#let data = json.decode(sys.inputs.data)

// we want to produce a 1200 by 630 image, but typst is designed for print
// so we generate it at 600pt by 315pt, and produce images at 144dpi
// effectively making 1pt in this document morally equivalent to 1dp

#set page(width: 600pt, height: 600pt, background: place(top, image("social-card-background.png")), margin: 0pt)
#set text(font: "Catamaran")
#set par(spacing: 10pt)

/*
#let data = json.decode(`
{
"title": "Failsafes and Safety Fails: How to crash a train and other lessons for software engineers",
"speakers": [
{"name": "Jack Skinner", "image": "PB9BKD.jpg"},
{"name": "Jack Skinner", "image": "PB9BKD.jpg"}
],
"room": "Goldfields Theatre",
"time": "Sunday 12pm"
}
`.text)
*/

#let wattleLeaf = rgb(0, 191, 111)

#let fillSpace(contentFunc, startSize: 40pt) = layout(containerSize => {
let size = startSize
while size > 0pt {
let content = contentFunc(size)
let (height,) = measure(
block(width: containerSize.width, content),
)
if (height <= containerSize.height) {
content
break
}
size = size - 0.25pt
}
})

#let compressHorizontally(content) = layout(containerSize => {
let (width,) = measure(content)
if width < containerSize.width {
content
} else {
scale(x: (containerSize.width / width) * 100%, reflow: true, content)
}
})

#place(bottom, rect(width: 100%, height: 80pt, fill: wattleLeaf))

#block(height: 100%)[
#grid(
columns: if data.showPhotos {(1fr, 200pt)} else {(1fr)},
rows: (75pt, 1fr, 100pt, 80pt),
//stroke: black,
grid.cell(x: 0, y: 1, colspan: if data.showPhotos {2} else {1}, align: horizon, fillSpace(baseSize => {
block(
inset: 24pt,
{
par(spacing: 1em, text(size: baseSize * 0.6, weight: 400)[Join me at PyCon AU 2024 for])
text(size: baseSize, font: "Catamaran", weight: 700)[#data.title]
par(spacing: 1em, text(size: baseSize * 0.6, weight: 400)[PyCon AU will be at the Melbourne Convention and Exhibition Centre on 22–26 November 2024.])
}
)
})),
grid.cell(x: 0, y: 2, align: horizon, {
block(
inset: 24pt,
{
par(spacing: 1em, text(size: 24pt, weight: 400)[Register today!])
par(spacing: 1em, text(size: 24pt, weight: 700)[pycon.org.au/attend])
}
)
}),
grid.cell(x: 0, y: 3, align: horizon,
block(inset: 18pt)[
#compressHorizontally(
text(
size: 24pt,
weight: 700,
fill: white,
if data.speakers.len() > 1 [
#data.speakers.at(0).name and #data.speakers.at(1).name
] else [
#data.speakers.at(0).name
]
)
)
#text(
size: 18pt,
weight: 400,
fill: white,
[#data.room, #data.time]
)
]
),
if data.showPhotos {
grid.cell(x: 1, y: 2, rowspan: 2, align: bottom,
block(inset: 14pt,
if data.speakers.len() > 1 {
grid(
columns: (1fr, 1fr),
gutter: 0pt,
move(dy: -10pt,
box(
radius: 50%,
clip: true,
width: 1fr,
image(data.speakers.at(0).image, width: 86pt, height: 86pt)
)
),
move(dy: -50pt,
box(
radius: 50%,
clip: true,
width: 1fr,
image(data.speakers.at(1).image, width: 86pt, height: 86pt)
)
)
)
} else {
box(
radius: 50%,
clip: true,
width: 100%,
image(data.speakers.at(0).image, width: 172pt, height: 172pt, fit: "cover")
)
}
)
)
}
)]
Loading