Skip to content
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
33 changes: 29 additions & 4 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,46 @@
<meta name="theme-color" content="#f3f7fb" />
<meta
name="description"
content="Track Syscoin Sentry Node count, locked supply, rewards, governance proposals, setup guidance, and market context in one clean dashboard."
content="Live Syscoin Sentry Node stats, governance proposals, rewards, setup guidance, and market data."
/>
<meta property="og:title" content="Sysnode | Syscoin Sentry Node Dashboard" />
<meta
property="og:description"
content="Track Syscoin Sentry Node count, locked supply, rewards, governance proposals, setup guidance, and market context in one clean dashboard."
content="Live Syscoin Sentry Node stats, governance proposals, rewards, setup guidance, and market data."
/>
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Sysnode" />
<meta property="og:url" content="https://sysnode.info/" />
<meta name="twitter:card" content="summary" />
<meta
property="og:image"
content="https://sysnode.info/social-card.png?v=20260518b"
Comment thread
bigpoppa-sys marked this conversation as resolved.
/>
<meta
property="og:image:secure_url"
content="https://sysnode.info/social-card.png?v=20260518b"
/>
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta
property="og:image:alt"
content="Sysnode social share card with the Syscoin logo, Sysnode wordmark, and a connected globe network illustration."
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@syscoin" />
<meta name="twitter:creator" content="@syscoin" />
<meta name="twitter:title" content="Sysnode | Syscoin Sentry Node Dashboard" />
<meta
name="twitter:description"
content="Track Syscoin Sentry Node count, locked supply, rewards, governance proposals, setup guidance, and market context in one clean dashboard."
content="Live Syscoin Sentry Node stats, governance proposals, rewards, setup guidance, and market data."
/>
<meta
name="twitter:image"
content="https://sysnode.info/social-card.png?v=20260518b"
/>
<meta
name="twitter:image:alt"
content="Sysnode social share card with the Syscoin logo, Sysnode wordmark, and a connected globe network illustration."
/>
<title>Sysnode | Syscoin Sentry Node Dashboard</title>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
Expand Down
Binary file added public/social-card.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
63 changes: 59 additions & 4 deletions src/components/PageMeta.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

const SITE_NAME = 'Sysnode';
const DEFAULT_TITLE = 'Sysnode | Syscoin Sentry Node Dashboard';
const DEFAULT_DESCRIPTION =
'Track Syscoin Sentry Node count, locked supply, rewards, governance proposals, setup guidance, and market context in one clean dashboard.';
'Live Syscoin Sentry Node stats, governance proposals, rewards, setup guidance, and market data.';
const SITE_URL = 'https://sysnode.info';
const SOCIAL_IMAGE_URL = `${SITE_URL}/social-card.png?v=20260518b`;
const SOCIAL_IMAGE_ALT =
'Sysnode social share card with the Syscoin logo, Sysnode wordmark, and a connected globe network illustration.';
const TWITTER_HANDLE = '@syscoin';

function ensureMeta(selector, attributeName, attributeValue) {
let element = document.head.querySelector(selector);
Expand All @@ -18,8 +24,10 @@ function ensureMeta(selector, attributeName, attributeValue) {
}

export default function PageMeta(props) {
const location = useLocation();
const description = props.description || DEFAULT_DESCRIPTION;
const fullTitle = props.title ? `${props.title} | ${SITE_NAME}` : DEFAULT_TITLE;
const pageUrl = `${window.location.origin}${location.pathname}${location.search}${location.hash}`;

useEffect(
function syncDocumentMeta() {
Expand Down Expand Up @@ -48,12 +56,50 @@ export default function PageMeta(props) {
);
ensureMeta('meta[property="og:url"]', 'property', 'og:url').setAttribute(
'content',
window.location.href
pageUrl
);
ensureMeta('meta[property="og:image"]', 'property', 'og:image').setAttribute(
'content',
SOCIAL_IMAGE_URL
);
ensureMeta(
'meta[property="og:image:secure_url"]',
'property',
'og:image:secure_url'
).setAttribute('content', SOCIAL_IMAGE_URL);
ensureMeta(
'meta[property="og:image:type"]',
'property',
'og:image:type'
).setAttribute('content', 'image/png');
ensureMeta(
'meta[property="og:image:width"]',
'property',
'og:image:width'
).setAttribute('content', '1200');
ensureMeta(
'meta[property="og:image:height"]',
'property',
'og:image:height'
).setAttribute('content', '630');
ensureMeta(
'meta[property="og:image:alt"]',
'property',
'og:image:alt'
).setAttribute('content', SOCIAL_IMAGE_ALT);
ensureMeta('meta[name="twitter:card"]', 'name', 'twitter:card').setAttribute(
'content',
'summary'
'summary_large_image'
);
ensureMeta('meta[name="twitter:site"]', 'name', 'twitter:site').setAttribute(
'content',
TWITTER_HANDLE
);
ensureMeta(
'meta[name="twitter:creator"]',
'name',
'twitter:creator'
).setAttribute('content', TWITTER_HANDLE);
ensureMeta('meta[name="twitter:title"]', 'name', 'twitter:title').setAttribute(
'content',
fullTitle
Expand All @@ -63,8 +109,17 @@ export default function PageMeta(props) {
'name',
'twitter:description'
).setAttribute('content', description);
ensureMeta('meta[name="twitter:image"]', 'name', 'twitter:image').setAttribute(
'content',
SOCIAL_IMAGE_URL
);
ensureMeta(
'meta[name="twitter:image:alt"]',
'name',
'twitter:image:alt'
).setAttribute('content', SOCIAL_IMAGE_ALT);
},
[description, fullTitle]
[description, fullTitle, pageUrl]
);

return null;
Expand Down
52 changes: 52 additions & 0 deletions src/components/PageMeta.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { act } from 'react';
import { render, waitFor } from '@testing-library/react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';

import PageMeta from './PageMeta';

afterEach(() => {
document.title = '';
document.head
.querySelectorAll(
'meta[name="description"], meta[property^="og:"], meta[name^="twitter:"], link[rel="canonical"]'
)
.forEach((element) => {
element.remove();
});
});

test('updates og:url when only the location changes', async () => {
const staticOgUrl = document.createElement('meta');
staticOgUrl.setAttribute('property', 'og:url');
staticOgUrl.setAttribute('content', 'https://sysnode.info/');
document.head.appendChild(staticOgUrl);

const history = createMemoryHistory({
initialEntries: ['/verify-email?token=alpha'],
});

render(
<Router history={history}>
<PageMeta title="Verify email" description="Finish creating your Sysnode account." />
</Router>
);

expect(document.head.querySelector('meta[property="og:url"]')).toHaveAttribute(
'content',
'http://localhost/verify-email?token=alpha'
);
expect(document.head.querySelector('link[rel="canonical"]')).toBeNull();

await act(async () => {
history.push('/verify-email?token=beta');
});

await waitFor(() =>
expect(document.head.querySelector('meta[property="og:url"]')).toHaveAttribute(
'content',
'http://localhost/verify-email?token=beta'
)
);
expect(document.head.querySelector('link[rel="canonical"]')).toBeNull();
});
2 changes: 1 addition & 1 deletion src/pages/Home.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export default function Home() {
<main className="page-main">
<PageMeta
title="Overview"
description="Track Syscoin Sentry Node count, locked supply, rewards, proposals, locations, and SYS market context in one clean dashboard."
description="Live Syscoin Sentry Node stats, governance proposals, rewards, setup guidance, and market data."
/>

<section className="hero">
Expand Down
Loading