Skip to content

Commit 7120471

Browse files
authored
Add social media cards. (#1194)
https://ep2025-social.ep-preview.click/media/social_media_cards example individual card: https://ep2025-social.ep-preview.click/media/card/jimena-bermudez - click the card to download svg card without text - download all cards with `node scripts/download_social.cjs` - one background easy to change https://ep2025-social.ep-preview.click/social/bg.png
1 parent 10d15ff commit 7120471

File tree

12 files changed

+946
-0
lines changed

12 files changed

+946
-0
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"prettier": "^3.5.3",
4949
"prettier-plugin-astro": "^0.14.1",
5050
"tsx": "^4.19.3",
51+
"puppeteer": "^24.7.2",
5152
"typescript": "^5.8.3"
5253
},
5354
"prettier": {

pnpm-lock.yaml

Lines changed: 593 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/social/bg.png

108 KB
Loading

public/social/circle.png

8.49 KB
Loading

public/social/color.png

73.3 KB
Loading

public/social/star.svg

Lines changed: 3 additions & 0 deletions
Loading

public/social/web.png

65.4 KB
Loading

scripts/download_social.cjs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const puppeteer = require("puppeteer");
2+
3+
(async () => {
4+
const browser = await puppeteer.launch({
5+
executablePath: process.env.CHROME_BIN,
6+
args: ["--no-sandbox", "--disable-setuid-sandbox"],
7+
});
8+
const page = await browser.newPage();
9+
await page.goto("http://localhost:4321/media/social_media_cards");
10+
11+
const elements = await page.$$(".social");
12+
13+
for (let i = 0; i < elements.length; i++) {
14+
const el = elements[i];
15+
16+
// Get the slug from the element
17+
const slug = await page.evaluate((el) => el.getAttribute("data-slug"), el);
18+
19+
// Fallback if slug is missing
20+
const filename = slug ? `social-${slug}.png` : `social-${i}.png`;
21+
22+
await el.screenshot({ path: filename });
23+
}
24+
25+
await browser.close();
26+
})();

src/components/SocialMediaCard.astro

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
---
2+
import { getCollection, getEntries, type CollectionEntry } from "astro:content";
3+
import { Image } from "astro:assets";
4+
5+
const { entry } = Astro.props;
6+
7+
const sessions = await getEntries(entry.data.submissions);
8+
---
9+
<svg width="900" height="900" class="h-full w-full -z-10">
10+
<image href="http://localhost:4321/social/bg.png" width="900" height="900" />
11+
</svg>
12+
{
13+
entry.data.avatar ? (
14+
<svg width="900" height="900">
15+
<defs>
16+
<clipPath id="curvedCornerClip">
17+
<path
18+
d="
19+
m 885 885 l -240 0 a 240 240 0 0 1 -240 -240 a 240 240 0 0 1 240 -240 a 240 240 0 0 1 240 240 z
20+
"
21+
/>
22+
</clipPath>
23+
</defs>
24+
<image
25+
href={entry.data.avatar}
26+
x="400"
27+
y="400"
28+
width="500"
29+
height="560"
30+
clip-path="url(#curvedCornerClip)"
31+
preserveAspectRatio="xMidYMid slice"
32+
/>
33+
</svg>
34+
<p
35+
class="box fit-text"
36+
>
37+
{sessions.map((session) => <>{session.data.title} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</>)}
38+
</p>
39+
40+
<p lang="en" class="box2 fit-text">
41+
{entry.data.name}
42+
</p>
43+
44+
):(
45+
46+
<p
47+
class="box3 fit-text"
48+
>
49+
{sessions.map((session) => <>{session.data.title} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</>)}
50+
</p>
51+
52+
<p lang="en" class="box4 fit-text">
53+
{entry.data.name}
54+
</p>
55+
56+
)
57+
}
58+
59+
60+
61+
<style>
62+
.box {
63+
margin-top:240px;
64+
width: 430px;
65+
height: 300px;
66+
font-size: 100px;
67+
color: rgb(239, 215, 123);
68+
padding: 1rem;
69+
}
70+
71+
.box2 {
72+
73+
width: 420px;
74+
height: 200px;
75+
font-size: 70px;
76+
color:white;
77+
padding: 2rem;
78+
}
79+
80+
.box3 {
81+
margin-top:280px;
82+
width: 800px;
83+
height: 300px;
84+
font-size: 100px;
85+
color: rgb(239, 215, 123);
86+
padding: 1rem;
87+
}
88+
89+
.box4 {
90+
91+
width: 580px;
92+
height: 200px;
93+
font-size: 70px;
94+
color:white;
95+
padding: 2rem;
96+
}
97+
98+
99+
.box, .box2, .box3, .box4 {
100+
box-sizing: border-box;
101+
overflow: hidden;
102+
display: flex;
103+
align-items: center;
104+
justify-content: center;
105+
text-align: center;
106+
font-family: Inter, sans-serif !important;
107+
font-weight: bold;
108+
line-height: 1em;
109+
}
110+
</style>

src/pages/media/card/[slug].astro

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
---
2+
import { getCollection, getEntries, type CollectionEntry } from "astro:content";
3+
import SocialMediaCard from "@components/SocialMediaCard.astro";
4+
5+
export async function getStaticPaths() {
6+
const entries = await getCollection("speakers");
7+
return entries.map((entry) => ({
8+
params: { slug: entry.id },
9+
props: { entry },
10+
}));
11+
}
12+
13+
const { entry } = Astro.props;
14+
---
15+
<!DOCTYPE html>
16+
<html lang="en">
17+
<head>
18+
<meta charset="utf-8" />
19+
<meta name="viewport" content="width=device-width,initial-scale=1" />
20+
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
21+
<link rel="preconnect" href="https://fonts.googleapis.com">
22+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
23+
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap" rel="stylesheet">
24+
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
25+
</head>
26+
<body class="overflow-auto">
27+
{
28+
entry &&
29+
<div class="social relative w-[900px] h-[900px] overflow-hidden">
30+
<SocialMediaCard entry={entry} />
31+
</div>
32+
}
33+
34+
</body>
35+
</html>
36+
37+
<style is:global>
38+
39+
.social {
40+
width: 900px;
41+
height: 900px;
42+
}
43+
44+
.social svg {
45+
position: absolute;
46+
top:0;
47+
left:0;
48+
width: 100%;
49+
height: 100%;
50+
object-fit: contain;
51+
}
52+
53+
body * {
54+
font-family: Inter, sans-serif;
55+
}
56+
57+
.avatar {
58+
object-position:50% 25%;
59+
}
60+
61+
</style>
62+
63+
<script is:inline>
64+
function fitText(container) {
65+
let fontSize = 100; // Start big
66+
container.style.fontSize = fontSize + 'px';
67+
68+
while (
69+
(container.scrollWidth > container.clientWidth || container.scrollHeight > container.clientHeight)
70+
&& fontSize > 5
71+
) {
72+
fontSize -= 1;
73+
container.style.fontSize = fontSize + 'px';
74+
}
75+
}
76+
77+
function fitAllText() {
78+
const boxes = document.querySelectorAll('.fit-text');
79+
boxes.forEach(box => fitText(box));
80+
}
81+
82+
fitAllText();
83+
84+
window.addEventListener('resize', fitAllText);
85+
</script>
86+
87+
<script define:vars={{slug: entry.data.slug}}>
88+
document.addEventListener('DOMContentLoaded', () => {
89+
document.querySelectorAll('.social').forEach((socialDiv, index) => {
90+
91+
socialDiv.addEventListener('click', () => {
92+
const svgs = socialDiv.querySelectorAll('svg');
93+
94+
if (svgs.length === 0) {
95+
alert('No SVGs found!');
96+
return;
97+
}
98+
99+
const xmlns = "http://www.w3.org/2000/svg";
100+
const combinedSvg = document.createElementNS(xmlns, "svg");
101+
combinedSvg.setAttribute("xmlns", xmlns);
102+
combinedSvg.setAttribute("width", "900");
103+
combinedSvg.setAttribute("height", "900");
104+
combinedSvg.setAttribute("viewBox", "0 0 900 900");
105+
106+
svgs.forEach(svg => {
107+
const g = document.createElementNS(xmlns, "g");
108+
g.innerHTML = svg.innerHTML;
109+
combinedSvg.appendChild(g);
110+
});
111+
112+
const serializer = new XMLSerializer();
113+
const svgString = serializer.serializeToString(combinedSvg);
114+
115+
const blob = new Blob([svgString], {type: "image/svg+xml"});
116+
const url = URL.createObjectURL(blob);
117+
118+
const a = document.createElement('a');
119+
a.href = url;
120+
a.download = slug ? `social-${slug}.svg` : `social-${index +1}.svg`;
121+
a.style.display = "none";
122+
document.body.appendChild(a);
123+
a.click();
124+
125+
URL.revokeObjectURL(url);
126+
document.body.removeChild(a);
127+
});
128+
});
129+
});
130+
</script>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
import { getCollection, getEntries, type CollectionEntry } from "astro:content";
3+
import SocialMediaCard from "@components/SocialMediaCard.astro";
4+
5+
6+
const speakers = await getCollection("speakers");
7+
type Speaker = CollectionEntry<"speakers">;
8+
---
9+
<!DOCTYPE html>
10+
<html lang="en">
11+
<head>
12+
<meta charset="utf-8" />
13+
<meta name="viewport" content="width=device-width,initial-scale=1" />
14+
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
15+
<link rel="preconnect" href="https://fonts.googleapis.com">
16+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
17+
<link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap" rel="stylesheet">
18+
<script src="https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
19+
</head>
20+
<body class="overflow-auto">
21+
{
22+
speakers.map((entry: Speaker) => (
23+
<a href=`/media/card/${entry.data.slug}`>
24+
<div class="social relative w-[900px] h-[900px] overflow-hidden" data-slug={entry.data.slug}>
25+
<SocialMediaCard entry={entry} />
26+
</div>
27+
</a>
28+
))
29+
}
30+
31+
</body>
32+
</html>
33+
34+
<style is:global>
35+
36+
.social {
37+
width: 900px;
38+
height: 900px;
39+
}
40+
41+
.social svg {
42+
position: absolute;
43+
top:0;
44+
left:0;
45+
width: 100%;
46+
height: 100%;
47+
object-fit: contain;
48+
}
49+
50+
body * {
51+
font-family: Inter, sans-serif;
52+
}
53+
54+
.avatar {
55+
object-position:50% 25%;
56+
}
57+
58+
</style>
59+
60+
<script is:inline>
61+
function fitText(container) {
62+
let fontSize = 100; // Start big
63+
container.style.fontSize = fontSize + 'px';
64+
65+
while (
66+
(container.scrollWidth > container.clientWidth || container.scrollHeight > container.clientHeight)
67+
&& fontSize > 5
68+
) {
69+
fontSize -= 1;
70+
container.style.fontSize = fontSize + 'px';
71+
}
72+
}
73+
74+
function fitAllText() {
75+
const boxes = document.querySelectorAll('.fit-text');
76+
boxes.forEach(box => fitText(box));
77+
}
78+
79+
fitAllText();
80+
81+
window.addEventListener('resize', fitAllText);
82+
</script>

src/pages/robots.txt.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Disallow: /
88
const prodRobots = `
99
User-agent: *
1010
Disallow: /_astro/
11+
Disallow: /media/
1112
Disallow: /*?
1213
Allow: /
1314

0 commit comments

Comments
 (0)