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: add HTML page for share links #12

Open
wants to merge 7 commits into
base: add-open-graph-image
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"express": "^4.18.2",
"graphql": "^16.6.0",
"react-jsx": "^1.0.0",
"remove-markdown": "^0.5.0",
"satori": "^0.7.2",
"sharp": "^0.32.1",
"winston": "^3.8.2"
Expand All @@ -42,6 +43,7 @@
"@types/jest": "^29.5.1",
"@types/node": "^18.15.11",
"@types/react": "^18.2.0",
"@types/remove-markdown": "^0.3.1",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"dotenv": "^16.0.3",
"eslint": "^8.38.0",
Expand Down
27 changes: 27 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { voteReportWithStorage, ogImageWithStorage, rpcError, rpcSuccess } from
import log from './helpers/log';
import { queues } from './lib/queue';
import { ImageType } from './lib/ogImage';
import { shareProposalPage, shareSpacePage } from './lib/sharePage';

const router = express.Router();

Expand Down Expand Up @@ -111,4 +112,30 @@ router.get('/og/:type(space|proposal|home)/:id?.:ext(png|svg)?', async (req, res
}
});

router.get('/:spaceId', async (req, res) => {
const { spaceId } = req.params;

try {
res.setHeader('Content-Type', 'text/html');
res.send(await shareSpacePage(spaceId));
} catch (e) {
log.error(e);
res.setHeader('Content-Type', 'application/json');
return rpcError(res, 'INTERNAL_ERROR', spaceId);
}
});

router.get('/:spaceId/proposal/:proposalId', async (req, res) => {
const { spaceId, proposalId } = req.params;

try {
res.setHeader('Content-Type', 'text/html');
res.send(await shareProposalPage(spaceId, proposalId));
} catch (e) {
log.error(e);
res.setHeader('Content-Type', 'application/json');
return rpcError(res, 'INTERNAL_ERROR', spaceId);
}
});

