This post series is indexed at NgateSystems.com. You'll find a super-useful keyword search facility there too.
Last reviewed: Nov '24
1. Introduction
The design of a complex webapp raises some tricky issues for its developer. Complexity must somehow be marshalled into patterns that are comprehensible to the webapp's users. Part of the answer will be the creation of consistent patterns, but the developer may then struggle to ensure that consistency is maintained within these patterns. This post is all about the arrangements that Sveltekit has provided to make a webapp developer's task easier.
- Routes - making your webapp comprehensible
The simple "Inventory Maintenance" webapp created in the last section of this series has a silly design - a single address displays a list of current products and adds a button that launches a popup to add new ones. This is both confusing and restrictive. In the real world, a system like this would need to serve two entirely different groups of users - readers on the one hand and maintainers on the other. It would be much better if each of these two bits of functionality could be addressed separately as in, say:
- products display page: http://localhost:5173/products-display
- products maintenance page: http://localhost:5173/products-maintenance
In the webapp design world, addresses like these are referred to as routes (they represent the "route" which users follow to reach content). The content displayed by following a route is referred to as a page
Webapp users love routes. Once each functional component of webapp acquires an independent existence it becomes much easier to visualise the structure and purpose of the information system. Route designs dovetail nicely with the browser's "breadcrumb-trail" arrangements that enable you to navigate up and down the branches of a webapp's page hierarchy. Also, if a page is frequently accessed, users can save its route as a bookmark, enabling them to return to it easily by clicking the saved link. Designers, too, like this arrangement because it creates a clear structure wherein, for example, they can deliver security arrangements that leave some application areas publicly available whereas others are protected by login.
As you'll see in a moment, Svelte fixes this problem by baking the webapp route structure into the webapp project's file structure.
- Layouts - delivering visual consistency
Users also find it helpful if families of related pages employ common elements such as a standard menu bar and page trailer. This gives the page family a consistent appearance and makes it easier for users to navigate the webapp. But in the past, developers have struggled to keep these common elements in agreement.
Svelte fixes this problem by arranging a mechanism to source common elements from central locations. In Svelte, these are called "layouts".
- Components - delivering code consistency
Finally, developers grappling with the challenge of maintaining consistency and clarity within the codebase of a large project will feel happier if bits of it that need to operate identically are forced to do so because they use a shared component. Svelte is from top to bottom, a component-based architecture.
2. Routing in Svelte
SvelteKit uses your project's filing structure to define the required page structure. If, for example, you want a mywebapp/products-display
page, you need to create a project folder called src/routes/products-display
with a +page.svelte
file inside it. Once this arrangement is in place, your webapp delivers a separate physical page for each route.
Here's a sample source folder structure for a SvelteKit project. To make things a little more realistic here I've added a "product-details" sub-folder to the "products-display" folder. The purpose of this is to deliver more comprehensive information on a product listed by the "products-display" route:
Hang on though. Isn't this going to be a bit confusing? Lots of files all called +page.svelte? Yes - at least at first sight. But it's not so difficult to discipline yourself to check for the owner of a +page.svelte
before you dive in and start editing. And once you've developed a SvelteKit project or two you'll begin to appreciate the value of the convention in declaring the purpose of a file. As you'll see later, there are quite a few variations, and these give the designer total control over the webapp's structure with consequent benefits for both its responsiveness and its maintainability.
Try out these folder and file-naming arrangements in your current svelte-dev
project by creating new products-display
and products-display/product-details
folders containing the following +page.svelte
files:
// products-display/+page.svelte - remove before running
<div style="text-align: center;">
<a href="/products-display/product-details">View Product Details for Product 1</a>
</div>
// products-display/product-details/+page.svelte - remove before running
<div style="text-align: center;">
<span>Here are the Product Details for Product 1</span>
</div>
Now run your dev server, start the webapp and change the root address to localhost:5173/products-display
(making allowance for any variation in port number, of course). It should look like this:
Now, what happens when you click the link? Unsurprisingly, the display changes to this:
Note that the page address has also changed. The link has transferred you to a new address - localhost:5173/products-display/product-details
. Try using the browser's navigation buttons to return to the original page. Yes, everything works exactly as you would expect. Each of these +page.svelte
files has created a navigation route based on its position in the development project hierarchy. You can also bookmark these. All you've had to do to achieve this is to submit yourself to Svelte's file naming conventions. I'd say that's a price well worth paying!
In the real world, the "Products display -> Product details" arrangement would need to be dynamic. Currently, there's nothing in the product-details/+page.svelte
file to enable it to respond to a request for details on a particular product. In practice, the <a>
links on the products
page would carry information to enable them to specify which particular product they were interested in, and the product-details
page would react accordingly. I'll describe how Svelte would handle this when I cover "components" later in this post.
3. Layouts in Svelte
The "home page" on a company's website is usually an elaborate showcase featuring some sort of header banner alongside a logo and contact details. Customers would probably find it useful to see some of this information on subordinate pages too.
Although it would be perfectly possible to duplicate these details, this would be very undesirable from a maintenance point of view.
Svelte's "layout" facility provides a neat solution to the problem.
Create a +layout.svelte
file in the products-display
folder containing the following code:
// products-display/+layout.svelte - remove before running
<header>
<h3 style="display:flex; justify-content:space-between">
<a href="/about">About</a>
<span>Magical Products Company</span>
<a href="/inventory_search">Search</a>
</h3>
</header>
<slot></slot>
<trailer>
<div style="text-align: center; margin: 3rem; font-weight: bold; ">
<span>© 2024 Magical Products Company</span>
</div>
</trailer>
Now check out the products-display
and products-display/product-details
pages in your browser. Magic! You'll find that the header and trailer layouts have now been wrapped automatically around both pages. The localhost:5173/products-display
page, for example, should now look like:
The <slot></slot>
element within the +layout.svelte
file defines where the content of subject pages will be inserted. See +layout in Svelte docs for more information about the arrangement. When a layout is positioned at the top of a tree of pages, it is applied to all subordinate pages.
Svelte layouts are especially handy for building a "nav" (navigation) bar. This is the array of links or buttons used in many applications to provide a standard, centralized menu for accessing site pages. It is usually positioned at the head of the display, but you could just as easily style the nav bar to appear at the side.
4. Components in Svelte
While the standard "header/footer" layout arrangement is useful, there are many other situations where you might want to introduce a standard piece of HTML into the body of a page. Repeating yourself is never a good idea in coding and the "DRY" (don't repeat yourself) principle applies just as much to HTML as to Javascript.
A Svelte component is a self-contained unit of code that combines HTML, CSS, and JavaScript to create a piece of the user interface (UI). Typically, you would store this in a src/lib/MyComponent.svelte
file. Here "MyComponent" would be some appropriate meaningful expression of the component's purpose.
Note the capitalisation of the first character of the component's filename and the absence of hyphenation. This isn't mandatory, but is a useful naming convention that makes them stand out as programmatic elements when referenced in a parent page's template section.
Just as with a Javascript function, a Svelte component will usually need to be provided with parameters to enable it to achieve its purpose so, for example, a UserProfile.svelte
might be referenced in the HTML body section of a +page.svelte
file with a statement like:
<UserProfile userId={userId} />
Here, the userId
parameter will have been declared by the +page.svelte
file with a let userId
statement and seeded with a value. The component will also have been imported into +page.svelte
, in a <script>
section that looks like this:
<script>
import { UserProfile} from "$lib/UserProfile.svelte";
let userId;
<script/>
Note the "$" shortcut used in the import declaration. Svelte works out the actual route automatically, saving you thejb of working out all the conventional "./" and "//" relative location designators.
Meanwhile, in the UserProfile.svelte
file, the parameter will have been declared as a prop (ie a parameter) with an export statement
<script>
export userId;
<script/>
The UserProfile.svelte
file, (which in all other respects looks just like any other +page.svelte
file), is now free to reference the userId parameter both in its <script>
and template sections.
You'll recall this is exactly the same way a +page.svelte
file gets its data from an associated +page.server.js
file. This is because all +page.svelte
files are themselves components.
And now that you know this, you'll maybe guess that component structures open a way to write products-display
pages that link programmatically to product-details
pages.
First, SvelteKit replaces the src/routes/products-display/product-detail
route with a new, dynamic "parameterised" src/routes/products-display/[productNumber]/
route. This enables it to reference individual +page.svelte
product-details
pages from "anchor" links such as <a href="/products-display/1">View Product 1</a>
. When the SvelteKit router is asked to parse this address it passes it to the following modified version of the original product-details
+page.svelte
file:
// src/routes/products-display/[productNumber]/+page.svelte - remove before running
<script>
export let data;
</script>
<div style="text-align: center;">
<span>Here's the Product Details for Product { data.props.productNumber}</span>
</div>
As you'll see, this is looking for a load() function to supply the productNumber
parameter. You've seen this before (see Post 2.3, so you'll know that this needs to be supplied by a +page.server.js
file. Here it is:
// src/routes/products-display/[productNumber]/+page.server.js - remove before running
export async function load(event) {
const productNumber = event.params.productNumber;
return {
props: {
productNumber
}
};
}
This digs the productNumber
parameter out of the SvelteKit event
object (the complex of properties and methods that follows a transaction throughout its lifecycle) and returns it to the +page.svelte
file.
That was fun! Just to tie things up neatly, here is a version of products-display/+page.svelte
that lists all products in the collection as anchor tags that link them dynamically to product-details pages:
// src/routes/products-display/+page.svelte - remove before running
<script>
export let data;
</script>
<div style="text-align: center">
<h3>Current Product Numbers</h3>
{#each data.products as product}
<!-- display each anchor on a separate line-->
<div style = "margin-top: .35rem;">
<a href="/products-display/{product.productNumber}"
>View Detail for Product {product.productNumber}</a
>
</div>
{/each}
</div>
I won't go into full details for the remaining files. The src/routes/products-display/+page.server.js
file that provides the load()
function for the above is perfectly standard. But the load()
function for the dynamic route's src/routes/products-display/[productNumber]/+page.svelte
file now has to use the productNumber
value from the event.params
object to get the associated productDetails
field from the database. Here are the essentials of the code for this:
// src/routes/products-display/[productNumber]/+page.svelte - fragment
export async function load(event) {
const productNumber = parseInt(event.params.productNumber, 10);
// Now that you have the product number, you can fetch the product details from the database
const productsCollRef = collection(db, "products");
const productsQuery = query(productsCollRef, where("productNumber", "==", productNumber));
const productsSnapshot = await getDocs(productsQuery);
const productDetails = productsSnapshot.docs[0].data().productDetails;
return {
productNumber: productNumber, productDetails: productDetails
};
}
5. Beneath the hood - Svelte "rendering" and "pre-rendering" internals
The Svelte +page.svelte
and +page.server.js
file types described above deliver a "default" design in which pages are built on the server and initially displayed to the client as pure HTML. Here, in a process Svelte calls "hydration", javascript is added to make the page interactive. This, for example, would be the point at which a button's "on:click" capabilities are added. Finally, "state" data used on the server to generate the initial HTML is replayed to ensure that everything is still consistent.
Overall this arrangement:
- generates a near-instant initial response when a page is requested by the browser
- ensures that sensitive application logic, vulnerable form validation logic and valuable security keys can be hidden inside
+page.server.js
files - makes the most of the (generally) superior processing capability of the server
- encourages better "indexing" by search engines. While search engines have got better in recent years at indexing content that was rendered with client-side JavaScript, server-side rendered content is indexed more frequently and reliably.
That said, there will always be cases where improvements might be made to the default arrangement, and Svelte is very helpful in providing ways of enabling this.
For example, if the data accessed by a +page.server.js isn't changing very much (perhaps not even changing at all) you can instrument Svelte to pre-render selected pages when the webapp is prepared for "deployment" (see Post 4.3)
Additionally, there may be cases when client-side processing would be a more practical way of loading the data for a +page.svelte
file. Perhaps it doesn’t matter that the data fetch happens asynchronously after the initial page load or if the data can be fetched via APIs that don’t require server-side security. In this case, Svelte offers a +page.js
file to export a load
function. Unlike a +page.server.js
file, a +page.js
file can be inspected in the browser
For more information about this and other specialist arrangements, please refer to the SvelteKit Page Options docs
Top comments (0)