Nathan Lamont

Notes to Self

Vue 3, Nuxt, Nuxt Content, & TypeScript

Having a love/hate relationship with Vue 3 and Nuxt 3. One of the pain points is that most of modern JavaScript relies on TypeScript, which in turn has "first class" tooling in VS Code ("Visual Studio Code").

For context, TypeScript is a language that adds typing to JavaScript. This typing is only manifested in the code editor or in the tools that compile the TypeScript into JavaScript.

Microsoft controls both TypeScript and VS Code. VS Code is loved by many but is not a "native" app -- slower, less native UI, more memory than my preferred tool, Sublime.

So Vue "strongly recommends" using VS Code, but I want to use Sublime.

The TypeScript tools run in the background and communicate with the editor when there is something wrong. I have them installed on Sublime, and they work, although the UI is not great (problems sometimes manifested by red underlines, sometimes by a text panel that appears at the bottom; no idea how to dismiss the text panel or how to make it reappear). And then sometimes it just stops working, silently. The only way to know is by making an intentional error. Restarting Sublime is the only solution.

Nuxt in particular does "magic" where certain functions are automatically imported. Using mysterytheaterbrowser.org as a test, I thought it wouldn't work at all, e.g. the useAsyncData function was not defined.

But after restarting Sublime, it appears to be OK.

There are three things I got working with BC project (Vue 3, not Nuxt) that I want working with a Nuxt project:

  1. Sublime reporting errors (this is done)
  2. Errors on command line. Maybe that inefficiently runs a different TypeScript checking service, but with BC project, it is a more reliable way to review and understand errors.
  3. Pre-commit code formatting

#1 is done. #2 is being held up by a problem in a related module (jiti), #3 solution is below.

Pre-commit Code Formatting - eslint, prettier, lint-staged, and husky

eslint is a code syntax checker/fixer and prettier is a code format checker/fixer. lint-staged is a node tool for running linters as git hooks, and husky is a node tool for managing git hooks.

1. Install and configure lint-stage, eslint and prettier

$ yarn add -D lint-staged eslint @nuxtjs/eslint-config-typescript typescript eslint-plugin-prettier eslint-config-prettier prettier

Note the purpose of the less-obvious packages:

  • used in .eslintrc.cjs, in extends array. Order matters.
    • @nuxtjs/eslint-config-typescript: configures eslint for a nuxt project, used in
    • eslint-config-prettier: turns off things in eslint that conflict with things in prettier
    • eslint-plugin-prettier: does some configuration of things that eslint needs, e.g. the prettier plugin
  • typescript package must be expressly installed for the @nuxtjs/eslint-config-typescript package

2. Configure prettier and eslint

// .eslintrc.cjs

module.exports = {
  root: true,
  extends: [
    '@nuxtjs/eslint-config-typescript',
    'plugin:prettier/recommended',
  ],
  rules: {
    'no-unused-vars': 'off',
    '@typescript-eslint/no-unused-vars': 'warn',
  },
};

// .prettierrc.json

{
  "singleQuote": true // optional
}

3. Install husky and set up

$ yarn add husky --dev # install package
$ yarn husky install # it creates needed files
$ npx husky add .husky/pre-commit "yarn lint-staged" # make hook
// package.json
{
    "scripts": {
    ...
    "postinstall": "nuxt prepare && husky install",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
    },
    ...
    // this is what happens when the hook above is invoked
    "lint-staged": {
    "*.{js,jsx,vue,ts,tsx}":
    "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix"
  }
}

Lessons learned re: Nuxt, Pinia (new store), Transitions

You wish to be able to dynamically specify page transitions based on whatever attributes of content you wish (not just what's in the route path).

Arguably, this is a concern outside of the purview of Nuxt — it's a purely in-browser, dom-related decoration.

But, nuxt provides a mechanism for specifying the page transition, and so using the store and reactive variables becomes temptingly tidy.

<!-- in app.vue -->
<NuxtPage
  :transition="{
    name: 'some-transition',
    mode: 'out-in'
  }"
