Frequently Asked Questions

Technical Setup & Preview Mode with Remix

How do I set up preview mode in a Remix application using Hygraph?

To set up preview mode in Remix with Hygraph, you need to:

Why use a form to exit preview mode in Remix instead of a link?

Remix handles layouts and routes differently than Next.js. Using a form submission ensures a full reload, updating navigation and showing published content. If you use a link, parent route data may not refresh, causing navigation to still show draft content until a manual reload. Source

Where can I find a demo and the complete code for preview mode with Remix and Hygraph?

You can see a live demo here, clone the project here, and view the complete code here.

How do I integrate Hygraph's GraphQL API with Remix?

Use the graphql-request client to connect to Hygraph's GraphQL API. Define queries for your content and use loader functions in Remix to fetch data. For more details, see the Hygraph API Reference.

Features & Capabilities

What are the key features of Hygraph?

Hygraph offers a GraphQL-native architecture, content federation, scalability, and a wide range of integrations (including Netlify, Vercel, Shopify, BigCommerce, AWS S3, Cloudinary, Lokalise, and more). It supports rapid content delivery, intuitive user interfaces, and robust security features. Learn more at the Hygraph Features page.

Does Hygraph support integrations with other platforms?

Yes, Hygraph integrates with platforms such as Netlify, Vercel, Shopify, BigCommerce, commercetools, AWS S3, Cloudinary, Bynder, Mux, Lokalise, Crowdin, EasyTranslate, Smartling, Aprimo, Ninetailed, AltText.ai, Adminix, and Plasmic. See the full list at the Hygraph Integrations page.

Does Hygraph provide an API for content management?

Yes, Hygraph provides a powerful GraphQL API for efficient content fetching and management. Learn more at the API Reference.

How does Hygraph optimize content delivery performance?

Hygraph is designed for optimized content delivery, ensuring rapid distribution and responsiveness. This reduces bounce rates, increases conversions, and positively impacts user experience and search engine rankings. For more details, visit this page.

Pricing & Plans

What is Hygraph's pricing model?

Hygraph offers a free forever Hobby plan, a Growth plan starting at $199/month, and custom Enterprise plans. For full details, visit the pricing page.

Security & Compliance

What security and compliance certifications does Hygraph have?

Hygraph is SOC 2 Type 2 compliant, ISO 27001 certified, and GDPR compliant. It offers features like SSO integrations, audit logs, encryption at rest and in transit, and sandbox environments. Learn more at the Hygraph Security Features page.

Use Cases & Benefits

Who can benefit from using Hygraph?

Hygraph is ideal for developers, IT decision-makers, content creators, project/program managers, agencies, solution partners, and technology partners. It serves modern software companies, enterprises seeking modernization, and brands aiming to scale, improve development velocity, or re-platform from traditional solutions. See case studies.

What business impact can customers expect from using Hygraph?

Customers can expect time-saving through streamlined workflows, ease of use, faster speed-to-market, and enhanced customer experience via scalable content delivery. These benefits help modernize tech stacks and improve operational efficiency. Learn more.

What industries are represented in Hygraph's case studies?

Industries include Food and Beverage (Dr. Oetker), Consumer Electronics (Samsung), Automotive (AutoWeb), Healthcare (Vision Healthcare), Travel and Hospitality (HolidayCheck), Media and Publishing, eCommerce, SaaS (Bellhop), Marketplace, Education Technology, and Wellness and Fitness. See all case studies.

Can you share specific customer success stories using Hygraph?

Yes. Komax achieved 3X faster time to market, Autoweb saw a 20% increase in website monetization, Samsung improved customer engagement with a scalable platform, and Dr. Oetker enhanced their digital experience using MACH architecture. Explore more success stories.

Pain Points & Solutions

What problems does Hygraph solve?

Hygraph addresses operational pains (reliance on developers for content updates, outdated tech stacks, conflicting global team needs, clunky content creation), financial pains (high costs, slow speed-to-market, expensive maintenance, scalability challenges), and technical pains (boilerplate code, overwhelming queries, evolving schemas, cache and OpenID integration issues). Learn more.

How does Hygraph solve pain points for different personas?

For developers: Hygraph reduces boilerplate code and streamlines query management. For content creators/project managers: It empowers non-technical users to update content independently and offers a user-friendly interface. For business stakeholders: Hygraph lowers operational costs, accelerates speed-to-market, and supports scalability for growth. Details here.

