1. Introduction
High quality offline-enabled web content is not easily discoverable by users right now. Users would have to know which websites work offline, or they would need to have an installed PWA, to be able to browse through content while offline. This is not a great user experience as there no entry points to discover available content. To address this, the spec covers a new API which allows developers to tell the browser about their specific content.
The content index allows websites to register their offline enabled content with the browser. The browser can then improve the website’s offline capabilities and offer content to users to browse through while offline. This data could also be used to improve on-device search and augment browsing history.
Using the API can help users more easily discover content for the following example use cases:
-
A news website prefetching the latest articles in the background.
-
A content streaming app registering downloaded content with the browser.
1.1. Example
function deleteArticleResources( id) { return Promise. all([ caches. open( 'offline-articles' ) . then( articlesCache=> articlesCache. delete ( `/article/ ${ id} ` )), // This is a no-op if the function was called as a result of the // `contentdelete` event. self. registration. index. delete ( id), ]); } self. addEventListener( 'activate' , event=> { // When the service worker is activated, remove old content. event. waitUntil( async function () { const descriptions= await self. registration. index. getAll(); const oldDescriptions= descriptions. filter( description=> shouldRemoveOldArticle( description)); await Promise. all( oldDescriptions. map( description=> deleteArticleResources( description. id))); }()); }); self. addEventListener( 'push' , event=> { const payload= event. data. json(); // Fetch & store the article, then register it. event. waitUntil( async function () { const articlesCache= await caches. open( 'offline-articles' ); await articlesCache. add( `/article/ ${ payload. id} ` ); await self. registration. index. add({ id: payload. id, title: payload. title, description: payload. description, category: 'article' , icons: payload. icons, url: `/article/ ${ payload. id} ` , }); // Show a notification if urgent. }()); }); self. addEventListener( 'contentdelete' , event=> { // Clear the underlying content after user-deletion. event. waitUntil( deleteArticleResources( event. id)); });
In the above example, shouldRemoveOldArticle
is a developer-defined function.
2. Privacy Considerations
Firing the contentdelete
event may reveal the user’s IP address after the user has left the page. Exploiting
this can be used for tracking location history. The user agent SHOULD limit tracking by capping the duration of the
event.
When firing the contentdelete
event, the user agent SHOULD prevent the website from adding new content.
This prevents spammy websites from re-adding the same content, and the users from being shown content they just
deleted.
Displaying all registered content can cause malicious websites to spam users with their content to maximize exposure. User agents are strongly encouraged to not surface all content, but rather choose the appropriate content to display based on a set of user agent defined signals, aimed at improving the user experience.
3. Infrastructure
3.1. Extensions to service worker registration
A service worker registration additionally has:
-
Content index entries (a map), where each key is a DOMString, and each item is a content index entry.
-
An entry edit queue (a parallel queue), initially the result of starting a new parallel queue.
3.2. Content index entry
A content index entry consists of:
-
A description (a
ContentDescription
). -
A launch url (a URL).
-
A service worker registration (a service worker registration).
3.2.1. Display
The user agent MAY display a content index entry (entry) at any time, as long as entry exists in a service worker registration's content index entries.
Note: User agents should limit surfaced content to avoid showing too many entries to a user.
-
The UI MUST prominently display the entry’s service worker registration's scope url's origin.
-
The UI MUST display entry’s description's
title
. -
The UI MAY display entry’s description's
description
. -
The UI MAY use entry’s description's
category
in display decisions. -
The UI MAY display any of entry’s icons as images.
-
The UI SHOULD provide a way for the user to delete the underlying entry exposed by the UI, in which case run delete a content index entry for entry.
-
The UI MUST provide a way for the user to activate it (for example by clicking), in which case run activate a content index entry for entry.
3.2.2. Undisplay
4. Algorithms
4.1. Delete a content index entry
-
Let id be entry’s description's
id
. -
Let contentIndexEntries be entry’s service worker registration's content index entries.
-
Enqueue the following steps to entry’s service worker registration's entry edit queue:
-
Undisplay entry.
-
Remove contentIndexEntries[id].
-
Fire a content delete event for entry.
-
4.2. Activate a content index entry
-
Let activeWorker be entry’s service worker registration's active worker.
-
If activeWorker is null, abort these steps.
-
Let newContext be a new top-level browsing context.
-
Queue a task to run the following steps on newContext’s
Window
object’s environment settings object's responsible event loop using the user interaction task source:-
HandleNavigate: Navigate newContext to entry’s launch url with exceptions enabled and replacement enabled.
-
If the algorithm steps invoked in the step labeled HandleNavigate throws an exception, abort these steps.
-
Let frameType be "`top-level`".
-
Let visibilityState be newContext’s active document's
visibilityState
attribute value. -
Let focusState be the result of running the has focus steps with newContext’s active document as the argument.
-
Let ancestorOriginsList be newContext’s active document's relevant global object's
Location
object’s ancestor origins list's associated list. -
Let serviceWorkerEventLoop be activeWorker’s global object's event loop.
-
Queue a task to run the following steps on serviceWorkerEventLoop using the DOM manipulation task source:
-
If newContext’s
Window
object’s environment settings object's creation URL's origin is not the same as the activeWorker’s origin, abort these steps. -
Run Create Window Client with newContext’s
Window
object’s environment settings object, frameType, visibilityState, focusState, and ancestorOriginsList as the arguments.
-
-
Is creating a new Browsing Context the right thing to do here? (issue)
4.3. Fire a content delete event
ContentIndexEvent
on entry’s service worker registration with
the following properties:
id
-
entry’s description's
id
.
5. API
5.1. Extensions to ServiceWorkerGlobalScope
partial interface ServiceWorkerGlobalScope {attribute EventHandler ; };
oncontentdelete
5.1.1. Events
The following is the event handler (and its corresponding event handler event type) that must be
supported, as event handler IDL attributes, by all objects implementing ServiceWorker
interface:
event handler event type | event handler | Interface |
---|---|---|
contentdelete
| oncontentdelete
| ContentIndexEvent
|
5.2. Extensions to ServiceWorkerRegistration
partial interface ServiceWorkerRegistration { [SameObject ]readonly attribute ContentIndex index ; };
A ServiceWorkerRegistration
has a content index (a ContentIndex
), initially a
new ContentIndex
whose service worker registration is the context
object's service worker registration.
The index
attribute’s getter must return the context object's content index.
5.3. ContentIndex
In only one current engine.
OperaNoneEdgeNone
Edge (Legacy)NoneIENone
Firefox for AndroidNoneiOS SafariNoneChrome for Android84+Android WebView84+Samsung InternetNoneOpera Mobile60+
enum {
ContentCategory ,
"" ,
"homepage" ,
"article" ,
"video" , };
"audio" dictionary {
ContentDescription required DOMString ;
id required DOMString ;
title required DOMString ;
description ContentCategory = "";
category sequence <ImageResource >= [];
icons required USVString ; }; [
url Exposed =(Window ,Worker )]interface {
ContentIndex Promise <undefined >add (ContentDescription );
description Promise <undefined >delete (DOMString );
id Promise <sequence <ContentDescription >>getAll (); };
In only one current engine.
OperaNoneEdgeNone
Edge (Legacy)NoneIENone
Firefox for AndroidNoneiOS SafariNoneChrome for Android84+Android WebView84+Samsung InternetNoneOpera Mobile60+
A ContentIndex
has a service worker registration (a service worker registration).
5.3.1. add()
In only one current engine.
OperaNoneEdgeNone
Edge (Legacy)NoneIENone
Firefox for AndroidNoneiOS SafariNoneChrome for Android84+Android WebView84+Samsung InternetNoneOpera Mobile60+
add(description)
method, when invoked, must return a new promise promise and run
these steps in parallel:
-
Let registration be the context object's service worker registration.
-
If registration’s active worker is null, reject promise with a
TypeError
and abort these steps. -
If any of description’s
id
,title
,description
, orurl
is the empty string, reject promise with aTypeError
and abort these steps. -
Let launchURL be the result of parsing description’s
url
with context object's relevant settings object's API base URL.Note: A new service worker registration might be introduced later with a narrower scope.
-
Let matchedRegistration be the result of running Match Service Worker Registration algorithm with launchURL as its argument.
-
If matchedRegistration is not equal to registration, reject promise with a
TypeError
and abort these steps. -
If registration’s active worker's set of extended events does not contain a
FetchEvent
, reject promise with aTypeError
and abort these steps. -
Let icons be an empty list.
-
Optionally, the user agent MAY select icons to use from description’s
icons
. In which case run the following steps for each image resource (resource) of description’sicons
' selected icons after successfully parsing it:-
Let response be the result of awaiting a fetch using a new request with the following properties:
- URL
-
resource’s src.
- Client
- Keepalive flag
-
Set.
- Destination
-
"`image`".
- Mode
-
"`no-cors`".
- Credentials mode
-
"`include`".
-
If response is a network error, reject promise with a
TypeError
and abort these steps. -
If response cannot be decoded as an image, reject promise with a
TypeError
and abort these steps. -
Append response to icons.
-
-
Let entry be a new content index entry with:
- description
-
description.
- launch url
-
launchURL
- service worker registration
-
registration.
- icons
-
icons
-
Let id be description’s
id
. -
Let contentIndexEntries be registration’s content index entries.
-
Enqueue the following steps to registration’s entry edit queue:
Note: Adding a description with an existing ID would overwrite the previous value.
5.3.2. delete()
In only one current engine.
OperaNoneEdgeNone
Edge (Legacy)NoneIENone
Firefox for AndroidNoneiOS SafariNoneChrome for Android84+Android WebView84+Samsung InternetNoneOpera Mobile60+
delete(id)
method, when invoked, must return a new promise promise and run these
steps in parallel:
-
Let registration be the context object's service worker registration.
-
Let contentIndexEntries be registration’s content index entries.
-
Enqueue the following steps to registration’s entry edit queue:
5.3.3. getAll()
getAll()
method, when invoked, must return a new promise promise and run these
steps in parallel:
-
Let registration be the context object's service worker registration.
-
Let contentIndexEntries be registration’s content index entries.
-
Let descriptions be an empty list.
-
Enqueue the following steps to registration’s entry edit queue:
-
For each id → entry of contentIndexEntries:
-
Append entry’s description to descriptions.
-
-
Resolve promise with descriptions.
-
5.4. ContentIndexEvent
In only one current engine.
OperaNoneEdgeNone
Edge (Legacy)NoneIENone
Firefox for AndroidNoneiOS SafariNoneChrome for Android84+Android WebView84+Samsung InternetNoneOpera Mobile60+
dictionary :
ContentIndexEventInit ExtendableEventInit {required DOMString ; }; [
id Exposed =ServiceWorker ]interface :
ContentIndexEvent ExtendableEvent {(
constructor DOMString ,
type ContentIndexEventInit );
init readonly attribute DOMString ; };
id