Search titles and sections of every page. ↑↓ to move, ↵ to open

Tutorial

2. Your first page

1 min read Edit on GitHub

Shelf's home page lists the saved links. Before there's a database, start with a few links in Go, to see how data gets from the server to the page.

#The data

The links need a type the whole app can use, so give them a package of their own. Create internal/shelf/shelf.go:

// Package shelf keeps the links.
package shelf

import "time"

// Link is a page saved to read later.
type Link struct {
	ID      int64     `json:"id"`
	URL     string    `json:"url"`
	Title   string    `json:"title"`
	Read    bool      `json:"read"`
	AddedAt time.Time `json:"addedAt"`
}

The json tags name the fields as the page sees them.

#The load

Replace src/routes/page.server.go with a Load that returns links:

package routes

import (
	"time"

	"github.com/Unfield/Tern/kit"

	"shelf/internal/shelf"
)

var Access = kit.Public

type Data struct {
	Links []shelf.Link `json:"links"`
}

func Load(ctx *kit.Ctx) (Data, error) {
	return Data{Links: []shelf.Link{
		{ID: 2, URL: "https://svelte.dev/blog/runes", Title: "Introducing runes", AddedAt: time.Now()},
		{ID: 1, URL: "https://go.dev/blog/go1.26", Title: "Go 1.26 is released", Read: true, AddedAt: time.Now()},
	}}, nil
}

Load runs on the server for every request to the page, and again when the browser navigates to it. Whatever it returns becomes the page's data.

#The page

Replace src/routes/+page.svelte:

<script lang="ts">
	import type { PageProps } from './$types';

	let { data }: PageProps = $props();
</script>

<svelte:head>
	<title>Shelf</title>
</svelte:head>

<h1>Your shelf</h1>

{#if data.links.length === 0}
	<p class="empty">Nothing saved yet.</p>
{:else}
	<ul class="links">
		{#each data.links as link (link.id)}
			<li class:read={link.read}>
				<a href={link.url}>{link.title}</a>
				<span class="host">{new URL(link.url).host}</span>
			</li>
		{/each}
	</ul>
{/if}

<style>
	h1 {
		margin: 0 0 1.5rem;
		font-size: 1.75rem;
	}
	.empty {
		color: var(--tern-muted);
	}
	.links {
		margin: 0;
		padding: 0;
		list-style: none;
	}
	.links li {
		display: flex;
		align-items: baseline;
		gap: 0.75rem;
		padding: 0.75rem 0;
		border-bottom: 1px solid var(--tern-border);
	}
	.links a {
		color: var(--tern-fg);
		font-weight: 500;
		text-decoration: none;
	}
	.links a:hover {
		text-decoration: underline;
	}
	.read a {
		color: var(--tern-muted);
	}
	.host {
		color: var(--tern-muted);
		font-size: 0.875rem;
	}
</style>

The two links appear, the one already read in a lighter color. The --tern-* colors come from Tern UI's theme, so the page follows the system's light or dark mode.

The layout still links to the About page you deleted. In src/routes/+layout.svelte, remove it from the links list, leaving Home; step 7 gives the layout a new header.

#Types from Go

Hover data.links in your editor: it's a Link[], with id: number, url: string, read: boolean and addedAt: string. Tern generated that type from the Go struct, through the ./$types import. Rename a field in Go, and npm run check reports every place in your Svelte code that still uses the old name. time.Time arrives as a string, as JSON sends it, so the page turns it into a date with new Date(link.addedAt).

The page was rendered on the server, in Go: view the page's source and the links are in the HTML. Then the browser takes over, and later navigations only fetch the data.

Next: A database.