Tutorial

Adding a Page

Let's add a page together.

We'll write the Registration page from scratch. Follow along in your editor and you'll finish with a page that feels like it belongs.

You need to know HTML and CSS. Svelte is mostly ordinary HTML, and the few places it isn't are pointed out as they come up.

The shape that follows is the one the rest of the site uses, not a hard rule. Lay your page out in whatever way best suits its content.

Create the file

The directory path is the URL.

First, where should the page be located? The site has four main sections:

Program
Talks, tutorials, the schedule, and the call for proposals.
Attend
Getting to the conference and into it: venue, travel, hotels.
Sponsor
For the companies who fund the conference.
About
The conference itself: who runs it, and the policies it runs by.

Let's put the registration page at /attend/registration. To do so, we'll create the file:


    site/(main)/attend/registration/+page.svelte
  

A +page.svelte file becomes the page at the URL formed by the directories above it.

A page under attend/ inherits that section's navigation. To retrieve it, open the file and start with a <script> block:

<script>
  import { getContext } from '$lib/layout';

  const { BreadcrumbNav, SectionNav } = getContext();
</script>

Title the page

What the browser and search engines read.

Under the script block, add:

<svelte:head>
  <title>Registration — PGConf.dev 2027</title>
  <meta
    name="description"
    content="Registration types, prices, and taxes for PGConf.dev 2027 in Montréal, plus the refund deadline and complimentary registrations for speakers."
  />
</svelte:head>

<svelte:head>'s contents are rendered in the document's <head>. Give every page a title ending in — PGConf.dev 2027 and a description, which is the text that shows up in search results and link previews.

Aim for a description ~140 characters long. It should be a sentence describing what's on the page, not a list of keywords. Make it specific enough that it couldn't be pasted onto another page unchanged.

Write the header

Breadcrumbs, label, title, and opening sentence.

Below the head block, add the breadcrumb trail:

<BreadcrumbNav>
  <li>Registration</li>
</BreadcrumbNav>

Then, add the page's header:

<header class="(main)">
  ...
</header>

(main) is a placement class: it spans the header across the full width of the page, margins included.

Within the header, add the page's heading and lede paragraph:

<hgroup>
  <p class="action">
    Attend · Registration
  </p>
  <h1>Register for PGConf.dev 2027.</h1>
</hgroup>

<p class="lede mute text">
  Three registration types, one price for the whole four days.
</p>

A paragraph before the heading labels it and one after summarizes it. See Text › Headings for more information.

The other three classes are text utilities:

.lede (or .lead)
Marks the paragraph as a lede, i.e. the sentence or two that introduce what follows. It's set a step larger than body text to match that role.
.mute
Softens the text color, so the paragraph supports the heading above it rather than competing with it.
.text
Holds the paragraph to a readable line length. A (main) block spans the full width of the page, which is far wider than comfortable prose.

Save and load /attend/registration. We have a page!

Wire up section navigation

The list of pages beside this one.

Next, let's make the page feel like part of its section. Below the header, add:

<hr class="section" />

<SectionNav />

<hr class="section"> is a horizontal rule with section-sized spacing around it.

The nav won't list the new page yet, though. To fix this, open site/(main)/attend/SectionNav.svelte and add a list item:

<li>
  <Link href={resolve("/attend/registration")}>Registration</Link>
</li>

Two helpers appear there, and we'll eventually want both on our own page:

<Link>
An <a> with a couple of extras: it marks itself as the current page when it points at one, and opens in a new tab when it points off-site. Use it for every link you write.
resolve()
Builds the path to another page on the site. Give it the full path from the root, e.g. resolve("/attend/travel#entry-requirements").

Add a table of contents

Your first block in the margin.

Below the section navigation, let's list the sections we're about to write:

<nav class=") note" aria-labelledby="on-this-page">
  <h2 id="on-this-page" class="over">
    On this page
  </h2>

  <ol>
    <li>
      <a class="li-number" href="#registration-types">
        Registration types
      </a>
    </li>
    <li>
      <a class="li-number" href="#prices-and-taxes">
        Prices &amp; taxes
      </a>
    </li>
  </ol>
</nav>

) is a placement class that puts the nav in the right margin, where every page on the site keeps its table of contents.

