Skip to content
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
11 changes: 11 additions & 0 deletions src/components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {CONTENT_WRAPPER_ID, DEFAULT_THEME, MENU_ID} from '../../constants';
import {EnvironmentContext} from '../../contexts';
import {useLocale} from '../../hooks/useLocale';
import {block} from '../../utils';
import type {JsonLd} from '../../utils/structuredData';
import {CustomScrollbar} from '../CustomScrollbar';
import {Footer} from '../Footer/Footer';
import {Menu} from '../Menu/Menu';
Expand All @@ -31,6 +32,8 @@ export type LayoutProps = {
noScroll?: boolean;
meta?: MetaProps;
hideLocalePicker?: boolean;
/** Schema.org blocks rendered as JSON-LD inside <head>. */
jsonLd?: JsonLd[];
};

export const Layout: React.FC<LayoutProps> = ({
Expand All @@ -43,6 +46,7 @@ export const Layout: React.FC<LayoutProps> = ({
noScroll = false,
hideLocalePicker = false,
meta = {},
jsonLd,
}) => {
const locale = useLocale();

Expand Down Expand Up @@ -104,6 +108,13 @@ export const Layout: React.FC<LayoutProps> = ({
<Head>
<title>{`Gravity UI${title ? ` – ${title}` : ''}`}</title>
<Meta {...meta} />
{jsonLd?.map((item, index) => (
<script
key={index}
type="application/ld+json"
dangerouslySetInnerHTML={{__html: JSON.stringify(item)}}
/>
))}
</Head>
<ThemeProvider theme={DEFAULT_THEME} direction={isRtl ? 'rtl' : 'ltr'}>
<React.Fragment>
Expand Down
25 changes: 25 additions & 0 deletions src/pages/components/[libId]/[componentId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ import {Component} from '../../../components/Component/Component';
import {ComponentsLayout} from '../../../components/ComponentsLayout/ComponentsLayout';
import {Layout} from '../../../components/Layout/Layout';
import {libs} from '../../../content/components';
import {useLocale} from '../../../hooks/useLocale';
import {getMaintainers} from '../../../utils';
import {getCanonicalUrlForLocale} from '../../../utils/canonical';
import {getLibComponents} from '../../../utils/components';
import {getI18nProps} from '../../../utils/i18next';
import {getComponentMeta} from '../../../utils/meta';
import {getBreadcrumbJsonLd, getTechArticleJsonLd} from '../../../utils/structuredData';

export const getServerSideProps: GetServerSideProps = async (ctx) => {
const components = getLibComponents(ctx.params?.libId as string);
Expand Down Expand Up @@ -72,6 +75,7 @@ export const ComponentPage = ({
readmeContent: string;
}) => {
const {t} = useTranslation();
const locale = useLocale();
const componentsLib = libs.find((item) => item.id === lib.config.id);
const component = componentsLib?.components.find((item) => item.id === componentId);

Expand All @@ -93,6 +97,26 @@ export const ComponentPage = ({

const maintainers = getMaintainers(lib, `/src/components/${component.title}`);

const componentPath = `/components/${lib.config.id}/${component.id}`;
const componentUrl = getCanonicalUrlForLocale(locale, componentPath);

const structuredData = [
getBreadcrumbJsonLd([
{name: t('menu_components'), url: getCanonicalUrlForLocale(locale, '/components')},
{
name: lib.config.title,
url: getCanonicalUrlForLocale(locale, `/components/${lib.config.id}`),
},
{name: component.title, url: componentUrl},
]),
getTechArticleJsonLd({
headline: `${lib.config.title} – ${component.title}`,
description: componentMeta.description,
url: componentUrl,
locale,
}),
];

const sections = React.useMemo<Section[]>(() => {
return libs.map(({id, title, components}) => {
return {
Expand All @@ -118,6 +142,7 @@ export const ComponentPage = ({
hideFooter
noScroll={!isMobile}
meta={componentMeta}
jsonLd={structuredData}
>
<ComponentsLayout libId={lib.config.id} componentId={componentId} sections={sections}>
<Component
Expand Down
25 changes: 25 additions & 0 deletions src/pages/design/[sectionId]/[articleId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import {DesignArticle} from '../../../components/DesignArticle/DesignArticle';
import {DesignLayout} from '../../../components/DesignLayout/DesignLayout';
import {Layout} from '../../../components/Layout/Layout';
import {sections as designSections} from '../../../content/design';
import {useLocale} from '../../../hooks/useLocale';
import {getCanonicalUrlForLocale} from '../../../utils/canonical';
import {getI18nProps} from '../../../utils/i18next';
import {getDesignArticleMeta} from '../../../utils/meta';
import {getBreadcrumbJsonLd, getTechArticleJsonLd} from '../../../utils/structuredData';

export const getServerSideProps: GetServerSideProps = async (ctx) => {
const sectionId = ctx.params?.sectionId as string;
Expand Down Expand Up @@ -40,6 +43,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {

export const ArticlePage = ({section, article}: {section: Section; article: Article}) => {
const {i18n, t} = useTranslation();
const locale = useLocale();

const sectionId = section.id;
const articleId = article.id;
Expand Down Expand Up @@ -75,12 +79,33 @@ export const ArticlePage = ({section, article}: {section: Section; article: Arti
};
});

const articlePath = `/design/${sectionId}/${articleId}`;
const articleUrl = getCanonicalUrlForLocale(locale, articlePath);

const structuredData = [
getBreadcrumbJsonLd([
{name: t('menu_design'), url: getCanonicalUrlForLocale(locale, '/design')},
{
name: sectionTitle,
url: getCanonicalUrlForLocale(locale, `/design/${sectionId}`),
},
{name: articleTitle, url: articleUrl},
]),
getTechArticleJsonLd({
headline: `${sectionTitle} – ${articleTitle}`,
description: articleMeta.description,
url: articleUrl,
locale,
}),
];

return (
<Layout
title={`${sectionTitle} – ${articleTitle}`}
hideFooter
noScroll={!isMobile}
meta={articleMeta}
jsonLd={structuredData}
>
<DesignLayout sections={sections} sectionId={sectionId} articleId={articleId}>
<DesignArticle article={article} sectionId={sectionId} sections={sections} />
Expand Down
4 changes: 3 additions & 1 deletion src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import backgroundAsset from '../assets/background.jpg';
import {Landing} from '../components/Landing/Landing';
import {Layout} from '../components/Layout/Layout';
import {getI18nProps} from '../utils/i18next';
import {getOrganizationJsonLd, getWebSiteJsonLd} from '../utils/structuredData';

export const getServerSideProps: GetServerSideProps = async (ctx) => {
const [libs, i18nProps] = await Promise.all([
Expand Down Expand Up @@ -35,7 +36,8 @@ const Home = ({libs}: {libs: LibWithMetadata[]}) => {
type="image/jpeg"
/>
</Head>
<Layout isPageConstructor>
{/* Emitted on the homepage only: these describe the site as a whole, not each page. */}
<Layout isPageConstructor jsonLd={[getOrganizationJsonLd(), getWebSiteJsonLd()]}>
<Landing libs={libs} backgroundImageSrc={backgroundAsset.src} />
</Layout>
</>
Expand Down
14 changes: 13 additions & 1 deletion src/pages/libraries/[libId]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {ServerApi} from '../../../api/server';
import {Layout} from '../../../components/Layout/Layout';
import {Library} from '../../../components/Library/Library';
import {getI18nProps, getLibraryMeta, isValidLibId} from '../../../utils';
import {SITE_URL} from '../../../utils/canonical';
import {getSoftwareSourceCodeJsonLd} from '../../../utils/structuredData';

export const getServerSideProps: GetServerSideProps = async (ctx) => {
const libId = ctx.params?.libId as string;
Expand Down Expand Up @@ -33,10 +35,20 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
export const LibraryPage = ({lib}: {lib: LibWithFullData}) => {
const {t} = useTranslation();

const meta = getLibraryMeta({id: lib.config.id, title: lib.config.title}, t);

return (
<Layout
title={lib?.config.title ?? ''}
meta={getLibraryMeta({id: lib.config.id, title: lib.config.title}, t)}
meta={meta}
jsonLd={[
getSoftwareSourceCodeJsonLd({
name: lib.config.title,
description: meta.description,
githubId: lib.config.githubId,
url: `${SITE_URL}/libraries/${lib.config.id}`,
}),
]}
>
<Library lib={lib} />
</Layout>
Expand Down
89 changes: 89 additions & 0 deletions src/utils/structuredData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {SITE_URL} from './canonical';

const SCHEMA_CONTEXT = 'https://schema.org';

/**
* Profiles the site itself links to, so they are authoritative rather than guessed.
* The npm organisation is deliberately absent: it is not linked from the site and could not
* be verified, and an unverified sameAs is worse than a missing one.
*/
const OFFICIAL_PROFILES = [
'https://github.com/gravity-ui',
'https://t.me/gravity_ui',
'https://www.figma.com/community/file/1271150067798118027/Gravity-UI-Design-System-(Beta)',
];

export type JsonLd = Record<string, unknown>;

export const getOrganizationJsonLd = (): JsonLd => ({
'@context': SCHEMA_CONTEXT,
'@type': 'Organization',
name: 'Gravity UI',
url: `${SITE_URL}/`,
logo: `${SITE_URL}/favicon-512x512.png`,
sameAs: OFFICIAL_PROFILES,
});

export const getWebSiteJsonLd = (): JsonLd => ({
'@context': SCHEMA_CONTEXT,
'@type': 'WebSite',
name: 'Gravity UI',
url: `${SITE_URL}/`,
// No `potentialAction: SearchAction` on purpose: the site has no search endpoint, so
// declaring one would be markup that does not describe the page.
});

export const getBreadcrumbJsonLd = (items: {name: string; url: string}[]): JsonLd => ({
'@context': SCHEMA_CONTEXT,
'@type': 'BreadcrumbList',
itemListElement: items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: item.url,
})),
});

export const getSoftwareSourceCodeJsonLd = (params: {
name: string;
description?: string;
githubId: string;
url: string;
}): JsonLd => {
const {name, description, githubId, url} = params;

return {
'@context': SCHEMA_CONTEXT,
'@type': 'SoftwareSourceCode',
name,
url,
codeRepository: `https://github.com/${githubId}`,
programmingLanguage: 'TypeScript',
...(description ? {description} : {}),
};
};

export const getTechArticleJsonLd = (params: {
headline: string;
description?: string;
url: string;
locale: string;
}): JsonLd => {
const {headline, description, url, locale} = params;

return {
'@context': SCHEMA_CONTEXT,
'@type': 'TechArticle',
headline,
url,
inLanguage: locale,
isPartOf: {
'@type': 'WebSite',
name: 'Gravity UI',
url: `${SITE_URL}/`,
},
// datePublished/dateModified are omitted rather than invented: the readme content is
// fetched from GitHub raw URLs, which carry no reliable authoring date.
...(description ? {description} : {}),
};
};
Loading