What KPIs and metrics are associated with the pain points Hygraph solves?

KPIs include time saved on content updates, system uptime, consistency across regions, user satisfaction scores, reduction in operational costs, time to market, maintenance costs, scalability metrics, and performance during peak usage. See more on CMS KPIs.

Support & Implementation

What support and training does Hygraph offer?

Hygraph provides 24/7 support via chat, email, and phone. Enterprise customers receive dedicated onboarding and expert guidance. All users have access to documentation, video tutorials, webinars, and a community Slack channel. Contact support.

How easy is it to get started with Hygraph?

Hygraph is easy to start with, even for non-technical users. You can sign up for a free account and use resources like documentation and onboarding guides. For example, Top Villas launched a new project in just 2 months. Get started here.

What customer feedback has Hygraph received regarding ease of use?

Customers praise Hygraph for its intuitive interface and ease of use, noting that even non-technical users can start using it right away. The UI is described as logical and user-friendly for both technical and non-technical teams. Source.

Product Information & Documentation

Where can I find technical documentation for Hygraph?

Comprehensive technical documentation is available at Hygraph Documentation, covering everything needed to build and deploy projects.

Company & Vision

What is Hygraph's vision and mission?

Hygraph's vision is to unify data and enable content federation, empowering businesses to create impactful digital experiences. Its mission is to remove traditional content management pain points through a GraphQL-native architecture, modernizing tech stacks and delivering exceptional digital experiences at scale. Learn more.

Customer Proof

Who are some of Hygraph's customers?

Hygraph is trusted by companies such as Sennheiser, Holidaycheck, Ancestry, Samsung, Dr. Oetker, Epic Games, Bandai Namco, Gamescom, Leo Vegas, and Clayton Homes. See case studies.

Velocity at Scale: Join the Launch of Hygraph’s Latest AI Innovations

How to Create a Preview Mode with Remix and Hygraph

In this example, we will show you how to create and initialize a Preview Mode for your content using Remix and Hygraph.
João Pedro Schmitz

Written by João 

Apr 20, 2022
preview-mode-with-remix-graphcms

We’ve been using Next.js for a long time here at Hygraph and one of the features we love the most in Next is preview mode. It allows you to write a draft on your headless CMS and preview it immediately on your website.

In February, we released the new Hygraph Docs Starter, which allows you to create and manage documentation for your business, product, and services. The starter is built with Remix, TypeScript, GraphQL, and styled with TailwindCSS.

We chose Remix for the docs starter, as it is a Full stack web framework that focuses on the web fundamentals with an edge-first approach, without sacrificing a modern UX. We also wanted to try and use it on a real-world project.

After releasing the starter and using it, we added preview mode support for it, so content editors could quickly preview the changes, without needing to publish.

In this guide, you’ll learn how to set up a Remix application, fetch data from a GraphQL API, and configure a preview mode on the app with Hygraph.

Want to try the application before following the tutorial? See the demo. To test preview mode, click here.

#Creating the Remix Application

The easiest way to create a Remix application is by using the CLI.

npx create-remix@latest

The CLI will prompt a few questions, regarding the folder where you want to create your app, the type, and a few other things.

The only elements that are important for us in this tutorial is the type of the app, and if you want to use TypeScript or JavaScript. Make sure to select the option “Just the basics”, and also TypeScript.

? What type of app do you want to create? Just the basics
? TypeScript or JavaScript? TypeScript
? Do you want me to run `npm install`? Yes

#Creating the Hygraph Project

Since the focus here is on Remix and setting up preview mode, we won’t create a big project on Hygraph. The application we’ll build is a news website, with a page to see all the news and a dynamic route to see the news itself.

To create the Hygraph project, you can use the clone button below.

Clone project

#Integrating with the GraphQL API

To simplify the tutorial, we’ll use graphql-request, which is a straightforward GraphQL client created and maintained by our friends from Prisma.

