Site settings
Title, description, language, favicon and social image for the site and for each page. In a Next.js project they're written as metadata in your code.
Site settings is where the things a search engine or a shared link shows are set: the title, the description, the favicon, the social image. There’s one form for the site and one for each page, and everything you type is written as Next.js metadata in your own files.
In the released version it’s for Next.js projects, and it needs the App Router: metadata is written in the root layout. In a project that isn’t Next.js the button isn’t there. The next release brings it to other kinds of project, where the same settings are tags in the site’s <head>. Everything up to that section is about Next.js.
Open it
Click the globe in the top bar (“Site settings”). The settings take the canvas’s place, with a list on the left: “General” under Site settings, then every page under Page settings. To go straight to a page’s form, right-click the page in the page menu → “Page settings…”.
Esc goes back to the canvas. A field is saved when you leave it, or on Enter.
The site
| Field | What it is |
|---|---|
| Title | The title of Home, and of every page that has none of its own |
| Title template | How a page’s title is completed, with %s where the page’s title goes: %s · Oak Studio |
| Description | The site’s description |
| Language | The language of the site, like en or es |
| Site URL | The address the site is published at. Next.js uses it to make the addresses in share links absolute (the social image’s, for instance). |
| Search engines | Off asks search engines not to index the site |
| Favicon | A square image, 64 × 64 pixels or more |
| Social preview | The image a shared link shows, 1200 × 630 pixels |
Beside the fields, a preview shows the title and description the way a search result would.
A page
Each page has a Title, a Description, the Search engines switch and its own Social preview. The page’s title goes into the site’s template, and the preview shows the result. A page without a social image uses the site’s.
Dynamic pages
A route like /work/[slug] is one form for all its pages, so its title and description can take values from the item each page shows. Under the fields, midcode lists the variables it found: the fields the page reads from its item, and the keys of the data it comes from. Click one to put it at the cursor, or type it yourself in double braces:
{{name}} · WorkThe preview fills the variables with the first item’s values. The social preview can come from a field too: choose it in the list, next to “Upload an image…”.
Client components
metadata can’t be exported from a file that starts with 'use client'. For such a page midcode writes the metadata in a layout.tsx beside it, and the form says so.
What midcode writes
Each text field is one edit that changes one value (or adds one property) and goes through the history: ⌘Z takes it back, and it shows in Publish.
The site
The site’s fields go in export const metadata of the root layout. Here are several edits at once (title, template, description, site URL, and Search engines turned off):
export const metadata: Metadata = {- title: 'Create Next App',- description: 'Generated by create next app',+ title: { default: 'Oak Studio', template: '%s · Oak Studio' },+ description: 'Furniture made to last.',+ metadataBase: new URL('https://oak.studio'),+ robots: { index: false }, }Language is the lang of the layout’s <html>:
- <html lang="en">+ <html lang="es">If the layout has no metadata yet, midcode adds export const metadata: Metadata = { title: 'Oak Studio' } after the imports, and the import type { Metadata } from 'next' it needs in a TypeScript file. Turning Search engines back on removes robots again. Other keys you wrote in the object are never touched.
A page
A page’s fields go in the metadata its own file exports:
export const metadata: Metadata = {- title: 'About',+ title: 'About us',+ description: 'Who we are and how we work.', }A dynamic page
Variables need code that runs for each item, so midcode writes generateMetadata, reading the same item the page shows. Given this page:
import { notFound } from 'next/navigation'
import { projects } from '@/content/projects'
export function generateStaticParams() {
return projects.map((p) => ({ slug: p.slug }))
}
export default async function ProjectPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const project = projects.find((p) => p.slug === slug)
if (!project) notFound()
return <main>…</main>
}setting the title to {{name}} · Work adds:
import { notFound } from 'next/navigation' import { projects } from '@/content/projects'+import type { Metadata } from 'next' export function generateStaticParams() { return projects.map((p) => ({ slug: p.slug })) } +export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {+ const { slug } = await params+ const project = projects.find((p) => p.slug === slug)+ return { title: `${project?.name} · Work` }+}+The parameter and the lookup are copied from the page’s own component. From then on each field is one more property of the object it returns: a description of {{summary}} adds description: project?.summary, and a social preview from the cover field adds openGraph: { images: project?.cover }.
If the page had a static metadata, generateMetadata replaces it and carries its other keys over.
Images
Images aren’t written in code. They’re saved as the files Next.js looks for:
| Image | File |
|---|---|
| Favicon | app/icon.png (or .svg, .jpg…; an .ico is saved as app/favicon.ico) |
| The site’s social preview | app/opengraph-image.png |
| A page’s social preview | opengraph-image.png in the page’s folder, like app/about/opengraph-image.png |
The file keeps its extension. The image it replaces goes to the Trash.
A client page
import type { Metadata } from 'next'
export const metadata: Metadata = { title: 'Contact' }
export default function Layout({ children }: { children: React.ReactNode }) {
return children
}That file is created the first time you set a field on a page that is a client component, next to its page.tsx.
Other projectsNext release
In the next release the globe is also there in projects whose <head> is written in a file. The form is the same, and what it writes is HTML tags instead of Next.js metadata.
| Project | Where the site’s <head> is |
|---|---|
| Vite, plain HTML | index.html |
| Angular | src/index.html |
| SvelteKit | src/app.html |
| Create React App, Vue CLI | public/index.html |
| Astro | The first layout in src/layouts/ that has a <head>, or src/pages/index.astro |
| A site a server renders (Laravel, Django, Rails, Hugo, Jekyll, Eleventy…) | Its layout: the first template that closes a <head> |
Setting the title, the description, the language, turning Search engines off and choosing both images in a new Vite project leaves this:
<!doctype html>-<html lang="en">+<html lang="es"> <head> <meta charset="UTF-8" />- <link rel="icon" type="image/svg+xml" href="/vite.svg" />+ <link rel="icon" href="/favicon.png" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />- <title>Vite + React</title>+ <title>Oak Studio</title>+ <meta name="description" content="Furniture made to last." />+ <meta name="robots" content="noindex" />+ <meta property="og:image" content="/og-image.jpg" /> </head>A tag that’s already there has its value changed. A new one goes last in the
<head>, indented like its neighbours and closed the way they are (>or/>).Images are copied into the folder the site serves as it is (
public/,static/in SvelteKit, next to the pages in a folder of HTML) asfavicon.pngandog-image.jpg, keeping their extension. Removing an image takes its tag out and leaves the file, since something else may use it. If midcode can’t tell which folder that is, it asks you to put the image there and link it in the code.Turning Search engines back on removes the
robotstag.There’s no Title template and no Site URL: those are Next.js metadata, and a
<head>has no tag for them.Pages have their own form only in a folder of plain HTML, where each page has its own
<head>. Everywhere else a page takes the site’s.What a template or a bundler computes is shown as code and left alone:
{{ .Site.Title }},@yield('title'),<%= htmlWebpackPlugin.options.title %>,%VITE_APP_TITLE%. Trying to set it answers “This is written in code here: change it in public/index.html.”
It has been tried with plain HTML, Vite + Vue, SvelteKit, Astro, Hugo, Jekyll, Laravel and Vue CLI.
Nuxt and Docusaurus
These two don’t write a <head>: they say what goes in it in the object their config file exports. midcode reads the settings from that object and writes them into it.
| Setting | Nuxt (nuxt.config) | Docusaurus (docusaurus.config) |
|---|---|---|
| Title | app.head.title | title |
| Description | An item of app.head.meta: { name: 'description', content } | tagline |
| Language | app.head.htmlAttrs.lang | i18n.defaultLocale, and its place in i18n.locales |
| Hidden from search engines | An item of app.head.meta: { name: 'robots', content: 'noindex' } | noIndex: true |
| Favicon | An item of app.head.link: { rel: 'icon', href }, the file in public/ | favicon, the file in static/img |
| Social image | An item of app.head.meta: { property: 'og:image', content }, the file in public/ | themeConfig.image, the file in static/img |
Each edit is one value, or a new property or item written like the ones beside it, and the file has to still parse afterwards. A value the config computes shows as code and isn’t written. In a Docusaurus site with several languages, a new default language has to be one of the listed locales already. Tried with Nuxt 4 and Docusaurus 3.
Not covered
React Router and Remix write their <head> in a component (app/root.tsx), Gatsby keeps these settings in its own config, and WordPress keeps them in its database: midcode doesn’t write those yet.
AnalyticsNext release
In the next release, Site settings has an “Analytics” row under “Search engines”. Pick a provider, give it the ID it asks for, and midcode writes that provider’s script into the site. Pick “None” and the script is taken out. Either one is a single edit you can undo, and it shows in Publish like any other.
| Provider | It asks for | What’s written |
|---|---|---|
| Vercel Analytics | Nothing | <script defer src="/_vercel/insights/script.js"> |
| Plausible | The site’s domain | Plausible’s script, with data-domain |
| Umami | The website ID | Umami’s script, with data-website-id |
| Google Analytics | The measurement ID (G-…) | The gtag.js script and the short snippet that starts it |
Nothing is written until the ID is there, and an ID that doesn’t look like the provider’s is refused. Changing provider replaces the old tags with the new ones. A site that already has one of these scripts shows it here; a self-hosted Plausible or Umami keeps the address its script loads from.
Where the tags go follows the rest of Site settings:
Next.js (App Router): JSX, last in the
<body>ofapp/layout.tsx.A
<head>in markup (index.html,src/app.html, an Astro layout, the layout of a site a server renders): last in the<head>.Nuxt: an entry in
app.head.scriptinnuxt.config. Docusaurus: an entry inscriptsin its config. Google Analytics isn’t written there: Docusaurus sets it up with its own plugin.
midcode installs nothing: the tags are your site’s. They count visits on the published site, not in the canvas. Vercel Analytics also has to be turned on for the project in Vercel.
The numbers
“Analytics”, under “General” in the list on the left, shows the site’s visits for the last 7, 30 or 90 days: visitors, visits, pageviews, bounce rate and visit length, a chart by day, the top pages and where visits come from.
It works for Plausible and Umami, which answer with an API key. The first time, the view asks for one:
Plausible: Account settings → API keys → New API key, of the “Stats API” kind.
Umami Cloud: Settings → API keys → Create key.
midcode tries the key before keeping it, and says so when the provider refuses it or the key isn’t for this site. The key is kept encrypted on your Mac, is sent only to that provider (to the server your script is loaded from, when you host it yourself) and is never shown again. “Forget the key” removes it.
Google Analytics and Vercel Analytics don’t give their numbers this way: for those the view has a button that opens their own page.
Limits
These limits are about Next.js projects. Site settings needs the App Router: without a root layout (
app/layout.tsxorsrc/app/layout.tsx), the site form answers “This project has no root layout (app/layout.tsx).”Metadata that’s built in code is shown, not edited. A value that isn’t plain text appears as code under “Written in code”, and a click opens the file. If
metadatais a variable or the result of a call, or the root layout usesgenerateMetadata, the form says the metadata is built in code and sends you to the file.A page that isn’t dynamic and exports no
metadatayet doesn’t get one from the form. The write is refused with “Couldn’t tell which item this page shows”. Addexport const metadata = {}to the page’s file (or ask your agent) and the fields write into it.Variables need midcode to tell which item the page shows: a page component that takes
paramsand looks its item up with them, as in the example. When it can’t, it asks you to writegenerateMetadatayourself. The same goes for variables on a page that is a client component.The fields are the title, the description, indexing, the site URL and the social image. Keywords, Twitter cards, canonical URLs and the rest of Next.js metadata are left as you wrote them.
Images, and the layout created for a client page, show in Publish but aren’t steps in the undo history. A replaced image is in the Trash.