/>

Nuxt also provides a mechanism in an individual page template for defining its transition (definePageMeta({ pageTransition: 'some-value' })), but it's not suitable because it can only be a static value.

// we can't use definePageMeta b/c it is a not a real function - it's a macro
// and cannot refer to local vars
definePageMeta({
  pageTransition: {
    name: 'some-transition' // ok
    // name: someRef.value // error (counter is not defined)
    // name: useSomeStore().someValue // error (no active Pinia)
  }
})

So, we need a store/computed value to hold the transition.

The transition cannot be altered during the incoming page's <script setup> lifecycle. And we can't embed dynamic info in the route's meta, or, as above, the page meta. So we are left with our custom link type in which we can declare the relationship of the linked-to content to the currently presented content.

Nuxt Content 2 is much changed, and not as well documented. The documentation does not distinguish between what is available in a statically generated site and what is not.

Gone is search which magically created a large static index of the content type to be searched. You can use where clauses but it appears that if it contains conditions that aren't present at render time, no index is created to magically load and search through. There are no errors generated - just on the static site, resources will not be found. Presumably there is some hash being created by the query being used to generate deterministic file names.

Also, possibly only top-level calls to queryContent within useAsyncData create the necessary static resources. That is, `queryContent().then(() => queryContent()) does not work.

Even this does not work on a statically generated site:

<template>
  <div class="bg-white">
    <h1>Search results</h1>
    <button @click="test">Test</button>
    <pre>{{ data }}</pre>
  </div>
</template>

<script setup lang="ts">

const episodeNumbers = ref([1, 2])

const data = await useAsyncData('search-test', () => {
  return queryContent('episodes')
    .where({ id: { $in: episodeNumbers.value}})
    .only(['id', 'title'])
    .find()
});

const test = () => {
  episodeNumbers.value[0] = episodeNumbers.value[0] + 1
  episodeNumbers.value[1] = episodeNumbers.value[1] + 1
  data.refresh()
}
</script>

Your viable solution: make a secret page that loads all the context you want to index. When you want to search it, use the exact same query in the component doing the searching, and manually filter the results.

Nuxt 3 + Nuxt Content 2 = bad build times?

Getting really slow build times for updated Mystery Theater.

VersionLocalNetlify
Nuxt 2 / Content 125sec4min
Nuxt 3 / Content 24min11min

Made a bare bones version of site in Nuxt 2 and Nuxt 3 - basically just an index page pointing to all 1399 episode pages with the following results:

VersionLocal
Nuxt 2 / Content 111sec
Nuxt 3 / Content 216sec

Adding link calculation only added 2 seconds.

Ah -- adding artists (with episodes) added 3 minutes. Probably episode look ups per artist. Confirmed… removing episode look ups reduced to 21 seconds. Two lookups, both look like:

const { data: actor } = await useAsyncData(`episodes-actor-${id}`, () =>
  queryContent('episodes')
    .where({ actorIds: { $contains: id } })
    .only(episodeProperties)
    .sort({ id: 1, $numeric: true })
    .find()
);

Let's attempt storing episode IDs with the actor? That reduced the time, but only by a third, 2:36.

const { data: actor } = await useAsyncData(`episodes-actor-${id}`, () =>
  queryContent('episodes')
    .where({ id: { $in: artist.value.actor ?? [] } })
    .only(episodeProperties)
    .sort({ id: 1, $numeric: true })
    .find()
);

Sigh. Storing the episode title (removing the extra queryContent altogether) along with its ID gives enough info for the artist's page and reduces generation time to 25 seconds.

Applying the same change to the actual mysterytheater.org, we get:

VersionLocal BareLocal FullNetlify
Nuxt 2 / Content 100:1600:2504:00
Nuxt 3 / Content 203:1604:0011:00
Nuxt 3 / Content 2 FLAT00:2501:4406:41

Still room for improvement. Why is Nuxt 3 full still 3x bare? (Removing fetching of all episode IDs to get count of episodes on index page removed 40 seconds, about 40% less time)