The .env
-file should be placed in root and can be found in gbif-configuration/gbif-web
.
Make sure you have the correct version on Node installed. We manage node versions with nvm. Type nvm use
to install the required version. You can also do so manually, see .nvmrc
for the required version.
npm install
Start developing with
npm run develop
It needs the graphql server and translations endpoints to start.
- New GBIF.org
This project requires Node.js version v22.3.0
.
To initiate development mode, execute npm run develop
.
- Build: Run
npm run build
. - Start: Run
npm run start
.
- Build for Hosted Portals: Run
npm run build:hp
. - Start for Hosted Portals: Run
npm run start:hp
.
Environment variables are sourced from various .env
files, depending on the running environment.
The .env
files' loading is managed by Vite. For comprehensive information on Vite's handling of environment variables, visit: Env Variables and Modes.
The .env
files for this project are located here: gbif-configuration.
Environment variables starting with PUBLIC_
are accessible in the client-side build. It is crucial to NEVER prefix sensitive variables, such as private API keys, with PUBLIC_
.
GBIF.org leverages both server-side rendering and client-side hydration.
Files specific to GBIF.org are found in the root at /gbif and in the src folder at /src/gbif.
In /gbif, you will find:
This file, loaded by server.js
, is populated by the server-side rendered app corresponding to the requested route. It also initiates the client-side build for client-side takeover through hydration.
This script manages the production and development server for GBIF.org, loading index.html
and a render function that will a server-side the appication based on the incoming request.
A Vite configuration file for GBIF.org. For detailed configuration options, see: Configuring Vite.
Note: Some configuration options are superseded by build:client
, build:server
scripts, and the server.js
file.
In /src/gbif, you will find:
A configuration file modifying shared code functionalities, such as root predicates, theming, internationalization, etc. This configuration is injected in a context near the root and accessible through a hook called useConfig
.
The entry script for the client build, responsible for hydrating the server-side rendered application.
The server-side entry script, exposing a render
function for application rendering based on a Express request.
This function renders the HTML of the app and acquires headHtml
, htmlAttributes
, and bodyAttributes
using react-helmet-async
. These are then injected into index.html
by server.js
, forming the HTML sent to the client.
Defines routes for gbif.org using a custom type SourceRouteObject
, enabling additional properties beyond those supported by react-router-dom
.
Routes are processed by configureRoutes
, which, among other tasks, replicates routes for each language specified in config.ts
and injects the config and locale into loader functions. The resulting output is compatible with react-router-dom
.
The Hosted Portal build only leverage client-side rendering, intended for integration within hosted portals.
Files specific to Hosted Portals are located in the root at /hp and in the src folder at /src/hp.
In /hp, you will find:
This file is instrumental for testing the Hosted Portal code. It loads the Hosted Portal build, rendering the application with an included configuration. This file serves as a prototype for demonstrating the Hosted Portal build's utilization and does not mirror the final build on the Hosted Portals.
Utilizes index.html
to demonstrate the Hosted Portal build, employed by the npm run start:hp
script.
The Vite configuration file for the Hosted Portal build. This configuration remains unaltered by any scripts or files.
In /src/hp, you will find:
The entry file for the Hosted Portal build, rendering the application client-side as per the provided configuration.
This script exports a render
function that takes a rootElement
and a config
.
Houses route definitions for hosted portals, utilizing a custom type SourceRouteObject
, which accommodates additional properties not supported by react-router-dom
.
Routes are processed by configureRoutes
, duplicating routes for each language in config.ts
and injecting the config and locale into loader functions. The final output is compatible with react-router-dom
.
Code not contained within the gbif
or hp
folders is or can be shared across the application.
Handled by react-router-dom version 6.
We utilize a custom type SourceRouteObject
for route definition, enhancing standard functionalities.
configureRoutes
transforms our custom route definitions into a format compatible with react-router-dom
.
Our custom type for route definition, incorporating additional functionalities.
type SourceRouteObject = Omit<RouteObject, 'loader' | 'children' | 'lazy'> & {
// 'key' is optionally used to activate or deactivate the route in the global configuration.
key?: string;
// 'loader' is an optional function that supersedes the default loader, adding unique functionality and parameters.
loader?: (args: LoaderArgs) => Promise<any>;
// 'loadingElement' is an optional React node rendered during the navigation process to this route.
loadingElement?: React.ReactNode;
// 'children' are SourceRouteObjects, allowing nested route definitions within this route object.
children?: SourceRouteObject[];
// 'gbifRedirect' is an optional function enabling redirection to gbif.org for routes not active on hosted portals.
gbifRedirect?: (params: Record<string, string | undefined>) => string;
// 'lazy' is a function for lazy loading the route's component, improving performance by loading the component only when required.
lazy?: () => Promise<Pick<RouteObject, 'element'>>;
};
This function transforms our custom route definitions to a format compatible with react-router-dom
. It performs the following operations:
- Duplicates each route for every language, appending a specific path prefix to differentiate them.
- Wraps the root routes with the
I18nProvider
, thereby making the locale accessible to the route and its child components. - Injects the configuration and the selected locale into the loaders, customizing their behavior based on these settings.
- Removes any routes that are not enabled in the global configuration, ensuring only active routes are processed.
- Adds a
LoadingElementWrapper
to every route element. This wrapper enables the display of theloadingElement
specified for the route currently being navigated to. - Dispatches a
StartLoadingEvent
within the loader. This event informs theLoadingElementContext
to display a loading element if one is available. - Returns an array of routes that is compatible with
react-router-dom
, ensuring seamless integration with this library. - Generates and returns a
RouteMetadata
array. This array facilitates access to some of the custom properties of the routes during runtime, using a context for dynamic retrieval.
{
// A key identifying the page for enablement/disablement in the global config
key: 'dataset-page',
// The relative path of the route, considering parent route paths for absolute path determination
path: 'dataset/:key',
// A custom loader function receiving global config, selected locale, request, and parameters
loader: datasetLoader,
// A function generating a redirect link to gbif.org if disabled by the global config
gbifRedirect: (params) => {
if (typeof params.key !== 'string') throw new Error('Invalid key');
return `https://www.gbif.org/dataset/${params.key}`;
},
// An element displayed during navigation to this route
loadingElement: <DatasetLoadingPage />,
// The primary element displayed upon loader completion
element: <DatasetPage />,
// An element displayed in case of loader or element errors
errorElement: <DatasetErrorPage />,
// Function for lazy loading the route component, improving performance by loading only when needed. 'element' should not be used in conjunction with 'lazy'
async lazy: () {
const { DatasetPage } = await import('@/routes/dataset/key/Page');
return { element: <DatasetPage /> }
}
// Child routes for features like tabs, with the default route marked by { index: true }
children: [
{
index: true,
element: <DatasetAboutTab />,
},
{
path: 'dashboard',
element: <DatasetDashboardTab />,
},
],
}
More on react-router-dom
RouteObject
options: Route
.
Note: Some example properties are custom or override the default RouteObject
. Refer to the SourceRouteObject
section above for all custom properties.
Styling in this project is implemented using tailwindcss.
Additionally, we utilize the component library shadcn/ui, which enables us to add components incrementally and provides full flexibility to modify these components as needed.
Classes can be conditionally added to an element by using the cn
function from '@/utils/shadcn'
.
Here is an example of how to use the function:
import { cn } from '@/utils/shadcn';
const isActive = true;
const className = cn('border', { 'text-sky-500': isActive });
console.log(className === 'border text-sky-500'); // true
CSS modules can also be utilized by creating a file ending in .module.css
.
Such a file can be imported as follows:
import styles from 'myStyles.module.css';
function Component() {
return <p className={styles.myClassName}>Test</p>;
}
The class names in a module.css
file are transformed to be scoped specifically to that file.
To define a class without transforming the class name, you can do so in the following manner:
:global {
.my-class {
background-color: green;
}
}
For using CSS pre-processors like .scss
, .sass
, .less
, or .styl
, they can easily be integrated following this guide: CSS Pre-processors.
The global configuration is essential for customizing the shared code across GBIF.org and the Hosted Portals.
In GBIF.org, the configuration is incorporated into the final bundle and can be found here: config.ts.
Each Hosted Portal embeds its specific configuration at the initiation of its code on the respective Hosted Portal.
Here is a detailed example of a configuration object that utilizes the full range of configuration options:
export const config: Config = {
// The default title for the browser tab, serving as a fallback when a page lacks a specific title
defaultTitle: 'GBIF',
// The GraphQL endpoint for fetching data displayed on the site
graphqlEndpoint: 'https://graphql.gbif-staging.org/graphql',
// The set of languages available on the site
languages: [
{
// Language code for the 'lang' attribute in the HTML document and URL prefix (unless it's the default language)
code: 'en',
// The human-readable label for the language
label: 'English',
// Indicates the default language, available at the root URL '/'
default: true,
// Text direction, either 'ltr' (left-to-right) or 'rtl' (right-to-left)
textDirection: 'ltr',
},
{
code: 'ar',
label: 'العربية',
// As a non-default language, its code is used as a URL prefix
default: false,
textDirection: 'rtl',
},
],
// The root occurrence predicate for the site, detailed documentation available at https://www.gbif.org/developer/occurrence#predicates
occurrencePredicate: {
type: 'and',
predicates: [
{
type: 'range',
key: 'year',
value: {
gte: '2012',
},
},
],
},
// Configuration for the site's theme
theme: {
colors: {
// The primary color, used in elements like buttons
primary: 'hsl(104 57.0% 36.5%)',
// Foreground color for primary-colored elements; defaults to black or white for optimal contrast
primaryForeground: 'black',
},
// Border radius for elements such as cards and buttons, measured in rem
borderRadius: 0.5,
},
};
import React from 'react';
import { Helmet } from 'react-helmet-async';
import { LoaderArgs } from '@/types';
import { OccurrenceQuery, OccurrenceQueryVariables } from '@/gql/graphql';
import { createGraphQLHelpers } from '@/utils/createGraphQLHelpers';
/*
This function generates helper utilities for efficient and type-safe data fetching, based on a GraphQL query. The preceding GraphQL comment ensures codegen generates relevant types for the query. During development, the codegen will monitor for changes and update types accordingly. It's essential to supply these generated types to the `createGraphQLHelpers` function for type safety. The `load` function enforces the use of all required variables with the correct types. The `useTypedLoaderData` hook ensures type safety for the query response, meaning you'll be aware of the response types and need to handle nullable values. The query must be uniquely named to facilitate the generation of type names.
*/
const { load, useTypedLoaderData } = createGraphQLHelpers<
OccurrenceQuery,
OccurrenceQueryVariables
>(/* GraphQL */ `
query Occurrence($key: ID!) {
occurrence(key: $key) {
eventDate
scientificName
coordinates
dataset {
key
title
}
}
}
`);
// Example of a page component using data from the above query
export function DetailedOccurrencePage() {
const { data } = useTypedLoaderData();
if (data.occurrence == null) throw new Error('404');
const occurrence = data.occurrence;
return (
<>
{/* Helmet modifies the document head based on the current page. More info: https://www.npmjs.com/package/react-helmet-async */}
<Helmet>
<title>{occurrence.scientificName}</title>
</Helmet>
<h1>{occurrence.scientificName}</h1>
</>
);
}
// Function for fetching data for this page, executed on the server (for SSR) and client (for CSR)
export async function detailedOccurrenceLoader({ request, params, config, locale }: LoaderArgs) {
const key = params.key;
if (key == null) throw new Error('No key provided in the URL');
return load({
endpoint: config.graphqlEndpoint,
request,
variables: {
key,
},
});
}
// Optional component to display a page-specific loading screen during navigation. For building comprehensive loading screens, consider using the 'skeleton' component from shadcn/ui: https://ui.shadcn.com/docs/components/skeleton
export function DetailedOccurrencePageLoading() {
return <div>Loading...</div>; // Placeholder text; replace with skeleton component as needed
}
To integrate the newly created page into the application, it needs to be registered in the router configuration. This process involves updating two separate files: Hosted Portals Routes and GBIF.org Routes. If the new page is intended to be available on both Hosted Portals and GBIF.org, it must be added to both files.
Here's an example of adding the demo page:
{
key: 'occurrence-page',
path: 'occurrence/:key',
loader: detailedOccurrenceLoader,
loadingElement: <DetailedOccurrencePageLoading />,
element: <DetailedOccurrencePage />,
}
For more information about configuring individual routes, refer to the Routing section.
Currently, a standard method for adding client-side only routes hasn't been established. However, the approach is similar to server-side routes, excluding the loadingElement
and loader
. Data fetching and loading state management should be handled directly within the component.
Code splitting can make sense when a specific part of your application is large or not immediately necessary, thereby improving the initial load performance. Vite will automatically create a new bundle for the code that is lazy loaded, ensuring that these larger or less critical components don't affect the initial page load speed.
import { Suspense, lazy } from 'react';
const MyLazyComponent = lazy(() => import('@/components/MyLazyComponent'));
function Component() {
return (
<>
<p>This text will be there when the component renders</p>
<Suspense fallback={<p>MyLazyComponent is loading...</p>}>
<MyLazyComponent />
</Suspense>
</>
);
}
This approach is particularly beneficial for large components or features that are not essential to the initial user experience, allowing the primary content to load quickly while deferring the loading of these components until they are needed.
It could make sense to wrap a Suspense
component with an ErrorBoundary
to handle scenarios where the dynamically imported component fails to load. This approach provides a robust error handling mechanism, ensuring that your application can gracefully manage loading errors and enhance the overall user experience.
By implementing an ErrorBoundary
around Suspense
, you can effectively catch and handle any unexpected issues that might arise during the lazy loading of components, maintaining application stability even in the face of unforeseen errors.
Lazy loading an entire page is a strategy to reduce the size of the main bundle, resulting in faster hydration processes. react-router-dom
natively supports this feature.
Here's an example of how to lazy load a route:
{
key: 'occurrence-search-page',
path: 'occurrence/search',
async lazy: () {
const { OccurrenceSearchPage } = await import('@/routes/occurrence/search/Page');
return { element: <OccurrenceSearchPage /> }
},
loader: occurrenceSearchLoader,
loadingElement: <OccurrenceSearchPageLoading />,
},
Note: To ensure proper code splitting, the element
should be placed in a different file than the loader
and loadingElement
. This division allows the main bundle to remain smaller, accelerating the initial load. Additionally, the loader
will load the necessary data concurrently while the JavaScript for the page is being loaded, optimizing resource utilization and improving user experience.
Code formatting within this project is managed using Prettier. To ensure consistency across the codebase, you are encouraged to install the Prettier extension in your code editor. Ideally, enable the 'format on save' feature for automatic formatting. The project's root directory contains a Prettier configuration file, which specifies the formatting rules to be applied. Adhering to these guidelines will help maintain a uniform coding style throughout the project.
This project employs ESLint to enforce coding standards and ensure high-quality code contributions. To facilitate adherence to these standards, it's recommended that you install the ESLint extension in your code editor. This will enable real-time linting, alerting you to any violations of the ESLint rules as you code.
The root directory of the project contains an ESLint configuration file which outlines the specific rules to be followed. By integrating this tool into your development workflow, you can contribute code that consistently meets the project's quality and style guidelines.
Loading screens are displayed only when navigating between routes, not during the initial rendering. This leads to a suboptimal experience for the Hosted Portals where server-side rendering is not utilized.
The loading screens are shown while technically still on the route being navigated away from. This means that the URL in the browser will still reflect the previous route while the new one is loading.
Loading screens do not apply when navigating to the same route with different parameters.
Document any recurrent errors here, along with their solutions.