The Six UI frameworks supported by Astro for templating are primarily used for building Single Page Applications. But the application it generates isn't a SPA.
So to enjoy the advantages of a SPA, you can install Astro SPA to turn your existing Astro project into SPA. It's not only just a SPA component/library but it comes with many other features also.
Once you have set up your Astro project simply run the following command:
# yarn
yarn add astro-spa
# npm
npm i astro-spa
# pnpm
pnpm add astro-spa
Then import the component like this:
---
import { Spa } from "astro-spa";
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Astro-SPA</title>
</head>
<body>
<h1>Astro-SPA</h1>
<Spa />
</body>
</html>
And that's it, you're now ready to go!
Why Astro SPA? It's a good question. Well, Astro SPA is a SPA component/library that is built using Astro components. That means, it utilizes all the exclusive features of Astro components!
By default, Astro SPA will ship only 1278 bytes of JS code (min+gzipped)! And if you configure it and remove some features, it can ship even less than 1KB! Thanks to Astro components, we can detect which features are used and which are not on the server-side and generate only the required JS code.
But even with that small size, Astro SPA comes with a lot of features that aren't available in other SPA libraries. It comes with animations, caching, containerization, local link detection, prefetching, progress bar, and more (Check below for more details)! All of these in that extremely small file size!
Prefetching is a feature that allows you to load the page content before the user visits it for blazing-fast navigation. Prefetching is enabled by default. So, you can enjoy the benefits without having to worry about configuring Astro SPA.
Astro SPA prefetches the internal links using the Intersection Observer API. So, it won't start prefetching until the user reaches the link and thus it saves bandwidth. It also upgrades to fetch requests on mouseover
and touchstart
events if the resource hasn't been fetched already. It also caches the prefetched resources using the Cache API to avoid extra network requests.
If you want to disable prefetching, you can do so by tweaking the prefetch
prop to false
.
When you click on an internal link, Astro SPA will intercept the click and try to serve the request from the cache. If the resource is not in the cache, it will fetch the requested page and replace the current documentElement
with the new documentElement
.
If you want to replace only a specific part of the page, you can use the containerSelector
prop to specify the selector of the container. Astro SPA will replace the content of the container with the content of the new container.
The back/forward navigation works as usual when using Astro SPA. It works by intercepting the popstate
event and replacing the current content with the new content.
Astro SPA will execute every script the new page has. If you are using containerization, it'll execute scripts only in the container and the head of the new document. Check the section, Script Execution in Astro SPA for more details.
By default, Astro SPA will show a progress bar while the new page is loading. You can disable this by tweaking the progressBar
prop to false
. You can also configure the progress bar by passing the progressBarOptions
prop to the <Spa />
component (Check below for more details).
There are two types of progress bars in Astro SPA. The first one is similar to the one shown in the ReactJS website. The second one is similar to the one shown in the PreactJS website. The first one is the default one. You can also choose the second one by passing the { secondary: true }
option to the progressBarOptions
prop.
By default, Astro SPA will show a fade-in animation when the new page loads. You can disable this by tweaking the defaultAnimation
prop to false.
You can also add your own animation using the lifecycle events provided by Astro SPA. One caveat is that you'll have to write the animation logic from scratch. There are plans to ease the animation process in the future which will utilize the new Shared Elements Transitions which is planned to be released in Chrome v97. The expected release date of Chrome v97 is January 4, 2022.
To avoid wasting the limited bandwidth of the user, no links will be prefetched if the user has turned on data saver. And also links will be prefetched only if they are in the viewport for more than 500ms (500ms is the default value).
You can also limit the number of prefetched resources by passing the limit
prop to the <Spa />
component (Check below for more details).
The prefetched and fetched resources are cached using the Cache API. Caching is enabled by default so you don't have to worry about configuring anything.
The entire cache is cleared on page load/reloads to ensure there's no stale content. If you want to clear the cache, you can do so by calling caches.delete("astro-spa")
.
Astro SPA is a SPA framework and it works within a single page. So, your scripts have to be written in a way that they can be executed without clashing with the scripts of the current page.
For example, you should wrap your scripts with IIFEs (Immediately Invoked Function Expression). Otherwise, if you have constant variables in your scripts, you may get a TypeError
like Uncaught TypeError: Assignment to constant variable
.
Also, you should place the <Spa />
component after all the body content. Otherwise, the Astro SPA code may get executed when no links are available on the page. If you still want to put the <Spa />
component in the head, you'll have to call the spa.scan()
function after all the body content.
Another thing to note is that no load
or DOMContentLoaded
events will be triggered on the new page. To combat this, you can utilize the navigate
and mount
lifecycle events (Check below for more details).
And, don't use the window.location.reload()
method. It will clear the entire cache and thus you will lose the content of the new page. Instead, use the spa.navigate()
method to reload the current page. For example, if you want to reload the current page, you can do so by writing the following code:
spa.navigate(location.pathname, { replace: true });
Analytics solutions are very popular and used to provide insights into the user's behavior. It's an important feature for most websites to have.
Astro SPA comes with built-in support for Google Analytics. You can use the analytics
prop to configure the Google Analytics tracking code (Check below for more details). It's inspired by the Minimal Google Analytics project by DavidKuennen. The analytics only add 478 bytes to the bundle size (min+gzipped).
One thing to note is that the analytics solution provided by Astro SPA has a built-in Unique User Identification mechanism. To comply with the GDPR regulations, it doesn't store any data in the user's browser/device. It takes the user's IP address and the value of navigator.userAgent
& new Date().getFullYear()
and creates a hash of it. This hash is then used to identify a unique user.
It's important to note that the Unique User Identification mechanism is not 100% accurate. It's possible that the same user will be identified as a different user and multiple users will be counted as a single user.
A big problem with counting multiple users as a single user is that it can grow exponentially over time if an extremely large number of users visit a website . This is why Astro SPA also takes new Date().getFullYear()
into account when creating the hash. Though a year is a long time, it still greatly reduces the number of duplicate users. You can tweak this also by passing the trackingPeriod
key to the analytics
object prop (Check below for more details).
The analytics solution also exposes two helper functions to track events and exceptions. The first one is spa.trackEvent()
and the second one is spa.trackException()
.
Note: If you want to disable the Unique User Identification mechanism, you can do so by tweaking the
fingerprinting
prop tofalse
. If disabled the cid will be generated by hashing the values ofMath.random()
andnew Date()
. Note that the value of the cid is generated every time the page is loaded. But as Astro SPA is a SPA library, the cid will be generated only once. So, the user will still be counted as a single user per session until the page is reloaded.
Astro SPA doesn't have any other analytics solution built-in. But, you can integrate any other analytics solution you want with Astro SPA. You can use the navigate
and mount
lifecycle events to track the user when he navigates away from one page to another page.
There are a total of 4 helper functions that you can use in your Astro SPA project.
You can use the prefetch
function to prefetch links programmatically. It accepts a single parameter, which is the link to be prefetched.
const url = "https://example.com";
spa.prefetch(url); // window.spa.prefetch(url)
You can use the navigate
function to navigate to a link programmatically. It accepts a single parameter, which is the link to be navigated.
const searchQuery = "query";
spa.navigate(`/search?${searchQuery}`); // window.spa.navigate(`/search?${searchQuery}`)
You can use the observe
function to start observing a new link programmatically. It accepts a single parameter, which is the link to be observed. It'll prefetch and add proper event handlers to the link.
const newLink = document.querySelector("#new-link");
spa.observe(newLink); // It will start observing the new link
You can use the scan helper function to start observing all the new links injected later into the document via JS like observe function.
You can use the scan
function to start observing all the new links injected later into the document via JavaScript. It doesn't accept any parameters.
const newLinks = ["a link", "another link", "a third link"];
newLinks.forEach((link) => {
document.body.appendChild(link);
});
spa.scan(); // It will start observing all the new three links
There are a total of 2 lifecycle events that you can use in your Astro SPA project, which are navigate
and mount
.
You can use the navigate
lifecycle event to execute code when the user clicks on a new link.
window.addEventListener("navigate", () => {
console.log("navigated");
});
// or
window.onNavigate = () => {
console.log("navigated");
};
You can use the mount
lifecycle event to execute code when the user navigates to a new page. The event is called after the new document is rendered and all the scripts have been executed.
window.addEventListener("mount", () => {
console.log("mounted");
});
// or
window.onMount = () => {
console.log("mounted");
};
<Spa />
Component)There are a total of 21 configuration options that can be passed to the <Spa />
component. All of them are optional. Among them, three are object
type and has children configuration options. All of the configuration options, their types, default values, and descriptions are listed below.
Type: analytics
(Check below for more details)
Default: analytics
Astro SPA comes with built-in support for Google Analytics. You can use the analytics
prop to configure the Google Analytics tracking code. Check the section, Google Analytics for more details. The analytics
prop accepts an object with the following properties:
export interface analytics {
trackingID: string; // The tracking ID of the Google Analytics account
anonymizeIP?: boolean; // Whether to anonymize the IP address of the user
colorDepth?: boolean; // Whether to track the color depth of the user's screen
characterSet?: boolean; // Whether to track the character set of the user's browser
screenSize?: boolean; // Whether to track the screen size of the user's browser
language?: boolean; // Whether to track the language of the user's browser
fingerprinting?: boolean; // Whether to use browser fingerprinting techniques to identify unique users
trackingPeriod?: number; // The amount of time to track the user's browser
}
// Defaults
const analytics: analytics = {
trackingID: "",
anonymizeIP: true,
colorDepth: true,
characterSet: true,
screenSize: true,
language: true,
};
Type: Partial<HTMLScriptElement>
Default: undefined
The attributes supported by the <script>
element. You have to write the attributes in the following format: { attributeName: attributeValue }
. For example, { async: true }
.
Type: boolean
Default: true
Whether or not Cache API will be used for caching the fetched and prefetched resources.
Type: string
Default: ""
Example: "#root"
The querySelector
of the content container of the page, which will get replaced instead of the whole document.
Type: boolean
Default: true
Whether or not the default fade-in animation will be displayed while the new page loads.
Type: number
Default: 500
The amount of time each link has to stay inside the viewport before being prefetched, in milliseconds.
Type: boolean
Default: false
Whether the code should be included in an external JavaScript file or be inlined. If enabled, the hash of the code will be appended to the filename.
Type: boolean
Default: false
Whether or not the code will fall back to setTimeout
if the browser doesn't support requestIdleCallback
.
Note: No polyfill gets included when this option is set to true.
Type: boolean
Default: false
Whether or not the internal links will be prefetched with a higher priority.
Type: string[]
Default: undefined
The URLs, which should not be fetched or prefetched. However, they won't be excluded from SPA navigation.
Type: IntersectionObserverInit
(Check below for more details)
Default: IntersectionObserverInit
The options
object for the IntersectionObserver
constructor used for prefetching. It's a bit different from the original options
object of the IntersectionObserver
constructor. The full interface of the IntersectionObserverInit
object is as follows:
export interface IntersectionObserverInit {
root?: string; // The querySelector of the HTML element to observe for in-viewport links to prefetch. However, the links will be fetched on the mouseover and touchstart events. Example: "#viewport".
rootMargin?: string; // The CSS margin property for the margin around the root element.
threshold?: number | number[]; // The percentage of the area of each link that must have entered the viewport to be fetched, in its decimal form (0.25 = 25%).
}
// Defaults
const intersectionObserverOptions: IntersectionObserverInit = {
root = "",
rootMargin = "",
threshold = 0.25,
};
Type: number
Default: undefined
The maximum number of links that can be prefetched.
Type: boolean
Default: true
Whether or not the code will check for if any element is using the data-active-class
attribute and whether its href
matches the current URL or not.
Type: boolean
Default: true
Whether or not the internal links will be prefetched.
Type: boolean
Default: true
Whether or not the prefetching of the internal links will be upgraded to fetch on mouseover
and touchstart
events.
Type: boolean
Default: true
Whether or not the progress bar will be displayed.
Type: progressBarOptions
(Check below for more details)
Default: progressBarOptions
When the progressBar
prop is set to true
(default), you can customize the progress bar by passing the progressBarOptions
object. The full interface is as follows:
export interface progressBarOptions {
height?: string; // The height of the progress bar in CSS units.
secondary?: boolean; // Whether or not the secondary progress bar will be displayed. Check the section, [Showing Progress Bar](#showing-progress-bar) for more details.
colors?: {
foreground?: string; // The foreground color of the progress bar.
background?: string; // The background color of the progress bar. This option is only respected when the `secondary` option is set to true.
};
}
// Defaults
const progressBarOptions: progressBarOptions = {
height: "1vh",
secondary: false,
colors: {
foreground: "#42b3f5",
background: "#4248f5",
},
};
Type: boolean
Default: true
Whether or not the code will scan the DOM on mount
and prefetch the new links.
Type: boolean
Default: true
Whether or not the new documentElement
or the new content container will be scrolled into view using Element.scrollIntoView()
.
Type: boolean
| scrollIntoViewOptions
Default: { behavior: "smooth" }
The boolean parameter or the parameter object for the scrollIntoView
function. For more details, check the MDN documentation of the Element.scrollIntoView() function.
Type: number
or false
(true
is not a valid value)
Default: 2000
The value of the timeout
property of the options
object parameter of the requestIdleCallback
function. Check the MDN documentation of the requestIdleCallback function for more details.
If the timeout
prop is set to false
, no timeout
will be set for the requestIdleCallback
function.
Note: There's a difference between
{ timeout : 0 }
and notimeout
at all. The former will schedule theidleCallback
function to be run asynchronously and will call the function after the shortest possible delay. And the latter won't call theidleCallback
function until the main thread is free.
Type: boolean
If present, the link won't be prefetched or navigated to.
Type: boolean
If present, the link won't be prefetched.
Type: boolean
If present, the link will be prefetched with a higher priority.
Type: boolean
If present, prefetching of the link won't be upgraded to fetch on mouseover
and touchstart
events.
Type: string
If the localLinkDetector
prop is set to true
, and the data-active-class
attribute is present, the link will be prefetched when the href
attribute matches the current URL or not.
https://softhardsystem.com/ (It's a production website)
https://astro-spafy-component-demo.netlify.app/
https://ohka-bots-site-astro-ksoqn4flk7-li4hm4z1a-tc-001.vercel.app/ (Thanks to @Tc-001)
There are plans to release v2 which will utilize the new AppHistory API that is planned to be shipped in Chrome v100. The expected release date of Chrome v100 is March 29, 2021 according to the new four-week release cycle.