The Project, File by File
index.html is the entry point and main.ts mounts the app: which files in a Vue project you edit, and which are generated.
createApp(App).mount('#app')A generated project has more files than you will ever edit. Knowing which is which keeps you from changing a file that is rewritten on every build.
The entry point is index.html
It sits in the project root, not in public/. Vite treats it as source: it reads the file, follows <script type="module" src="/src/main.ts"> and serves whatever that import graph needs. The file holds one empty element, usually <div id="app"></div>. You edit it to change the page title, the language attribute or a meta tag.
main.ts starts the application
createApp(App) creates an application instance from the root component and .mount('#app') renders it into that empty element. Global CSS is imported at the top. Plugins such as a router are registered here with app.use(), which Stage 3 covers.
src, public and the generated rest
src/App.vue is the root component and src/components/ holds the others. src/assets/ holds files you import from code: Vite processes them, adds a content hash and rewrites the URL. public/ holds files served unchanged at the site root, for a favicon.ico or a robots.txt. vite.config.ts configures the build, including the Vue plugin that compiles .vue files and the @ alias for src. The tsconfig files and env.d.ts exist for type checking. node_modules/ and dist/ are generated; leave them alone and keep both out of version control.
Example
<script setup lang="ts">
const appName = 'Dashboard'
</script>
<template>
<header>
<img src="./assets/logo.svg" width="48" height="48" alt="" />
<img src="/favicon.ico" width="16" height="16" alt="" />
<h1>{{ appName }}</h1>
</header>
</template>
<style scoped>
header {
display: flex;
align-items: center;
gap: 0.5rem;
}
</style>When to use it
- Finding where to set the page title or add a meta tag in a Vue project
- Deciding whether a new image belongs in src/assets or in public
- Explaining to a teammate why editing a file in dist has no effect
More examples
index.html, the real entry point
Everything the user first downloads. The body is deliberately almost empty: Vue fills #app once main.ts runs. Edit this file for the title, the favicon link and meta tags, and nothing else; the build rewrites the script src to the hashed bundle for you.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dashboard</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>src/main.ts
The one place where the framework meets the page. createApp takes the root component and returns an application instance; mount takes a selector or a DOM element and can only be called once per instance. Anything that must happen before the first render, such as installing a plugin, goes between those two calls.
// src/main.ts
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')vite.config.ts
@vitejs/plugin-vue is what makes a .vue import work; without it the file is just unknown syntax. The alias lets you write import UserCard from '@/components/UserCard.vue' from any depth instead of counting ../ segments. Changing this file restarts the dev server.
// vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})
Discussion