GioJSdocs
On this page

Project Structure

A tour of the files and folders in a GioJS app.

A new project is intentionally small. Everything is driven by file conventions under app/.

text
my-app/
  app/
    layout.tsx           # root layout: imports ./globals.css, exports metadata
    globals.css          # global styles (the CSS pipeline bundles and minifies them)
    (site)/              # route group: no URL segment
      layout.tsx         # the site's navigation and footer - hydrated, so its links soft-navigate
      page.tsx           # the / route
      about/page.tsx     # the /about route
      posts/[id]/page.tsx  # dynamic route -> /posts/:id
    not-found.tsx        # 404 page
    error.tsx            # error page
  components/            # your shared components (layout/SiteShell, layout/Navbar, layout/Footer)
  public/                # static files, served at the site root and under /public/
    fonts/               # the self-hosted .woff2 files gio.toml's [[fonts]] name
  gio.toml               # server configuration (fonts, images, security, ...)
  .env.example           # the environment variables the app reads - copy to .env.local
  .gitignore             # node_modules/, .gio/, .env*.local, build output
  AGENTS.md              # how the framework works, for coding agents
  package.json           # dev / build / start scripts
  tsconfig.json          # includes .gio/routes.d.ts for typed routes
  .gio/                  # generated at startup (route types, client build, caches) - not committed

The app directory

Routes are folders. A page.tsx (or .jsx) makes a folder a route; a layout.tsx wraps the pages beneath it. Dynamic segments use [brackets] ([...slug] and [[...slug]] for catch-alls), (group) folders organize routes without adding a URL segment, and _private folders are never routable. Any folder can also hold not-found, error and loading files for its part of the tree - see Layouts & Pages and File Conventions.

The root layout

app/layout.tsx renders the <html> document every page shares. It imports the global stylesheet and exports the site's default metadata - a title template that each page's own metadata fills in:

app/layout.tsx
import type { LayoutProps, Metadata } from '@gio.js/core';
import './globals.css';

export const metadata: Metadata = {
  title: { default: 'My App', template: '%s | My App' },
  description: 'A GioJS application.',
};

export default function RootLayout({ children }: LayoutProps) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
      </head>
      <body>{children}</body>
    </html>
  );
}

The root layout is server-rendered HTML that never hydrates: put interactive components and context providers in a nested layout or the pages. That is why the starter's navigation lives in app/(site)/layout.tsx: a GioLink there prefetches and soft-navigates, while in the root layout it would be a plain link. The 404 and error pages sit outside the group and render the same SiteShell themselves. CSS can be imported from any page, layout or component, including CSS Modules - see CSS & Styling.

gio.toml

Server configuration, read once at startup. Every key is optional and an unknown key stops the server with a hint, so typos never go unnoticed. The starter self-hosts its fonts through [[fonts]]: the .woff2 files ship in public/fonts/, are copied into .gio/fonts/ at every start (no network needed) and are served from /_gio/fonts with preload links, instead of loading from a third-party CDN on every visit:

toml
#:schema ./node_modules/@gio.js/server/gio.schema.json
[app]
name = "my-app"

[server]
host = "0.0.0.0"
port = 3000   # the PORT or GIO_PORT env var overrides it
http2 = true

[images]
allowed_widths = [640, 828, 1080, 1200, 1920]
quality = 80

[[fonts]]
family = "Fraunces"
url = "/public/fonts/fraunces-400-normal.woff2"

[[fonts]]
family = "Fraunces"
url = "/public/fonts/fraunces-400-italic.woff2"
style = "italic"

[[fonts]]
family = "JetBrains Mono"
url = "/public/fonts/jetbrains-mono-400-normal.woff2"

[[fonts]]
family = "JetBrains Mono"
url = "/public/fonts/jetbrains-mono-600-normal.woff2"
weight = 600

The #:schema line gives editors autocomplete and inline docs. A font url can also be an https:// address, downloaded on the first start - see Font Optimization and gio.toml Configuration.

.env.example and .gitignore

.env.example lists the variables the app reads, with comments - copy it to .env.local for your own values. .env*.local files and .gio/ are git-ignored. (The scaffolder's package carries the file as _gitignore, because npm drops files named .gitignore from published packages, and renames it when it creates your project.) See Environment Variables.

public/

Files in public/ are served directly by the Rust layer - images, stylesheets, fonts. Static files never touch Node. Files answer at the site root as well as under /public/*: public/robots.txt is both /robots.txt and /public/robots.txt, so favicon.ico, manifest.json, apple-touch-icon.png, and .well-known/ files land where browsers and crawlers look for them. gio export writes public/ to both places in out/ too.

  • A public file wins over a page with the same path (the Next.js precedence). In a static export, where a public file and a rendered page would need the same output file (public/index.html and app/page.tsx), the page is kept and the export lists the file as skipped.
  • Not served at the root: dotfiles (except under .well-known/), symlinks, and a top-level public/_gio/ (the server's internal namespace). These stay reachable under /public/* only. Directory listings are never served.
  • Guards, header rules, and [[rate_limits]] written for a file's /public/... URL also apply at its root URL, so protecting /public/members/* protects /members/* too, and /_gio/image serves the file only to visitors those guards admit. Redirects and rewrites match only the URL requested - see Middleware.
  • Root-served files use Cache-Control: public, max-age=0, must-revalidate with Last-Modified, so browsers revalidate instead of keeping an old copy after a deploy.
  • The set of root-served files is indexed at startup, so the request path never pays a filesystem lookup. In development, edits to public/ refresh the index; in production, files added after startup need a restart.

Optional files