gg logo

gg

Documentation

Changelog

Blog

Pages

The first page for our gg application came out of the box when we ran gg init. Now let's dive deeper into how pages work.

Creating pagesCopy link to section

Each .tsx-file inside the src-directory must have a named export called page. We can create the actual page using the equally named method from the gg module and passing it a component. (We'll go more into details around components later.)

export const page = gg.page(PageComponent);

Components used for pages have a specific constraint, they must return exactly one child which has to be an html element. When compiling the page, gg will create a tree of all component and their elements, and stringify it into HTML.

Using external dataCopy link to section

gg allows you to use any kind of external data to create the content of your pages. You can use any source that you can access from Deno:

  • Local files
  • Remote files (by using the build-in fetch-API)
  • A headless CMS
  • Other Databases

To pass any kind of external data to a page, we need to create a function named getData and add it to the gg.page method.

1import { gg } from "../deps.ts"; 2 3const getData = async function() { 4 // Fetch and return the external data 5} 6 7const PageComponent = gg.component(() => ( 8 <html> 9 <body> 10 <p>Hello from gg!</p> 11 </body> 12 </html> 13)); 14 15export const page = gg.page(PageComponent, { getData });

There are no constraints about the structure of the data. However, it is recommended to specify strict types to increase the confidence that the code works as expected. gg also exports a type called GetData for this purpose. We can re-export it in our deps.ts file.

1export { default as gg } from "https://gg.thomasheyenbrock.com/code/<gg-token>/gg@0.1.0/mod.ts";+2export type { GetData } from "https://gg.thomasheyenbrock.com/code/<gg-token>/gg@0.1.0/mod.ts";

The type requires one generic argument, which should be the data that you intend to return from the function. Note that we don't return the data itself from the function. Instead, we return an object with a property data that contains the actual data.

-import { gg } from "../deps.ts";+1import { gg, GetData } from "../deps.ts";+2 +3type PageData = { title: string };+4 -const getData = async function() {+5const getData: GetData<PageData> = async function() { 6 // Fetch and return the external data+7 return { data: { title: "My gg website" } }; 8}

Now gg will pass the data to the function which we used to create the page component. More specifically, it will call the function with an object that contains a property named data. The value will be the return value of the getData function.

-const PageComponent = gg.component(() => (+9const PageComponent = gg.component((args) => ( 10 <html>+11 <head>+12 <title>{args.data.title}</title>+13 </head> 14 <body>

We also need strict typing here. TypeScript can't infer the type of the args argument and won't accept any implicit any typing. We can specify the type of args as a generic type argument of the gg.component function.

-const PageComponent = gg.component((args) => (+9const PageComponent = gg.component<{ data: PageData }>((args) => (

TypeScript will still be unhappy because there's something missing.

Cache headersCopy link to section

When a page uses external data, it won't be statically generated at build time. Instead, we keep the page as is and compile it at run-time.

This alone would negatively affect the performance of our website, expecially if aggregating the data in the getData is very time expensive. That is why gg adds HTTP cache-control headers to the response, thus telling the network layer (i.e. your CDN) and optionally the user agent (i.e. the users' browser) how to cache the site.

Configuring the caching is up to the developer. When creating a page together with a getData function, we also have to pass a property called headers, which is an object containing the following:

  • maxAgeNetworkLayer (required): The number of seconds that your CDN may cache the page (i.e. s-maxage).
  • maxAgeBrowser (optional): The number of seconds that a users' browser may cache the page (i.e. max-age).
  • staleWhileRevalidate (optional): The number of seconds that you want to allow showing stale (i.e. "outdated") content (i.e. stale-while-revalidate).

You can read more about how the cache-control HTTP header functions here.

Choosing values for those header directives essentially comes down to how often you expect the data for your page to change, and how fast you want those updates to be shipped to your users. There's a great video by Ryan Florence that goes in-depth about this topic.

Here are some essential examples:

  • When setting maxAgeNetworkLayer = 1, your CDN will revalidate the page every second, i.e. the content served will always be up-to-date. But the chance for any user having to wait longer is also higher.
  • When additionally setting staleWhileRevalidate = 10 you prevent long loading times for users. The CDN will continue serving the (possibly) outdated content for 10 seconds after starting the revalidation. After the revalidation is done, the up-to-date content is served again.
  • When setting maxAgeNetworkLayer = 86400, your CDN will revalidate the page at most once per day. This is fine if your content does not change very frequently and if you don't need to immediately ship the updates.
  • When setting maxAgeBrowser = 86400, also the user browser will cache the page for 24 hours. Until this time is passed, the browser won't even do a request to the server anymore. Be careful with using high values for this option, as you have no option to ever purge the browser caches of your users.
  • When setting maxAgeNetworkLayer = 31536000, your CDN will revalidate the page at most once every year. This makes sense for pages that are considered immutable, i.e. where you know upfront that the content will never change after initially shipping the page.

You can add the header values to a page like so.

-export const page = gg.page(PageComponent, { getData });+20export const page = gg.page(PageComponent, {+21 getData,+22 headers: { maxAgeNetworkLayer: 60, staleWhileRevalidate: 10 },+23});

Changing the external dataCopy link to section

What happens when the external data changes that we use to create our page? There are two answers, one for what happens during development and the other for what happens in production.

During development, the development server will stringify the data returned from getData. It then periodically reruns the getData function, comparing the stringified result to the previous one. If something in the data changes, the development server automatically re-compiled the page and reloads the browser window.

Note that this means that the data returned from getData must be JSON-stringifyable. Properties that are not stringifyable (like e.g. functions) are not compared between function executions.

When running your application in production, gg compiles the page on every request, also executing the getData function every time. That means your page will automatically be up-to-date with the latest external data that it uses. How fast this will happen, depends on the cache-control header you defined for the particular page.

Dynamic pagesCopy link to section

There are common use-cases where the content of a generic page depends on some identifiers. The generic page for a blog post depends on the name of the file where you wrote the blog. The generic page for a product on an E-commerce website depends on the id of the product stored in the CMS.

For these use-cases, gg offers dynamic pages. We call the identifiers that a dynamic page depends on params. Those params will be part of the URL. We can reflect the resulting paths in our filesystem by creating files or folders whose names are enclosed in square brackets. The text inside the brackets defines the name for the param.

Let's see some examples:

  • The file src/product/[productId].tsx creates a dynamic page that will be matched by the path /product/42. The param productId equals "42".
  • The file src/post/[name]/index.tsx creates dynamic page that will be matched by the path /post/my-blog-post. The param name equals "my-blog-post".
  • The file src/[userId]/[postId].tsx creates dynamic page that will be matched by the path /123/456. The param userId equals "123" and the param postId equals "456".

Note that you can only have at most one file or folder representing a param per directory. Otherwise, one path could match multiple pages.

However, it is perfectly valid to create a file or folder representing a param next to other files or folders that don't. Note that gg will always match the more specific path.

Take the scenario where you define two pages src/product/ski.tsx and src/product/[productName].tsx. In that case, the following happens:

  • The path /product/snowboard will obviously match the src/product/[productName].tsx page, as snowboard does not equal ski.
  • The path /product/ski matches both pages. gg will use the page src/product/ski.tsx, as it more specifically (in this case even exactly) matches the given path.

For dynamic pages, the values of the current params will be passed to the getData function. More specifically, the getData function is called with an object that contains a property called params. In this object you will find all parameters and their values.

1// src/product/[productId].tsx 2import { gg, GetData } from "../../deps.tsx" 3 4const getData: GetData<any> = async function({ params }) { 5 const product = await fetch( 6 `https://some.headless.cms/products/${params.productId}`, 7 ); 8 return { data: await product.json() }; 9} 10 11const PageComponent = gg.component<{ data: any }>((args) => ( 12 <html> 13 <body> 14 <p>{args.product.name}</p> 15 </body> 16 </html> 17));

Non existing pagesCopy link to section

You might also want to handle the case where a user tries to visit a page on your website that does not exist. In gg you can add a special page src/404.tsx that will be shown in such cases.

Note that this page must not use any external data. If you try to add a getData function to this page, gg will throw an error.

There are two cases of how a user could end up on this page:

  • The requested path matches no page in your application.
  • The requested path matches a dynamic page in your application, but there does not exist any content related to the given params.

The second case needs to be handled by the developer. We can instruct gg to show the 404-page by returning { doesNotExist: true } from the getData function of the dynamic page. In the example from above, this could be done like so.

5 const product = await fetch( 6 `https://some.headless.cms/products/${params.productId}`, 7 );+8 if (!product.ok) {+9 return { doesNotExist: true };+10 } 11 return { data: await product.json() };

Note that this is not required. It's also perfectly fine to handle this case within the page itself by e.g. returning empty data and showing different markup in the page component depending on whether the data exists or not.

Preloading pagesCopy link to section

It's a common use-case to preload other pages when on a certain page, in particular, if it's deemed as likely that the user might want to visit that site next.

gg also supports this use-case. If there's a link on your page that you want to preload, you just need to add a data-attribute called data-p to the anchor element.

1const regularLink = <a href="/buy">Buy now!</a>; 2const preloadedLink = <a href="/buy" data-p="">Buy now!</a>;

Note that gg will not preload the page immediately. To ensure the fastest possible loading time, it waits until the browser is idle (using requestIdleCallback) and until the link is actually visible (using the IntersectionObserver-API). Furthermore, preloading does not happen when the network connection is poor or if the user enabled "save data"-mode.

Credits to quicklick, which was the basis for the implementation of preloading in gg.


By now, you probably noticed the "special" syntax that we used inside the component functions. It is called JSX and we'll talk about it in the next guide.


Next → JSX