.note
Marks the nav as supporting material: a size smaller and in a muted tone.
.li-number
Numbers each item, so the list matches the numbered sections it points at.
.over
Sets the heading as an eyebrow (or overline): the small label that sits over the thing it introduces.

Use the margin

Not everything belongs in the flow of the page.

The margins aren't only for navigation. A returning visitor wants the prices without reading the page, so let's add a summary aside:

<aside class="main ) note border area" aria-labelledby="at-a-glance">
  ...
</aside>

Two placements this time. ) puts the box in the right margin, and main drops it into the main column when the window is too narrow for a margin, so it's on the page at every width.

.area
Fills the block with a tinted surface, so it reads as a unit set apart from the page.
.border
Outlines the block on all sides, the unfilled counterpart to .area.

Within the aside, write:

<h2 id="at-a-glance" class="over">At a glance</h2>

<hr />

<dl class="dedent">
  <div class="p">
    <dt class="over">Corporate</dt>
    <dd>CAD $550 · $632.36 with tax</dd>
  </div>

  <div class="p">
    <dt class="over">Student</dt>
    <dd>CAD $75 · $86.23 with tax</dd>
  </div>
</dl>

A <dl> is the right element for label-and-value pairs.

.p
Gives each pair paragraph spacing, so the pairs read as separate entries rather than one run of text.
.dedent
Drops the browser's default indent on definition lists.

Write the sections

One <section> per idea, each with an id.

Now the content: a flat run of <section> elements, one per idea, in the order the table of contents promised. Here's the second of the four:

<section id="prices-and-taxes" class="main">
  ...
</section>

main keeps the section in the centre column. Give each one an id that matches its heading, so the link a reader guesses is the link that works.

Within the section, write the heading and the copy:

<hgroup>
  <p class="section-number">
    Prices &amp; taxes
  </p>
  <h2>What you actually pay.</h2>
</hgroup>

<p class="lede">
  Prices are in Canadian dollars.
</p>

<p>
  Quebec charges two sales taxes, totalling <strong>14.975%</strong>.
</p>

.section-number numbers the section for you. See Layout › Counters for more information.

Add a table

Rows, columns, and a caption.

The prices are tabular data, so let's put them in a table:

<table class="stripe">
  <caption>
    Registration prices for PGConf.dev 2027
  </caption>
  <thead>
    <tr>
      <th>Type</th>
      <th>Price</th>
      <th>With tax</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Corporate</td>
      <td class="mono">$550</td>
      <td class="mono">$632.36</td>
    </tr>
  </tbody>
</table>
.stripe
Fills alternating rows, to make a longer table easier to scan.
.mono
Sets the figures in the monospace face, so the columns line up.

See Appearance › Tables for more information.

Add a callout

Set one block apart with color.

Let's close the page with somewhere to go for questions. This is where the two helpers from earlier come in, so first add their imports to the script block at the top of the file:

<script>
  import { resolve } from '$app/paths';
  import Link from '$lib/Link';
</script>

Then, let's add the callout:

<section class="main">
  <div class="action-area edge">
    <h3 class="h6">
      Questions about registration
    </h3>

    <p>
      If you need an invitation letter for a visa, see
      <Link href={resolve("/attend/travel#entry-requirements")}>
        entry requirements
      </Link>.
    </p>
  </div>
</section>
.action-area
Fills the block in the site's action color.
.edge
Runs a rule down the block's left side.
.h6
Gives the heading the size of an <h6> without changing what it is. Pick the heading level the document outline calls for, then size it with .h1 through .h6 if you need to.

See Appearance › Areas for more information.

Check your work

Before you open a pull request.

Resize the window with the page open. The margins appear at 48rem and 64rem, so anything you put in one deserves a look at all three widths.

Then, check that:

  • The <title> ends with — PGConf.dev 2027, and the meta description is a sentence of roughly 140 characters.
  • The page is listed in its section's SectionNav.svelte.
  • Every section has an id that matches its heading, and the table of contents links to each one.
  • Internal links go through resolve() with a full path.
  • npm run lint and npm run check pass, and npm run format has been run.

The finished page is at /attend/registration. Read its source beside any other page in site/(main)/; copying a neighbour is how the site is maintained.