export default router;
3 changes: 3 additions & 0 deletions src/helpers/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ export type Space = {
export type Proposal = {
id: string;
title: string;
body: string;
state: string;
choices: string[];
votes: number;
space?: Space;
};

export type Vote = {
ipfs: string;
voter: string;
Expand All @@ -41,6 +43,7 @@ const PROPOSAL_QUERY = gql`
proposal(id: $id) {
id
title
body
state
choices
votes
Expand Down
4 changes: 4 additions & 0 deletions src/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,7 @@ export function voteReportWithStorage(id: string) {
export function ogImageWithStorage(type: ImageType, id: string) {
return new ogImage(type, id, new StorageEngine('ogImages'));
}

export function capitalize(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
7 changes: 5 additions & 2 deletions src/lib/ogImage/templates/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { fontsData } from '../utils';
import { loadEmoji, getIconCode, apis } from '../twemoji';
import type { ImageType } from '../index';

export const WIDTH = 1200;
export const HEIGHT = 600;

async function loadDynamicAsset(emojiType: keyof typeof apis, _code: string, text: string) {
if (_code === 'emoji') {
return `data:image/svg+xml;base64,${btoa(await loadEmoji(emojiType, getIconCode(text)))}`;
Expand Down Expand Up @@ -46,8 +49,8 @@ export default async function render(type: ImageType, id: string) {
{content}
</div>,
{
width: 1200,
height: 600,
width: WIDTH,
height: HEIGHT,
fonts: fontsData as SatoriOptions['fonts'],
loadAdditionalAsset: async (code, text) => {
return loadDynamicAsset('twemoji', code, text);
Expand Down
115 changes: 115 additions & 0 deletions src/lib/sharePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import removeMd from 'remove-markdown';
import { fetchProposal, fetchSpace } from '../helpers/snapshot';
import { HEIGHT, WIDTH } from './ogImage/templates';
import { capitalize } from '../helpers/utils';

const MAX_DESCRIPTION_LENGTH = 300;

function template(title: string, description: string, url: string, ogImageUrl: string) {
const redirectUrl = `https://snapshot.org/#/${url}`;

return `<html>
<head>
<title>${title}</title>
<link rel="icon" href="https://snapshot.org/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta property="og:title" content="${title}" />
<meta property="og:description" content="${description}" />
<meta property="og:image" content="${ogImageUrl}" />
<meta property="og:url" content="${redirectUrl}" />
<meta property="og:type" content="article" />
<meta property="og:image:width" content=${WIDTH} />
<meta property="og:image:height" content=${HEIGHT} />
<meta name="color-scheme" content="dark light">
<style>
body, html {
width: 100%,
height: 100%;
padding: 20px;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
font-size: 20px;
font-family: 'Helvetica', 'Arial', 'sans-serif';
color: #444;
}

a, a:visited {
text-decoration: none;
color: #444;
}

@media (prefers-color-scheme: dark) {
body {
background-color: '#211f24';
color: #8b949e;
}

a, a:visited {
color: #8b949e;
}

#snapshot {
fill: #fff;
}
}
</style>
</head>
<body>
<script>
// window.location = "https://snapshot.org/#/${url}";
</script>
<div style="display: flex; flex-direction: column; text-align: center; gap: 10px">
<svg width="100%" style="max-width: 400px;" viewBox="0 0 590 126" preserveAspectRatio="xMidYMid meet" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Snapshot</title>
<g id="logo" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" fill-rule="nonzero">
<path d="M104.781694,54.7785 C104.270697,53.41 102.961707,52.5 101.498717,52.5 L59.2365129,52.5 L83.6138421,5.103 C84.3803368,3.612 83.9848395,1.7885 82.6653488,0.7525 C82.0283532,0.2485 81.2618586,0 80.498864,0 C79.6833697,0 78.8678754,0.287 78.21338,0.8505 L52.4990602,23.058 L1.21391953,67.3505 C0.107927276,68.306 -0.291069928,69.8495 0.219926491,71.218 C0.730922911,72.5865 2.03641376,73.5 3.49940351,73.5 L45.7616074,73.5 L21.3842782,120.897 C20.6177836,122.388 21.0132808,124.2115 22.3327715,125.2475 C22.9697671,125.7515 23.7362617,126 24.4992564,126 C25.3147506,126 26.1302449,125.713 26.7847403,125.1495 L52.4990602,102.942 L103.784201,58.6495 C104.893693,57.694 105.28919,56.1505 104.781694,54.7785 L104.781694,54.7785 Z" id="Path" fill="#FFAC33"></path>
<path d="M163.682969,89.9237288 C170.145678,89.9237288 175.401743,88.3231401 179.451163,85.1219625 C183.500582,81.8797443 185.525292,77.5294261 185.525292,72.071008 C185.484389,63.7397383 180.698711,58.4454832 171.168259,56.1882426 L171.168259,56.1882426 L159.38813,53.4179929 C155.543226,52.3919744 153.620775,50.6682635 153.620775,48.2468599 C153.620775,46.1537823 154.500194,44.5531936 156.259033,43.4450937 C158.017872,42.3369938 160.103936,41.7829438 162.517227,41.7829438 C168.039162,41.7829438 171.43413,43.8349807 172.70213,47.9390544 L172.70213,47.9390544 L185.034453,47.9390544 C184.011872,42.7679215 181.619034,38.7254089 177.855937,35.8115165 C174.09284,32.8976241 169.082195,31.440678 162.824001,31.440678 C156.565807,31.440678 151.452904,33.1028278 147.485291,36.4271276 C143.517678,39.7514273 141.533871,43.9375825 141.533871,48.9855932 C141.533871,57.0706185 146.135484,62.2007107 155.33871,64.3758698 L155.33871,64.3758698 L166.934775,67.1461195 C169.307162,67.7206899 170.984195,68.4594231 171.965872,69.3623194 C172.947549,70.2652156 173.438388,71.6195599 173.438388,73.4253524 C173.438388,75.2721855 172.57942,76.7701725 170.861485,77.9193131 C169.143549,79.0684538 166.832517,79.6430241 163.928388,79.6430241 C157.956517,79.6430241 154.254775,77.2831817 152.823162,72.5634969 L152.823162,72.5634969 L140,72.5634969 C140.818065,77.8987927 143.354065,82.1259887 147.608,85.2450847 C151.861936,88.3641808 157.220259,89.9237288 163.682969,89.9237288 Z M210.336032,88.1779661 L210.336032,51.6952798 C210.989244,49.0516068 212.397734,46.9163325 214.561501,45.2894569 C216.725268,43.6625813 219.378944,42.8491434 222.52253,42.8491434 C225.666116,42.8491434 228.115663,43.8456048 229.871172,45.8385274 C231.626681,47.8314501 232.504435,50.6174746 232.504435,54.1966011 L232.504435,54.1966011 L232.504435,88.1779661 L245.058366,88.1779661 L245.058366,52.0613268 C245.01754,45.7978555 243.282444,40.7952129 239.853078,37.0533989 C236.423711,33.311585 231.585855,31.440678 225.339509,31.440678 C219.093163,31.440678 214.092004,33.6166241 210.336032,37.9685165 L210.336032,37.9685165 L210.336032,32.7218425 L197.782101,32.7218425 L197.782101,88.1779661 L210.336032,88.1779661 Z M278.100767,89.0406995 C281.035204,89.1218834 283.806616,88.7159636 286.415004,87.82294 C289.023392,86.9299164 290.837037,85.9760049 291.855939,84.9612053 L291.855939,84.9612053 L291.855939,88.0664919 L303.715953,88.0664919 L303.715953,51.7772606 C303.715953,45.4449115 301.759662,40.4723938 297.84708,36.8597075 C293.934498,33.2470211 288.819612,31.440678 282.502422,31.440678 C276.225989,31.440678 271.049968,33.0034692 266.974362,36.1290518 C262.898756,39.2546344 260.453392,43.2326485 259.638271,48.0630943 L259.638271,48.0630943 L271.681687,48.0630943 C273.10815,44.0444882 276.287123,42.0351851 281.218606,42.0351851 C284.642116,42.0351851 287.270882,42.9688007 289.104905,44.8360318 C290.938927,46.7032629 291.855939,49.0981898 291.855939,52.0208125 L291.855939,52.0208125 L291.855939,56.5265224 C290.674013,55.6740908 288.819612,54.9028432 286.292736,54.2127795 C283.76586,53.5227158 281.320496,53.177684 278.956645,53.177684 C272.761723,53.2182759 267.606081,54.9028432 263.489719,58.2313856 C259.373356,61.5599281 257.315175,65.8829741 257.315175,71.2005237 C257.315175,76.5180732 259.291844,80.8208232 263.245182,84.1087737 C267.239277,87.3967242 272.191138,89.0406995 278.100767,89.0406995 Z M279.622568,79.4491525 C276.789883,79.4491525 274.2607,78.7642764 272.035019,77.3945241 C269.809339,76.0247718 268.696498,74.0708605 268.696498,71.5327901 C268.696498,68.9947197 269.829572,67.0609518 272.09572,65.7314863 C274.361868,64.4020209 276.911284,63.7372881 279.743969,63.7372881 C282.576654,63.7372881 285.085603,64.2610169 287.270817,65.3084746 C289.456031,66.3559322 290.85214,67.7055411 291.459144,69.3573012 L291.459144,69.3573012 L291.459144,73.8895698 C290.811673,75.501043 289.395331,76.8305085 287.210117,77.8779661 C284.984436,78.9254237 282.455253,79.4491525 279.622568,79.4491525 Z M331.149901,110 L331.149901,83.8135593 C332.496803,85.5347338 334.455933,86.9690459 337.027292,88.1164956 C339.59865,89.2639453 342.353677,89.8376701 345.292373,89.8376701 C353.128894,89.8376701 359.39403,87.0100263 364.08778,81.3547386 C368.78153,75.6994509 371.128405,68.7942628 371.128405,60.639174 C371.087589,52.4840853 368.740715,45.5788971 364.08778,39.9236095 C359.434845,34.2683218 353.16971,31.440678 345.292373,31.440678 C342.353677,31.440678 339.59865,32.034893 337.027292,33.223323 C334.455933,34.411753 332.496803,35.8460651 331.149901,37.5262593 L331.149901,37.5262593 L331.149901,32.7315588 L318.599222,32.7315588 L318.599222,110 L331.149901,110 Z M343.112444,78.5762712 C340.48607,78.5762712 338.068138,77.8841229 335.858649,76.4998265 C333.649159,75.11553 331.98162,73.3444448 330.856031,71.1865708 L330.856031,71.1865708 L330.856031,50.1778359 C331.98162,48.019962 333.649159,46.2488768 335.858649,44.8645803 C338.068138,43.4802838 340.48607,42.7881356 343.112444,42.7881356 C347.989996,42.7881356 351.84618,44.4574343 354.680997,47.7960317 C357.474125,51.1346291 358.870689,55.4300197 358.870689,60.6822034 C358.912378,65.9343871 357.515813,70.2297777 354.680997,73.5683751 C351.84618,76.9069725 347.989996,78.5762712 343.112444,78.5762712 Z M402.690751,89.9237288 C409.153461,89.9237288 414.409525,88.3231401 418.458945,85.1219625 C422.508364,81.8797443 424.533074,77.5294261 424.533074,72.071008 C424.492171,63.7397383 419.706493,58.4454832 410.176041,56.1882426 L410.176041,56.1882426 L398.395912,53.4179929 C394.551008,52.3919744 392.628557,50.6682635 392.628557,48.2468599 C392.628557,46.1537823 393.507976,44.5531936 395.266815,43.4450937 C397.025654,42.3369938 399.111718,41.7829438 401.525009,41.7829438 C407.046944,41.7829438 410.441912,43.8349807 411.709912,47.9390544 L411.709912,47.9390544 L424.042235,47.9390544 C423.019655,42.7679215 420.626816,38.7254089 416.863719,35.8115165 C413.100622,32.8976241 408.089977,31.440678 401.831783,31.440678 C395.573589,31.440678 390.460686,33.1028278 386.493073,36.4271276 C382.52546,39.7514273 380.541653,43.9375825 380.541653,48.9855932 C380.541653,57.0706185 385.143266,62.2007107 394.346492,64.3758698 L394.346492,64.3758698 L405.942557,67.1461195 C408.314944,67.7206899 409.991977,68.4594231 410.973654,69.3623194 C411.955332,70.2652156 412.44617,71.6195599 412.44617,73.4253524 C412.44617,75.2721855 411.587203,76.7701725 409.869267,77.9193131 C408.151331,79.0684538 405.840299,79.6430241 402.93617,79.6430241 C396.964299,79.6430241 393.262557,77.2831817 391.830944,72.5634969 L391.830944,72.5634969 L379.007782,72.5634969 C379.825847,77.8987927 382.361847,82.1259887 386.615782,85.2450847 C390.869718,88.3641808 396.228041,89.9237288 402.690751,89.9237288 Z M450.2193,89.0508475 L450.2193,52.1865298 C450.872513,49.5152025 452.281002,47.3575919 454.444769,45.7136981 C456.608536,44.0698044 459.262212,43.2478575 462.405798,43.2478575 C465.549384,43.2478575 467.998931,44.2547424 469.75444,46.2685123 C471.509949,48.2822821 472.387704,51.0974502 472.387704,54.7140165 L472.387704,54.7140165 L472.387704,89.0508475 L484.941634,89.0508475 L484.941634,52.5564059 C484.900808,46.2274149 483.165712,41.1724416 479.736346,37.3914859 C476.30698,33.6105303 471.469124,31.7200525 465.222778,31.7200525 C458.976432,31.7200525 453.975273,33.9187604 450.2193,38.3161762 L450.2193,38.3161762 L450.2193,7 L437.66537,7 L437.66537,89.0508475 L450.2193,89.0508475 Z M522.180675,89.9237288 C530.083331,89.9237288 536.50424,87.1945198 541.4434,81.7361017 C546.38256,76.2776836 548.85214,69.2597175 548.85214,60.6822034 C548.85214,52.1046893 546.38256,45.0867232 541.4434,39.6283051 C536.50424,34.169887 530.083331,31.440678 522.180675,31.440678 C514.236859,31.440678 507.795371,34.169887 502.856211,39.6283051 C497.917051,45.0867232 495.447471,52.1046893 495.447471,60.6822034 C495.447471,69.2597175 497.917051,76.2776836 502.856211,81.7361017 C507.836531,87.1945198 514.278019,89.9237288 522.180675,89.9237288 Z M522.618335,79.4491525 C518.185103,79.4491525 514.737033,77.7530185 512.274127,74.3607505 C509.81122,70.9684825 508.579767,66.554447 508.579767,61.1186441 C508.579767,55.6828411 509.81122,51.2688056 512.274127,47.8765376 C514.695985,44.4842696 518.144054,42.7881356 522.618335,42.7881356 C527.092616,42.7881356 530.540685,44.4842696 532.962543,47.8765376 C535.384402,51.2688056 536.595331,55.6828411 536.595331,61.1186441 C536.595331,66.554447 535.384402,70.9684825 532.962543,74.3607505 C530.540685,77.7530185 527.092616,79.4491525 522.618335,79.4491525 Z M579.732516,89.9237288 C584.295842,89.9237288 587.718337,89.2728505 590,87.971094 L590,87.971094 L590,76.5603841 C587.922057,78.0655401 585.538534,78.8181181 582.849431,78.8181181 C578.44908,78.8181181 576.248905,76.6620838 576.248905,72.3500152 L576.248905,72.3500152 L576.248905,43.609671 L588.77768,43.609671 L588.77768,33.2362984 L576.248905,33.2362984 L576.248905,18.3474576 L563.72013,18.3474576 L563.72013,33.2362984 L554.980545,33.2362984 L554.980545,43.609671 L563.72013,43.609671 L563.72013,73.6924516 C563.72013,78.9808377 565.248029,83.0081471 568.303828,85.7743798 C571.359627,88.5406125 575.169189,89.9237288 579.732516,89.9237288 Z" id="snapshot" fill="#000000"></path>
</g>
</g>
</svg>
<a href="${redirectUrl}">Click here if you are not redirected</a>
</div>
<div
</body>
</html>`;
}

export async function shareSpacePage(spaceId: string) {
const space = await fetchSpace(spaceId);

if (space) {
let description = `${space.followersCount} member${space.followersCount === 1 ? '' : 's'}`;
if (description.length > 0) {
description += ` | ${space.about}`;
}

return template(space.name, description, space.id, `/og/space/${space.id}`);
}

throw new Error('Space not found');
}

export async function shareProposalPage(spaceId: string, proposalId: string) {
const proposal = await fetchProposal(proposalId);

if (proposal && proposal.space?.id === spaceId) {
const sanitazedBody = removeMd(proposal.body);
const description = `[${capitalize(proposal.state)}] ${proposal.votes} vote${
proposal.votes !== 1 ? 's' : ''
} - ${sanitazedBody.substring(0, MAX_DESCRIPTION_LENGTH).replace(/\r?\n|\r/g, ' ')}${
sanitazedBody.length > MAX_DESCRIPTION_LENGTH ? '…' : ''
}`;

return template(
proposal.title,
description,
`${spaceId}/proposal/${proposalId}`,
`/og/proposal/${proposalId}`
);
}

throw new Error('Space/Proposal not found');
}
2 changes: 1 addition & 1 deletion test/unit/lib/votesReport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('VotesReport', () => {
});

describe('canBeCached()', () => {
const baseMockedProposal = { id: '', title: '', votes: 0, choices: [] };
const baseMockedProposal = { id: '', title: '', votes: 0, body: '', choices: [] };

it('raises an error when the proposal does not exist', () => {
const report = new VotesReport('test', storageEngine);
Expand Down
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1921,6 +1921,11 @@
"@types/scheduler" "*"
csstype "^3.0.2"

"@types/remove-markdown@^0.3.1":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@types/remove-markdown/-/remove-markdown-0.3.1.tgz#82bc3664c313f50f7c77f1bb59935f567689dc63"
integrity sha512-JpJNEJEsmmltyL2LdE8KRjJ0L2ad5vgLibqNj85clohT9AyTrfN6jvHxStPshDkmtcL/ShFu0p2tbY7DBS1mqQ==

"@types/scheduler@*":
version "0.16.3"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
Expand Down Expand Up @@ -4810,6 +4815,11 @@ regenerator-runtime@^0.11.0:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==

remove-markdown@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/remove-markdown/-/remove-markdown-0.5.0.tgz#a596264bbd60b9ceab2e2ae86e5789eee91aee32"
integrity sha512-x917M80K97K5IN1L8lUvFehsfhR8cYjGQ/yAMRI9E7JIKivtl5Emo5iD13DhMr+VojzMCiYk8V2byNPwT/oapg==

require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
Expand Down