diff --git a/docs/src/pages/faqs/index.mdx b/docs/src/pages/faqs/index.mdx
index 783c1414..bbbd952c 100644
--- a/docs/src/pages/faqs/index.mdx
+++ b/docs/src/pages/faqs/index.mdx
@@ -15,6 +15,8 @@
[podcast]: https://dev.to/viewsonvue/islands-architecture-in-vue-with-maximo-mussini-vue-170
[prettyUrls]: /config#prettyurls
+[vue-router]: https://next.router.vuejs.org/
+[@unhead/vue]: https://unhead.unjs.io/setup/vue/installation
# FAQs
@@ -98,7 +100,4 @@ Use Astro if you are not familiar with Vue, or don't like its template syntax.
- [unplugin-vue-components]: allows you to avoid the boilerplate
- [VitePress] and [vite-ssg]: for their different ideas on SSR
-[vue-router]: https://next.router.vuejs.org/
-[@unhead/vue]: https://github.com/@unhead/vue
-
diff --git a/docs/src/pages/guide/development.mdx b/docs/src/pages/guide/development.mdx
index 6598855d..35a2b636 100644
--- a/docs/src/pages/guide/development.mdx
+++ b/docs/src/pages/guide/development.mdx
@@ -10,6 +10,12 @@
[siteUrl]: /config#siteurl
[useDocuments]: /guide/documents
[frameworks]: /guide/frameworks
+[head and meta tags]: /guide/meta-tags
+[discussion]: https://github.com/ElMassimo/iles/discussions/6#discussioncomment-1479755
+[`@unhead/vue`]: https://unhead.unjs.io/setup/vue/installation
+[shortcodes]: /guide/markdown#advanced-provide-components-shortcodes
+[vue-router]: https://next.router.vuejs.org/
+[discussion]: https://github.com/ElMassimo/iles/discussions/6#discussioncomment-1479755
# Development 💻
@@ -35,8 +41,8 @@ In this section, we'll cover the basics to start building an application
│ ├── about.vue
│ └── index.mdx
│
- ├── app.ts
- └── site.ts
+ ├── site.ts
+ └── app.ts
@@ -56,6 +62,34 @@ Components in the src/components dir will be auto-imported on-demand,
extends this so that you don't need to import components in [MDX files][mdx].
+## Layouts 📐
+
+Components in the src/layouts dir will be available as layouts, and they should provide a default ``.
+
+Pages may specify a layout using the `layout` property in frontmatter:
+
+```md
+---
+layout: post
+---
+```
+
+Layouts and Vue pages can also specify a parent layout using a `layout` attribute in the `template`:
+
+```vue
+
+```
+
+> Layouts are optional; however, having a default layout is highly recommended.
+>
+> The `default` layout will be used for all pages unless specified.
+>
+> Pages may opt-out by specifying `layout: false`
+
+
+Layout files must be lowercase, as in `post.vue` or `default.vue`.
+
+
## Pages 🛣
[Routes will be auto-generated][routing] for files in the src/pages dir with the same file structure.
@@ -142,51 +176,176 @@ const posts = usePosts()
```
-## Layouts 📐
+## Site 🌐
-Components in the src/layouts dir will be available as layouts, and they should provide a default ``.
+`src/site.ts` can be used to provide site-wide information such as `title`, `description`, etc.
-Pages may specify a layout using the `layout` property in frontmatter:
+It can be accessed as `$site` in component instances, or by using `usePage`.
-```md
----
-layout: post
----
+It's also displayed in the page information in _Islands_ devtools.
+
+```ts
+export default {
+ title: 'About',
+ description: 'Learn more about what we do',
+}
```
-Layouts and Vue pages can also specify a parent layout using a `layout` attribute in the `template`:
+It can be accessed as `$site` in component instances, or by using `usePage`.
```vue
-
+
```
-> The `default` layout will be used for all pages unless specified.
+It's also displayed in the page information in _Islands_ devtools.
+
+> Adding `src/site.ts` to your project is optional.
+>
+> Alternatively, you can use composables from [`@unhead/vue`] to manage the [head and meta tags] of your page.
+
+## App 📱
+
+ will pre-configure a Vue 3 shell that, during development, will load your site comprising of your [layouts](#layouts) and [pages](#pages) within it.
+
+`src/app.ts` can be used to provide additional configurations with the `defineApp` helper, allowing you to customize:
+- using `enhanceApp`, the development and build logic of this "outer" shell.
+- your site's head and meta tags, scroll behavior, etc.
+
+> This "outer" shell is loaded only during development and is not included in your built site.
>
-> Pages may opt-out by specifying `layout: false`
+> Adding `src/app.ts` to your project is optional.
-
-Layout files must be lowercase, as in `post.vue` or `default.vue`.
-
+```ts
+import { defineApp } from 'iles'
-## Site 🌐
+export default defineApp({
+ enhanceApp ({ app, head, router }) {
+ // Configure the "outer" shell to customize it's development/build logic
+ },
+ enhanceIslands ({ app }) {
+ // Configure Vue Islands with plugins and other top level runtime logic
+ app.use(pinia)
+ },
+ head ({ frontmatter, site }) {
+ return {
+ meta: [
+ { property: 'author', content: site.author },
+ { property: 'keywords', content: computed(() => frontmatter.tags) },
+ ]
+ }
+ },
+ router: {
+ scrollBehavior (current, previous, savedPosition) {
+ // Configure the scroll behavior
+ },
+ },
+ mdxComponents: {
+ // Options for tag mapping in MDX
+ },
+ socialTags: true // default
+})
+```
-`src/site.ts` can be used to provide site-wide information such as `title`, `description`, etc.
+### `enhanceApp` (Development only)
-It can be accessed as `$site` in component instances, or by using `usePage`.
+- **Type:** `(context: AppContext) => void | Promise`
-It's also displayed in the page information in _Islands_ devtools.
+A hook that allows you to add additional development/build logic. See this [discussion] thread for few suggestions.
+
+### `enhanceIslands` (Vue Islands only)
+
+- **Type:** `(context: IslandContext) => void | Promise`
+
+A hook that allows you to extend your Vue Islands with plugins such as Pinia, i18n, Vuetify, and more, along with any other top-level runtime logic on your site.
+
+The hook will be invoked for every Vue Island in your site.
```ts
-export default {
- title: 'About',
- description: 'Learn more about what we do',
-}
+import { defineApp } from 'iles'
+import { createI18n } from 'vue-i18n'
+import { createPinia } from 'pinia'
+import { createVuetify } from 'vuetify'
+
+const i18n = createI18n({
+ // vue-i18n options ...
+})
+const pinia = createPinia()
+const vuetify = createVuetify({
+ // vuetify options ...
+})
+
+export default defineApp({
+ enhanceIslands({ app }) {
+ app.use(i18n)
+ app.use(pinia)
+ if(app._component.name === 'Island: ChatboxIsland') {
+ // To initialise Vuetify only within ChatboxIsland.vue
+ app.use(vuetify)
+ }
+ },
+})
```
+### `head`
+
+- **Type:** `HeadObject | (context: AppContext) => HeadObject`
+
+Set the page title, meta tags, additional CSS, or scripts using [`@unhead/vue`].
+
+### `router`
+
+- **Type:** `RouterOptions`
+
+Configure [`vue-router`][vue-router] by providing options such as `scrollBehavior` and `linkActiveClass`.
+
+### `mdxComponents`
+
+- **Type:** `MDXComponents | (context: AppContext) => MDXComponents`
+
+Provide an object that [maps tag names in MDX to components][shortcodes] to render.
+
+```ts
+import { defineApp } from 'iles'
+import Image from '~/components/Image.vue'
+
+export default defineApp({
+ mdxComponents: {
+ b: 'strong',
+ img: Image,
+ },
+})
+```
+
+### `socialTags`
+
+- **Type:** `boolean`
+- **Default:** `true`
+
+Some social tags can be inferred from the [site](#site).
+
+Set it to `false` to avoid adding social tags such as `og:title` and `twitter:description`.
+
+## Devtools 🛠
+
+Page information is available in a debug panel, similar to VitePress, but you may also access an _Islands_ inspector in Vue devtools.
+
+This can be useful when debugging [islands hydration][hydration].
+
+## Iles Configuration ⚙️
+
+You may provide a `iles.config.ts` configuration file at your project root to customize the various îles-specific features.
+
+You may also provide a `vite.config.ts` configuration file (or use the `vite` key in `iles.config.ts`) to add vite plugins and customize the various vite-specific features.
+
### Sitemap 🗺
-A sitemap can be automatically generated for your site, all you need to do is
-configure [siteUrl]. That will also make it available as `site.url` and `site.canonical`.
+A sitemap can be automatically generated when you build your site, all you need to do is configure [siteUrl] in your `iles.config.ts` configuration file.
+
+If configured, `siteUrl` will also make it available as `site.url` and `site.canonical`.
```ts
import { defineConfig } from 'iles'
@@ -206,8 +365,4 @@ export default defineConfig({
})
```
-## Devtools 🛠
-
-Page information is available in a debug panel, similar to VitePress, but you may also access an _Islands_ inspector in Vue devtools.
-
-This can be useful when debugging [islands hydration][hydration].
+> To learn more about further Iles and Vite configurations, refer to the [config] page.
\ No newline at end of file
diff --git a/docs/src/pages/guide/index.mdx b/docs/src/pages/guide/index.mdx
index 78b1ec50..1e580b64 100644
--- a/docs/src/pages/guide/index.mdx
+++ b/docs/src/pages/guide/index.mdx
@@ -1,6 +1,7 @@
[introduction]: /guide/introduction
[configuration reference]: /config
[starter]: https://github.com/ElMassimo/create-iles
+[recipes]: /recipes
# Getting Started
@@ -18,38 +19,86 @@ import stackblitzSrc from '/images/stackblitz.svg'
## Installation 💿
-Run the following command to create a new îles project.
+Run the following command to create a new îles project using the [starter] template. Then, follow the [usage](#usage) section.
```bash
pnpm create iles@next # or npm or yarn
```
-The [starter] comes with Vue components and examples. Once you have installed
-all dependencies, give it a try by running npm run dev to start the
-development server.
+## Add îles to an Existing Project 💙
-Alternatively, add `iles` to your `package.json` in an existing project.
+Add îles to your existing Vite-powered project using the following command:
-## Usage 🚀
+```bash
+npm add -D iles@latest # or pnpm, yarn, bun
+```
-The `iles` executable provides the following commands:
+îles serves pages from the `src/pages` folder, so ensure your pages are located there. They must be in one of the supported formats: `.vue`, `.md`, or `.mdx`. For example,
+
+
+
+Now, follow the next [usage](#usage) section. Also explore the [recipes] page to learn through examples.
-- iles dev: Starts the development server
-- iles build: Creates a production build of the site
-- iles preview: Preview the site after building
+## Usage 🚀
-The following shortcuts are added by default when using the [starter]:
+Add the iles executable to the `scripts` section of your `package.json` if it's not already there.
```json
"scripts": {
- "dev": "iles dev --open",
+ "dev": "iles dev",
"build": "iles build",
- "preview": "iles preview --open"
+ "preview": "iles preview --open",
+ "now": "npm run build && npm run preview"
},
```
-- npm run dev: Starts the development server
-- npm run build: Creates a production build of the site
-- npm run preview: Preview the site after building
+From your terminal or integrated terminal of your editor (Visual Studio Code),
+
+- iles dev: Starts the development server
+- iles build: Creates a production build of the site
+- iles preview: Preview the site after building
+
+```bash
+npm install # or pnpm, yarn, bun
+```
+
+### Start your development server:
+
+```bash
+npm run dev # or pnpm, yarn, bun
+```
+
+### Build for production:
+
+Generates the final output (by default, `dist` folder).
+
+```bash
+npm run build # or pnpm, yarn, bun
+```
+
+### Preview your site locally after building:
+
+```bash
+npm run preview # or pnpm, yarn, bun
+```
+
+### Alternatively, Build and Preview with a single command:
+
+```bash
+npm run now # or pnpm, yarn, bun
+```
diff --git a/docs/src/pages/guide/markdown.mdx b/docs/src/pages/guide/markdown.mdx
index 112758da..4bc96f01 100644
--- a/docs/src/pages/guide/markdown.mdx
+++ b/docs/src/pages/guide/markdown.mdx
@@ -120,7 +120,7 @@ imported if they are placed in the [components] dir.
```vue
@@ -132,7 +132,7 @@ They can also be used in MDX files as components, which is the easiest way to
reuse footers or sections that are repeated across documents.
```jsx
-import Acknowledgements from '~/components/Acknowledgements.mdx' // not needed
+import Acknowledgements from '~/components/Acknowledgements.mdx' // not required
And _without_ further ado:
diff --git a/docs/src/pages/guide/meta-tags.mdx b/docs/src/pages/guide/meta-tags.mdx
index e33c48b5..ebbf1072 100644
--- a/docs/src/pages/guide/meta-tags.mdx
+++ b/docs/src/pages/guide/meta-tags.mdx
@@ -1,6 +1,8 @@
-[@unhead/vue]: https://github.com/@unhead/vue
-[app]: /config#your-app
-[useHead]: https://github.com/@unhead/vue#api
+[@unhead/vue]: https://unhead.unjs.io/setup/vue/installation
+[app]: /guide/development#app-2
+[useHead]: https://unhead.unjs.io/usage/composables/use-head
+[useSeoMeta]: https://unhead.unjs.io/usage/composables/use-seo-meta
+[Head]: https://unhead.unjs.io/setup/vue/components
[site]: /guide/development#site
[frontmatter]: /guide/development#pages
@@ -12,32 +14,76 @@ for commonly used meta tags.
There are several ways to customize `title` and `meta` tags in , powered by [@unhead/vue].
-## [`useHead` Composable][useHead]
+## `useHead` Composable
-This helper can be used in the `setup` function or `
```
-Notice that values can be static or computed.
+> `useHead` options support both static values and reactive variables, such as `ref`, `computed`, and `reactive`.
+
+## `useSeoMeta` Composable
+
+[useSeoMeta] can be used within the `setup` function or `
+```
+
+> `useSeoMeta` options support both static values and reactive variables, such as `ref`, `computed`, and `reactive`.
-## [`` Component](https://github.com/@unhead/vue#head)
+## `` Component
-Besides [useHead], you can also manipulate head tags using the `` component:
+Besides [useHead](#usehead-composable) and [useSeoMeta](#useseometa-composable), you can also manipulate head tags using the [``][Head] component:
```vue
@@ -52,10 +98,10 @@ Besides [useHead], you can also manipulate head tags using the `[src/app.ts][app], which supports the
-same format as [useHead].
+same format as [useHead](#usehead-composable).
```ts
import { defineApp } from 'iles'
@@ -63,6 +109,7 @@ import { defineApp } from 'iles'
export default defineApp({
head: {
htmlAttrs: { lang: 'en-US' },
+ bodyAttrs: { class: 'dark-mode', 'data-theme': 'light' },
},
})
```
@@ -77,7 +124,7 @@ export default defineApp({
return {
meta: [
{ property: 'author', content: site.author },
- { property: 'keywords', content: computed(() => frontmatter.tags) },
+ { property: 'keywords', content: () => frontmatter.tags },
]
}
},
diff --git a/docs/src/pages/guide/static-assets.mdx b/docs/src/pages/guide/static-assets.mdx
new file mode 100644
index 00000000..1f0d996d
--- /dev/null
+++ b/docs/src/pages/guide/static-assets.mdx
@@ -0,0 +1,120 @@
+[@unhead/vue]: https://unhead.unjs.io/setup/vue/installation
+[app]: /guide/development#app-2
+[meta tags]: /guide/meta-tags
+[useHead]: https://unhead.unjs.io/usage/composables/use-head
+[useSeoMeta]: https://unhead.unjs.io/usage/composables/use-seo-meta
+[Head]: https://unhead.unjs.io/setup/vue/components
+[site]: /guide/development#site
+[frontmatter]: /guide/development#pages
+[vite static assets]: https://vite.dev/guide/assets.html
+[vite public directory]: https://vite.dev/guide/assets.html#the-public-directory
+[vite index html]: https://vite.dev/guide/#index-html-and-project-root
+
+# Static Assets 🍃
+
+ relies on the powerful static asset handling provided by [Vite][vite static assets] to efficiently serve and manage static assets, such as CSS and images.
+
+> Place CSS, images, JavaScript modules, and other assets within the `src` directory for bundling and optimization by [Vite][vite public directory].
+
+## `index.html`
+
+Îles automatically injects an `index.html` entry page as required by [Vite][vite index html], so you don't need to include an `index.html` file in your project root.
+
+However, you can provide a custom `index.html` entry page, giving you the flexibility to add external assets, such as `link` and `script` tags from CDNs, to all your pages.
+
+```html
+
+
+
+
+
+ Bootstrap demo
+
+
+
+
+
+
+
+```
+
+> During development, Îles will automatically add the following [shell][app] script to the injected or user-provided `index.html` in the project root.
+>
+> ``
+
+
+In Vue single-file components, `link` and `script` tags are ignored as side-effects, so external assets should not be included in the `template` section of layouts or pages.
+
+
+## `useHead` Composable
+
+[useHead] can be used within the `setup` function or `
+```
+
+> `useHead` options support both static values and reactive variables, such as `ref`, `computed`, and `reactive`.
+
+> In addition to using an `index.html` entry page and the `useHead` composable, external static assets can be added through other methods discussed in [meta tags] page.
+
+## Public folder
+
+The `public` directory at your project root stores static files that do not require processing during the build.
+
+Use the `public` folder for assets that:
+- Are not imported in source code (e.g., `robots.txt`).
+- Must retain their original file names.
+- Should be directly accessible via a URL (e.g., `/logo.png`).
+
+For example, an image at `public/logo.png` can be referenced as:
+
+```html
+
+```
+
+Files placed in the `public` folder are copied as-is to the final output (by default, `dist` folder), making it ideal for assets like images, fonts, favicons, `robots.txt`, and `manifest.webmanifest`.
\ No newline at end of file
diff --git a/docs/src/pages/recipes/index.vue b/docs/src/pages/recipes/index.vue
new file mode 100644
index 00000000..27d7c866
--- /dev/null
+++ b/docs/src/pages/recipes/index.vue
@@ -0,0 +1,35 @@
+
+ title: Recipes
+ alias: ['/recipes']
+
+
+
+
+
+
+
Recipes
+
Îles recipes are concise, focused guides covering various use cases with step-by-step instructions. They offer a great way to learn Îles while enhancing your project with new features or functionality through clear, practical examples.
+
+
+
+
diff --git a/docs/src/pages/recipes/vanilla-vue-to-iles.mdx b/docs/src/pages/recipes/vanilla-vue-to-iles.mdx
new file mode 100644
index 00000000..fa859057
--- /dev/null
+++ b/docs/src/pages/recipes/vanilla-vue-to-iles.mdx
@@ -0,0 +1,273 @@
+---
+date: 2025-01-23
+author: Ahmed Kaja
+title: 'Vanilla Vue to îles'
+twitter: '@techakayy'
+# image is relative to src/assets folder
+image: 'assets/recipes/vanilla-vue-to-iles/vanilla-vue.jpg'
+---
+{/* This component overrides MetaTags.vue */}
+
+
+[vite index html]: https://vite.dev/guide/#index-html-and-project-root
+[static assets]: /guide/static-assets
+[usage]: /guide#usage
+[frameworks]: /guide/frameworks
+[hydration]: /guide/hydration
+[VitePress]: https://vitepress.dev/
+[Vite-SSG]: https://github.com/antfu-collective/vite-ssg
+[project structure]: /guide/development
+[frameworks]: /guide/frameworks
+[client directives]: /guide/hydration
+[client scripts]: /guide/client-scripts
+
+New to Vue? 💚 Scaffold a new Vue project using the following command. Then, convert it into an îles project.
+
+```bash
+npm create vue@latest # or pnpm, yarn, bun
+```
+
+The scaffolded Vanilla Vue project is powered by Vite and serves as a great example of basic Vue concepts, including the Single File Component structure, props, and slots. It's a Single-Page Application (SPA) with snappy navigation, powered by `vue-router`.
+
+# Introduction
+
+In this guide, we will convert this SPA into a static Multi-Page Application (MPA) site. Once completed, we will build and deploy static HTML pages with `zero` JavaScript.
+
+The goal is to understand the concepts by migrating between the two flavors (SPA and MPA).
+
+ builds static Multi-Page Application (MPA) sites. Checkout [Vitepress] and [Vite-SSG] to build static Single Page Application (SPA) sites.
+
+
+
+## 1. Install îles
+
+Before installing, since îles pre-includes `vite`, remove `"vite": "6.x.x"` from your `package.json` to prevent multiple Vite installations, which could cause TypeScript issues.
+
+```bash
+npm add -D iles@latest # or pnpm, yarn, bun
+```
+
+Once completed, start your development server with the following CLI command. Alternatively, refer to the [usage] to add commands to your `package.json`.
+
+```bash
+npx iles dev # or pnpm, yarn, bun
+```
+
+## 2. Remove `index.html` or Its Entry Files
+
+The `index.html` entry page in your project root does not load any other assets apart from the entry file (`/src/main.ts`). Therefore, either remove it or delete the below script tag shown below.
+
+```html
+
+```
+
+If you want to retain this `index.html` to add external assets from CDN, refer to the [static assets] page for details. Additionally, remove the `title` and `meta` tags in your custom `index.html` as Îles will add them automatically.
+
+## 3. Add `iles.config.ts`
+
+Create an `iles.config.ts` file at your project root as shown below.
+
+```ts
+import { defineConfig } from 'iles'
+
+export default defineConfig({
+ siteUrl: 'https://myawesomeidea.com', // Your site URL
+ modules: [
+ // Add iles modules here, refer to Modules page
+ ],
+})
+```
+
+### Existing `vite.config.ts`
+
+The `vite.config.ts` file in your project root, you can either keep it or migrate its configuration into the `vite` key of `iles.config.ts` as shown below.
+
+Important Remove `@vitejs/plugin-vue` (Vite plugin for Vue)
+
+- Keeping `vite.config.ts`: Remove `@vitejs/plugin-vue` from its plugins array to avoid conflicts, as îles already includes it.
+
+- Migrating to `iles.config.ts`: Do not include `@vitejs/plugin-vue` in the migration. After migrating, remove `vite.config.ts` file from your project root. You can also skip migrating `resolve.alias` object, as Îles already covers that.
+
+```ts
+import { defineConfig } from 'iles'
+import vueDevTools from 'vite-plugin-vue-devtools'
+
+export default defineConfig({
+ siteUrl: 'https://myawesomeidea.com', // Your site URL
+ modules: [
+ // Add iles modules here, refer to Modules page
+ ],
+ vite: {
+ // Vite configuration goes here
+ plugins: [
+ // Add vite plugins here
+ vueDevTools(),
+ ]
+ }
+})
+```
+
+## 4. Add `app.ts`
+
+- Create an `src/app.ts` file as shown below.
+- Copy `import './assets/main.css'` from your entry file `src/main.ts` into `app.ts`.
+- Remove or archive `main.ts` as it's not required anymore.
+
+```ts
+import '@/assets/main.css' // from `src/main.ts`
+import { defineApp } from 'iles'
+
+export default defineApp({
+ head ({ frontmatter, site }) {
+ return {
+ meta: [
+ { property: 'author', content: site.author },
+ { property: 'keywords', content: () => frontmatter.tags },
+ ],
+ htmlAttrs: { lang: 'en-US' },
+ bodyAttrs: {},
+ }
+ },
+})
+```
+
+## 5. Add `site.ts`
+
+Create an `src/site.ts` file as shown below:
+
+```ts
+export default {
+ title: 'Cafe Tee Kaapi',
+ description: 'Sip, Savor, and Spark Ideas!',
+}
+```
+
+## 6. `App.vue` to Default Layout
+
+- Create a new `src/layouts` folder, copy `App.vue` into it, and rename it to `default.vue`.
+- Convert the `RouterLink` components to simple anchor tags, change the `to` prop to `href`.
+- Replace `` with ``. This slot will load all your pages in your site.
+
+Your `layouts/default.vue` will look like the below.
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+> Remove the `src/router` folder, as îles uses file-based routing. Any custom logic might need to be migrated to `app.ts`, your default layout, or one of your interactive Islands.
+
+## 7. View to Pages
+
+îles uses the `pages` and `layouts` folder conventions with filenames in kebab-case.
+- Rename the folder that contains all your pages (for example, `src/views`) to `src/pages`.
+- Rename the `.vue` file of your home page (for example, `HomeView.vue`) to `index.vue`.
+- Rename the `.vue` files of your other pages to follow kebab-case. For example, `AboutView.vue` could be renamed to `about.vue`
+- Add a `` tag at the top of your page files and add a `title` and `description` as `frontmatter`.
+
+Your `index.vue` page will look like the below.
+
+```vue
+
+ title: Home
+ description: First Coffee, Then Conquer!
+
+
+
+
+
+
+
+
+
+```
+
+## 8. Add a New Page
+
+îles provides excellent support for authoring your pages with markdown (`.md`, `.mdx`).
+
+- Create a third page `src/pages/our-story.md` as shown below.
+- Add a third anchor tag for `our-story` under `nav` in `layouts/default.vue`.
+
+```md
+---
+title: Our Story
+description: Crafting Memories, Brewing History!
+---
+
+# This is our story page
+```
+
+```vue
+
+
+```
+
+## 9. Build Your Site
+
+- Stop your development server by pressing `Cmd/Ctrl+C` in your terminal.
+- Use the following command to build your site.
+- Alternatively, refer to the [usage] to add commands to your `package.json`.
+
+```bash
+npx iles build # or pnpm, yarn, bun
+```
+
+Check the `dist` folder to see the three HTML pages generated along with their CSS assets.
+
+Note that `zero` JavaScript is shipped. To add interactive Islands, refer to the [frameworks] and [hydration] pages to learn about hydration client scripts.
+
+
+
+## 10. Preview Your Site
+
+- Use the following command to preview your site in your local browser.
+- Alternatively, drag and drop the `dist` folder from your Finder or file-explorer into [Netlify Drop](https://app.netlify.com/drop) for instant deployment and preview.
+- Test your production site by navigating through the different pages.
+
+```bash
+npx iles preview # or pnpm, yarn, bun
+```
+
+
+
+If you used Netlify Drop, once deployment has completed successfully, click the `Open Production deploy` button to preview your site.
+
+
+
+## What's Next
+
+Now, learn about the [project structure] to familiarize yourself with the various conventions and best practices in îles.
+
+To add interactive îslands to your static Îles site, explore using [frameworks], [client directives], and [client scripts].
+
+Well Done. Have a joyful Îles experience!
\ No newline at end of file
diff --git a/docs/src/site.ts b/docs/src/site.ts
index 26318029..451a6f59 100644
--- a/docs/src/site.ts
+++ b/docs/src/site.ts
@@ -21,6 +21,7 @@ const site = {
{ text: 'Guide', link: '/guide' },
{ text: 'Config', link: '/config' },
{ text: 'FAQs', link: '/faqs' },
+ { text: 'Recipes', link: '/recipes' },
],
sidebar: [
@@ -34,6 +35,7 @@ const site = {
{ text: 'Documents', link: '/guide/documents' },
{ text: 'Markdown', link: '/guide/markdown' },
{ text: 'Meta Tags', link: '/guide/meta-tags' },
+ { text: 'Static Assets', link: '/guide/static-assets' },
{ text: 'Hydration', link: '/guide/hydration' },
{ text: 'Frameworks', link: '/guide/frameworks' },
{ text: 'Client Scripts', link: '/guide/client-scripts' },
@@ -54,6 +56,21 @@ const site = {
{ text: 'Troubleshooting', link: '/faqs/troubleshooting' },
],
},
+ {
+ text: 'Recipes',
+ link: '/recipes',
+ children: [
+ {
+ text: 'Recipes',
+ link: '/recipes'
+ },
+ {
+ text: 'Vanilla Vue to Îles',
+ link: '/recipes/vanilla-vue-to-iles'
+ }
+ ],
+ },
+
],
}
diff --git a/docs/tsconfig.json b/docs/tsconfig.json
index 253b35b4..371b2a9f 100644
--- a/docs/tsconfig.json
+++ b/docs/tsconfig.json
@@ -3,7 +3,7 @@
"baseUrl": ".",
"module": "esnext",
"target": "esnext",
- "moduleResolution": "node",
+ "moduleResolution": "bundler",
"allowJs": true,
"jsx": "preserve",
"jsxFactory": "h",
@@ -24,5 +24,9 @@
},
"exclude": [
"node_modules/cypress"
+ ],
+ "include": [
+ "components.d.ts",
+ "composables.d.ts"
]
}
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 7061d125..e6e2a2e0 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -11,6 +11,7 @@ export default antfu(
usePage: 'readonly',
useRoute: 'readonly',
useHead: 'readonly',
+ useSeoMeta: 'readonly',
definePageComponent: 'readonly',
},
},
@@ -146,6 +147,11 @@ export default antfu(
'vue/block-tag-newline': 'off',
'regexp/no-useless-lazy': 'off',
'style/eol-last': 'off',
+ 'perfectionist/sort-imports': 'off',
+ 'perfectionist/sort-exports': 'off',
+ 'perfectionist/sort-named-imports': 'off',
+ 'perfectionist/sort-named-exports': 'off',
+ 'unicorn/new-for-builtins': 'off'
},
},
)
diff --git a/package.json b/package.json
index 5ae0e606..315a9503 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,7 @@
"packageManager": "pnpm@9.8.0",
"scripts": {
"build:all": "pnpm nx run-many --target=build --all --exclude docs --exclude vue-blog",
+ "dev": "pnpm -r --parallel --filter './packages/**' run dev",
"lint:all": "pnpm nx run-many --target=lint --all",
"lint:fix:all": "pnpm nx run-many --target=lint:fix --all",
"docs": "npm -C docs run dev",
@@ -20,17 +21,17 @@
"test": "packages/iles/bin/iles.js test"
},
"devDependencies": {
- "@antfu/eslint-config": "^2.24.0",
+ "@antfu/eslint-config": "^3.15.0",
"@nrwl/cli": "^15.9.3",
"@nrwl/nx-cloud": "^19.1.0",
"@nx/workspace": "^19.7.3",
"concurrently": "^8.2.2",
"conventional-changelog-cli": "^5.0.0",
"enquirer": "^2.4.1",
- "eslint": "^9.8.0",
- "eslint-plugin-format": "^0.1.2",
- "eslint-plugin-solid": "^0.14.1",
- "eslint-plugin-svelte": "^2.43.0",
+ "eslint": "^9.18.0",
+ "eslint-plugin-format": "^1.0.1",
+ "eslint-plugin-solid": "^0.14.5",
+ "eslint-plugin-svelte": "^2.46.1",
"execa": "^9.3.0",
"iles": "workspace:*",
"minimist": "^1.2.8",
@@ -38,17 +39,19 @@
"semver": "^7.6.3",
"tsx": "^4.19.2",
"typescript": "^5.6.3",
- "vitest": "^2.1.4"
+ "vitest": "^3.0.3"
},
"pnpm": {
"overrides": {
"@typescript-eslint/typescript-estree": "8.5.0",
"esbuild": "0.24.0",
- "vite": "5.4.10"
+ "vite": "6.0.11"
},
"peerDependencyRules": {
"allowedVersions": {
- "eslint": "9"
+ "@typescript-eslint/typescript-estree": "8.5.0",
+ "esbuild": "0.24.0",
+ "vite": "6.0.11"
}
}
}
diff --git a/packages/excerpt/package.json b/packages/excerpt/package.json
index eccb84a4..ff35becc 100644
--- a/packages/excerpt/package.json
+++ b/packages/excerpt/package.json
@@ -12,9 +12,9 @@
"dist",
"src"
],
- "types": "dist/excerpt.d.ts",
"exports": {
".": {
+ "types": "./dist/excerpt.d.ts",
"import": "./dist/excerpt.js",
"require": "./src/excerpt.cjs"
},
@@ -41,4 +41,4 @@
"typescript": "^5.6.3",
"unified": "^11.0.5"
}
-}
+}
\ No newline at end of file
diff --git a/packages/feed/package.json b/packages/feed/package.json
index f55577b5..42c37918 100644
--- a/packages/feed/package.json
+++ b/packages/feed/package.json
@@ -11,10 +11,10 @@
"dist",
"src"
],
- "types": "dist/feed.d.ts",
"type": "module",
"exports": {
".": {
+ "types": "./dist/feed.d.ts",
"import": "./dist/feed.js",
"require": "./src/feed.cjs"
},
@@ -40,4 +40,4 @@
"feed": "^4.2",
"pathe": "^1.1.2"
}
-}
+}
\ No newline at end of file
diff --git a/packages/headings/package.json b/packages/headings/package.json
index 4ec2e69c..6c493a1b 100644
--- a/packages/headings/package.json
+++ b/packages/headings/package.json
@@ -12,9 +12,9 @@
"dist",
"src"
],
- "types": "dist/headings.d.ts",
"exports": {
".": {
+ "types": "./dist/headings.d.ts",
"import": "./dist/headings.js",
"require": "./src/headings.cjs"
},
@@ -40,4 +40,4 @@
"typescript": "^5.6.3",
"unified": "^11.0.5"
}
-}
+}
\ No newline at end of file
diff --git a/packages/hydration/hydration.ts b/packages/hydration/hydration.ts
index 6c4bfc68..ead42d19 100644
--- a/packages/hydration/hydration.ts
+++ b/packages/hydration/hydration.ts
@@ -1,38 +1,39 @@
-import { AsyncFrameworkFn, FrameworkFn, Component, AsyncComponent, Props, Slots } from './types'
+import type { AsyncComponent, AsyncFrameworkFn, Component, EnhanceIslands, FrameworkFn, Props, Slots } from './types'
+
export { Framework, Props, Slots } from './types'
const findById = (id: string) =>
document.getElementById(id) || console.error(`Missing #${id}, could not mount island.`)
// Public: Hydrates the component immediately.
-export function hydrateNow (framework: FrameworkFn, component: Component, id: string, props: Props, slots: Slots) {
+export function hydrateNow (framework: FrameworkFn, component: Component, id: string, props: Props, slots: Slots, enhanceIslands: EnhanceIslands) {
const el = findById(id)
if (el) {
- framework(component, id, el, props, slots)
+ framework(component, id, el, props, slots, enhanceIslands)
el.setAttribute('hydrated', '')
}
}
-async function resolveAndHydrate (frameworkFn: AsyncFrameworkFn, componentFn: AsyncComponent, id: string, props: Props, slots: Slots) {
+async function resolveAndHydrate (frameworkFn: AsyncFrameworkFn, componentFn: AsyncComponent, id: string, props: Props, slots: Slots, enhanceIslands: EnhanceIslands) {
const [framework, component] = await Promise.all([frameworkFn(), componentFn()])
- hydrateNow(framework, component, id, props, slots)
+ hydrateNow(framework, component, id, props, slots, enhanceIslands)
}
// Public: Hydrate this component as soon as the main thread is free.
// If `requestIdleCallback` isn't supported, it uses a small delay.
-export function hydrateWhenIdle (framework: AsyncFrameworkFn, component: AsyncComponent, id: string, props: Props, slots: Slots) {
+export function hydrateWhenIdle (framework: AsyncFrameworkFn, component: AsyncComponent, id: string, props: Props, slots: Slots, enhanceIslands: EnhanceIslands) {
const whenIdle = window.requestIdleCallback || setTimeout
const cancelIdle = window.cancelIdleCallback || clearTimeout
const idleId: any = whenIdle(() =>
- resolveAndHydrate(framework, component, id, props, slots))
+ resolveAndHydrate(framework, component, id, props, slots, enhanceIslands))
if (import.meta.env.DISPOSE_ISLANDS)
onDispose(id, () => cancelIdle(idleId))
}
// Public: Hydrate this component when the specified media query is matched.
-export function hydrateOnMediaQuery (framework: AsyncFrameworkFn, component: AsyncComponent, id: string, props: Props, slots: Slots) {
+export function hydrateOnMediaQuery (framework: AsyncFrameworkFn, component: AsyncComponent, id: string, props: Props, slots: Slots, enhanceIslands: EnhanceIslands) {
const mediaQuery = matchMedia(props._mediaQuery as string)
delete props._mediaQuery
@@ -40,7 +41,7 @@ export function hydrateOnMediaQuery (framework: AsyncFrameworkFn, component: Asy
const hydrate = () => {
onChange()
- resolveAndHydrate(framework, component, id, props, slots)
+ resolveAndHydrate(framework, component, id, props, slots, enhanceIslands)
}
mediaQuery.matches ? hydrate() : onChange(hydrate)
@@ -50,7 +51,7 @@ export function hydrateOnMediaQuery (framework: AsyncFrameworkFn, component: Asy
}
// Public: Hydrate this component when one of it's children becomes visible.
-export function hydrateWhenVisible (framework: AsyncFrameworkFn, component: AsyncComponent, id: string, props: Props, slots: Slots) {
+export function hydrateWhenVisible (framework: AsyncFrameworkFn, component: AsyncComponent, id: string, props: Props, slots: Slots, enhanceIslands: EnhanceIslands) {
const el = findById(id)
if (el) {
// NOTE: Force detection of the element for non-Vue frameworks.
@@ -65,7 +66,7 @@ export function hydrateWhenVisible (framework: AsyncFrameworkFn, component: Asyn
if (import.meta.env.DEV)
el.style.display = ''
- resolveAndHydrate(framework, component, id, props, slots)
+ resolveAndHydrate(framework, component, id, props, slots, enhanceIslands)
}
})
const stopObserver = () => observer.disconnect()
@@ -79,4 +80,4 @@ export function hydrateWhenVisible (framework: AsyncFrameworkFn, component: Asyn
// Internal: Invoked before navigation when turbo is enabled, or before HMR.
export const onDispose = (id: string, fn: () => void) =>
- (window as any).__ILE_DISPOSE__?.set(id, fn)
+ (window as any).__ILE_DISPOSE__?.set(id, fn)
\ No newline at end of file
diff --git a/packages/hydration/package.json b/packages/hydration/package.json
index 4decb486..83a15386 100644
--- a/packages/hydration/package.json
+++ b/packages/hydration/package.json
@@ -41,10 +41,10 @@
"preact": "^10.24.3",
"preact-render-to-string": "^6.5.11",
"solid-js": "^1.9.3",
- "svelte": "^5.1.13",
+ "svelte": "^5.19.1",
"tsup": "8.2.4",
"typescript": "^5.6.3",
- "vite": "^5.4.10",
+ "vite": "6.0.11",
"vue": "^3.5.12"
}
}
diff --git a/packages/hydration/types.ts b/packages/hydration/types.ts
index ddf1e8d2..008e0ef2 100644
--- a/packages/hydration/types.ts
+++ b/packages/hydration/types.ts
@@ -2,6 +2,8 @@
import type Vue from './vue'
+export type { EnhanceIslands } from '../iles/types/shared'
+
export type Framework = 'vue' | 'preact' | 'solid' | 'svelte' | 'vanilla'
export type FrameworkFn = typeof Vue
export type AsyncFrameworkFn = () => Promise
diff --git a/packages/hydration/vue.ts b/packages/hydration/vue.ts
index 1948c7dc..819e7de4 100644
--- a/packages/hydration/vue.ts
+++ b/packages/hydration/vue.ts
@@ -1,12 +1,12 @@
-import { h, createApp as createClientApp, createStaticVNode, createSSRApp } from 'vue'
-import type { DefineComponent as Component, Component as App } from 'vue'
-import type { Props, Slots } from './types'
+import { createApp as createClientApp, createSSRApp, createStaticVNode, h } from 'vue'
+import type { Component as App, DefineComponent as Component } from 'vue'
+import type { EnhanceIslands, Props, Slots } from './types'
import { onDispose } from './hydration'
const createVueApp = import.meta.env.SSR ? createSSRApp : createClientApp
// Internal: Creates a Vue app and mounts it on the specified island root.
-export default function createVueIsland (component: Component, id: string, el: Element, props: Props, slots: Slots | undefined) {
+export default async function createVueIsland (component: Component, id: string, el: Element, props: Props, slots: Slots | undefined, enhanceIslands: EnhanceIslands) {
const slotFns = slots && Object.fromEntries(Object.entries(slots).map(([slotName, content]) => {
return [slotName, () => (createStaticVNode as any)(content)]
}))
@@ -17,6 +17,9 @@ export default function createVueIsland (component: Component, id: string, el: E
appDefinition.name = `Island: ${nameFromFile(component.__file)}`
const app = createVueApp(appDefinition)
+ if (enhanceIslands)
+ await enhanceIslands({ app })
+
app.mount(el!, Boolean(slots))
if (import.meta.env.DISPOSE_ISLANDS)
@@ -29,4 +32,4 @@ export default function createVueIsland (component: Component, id: string, el: E
function nameFromFile (file?: string) {
const regex = /(\w+?)(?:\.vue)?$/
return file?.match(regex)?.[1] || file
-}
+}
\ No newline at end of file
diff --git a/packages/icons/package.json b/packages/icons/package.json
index 79b52d7a..77823a25 100644
--- a/packages/icons/package.json
+++ b/packages/icons/package.json
@@ -11,10 +11,10 @@
"dist",
"src"
],
- "types": "dist/icons.d.cts",
"type": "module",
"exports": {
".": {
+ "types": "./dist/icons.d.cts",
"import": "./src/icons.mjs",
"require": "./dist/icons.cjs"
},
@@ -34,4 +34,4 @@
"devDependencies": {
"iles": "workspace:*"
}
-}
+}
\ No newline at end of file
diff --git a/packages/iles/package.json b/packages/iles/package.json
index c58f3901..134421d9 100644
--- a/packages/iles/package.json
+++ b/packages/iles/package.json
@@ -8,7 +8,6 @@
},
"main": "./dist/node/index.js",
"module": "./dist/node/index.js",
- "types": "./types/index.d.ts",
"exports": {
".": {
"types": "./types/index.d.ts",
@@ -81,17 +80,18 @@
"mico-spinner": "^1.4.0",
"micromatch": "^4.0.7",
"minimist": "^1.2.8",
+ "parse5": "^7.2.1",
"pathe": "^1.1.2",
"picocolors": "^1.0.1",
"unist-util-visit": "^5.0.0",
"unplugin-vue-components": "^0.27.3",
- "vite": "^5.4.10",
+ "vite": "6.0.11",
"vue": "^3.5.12",
"vue-router": "^4.4.0"
},
"devDependencies": {
"@preact/preset-vite": "^2.9.1",
- "@sveltejs/vite-plugin-svelte": "^4.0.0",
+ "@sveltejs/vite-plugin-svelte": "^4.0.4",
"@types/debug": "^4.1.12",
"@types/fs-extra": "^11.0.4",
"@types/micromatch": "^4.0.9",
@@ -117,4 +117,4 @@
"vite-plugin-solid": "^2.10.2",
"vue-tsc": "^2.1.10"
}
-}
+}
\ No newline at end of file
diff --git a/packages/iles/src/client/app/components/Island.vue b/packages/iles/src/client/app/components/Island.vue
index d8978932..f95f64a4 100644
--- a/packages/iles/src/client/app/components/Island.vue
+++ b/packages/iles/src/client/app/components/Island.vue
@@ -83,6 +83,7 @@ export default defineComponent({
const { _, ...slots } = this.$slots
const slotVNodes = mapObject(slots, slotFn => slotFn?.())
const hydrationPkg = `${isSSR ? '' : '/@id/'}@islands/hydration`
+ const userAppPkg = `${isSSR ? '@islands/user-app' : '/@id/virtual:user-app'}`
let renderedSlots: Record
const renderSlots = async () =>
@@ -94,13 +95,16 @@ export default defineComponent({
const frameworkPath = `${hydrationPkg}/${this.framework}`
return `import { ${hydrationFns[this.strategy]} as hydrate } from '${hydrationPkg}'
+import userApp from '${userAppPkg}'
+
+const { enhanceIslands } = userApp
${isEager(this.strategy)
? `import framework from '${frameworkPath}'
import { ${this.importName} as component } from '${componentPath}'`
: `const framework = async () => (await import('${frameworkPath}')).default
const component = async () => (await import('${componentPath}')).${this.importName}`
}
-hydrate(framework, component, '${this.id}', ${serialize(props)}, ${serialize(slots)})
+hydrate(framework, component, '${this.id}', ${serialize(props)}, ${serialize(slots)}, enhanceIslands)
`
}
diff --git a/packages/iles/src/client/app/composables/pageData.ts b/packages/iles/src/client/app/composables/pageData.ts
index b81543ee..91ba2f83 100644
--- a/packages/iles/src/client/app/composables/pageData.ts
+++ b/packages/iles/src/client/app/composables/pageData.ts
@@ -42,6 +42,12 @@ export function installPageData (app: App, siteRef: Ref): PageData {
const meta = reactiveFromFn(() => page.value.meta || {})
const frontmatter = reactiveFromFn(() => page.value.frontmatter || {})
const props = computedInPage(() => propsFromRoute(route))
+
+ siteRef.value = {
+ title: 'Îles',
+ description: 'An Îles Site',
+ ...siteRef.value,
+ }
const site = toReactive(siteRef)
const pageData: PageData = { route, page, meta, frontmatter, site, props }
diff --git a/packages/iles/src/client/app/head.ts b/packages/iles/src/client/app/head.ts
index 183008b5..769ba308 100644
--- a/packages/iles/src/client/app/head.ts
+++ b/packages/iles/src/client/app/head.ts
@@ -28,10 +28,10 @@ export function defaultHead ({ frontmatter, meta, route, config, site }: AppCont
{ property: 'og:site_name', content: site.title },
{ property: 'og:title', content: title },
{ property: 'og:description', content: description },
- { property: 'twitter:domain', content: site.canonical },
- { property: 'twitter:title', content: title },
- { property: 'twitter:description', content: description },
- { property: 'twitter:url', content: currentUrl },
+ { name: 'twitter:domain', content: site.canonical },
+ { name: 'twitter:title', content: title },
+ { name: 'twitter:description', content: description },
+ { name: 'twitter:url', content: currentUrl },
)
}
diff --git a/packages/iles/src/client/app/index.ts b/packages/iles/src/client/app/index.ts
index d175c9e7..81c4dd21 100644
--- a/packages/iles/src/client/app/index.ts
+++ b/packages/iles/src/client/app/index.ts
@@ -4,9 +4,7 @@ import { createHead } from '@unhead/vue'
import routes from '@islands/routes'
import config from '@islands/app-config'
-import userApp from '@islands/user-app'
-import siteRef from '@islands/user-site'
-import type { CreateAppFactory, AppContext, RouterOptions } from '../shared'
+import type { CreateAppFactory, AppContext, RouterOptions, UserApp, UserSite } from '../shared'
import App from './components/App.vue'
import { installPageData, forcePageUpdate } from './composables/pageData'
import { installMDXComponents } from './composables/mdxComponents'
@@ -32,8 +30,28 @@ function createRouter (base: string | undefined, routerOptions: Partial {
- const { head: headConfig, enhanceApp, router: routerOptions } = userApp
+ let userApp: UserApp
+ try {
+ userApp = unwrapModule(await import('@islands/user-app'))
+ }
+ catch (err) {
+ userApp = {}
+ }
+
+ let siteRef: UserSite
+ try {
+ siteRef = unwrapModule(await import('@islands/user-site'))
+ }
+ catch (err) {
+ siteRef = {}
+ }
+
+ const { head: headConfig, enhanceIslands, enhanceApp, router: routerOptions } = userApp
const { routePath = config.base, ssrProps } = options
const app = newApp(App)
@@ -78,7 +96,14 @@ export const createApp: CreateAppFactory = async (options = {}) => {
// Apply any configuration added by the user in app.ts
// if (headConfig) useHead(ref(typeof headConfig === 'function' ? headConfig(context) : headConfig))
if (headConfig) head.push(ref(typeof headConfig === 'function' ? headConfig(context) : headConfig))
- if (enhanceApp) await enhanceApp(context)
+
+ // enhanceIslands is called on the shell app during development otherwise user will have to duplicate `app.use(pinia)` in both enhanceIslands and enhanceApp
+ if (enhanceIslands) {
+ await enhanceIslands({ app })
+ }
+ if (enhanceApp) {
+ await enhanceApp(context)
+ }
await installMDXComponents(context, userApp)
return context
diff --git a/packages/iles/src/client/index.ts b/packages/iles/src/client/index.ts
index 133286ce..8e9a80ac 100644
--- a/packages/iles/src/client/index.ts
+++ b/packages/iles/src/client/index.ts
@@ -11,7 +11,7 @@ export { usePage, computedInPage } from './app/composables/pageData'
export { useMDXComponents, provideMDXComponents } from './app/composables/mdxComponents'
export { useVueRenderer } from './app/composables/vueRenderer'
export { useRouter, useRoute } from 'vue-router'
-export { useHead } from '@unhead/vue'
+export { useHead, useSeoMeta } from '@unhead/vue'
import type { ComponentOptionsWithoutProps, ComputedRef } from 'vue'
import type { UserApp, GetStaticPaths, Document } from '../../types/shared'
diff --git a/packages/iles/src/node/alias.ts b/packages/iles/src/node/alias.ts
index aa5fa37d..6e3c331a 100644
--- a/packages/iles/src/node/alias.ts
+++ b/packages/iles/src/node/alias.ts
@@ -30,9 +30,13 @@ export const APP_CONFIG_REQUEST_PATH = `/${APP_CONFIG_ID}`
export const USER_APP_ID = '@islands/user-app'
export const USER_APP_REQUEST_PATH = `/${USER_APP_ID}`
+export const USER_APP_ID_VIRTUAL = 'virtual:user-app'
+export const USER_APP_ID_VIRTUAL_RESOLVED = `\0${USER_APP_ID_VIRTUAL}`
export const USER_SITE_ID = '@islands/user-site'
export const USER_SITE_REQUEST_PATH = `/${USER_SITE_ID}`
+export const USER_SITE_ID_VIRTUAL = 'virtual:user-site'
+export const USER_SITE_ID_VIRTUAL_RESOLVED = `\0${USER_SITE_ID_VIRTUAL}`
export const NOT_FOUND_REQUEST_PATH = '@islands/components/NotFound'
@@ -40,7 +44,9 @@ export function resolveAliases (root: string, userConfig: UserConfig): AliasOpti
const paths: Record = {
'/@shared': SHARED_PATH,
[USER_APP_ID]: USER_APP_REQUEST_PATH,
+ [USER_APP_ID_VIRTUAL]: USER_APP_ID_VIRTUAL_RESOLVED,
[USER_SITE_ID]: USER_SITE_REQUEST_PATH,
+ [USER_SITE_ID_VIRTUAL]: USER_SITE_ID_VIRTUAL_RESOLVED,
[APP_CONFIG_ID]: APP_CONFIG_REQUEST_PATH,
}
diff --git a/packages/iles/src/node/build/render.ts b/packages/iles/src/node/build/render.ts
index 1f598617..7f82d0de 100644
--- a/packages/iles/src/node/build/render.ts
+++ b/packages/iles/src/node/build/render.ts
@@ -9,6 +9,7 @@ import type { Awaited, AppConfig, CreateAppFactory, IslandsByPath, RouteToRender
import type { bundle } from './bundle'
import { withSpinner } from './utils'
import { getRoutesToRender } from './routes'
+import { getUserShell } from '../plugin/html';
const commentsRegex = /||/g
@@ -55,7 +56,41 @@ export async function renderPage (
const { headTags, htmlAttrs, bodyTagsOpen, bodyTags, bodyAttrs } = await renderSSRHead(head)
- return `
+ const {
+ userShell,
+ isValidUserShell,
+ errorMsgUserShell,
+ } = await getUserShell(config)
+
+ if (!isValidUserShell) {
+ throw new Error(errorMsgUserShell)
+ }
+
+ // Tried prettier format(), but it adds closing tag to void tags (link) which results in css not loading correctly, hence dropped it
+ // let transformedHtml = await format(userShell, { parser: 'html' });
+ // TODO: prettierx is a prettier fork with option to format without this issue
+ // let transformedHtml = await format(userShell, { parser: 'html', htmlVoidTags: true });
+ let transformedHtml = userShell
+
+ // Add if missing
+ const doctypeRegex = /^\s*/i
+ if (!doctypeRegex.test(transformedHtml)) {
+ transformedHtml = `${transformedHtml}`;
+ }
+
+ // const ilesDevShellRegex = /';
+ const bodyCloseTag = '';
+ return await getUserShell(config, userShell.replace(bodyCloseTag, `${ilesDevShell}\n${bodyCloseTag}`))
+ }
+
+ if (!divAppForShell) {
+ errorMsgUserShell = `[îles] index.html doesn't contain an div#app placeholder to load shell. Add inside body.`
+ isValidUserShell = false
+ }
+
+ if (!isValidUserShell) {
+ throw new Error(errorMsgUserShell)
+ }
+
+ return {
+ userShellPath,
+ userShell,
+ isValidUserShell,
+ errorMsgUserShell,
+ }
+}
diff --git a/packages/iles/src/node/plugin/middleware.ts b/packages/iles/src/node/plugin/middleware.ts
index e232e71f..dbfe0a0a 100644
--- a/packages/iles/src/node/plugin/middleware.ts
+++ b/packages/iles/src/node/plugin/middleware.ts
@@ -17,6 +17,13 @@ const debug = createDebugger('iles:html-page-fallback')
export function configureMiddleware (config: AppConfig, server: ViteDevServer, defaultLayoutPath: string) {
restartOnConfigChanges(config, server)
+ // If user included the vite vue plugin via their own vite.config.ts, then iles's vite vue plugin will consume a transformed sfc and error out. So, error out and alert user
+ const vueVitePlugins = server.config.plugins.filter(plugin => plugin.name === 'vite-plugin-vue' || plugin.name === 'vite:vue')
+
+ if (vueVitePlugins.length > 1) {
+ throw new Error(`[îles] Duplicate Vue Vite plugin detected. Ensure @vitejs/plugin-vue is removed from the Vite plugins array in vite.config.ts or iles.config.ts. Use the 'vue' property in iles.config.ts to pass any configuration to the Vue Vite plugin which is already included by îles.\n`)
+ }
+
const htmlPagesMiddleware: Connect.NextHandleFunction = function ilesHtmlPagesMiddleware (req, res, next) {
let { url = '' } = req
diff --git a/packages/iles/src/node/plugin/plugin.ts b/packages/iles/src/node/plugin/plugin.ts
index 5b29d434..b067a5aa 100644
--- a/packages/iles/src/node/plugin/plugin.ts
+++ b/packages/iles/src/node/plugin/plugin.ts
@@ -2,12 +2,13 @@ import { promises as fs } from 'fs'
import { basename, resolve, relative } from 'pathe'
import type { PluginOption, ResolvedConfig, ViteDevServer } from 'vite'
import { transformWithEsbuild } from 'vite'
+import { getUserShell } from './html';
import MagicString from 'magic-string'
import type { AppConfig, AppClientConfig } from '../shared'
import { ILES_APP_ENTRY } from '../constants'
-import { APP_PATH, APP_COMPONENT_PATH, USER_APP_REQUEST_PATH, USER_SITE_REQUEST_PATH, APP_CONFIG_REQUEST_PATH, NOT_FOUND_COMPONENT_PATH, NOT_FOUND_REQUEST_PATH, DEBUG_COMPONENT_PATH } from '../alias'
+import { APP_PATH, APP_COMPONENT_PATH, USER_APP_REQUEST_PATH, USER_SITE_REQUEST_PATH, APP_CONFIG_REQUEST_PATH, NOT_FOUND_COMPONENT_PATH, NOT_FOUND_REQUEST_PATH, USER_APP_ID_VIRTUAL, USER_APP_ID_VIRTUAL_RESOLVED, USER_SITE_ID_VIRTUAL, USER_SITE_ID_VIRTUAL_RESOLVED, DEBUG_COMPONENT_PATH } from '../alias'
import { configureMiddleware } from './middleware'
import { serialize, pascalCase, exists, debug } from './utils'
import { parseId } from './parse'
@@ -124,6 +125,60 @@ export default function IslandsPlugins (appConfig: AppConfig): PluginOption[] {
server = devServer
return configureMiddleware(appConfig, server, defaultLayoutPath)
},
+ async transformIndexHtml (html, ctx) {
+ const indexHtmlFilePath = resolve(root, 'index.html')
+ if (ctx.filename === indexHtmlFilePath) {
+ // Validate user provided index.html
+ const {
+ userShell,
+ isValidUserShell,
+ errorMsgUserShell,
+ } = await getUserShell(appConfig, html)
+
+ if (!isValidUserShell) {
+ throw new Error(errorMsgUserShell)
+ }
+ return userShell
+ }
+ }
+ },
+ {
+ // app.ts (optional) - use virtual if not user-provided
+ name: 'iles:user-app',
+ enforce: 'pre',
+ resolveId (id) {
+ if (id === USER_APP_ID_VIRTUAL) {
+ return USER_APP_ID_VIRTUAL_RESOLVED
+ }
+ // Prevent import analysis failure if user-app (app.ts) doesn't exist.
+ if (id === appPath) return resolve(root, id.slice(1))
+ },
+ async load (id) {
+ if (id === USER_APP_ID_VIRTUAL_RESOLVED) {
+ const userAppExists = await exists(appPath)
+ return userAppExists ? `import userApp from "${appPath.replace('.ts', '')}"
+export default userApp` : 'export default {}'
+ }
+ },
+ },
+ {
+ // site.ts (optional) - use virtual if not user-provided
+ name: 'iles:site-app',
+ enforce: 'pre',
+ resolveId (id) {
+ if (id === USER_SITE_ID_VIRTUAL) {
+ return USER_SITE_ID_VIRTUAL_RESOLVED
+ }
+ // Prevent import analysis failure if site-app (site.ts) doesn't exist.
+ if (id === sitePath) return resolve(root, id.slice(1))
+ },
+ async load (id) {
+ if (id === USER_SITE_ID_VIRTUAL_RESOLVED) {
+ const userSiteExists = await exists(sitePath)
+ return userSiteExists ? `import siteRef from "${sitePath.replace('.ts', '')}"
+export default siteRef` : 'export default {}'
+ }
+ },
},
{
name: 'iles:detect-islands-in-vue',
@@ -216,8 +271,11 @@ export default function IslandsPlugins (appConfig: AppConfig): PluginOption[] {
}
if (isPage) {
+ const layoutPath = `${layoutsRoot}/${layout}.vue`
+ const layoutExists = await exists(resolve(root, layoutPath.slice(1)))
+
appendToSfc('layoutName', serialize(layout))
- appendToSfc('layoutFn', String(layout) === 'false'
+ appendToSfc('layoutFn', String(layout) === 'false' || !layoutExists
? 'false'
: `() => import('${layoutsRoot}/${layout}.vue').then(m => m.default)`)
}
diff --git a/packages/iles/tests/__snapshots__/build.spec.ts.snap b/packages/iles/tests/__snapshots__/build.spec.ts.snap
index 47134f13..f2e145ef 100644
--- a/packages/iles/tests/__snapshots__/build.spec.ts.snap
+++ b/packages/iles/tests/__snapshots__/build.spec.ts.snap
@@ -27,20 +27,20 @@ exports[`building docs site > html files 1`] = `
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
+
@@ -61,20 +61,20 @@ exports[`building docs site > html files 2`] = `
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
+
@@ -108,20 +108,20 @@ exports[`building docs site > html files 3`] = `
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
+
@@ -147,20 +147,20 @@ exports[`building docs site > html files 4`] = `
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
+
@@ -184,20 +184,20 @@ exports[`building docs site > html files 5`] = `
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
+
@@ -256,20 +256,20 @@ exports[`building docs site > html files 6`] = `
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
+
@@ -361,20 +361,20 @@ exports[`building docs site > html files 7`] = `
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
+
diff --git a/packages/iles/tests/build.spec.ts b/packages/iles/tests/build.spec.ts
index c9cd7492..bbc4da3b 100644
--- a/packages/iles/tests/build.spec.ts
+++ b/packages/iles/tests/build.spec.ts
@@ -18,8 +18,8 @@ describe('building docs site', () => {
expect(files.sort()).toEqual(expect.arrayContaining([
'404.html',
'_headers',
- 'assets/app-Y_77dvPh.css',
'assets/turbo.BkUC-S31.js',
+ 'assets/user-app-Y_77dvPh.css',
'favicon.ico',
'feed.rss',
'index.html',
@@ -45,7 +45,7 @@ describe('building docs site', () => {
})
test('styles', async () => {
- await assertSnapshot('assets/app-Y_77dvPh.css')
+ await assertSnapshot('assets/user-app-Y_77dvPh.css')
await assertSnapshot('assets/default--6gluetn.css')
})
test('sitemap', async () => {
@@ -99,7 +99,7 @@ async function assertHTML (path: string, { title }: any = {}) {
expectContent.toContain('')
expectContent.toContain(``)
expectContent.toContain('')
- expectContent.toContain('')
+ expectContent.toContain('')
expectContent.toContain(''
+ '