Best place/lifecycle method to set page titles in a single-page Svelte app
Asked Answered
G

1

9

I'm getting started with Svelte and building a single-page application (using page.js as the router). I thought I'd have a separate component to produce the <svelte:head> block, and when each component mounts it would write the page title to a store, which would then be read by the head component. It partially works - the page title is updated when I click through to different pages. However, if I go back in my browser's history, the title doesn't change back with the page. It does change if I then reload the page. So perhaps onMount() isn't the right lifecycle method. What approach can I take that will work with history state navigation?

Here's my app boiled down to a minimal example.

// index.js

import page from 'page'

import App from './views/App.svelte'

const app = new App({
    target: document.body,
    props: {
        route: null,
    },
})

function one() {
    app.$set({ route: 'one' })
}

function two() {
    app.$set({ route: 'two' })
}

page('/one', one)
page('/two', two)
page()

// App.svelte

<script>
    import One  from './One.svelte'
    import Two  from './Two.svelte'
    import Head from '../parts/Head.svelte'
    import Home from './Home.svelte'

    export let route
</script>

<Head />

{#if route === 'one'}
    <One />
{:else if route === 'two'}
    <Two />
{:else}
    <Home />
{/if}

// Head.svelte

<script>
    import { pageName } from '../stores.js'

    let displayPageName

    pageName.subscribe(value => {
        displayPageName = value
    })
</script>

<svelte:head>
    {#if displayPageName}
        <title>Test App &mdash; {displayPageName}</title>
    {:else}
        <title>Test App</title>
    {/if}
</svelte:head>

// stores.js

import { writable } from 'svelte/store'

export const pageName = writable(null)

// Home.svelte

<a href="/one">One</a> <a href="/two">Two</a>

// One.svelte

<script>
    import { onMount } from 'svelte'
    import { pageName } from '../stores.js'

    onMount(async () => {
        pageName.update(() => 'Component One')
    })
</script>

<a href="/two">Two</a>

// Two.svelte

<script>
    import { onMount } from 'svelte'
    import { pageName } from '../stores.js'

    onMount(async () => {
        pageName.update(() => 'Component Two')
    })
</script>

<a href="/one">One</a>
Graben answered 23/11, 2019 at 23:41 Comment(0)
B
13

For some reasons, Svelte doesn't seem to like the <title> in an {#if} block... I recommend computing the value in a reactive statement instead.

$: title = $pageName ? `Test App \u2014 ${$pageName}` : 'Test App'

$pageName is a special syntax to access the store's value without having to handle subscribe/unsubscribe yourself (docs).

Even with that, I found that my browser (tested in Chrome) was apparently OK with ignoring the <title> in the DOM when navigating back. We can force the value of document.title to workaround that. Another reactive block can take care of that:

$: {
  document.title = title
}

So your whole Head.svelte component would now looks like this:

<script>
  import { pageName } from '../stores.js'

  $: title = $pageName ? `Test App \u2014 ${$pageName}` : 'Test App'

  $: {
    document.title = title
  }
</script>

<svelte:head>
  <title>{title}</title>
</svelte:head>

And finally, in your example, you're not updating the store's value when you navigate to /, so you'd have a stale title in this case. I think you need to add a route like so:

page('/', () => app.$set({ route: null }))
Barris answered 25/11, 2019 at 9:21 Comment(1)
Hi there, thanks a lot for this very informative answer! It looks like you have a typo - it should be <title>{title}</title>. Also, in Home.svelte I had to explicitly set $pageName = null, otherwise at / the page title would render as "null". Otherwise, perfect, thanks again.Graben

© 2022 - 2024 — McMap. All rights reserved.