On the Docs Starter, we decided to use [GraphQL Code Generator](https://www.graphql-code-generatoHow r.com/), which is also a powerful but simple library, created by The Guild.

Let’s get started by installing the packages.

npm add graphql-request graphql

Now, let’s create a folder named lib inside app. In the lib folder, create a file named hygraph.server.ts. This file will export the GraphQL client, with the Hygraph endpoint.

// app/lib/hygraph.server.ts
import { GraphQLClient } from "graphql-request";
const endpoint = process.env.hygraph_ENDPOINT as string;
export const hygraph = new GraphQLClient(endpoint);

#Environment Variables with Remix

On the file we created, we’ll use a HYGRAPH_ENDPOINT environment variable, which is the endpoint of our GraphQL API.

The remix dev server provides built-in support for environment variables during development, but you’ll need to configure them on your server. For more information, I recommend checking the Remix documentation.

To add the environment variable, create a .env file on the root.

touch .env

And edit the file:

HYGRAPH_ENDPOINT=

You can find the Hygraph endpoint on Project Settings -> API Access -> Content API

Hygraph API Tokens.png

If you already started your server, you’ll need to restart it.

#Displaying all the Articles

Before focusing on preview mode, let’s update the index page, so it can show all the articles. The first thing we need to do is define a "loader" function that will be called on the server before rendering to provide data.

// app/routes/index.tsx
import type { LoaderFunction } from "@remix-run/node";
export const loader: LoaderFunction = async () => {
};
// …

Inside the loader function, we need to use the GraphQL client instance to send a request. We also need to define our query that will fetch all the articles.

// app/routes/index.tsx
import type { LoaderFunction } from '@remix-run/node';
import { json } from '@remix-run/node';
import { gql } from 'graphql-request';
import { hygraph } from '~/lib/hygraph.server';
const allArticlesQuery = gql`
{
articles {
id
title
url
createdAt
}
}
`;
export const loader: LoaderFunction = async () => {
const data = await hygraph.request(allArticlesQuery);
return json({
articles: data.articles,
});
};
// …

The API will return an object with all the articles. Since all loaders must return a Response object, we can use the json function to convert the object into a JSON response. We will now use this data on our page.

// …
type Article = {
id: string;
title: string;
url: string;
createdAt: string;
};
type LoaderData = {
articles: Article[];
};
export default function Index() {
const { articles } = useLoaderData<LoaderData>();
return (
<div style={{ fontFamily: 'system-ui, sans-serif', lineHeight: '1.4' }}>
<h1>Welcome to Remix</h1>
<ul>
{articles.map((article) => (
<li key={article.id}>
<a href={`/${article.url}`}>{article.title}</a>
</li>
))}
</ul>
</div>
);
}

If you start your server using npm run dev and go to [http://localhost:3000](http://localhost:3000), you’ll see: an error! 😔

Application Error.png

That error occurs because we haven’t defined the permissions for our API. On the ‘API Access’ page on Hygraph, go to the ‘Permanent Auth Tokens’ section, and create a new token for development. The default stage will be Draft.

Hygraph Create Token.png

On the token page, in the content permissions section, you also need to initialize the default permissions.

Content API Controls.png

We will also need a production token. The process to create it is the same, but the default stage must be Published.

After creating both tokens, we need to add two new environment variables to the .env file.

# .env
HYGRAPH_ENDPOINT=<api-url>
HYGRAPH_DEV_TOKEN=
HYGRAPH_PROD_TOKEN=

The last step is updating the index.tsx loader function.

// app/routes/index.tsx
// …
export const loader: LoaderFunction = async () => {
const preview = true;
const API_TOKEN = preview
? process.env.HYGRAPH_DEV_TOKEN
: process.env.HYGRAPH_PROD_TOKEN;
const data = await hygraph.request(
allArticlesQuery,
{},
{
authorization: `Bearer ${API_TOKEN}`,
},
);
return json({
articles: data.articles,
});
};
// …

You may notice that we’re defining a preview variable as ‘true’, which controls which token we’re using. Later, we’ll implement the logic to check if we’re in preview or not, but for now, we’ll keep it this way.

Now, if you refresh the page, you’ll see all the articles. 🎉

Welcome to Remix Index.png

#Dynamic Route for the Article Page

Now that the ‘index page’ is ready, we can start working on the ‘article page’. First, we need to define a dynamic route, because we don’t want to create new files for every new article. On Remix, routes that begin with a $ character indicate that the route is dynamic. So, let’s create a file named $slug.tsx in the routes folder. Below, you can find the complete code for the page.

// app/routes/$slug.tsx
import type { LoaderFunction } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import { gql } from 'graphql-request';
import { hygraph } from '~/lib/hygraph.server';
const getArticleQuery = gql`
query Article($url: String!) {
article(where: { url: $url }) {
title
url
content {
... on ArticleContentRichText {
json
}
}
}
}
`;
export const loader: LoaderFunction = async ({ params }) => {
const { slug } = params;
const preview = true;
const API_TOKEN = preview
? process.env.HYGRAPH_DEV_TOKEN
: process.env.HYGRAPH_PROD_TOKEN;
const data = await hygraph.request(
getArticleQuery,
{
url: slug,
},
{
authorization: `Bearer ${API_TOKEN}`,
},
);
return json(data);
};
type Article = {
title: string;
content: any;
};
type LoaderData = {
article: Article;
};
export default function Index() {
const { article } = useLoaderData<LoaderData>();
return (
<div style={{ fontFamily: 'system-ui, sans-serif', lineHeight: '1.4' }}>
<h1>{article.title}</h1>
</div>
);
}

The logic for this page is almost the same as the index route, but with a few differences.

  • On our GraphQL query, we define a $url variable, so we can filter the article based on it;

  • On the loader, to get the $slug value, we can use the params parameter, which is available in the loader function.

Rendering Rich Text content

The only thing missing on the ‘article page’ is rendering the Rich Text content. Luckily, that’s an easy task. Hygraph has a Rich Text Renderer library for React, which handles everything out of the box.

First, let’s install the package.

npm add @hygraph/rich-text-react-renderer

We also need to install the types.

npm add @hygraph/rich-text-types -D

Inside the article page, we need to update the Article type, and use the RichText component which was exported from the @hygraph/rich-text-react-renderer package.

// …
import { RichText } from '@hygraph/rich-text-react-renderer';
import type { ElementNode } from '@hygraph/rich-text-types';
// …
type Article = {
title: string;
content: {
json: {
children: ElementNode[];
};
};
};
// …
export default function Index() {
const { article } = useLoaderData<LoaderData>();
return (
<div style={{ fontFamily: 'system-ui, sans-serif', lineHeight: '1.4' }}>
<h1>{article.title}</h1>
<RichText content={article.content.json} />
</div>
);
}

If you open an article page, you’ll be able to see the content.

Article title Hygraph.png0

Are you lost? See the complete code until this part of the tutorial.

#Setting up Preview Mode

With the project ready, we can now start the preview mode logic. On both pages, we’re controlling the token based on a variable, which is set as true. However, what we really need to do is control preview mode based on a cookie. Lucily, that’s super easy to do with Remix. Before getting started, let’s understand how it will work.

With Hygraph, we can define Preview URLs for every schema. For our project, we only want to do that on the Article schema. The configuration for preview links looks like this:

remix preview urls hygraph

Let’s break it into parts. First, you’ll see that we have two preview links configurations, one for production, which we call “Preview”, and the other one for Development. The only difference between these two are the URL, but the structure will always be similar to this:

https://<your-url>/api/preview?secret=<secret>&slug=<slug>
  • <your-url> should be your application domain.

  • <secret> should be replaced with a secret token. Only your app and Hygraph will know this secret.

  • <slug> should be the path for the page that you want to preview. On Hygraph, you can use all the schema fields.

This route api/preview, will be responsible for checking if the secret matches with the one you have on your app, and it also needs to check if that slug exists in the CMS, otherwise the request should fail.

If the secret is valid and there’s a slug, we’ll set a cookie. Any requests containing this cookie on the article or index page will be considered preview mode. The token used on the API request changes, which allows us to see draft content on the app.

To get started, create a folder under routes named api and a file preview.ts inside it. This route is the one we also configured on the CMS.

// app/routes/api/preview.ts
import { gql } from 'graphql-request';
import { json, redirect } from '@remix-run/node';
import type { LoaderFunction } from '@remix-run/node';
import { hygraph } from '~/lib/hygraph.server';
const getArticleQuery = gql`
query Article($url: String!) {
article(where: { url: $url }) {
url
}
}
`;
export const loader: LoaderFunction = async ({ request }) => {
const requestUrl = new URL(request?.url);
const secret = requestUrl?.searchParams?.get('secret');
const slug = requestUrl?.searchParams?.get('slug');
// This secret should only be known to this API route and the CMS
if (secret !== process.env.PREVIEW_SECRET || !slug) {
return json({ message: 'Invalid token' }, { status: 401 });
}
// Check if the provided `slug` exists
const data = await hygraph.request(
getArticleQuery,
{
url: slug,
},
{
authorization: `Bearer ${process.env.HYGRAPH_DEV_TOKEN}`,
},
);
// If the slug doesn't exist prevent preview from being enabled
if (!data.article) {
return json({ message: 'Invalid slug' }, { status: 401 });
}
// Enable preview by setting a cookie
// …
};

In this file, we’re fetching the search params, checking if the secret matches an environment variable, and looking for the article on the CMS. If it exists, we need to set a cookie.

Remember to add the PREVIEW_SECRET on the .env file. You also need to restart your server.

Working with Cookies

To get started, create a file preview-mode.server.ts under a folder named utils. The first thing we need to do within this file is create a cookie and with Remix, that’s very easy.

// app/utils/preview-mode.server.ts
import { createCookie } from '@remix-run/node';
export const previewModeCookie = createCookie('stage', {
path: '/',
sameSite: process.env.NODE_ENV !== 'development' ? 'none' : 'lax',
secure: process.env.NODE_ENV !== 'development',
httpOnly: true,
secrets: [process.env.PREVIEW_SECRET as string],
});

The createCookie function exported from @remix-run/node package receives the cookie name as the first argument, and the cookie attributes as the second argument. You can find the complete list of cookie attributes on MDN.

As a plus, you could also define a maxAge attribute in seconds, which specifies the duration for the preview cookie to last for.

Continuing, create another file on the utils folder named parse-cookie.server.ts. This file will have only one responsibility: parsing a cookie received as an argument.

// app/utils/parse-cookie.server.ts
import type { Cookie } from '@remix-run/node';
export const parseCookie = async (request: Request, cookie: Cookie) => {
const cookieHeader = request.headers.get('Cookie');
const parsedCookie = (await cookie.parse(cookieHeader)) || {};
return parsedCookie;
};
For more information on how to work with cookies in Remix, check the docs.

Last but not least, on the api/preview.ts file, let’s add the logic for setting the cookie and redirecting to the article URL.

// app/routes/api/preview.ts
// …
export const loader: LoaderFunction = async ({ request }) => {
// …
// Enable preview by setting a cookie
const cookie = await parseCookie(request, previewModeCookie);
cookie.stage = 'draft';
return redirect(`/${data.article.url}`, {
headers: {
'Set-Cookie': await previewModeCookie.serialize(cookie),
},
});
};

Showing a Banner on Preview Mode

If you are in preview mode in the current state of the application, you won’t get any visual feedback on the application saying that you’re seeing draft content. That not ideal from a UX perspective. We also need to check on the loader function, on both pages (article and index), if we’re really in preview mode. Right now, we’re controlling that based on a variable, which is set as true.

First, let’s create a function to check if we are in preview mode. Go to the preview-mode.server.ts file, and add the following function:

// app/utils/preview-mode.server.ts
import { parseCookie } from '~/utils/parse-cookie.server';
// …
export async function isPreviewMode(request: Request) {
const cookie = await parseCookie(request, previewModeCookie);
return cookie?.stage === 'draft';
}

With the isPreviewMode function in place, we can now use it on the article and index page. The first thing we need to do is import the function, and use it on the loader. We are also going to return a isInPreview variable from the loader, so we can use it on the page to show a banner, which we will create later.

Let’s start by changing the article page.

// app/routes/$slug.tsx
// …
import { isPreviewMode } from '~/utils/preview-mode.server';
import { PreviewBanner } from '~/components/preview-banner';
// …
export const loader: LoaderFunction = async ({ params, request }) => {
const { slug } = params;
const preview = await isPreviewMode(request);
const API_TOKEN = preview
? process.env.HYGRAPH_DEV_TOKEN
: process.env.HYGRAPH_PROD_TOKEN;
const data = await hygraph.request(
getArticleQuery,
{
url: slug,
},
{
authorization: `Bearer ${API_TOKEN}`,
},
);
return json({
...data,
isInPreview: preview,
});
};
// …
type LoaderData = {
article: Article;
isInPreview: boolean;
};
export default function Index() {
const { article, isInPreview } = useLoaderData<LoaderData>();
return (
<div style={{ fontFamily: 'system-ui, sans-serif', lineHeight: '1.4' }}>
{isInPreview && <PreviewBanner />}
<h1>{article.title}</h1>
<RichText content={article.content.json} />
</div>
);
}

And now, the index page.

// app/routes/index.tsx
// …
import { isPreviewMode } from '~/utils/preview-mode.server';
import { PreviewBanner } from '~/components/preview-banner';
// …
export const loader: LoaderFunction = async ({ request }) => {
const preview = await isPreviewMode(request);
const API_TOKEN = preview
? process.env.HYGRAPH_DEV_TOKEN
: process.env.HYGRAPH_PROD_TOKEN;
const data = await hygraph.request(
allArticlesQuery,
{},
{
authorization: `Bearer ${API_TOKEN}`,
},
);
return json({
...data,
isInPreview: preview,
});
};
// …
type LoaderData = {
articles: Article[];
isInPreview: boolean;
};
export default function Index() {
const { articles, isInPreview } = useLoaderData<LoaderData>();
return (
<div style={{ fontFamily: 'system-ui, sans-serif', lineHeight: '1.4' }}>
{isInPreview && <PreviewBanner />}
<h1>Welcome to Remix</h1>
<ul>
{articles.map((article) => (
<li key={article.id}>
<a href={`/${article.url}`}>{article.title}</a>
</li>
))}
</ul>
</div>
);
}

With the pages updated, let’s create the PreviewBanner component.

// app/components/preview-banner.tsx
export function PreviewBanner() {
return (
<p style={{ fontWeight: 'bold' }}>
You&apos;re in <strong>preview mode</strong> (Content served from DRAFT)
&mdash;&nbsp;
<form action="/api/exit-preview" method="post">
<button type="submit">
Exit Preview Mode <span aria-hidden="true">&rarr;</span>
</button>
</form>
</p>
);
}

If you try the preview mode, you should see the banner on the page.

Preview Mode UX Hygraph

But you might be wondering, why are we using a <form> to exit preview mode, and not a link to an API route, like on Next.js?

Remix has a different approach when compared to Next.js, especially when we are talking about layouts and routes. When we have a parent route on our application, like a layout route, with a header where all the items are requested from the CMS, Remix will only load the data for the parts of the page that are changing on navigation.

If you click a link in the header Remix knows that the parent route will remain on the page but the child route's data will change because the url param for the document will change. With this insight, Remix will not refetch the parent route's data. To force a full reload, we can use an action, and that’s exactly what we are doing here.

This application doesn’t have any parent routes. However, if we take the docs starter for example, all the sidebar entries are coming from the CMS. When you enter preview mode, the items from the navigation will also update and show draft changes. If we use a link to exit, the parent route won’t change, and the navigation will still show the draft content until you reload. If you want to dive deeper, I recommend taking a look into this guide.

Exit Preview Mode

The last thing we need to do on our app is implementing the exit preview API route. This is very simple since the only thing it needs to do is change the cookie value and redirect to the homepage.

import { redirect } from '@remix-run/node';
import type { ActionFunction, LoaderFunction } from '@remix-run/node';
import { parseCookie } from '~/utils/parse-cookie.server';
import { previewModeCookie } from '~/utils/preview-mode.server';
export const loader: LoaderFunction = async () => {
return redirect('/');
};
export const action: ActionFunction = async ({ request }) => {
const cookie = await parseCookie(request, previewModeCookie);
cookie.stage = 'published';
return redirect(`/`, {
headers: {
'Set-Cookie': await previewModeCookie.serialize(cookie),
},
});
};

On the route, we also need to make sure to have a loader function, which redirects to the homepage, in case someone tries to access the route directly.

See all the changes we did on this preview mode section here.

I hope this post has helped you getting started with Remix and Hygraph, and setting up preview mode. The source code is available on GitHub. If you have any questions, make sure to post them to our community on Slack!

Blog Author

João Pedro Schmitz

João Pedro Schmitz

Front-End Engineer

Front-End Engineer in love with React. Learning every day.

Share with others

Sign up for our newsletter!

Be the first to know about releases and industry news and insights.