Skip to content

Commit

Permalink
doc: new learning structure
Browse files Browse the repository at this point in the history
  • Loading branch information
harlan-zw committed Nov 5, 2024
1 parent 55f331c commit 8cc9253
Show file tree
Hide file tree
Showing 34 changed files with 1,394 additions and 199 deletions.
31 changes: 9 additions & 22 deletions docs/app/composables/nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ export const menu = computed(() => {
icon: 'i-ph-books-duotone',
children: [
{
label: 'Mastering Page Titles',
description: 'Learn best practices for titles, setting them with title templates and more.',
label: 'Mastering Meta Management',
description: 'Learn title templates, dynamic meta tags, and social sharing in Vue. A complete guide to useHead() and meta patterns."',
icon: 'i-heroicons-h1-solid',
to: '/learn/mastering-page-titles',
to: '/learn/mastering-meta',
},
{
label: 'Controlling Web Crawlers',
Expand All @@ -60,29 +60,16 @@ export const menu = computed(() => {
to: '/learn/controlling-crawlers',
},
{
label: 'Trailing Slashes',
description: 'Learn how to set up your Nuxt app to properly handle trailing slashes.',
label: 'Launch & Listen',
description: 'Deploy your Vue app for SEO success. Complete guide to production URLs, indexing, monitoring, and performance tracking.',
icon: 'i-tabler-slashes',
to: '/learn/trailing-slashes',
to: '/learn/launch-and-listen',
},
{
label: 'SEO Go-Live Checklist',
description: 'Make sure you have everything in place when going live.',
label: 'Routes & Rendering',
description: 'Optimize Vue Router and content rendering for search engines. From dynamic routes to rendering strategies.',
icon: 'i-carbon-recording',
to: '/learn/going-live',
},
{
label: 'Community Videos',
to: '/learn/community-videos',
description: 'Learn from the Nuxt community in using Nuxt SEO.',
icon: 'i-ph-video-duotone',
},
{
label: 'Danny Postma\'s SEO Blueprint',
to: 'https://www.dannypostma.com/seo-course?via=harlan',
description: 'The SEO blueprint to get your website to #1 brought to you by Nuxt Indiehacker Danny Postma.',
icon: 'i-ph-rocket-duotone',
target: '_blank',
to: '/learn/routes-and-rendering',
},
],
},
Expand Down
91 changes: 38 additions & 53 deletions docs/app/layouts/learn.vue
Original file line number Diff line number Diff line change
@@ -1,31 +1,12 @@
<script setup lang="ts">
import { defu } from 'defu'
import { titleCase } from 'scule'
import { menu } from '~/composables/nav'
const navigation = computed(() => {
return menu.value[1].children.map((item) => {
return {
...item,
title: item.label,
children: item.children?.map((child) => {
return {
...child,
to: child.children?.length ? child.children[0].to : child.to,
title: child.label,
}
}),
}
})
})
function mapPath(data, node = 0) {
if (node < 1) {
return mapPath(data[0].children, node + 1)
}
function mapPath(data) {
return data.map((item) => {
if (item.children?.length && !item.page) {
item.title = titleCase(item.title)
item.children = mapPath(item.children, node + 1)
item.children = mapPath(item.children)
}
return {
...item,
Expand All @@ -34,31 +15,42 @@ function mapPath(data, node = 0) {
})
}
const { data: nav } = await useAsyncData(`docs-nav-learn`, () => queryCollectionNavigation('learn'), {
const { data: nav } = await useAsyncData(`docs-nav-learn`, () => queryCollectionNavigation('learn', [
'icon',
]), {
default: () => [],
transform(res) {
const nav = mapPath(res, 0)
return nav
// firstly we need to merge the children on the path key
const children = res[0].children
// map as an object path => data, merge data
const mappedChildren = children.reduce((acc, item) => {
if (!acc[item.path]) {
acc[item.path] = item
} else {
acc[item.path] = defu(acc[item.path], item)
// filter the children, they shouldn't contain the item.path
}
return acc
}, {})
return mapPath(Object.values(mappedChildren)).map(item => item.children.map(c => ({ icon: 'i-ph-dot-outline-duotone', ...c,})))
},
})
const route = useRoute()
const isOnSubPage = computed(() => {
return route.path.split('/').length > 3 || route.path.startsWith('/learn/controlling-crawlers')
})
const subPageNav = computed(() => {
if (!isOnSubPage.value) {
return []
}
// find the nav that starts with the current path
const currentPath = route.path.split('/').slice(0, 3).join('/')
return [
{
title: 'Introduction',
to: currentPath,
},
...nav.value.find(item => item.path === currentPath)?.children || [],
]
const breadcrumbs = useBreadcrumbItems({
overrides: computed(() => {
const segments = route.path.split('/')
const parent = nav.value.flat().find(i => i.path === segments.slice(0, 3).join('/'))
const currItem = nav.value.flat().find(i => i.path === route.path)
return [
null,
{
icon: 'i-ph-books-duotone',
},
parent,
{ label: currItem?.title },
]
}),
})
</script>

Expand All @@ -69,7 +61,7 @@ const subPageNav = computed(() => {
<UPage :ui="{ left: 'lg:col-span-3', center: 'lg:col-span-6' }">
<template #left>
<UPageAside class="max-w-[300px]">
<div v-if="!isOnSubPage" class="mb-10 relative inline-flex transition-all hover:shadow-lg flex-col rounded-lg font-bold border bg-gradient-to-r from-sky-700/10 to-blue-700/20 border-sky-700/20 px-5 py-3 gap-1">
<div class="mb-10 relative inline-flex transition-all hover:shadow-lg flex-col rounded-lg font-bold border bg-gradient-to-r from-sky-700/10 to-blue-700/20 border-sky-700/20 px-5 py-3 gap-1">
<div class="z-1 flex flex-col justify-between h-full">
<div>
<div class="flex items-center justify-between mb-1">
Expand All @@ -79,22 +71,15 @@ const subPageNav = computed(() => {
</div>
</div>
</div>
<UContentNavigation :navigation="navigation" />
</div>
<div v-else class="relative inline-flex transition-all hover:shadow-lg flex-col rounded-lg font-bold border bg-gradient-to-r from-sky-700/10 to-blue-700/20 border-sky-700/20 px-5 py-3 gap-1">
<div class="z-1 flex flex-col justify-between h-full">
<div>
<div class="flex items-center justify-between mb-1">
<div class="flex items-center gap-1">
<UIcon name="i-ph-books-duotone" class="text-blue-300" />Controlling Web Crawlers
</div>
</div>
<div class="space-y-5">
<div v-for="section in nav">
<UContentNavigation :navigation="section" />
</div>
</div>
<UContentNavigation :navigation="subPageNav" />
</div>
</UPageAside>
</template>
<UBreadcrumb :items="breadcrumbs" class="mt-10" />
<slot />
</UPage>
</div>
Expand Down
17 changes: 0 additions & 17 deletions docs/app/pages/learn/[...slug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -92,27 +92,10 @@ const repoLinks = computed(() => [
target: '_blank',
},
])
const breadcrumbs = useBreadcrumbItems({
overrides: [
null,
{
icon: 'i-ph-books-duotone',
},
{
icon: 'i-ph-robot-duotone',
label: 'Controlling Web Crawlers',
},
{
label: 'Robots.txt',
},
],
})
</script>

<template>
<div class="max-w-[66ch]">
<UBreadcrumb :items="breadcrumbs" class="mt-10" />
<UPageHeader v-bind="page" :ui="{ title: 'text-center text-balance xl:leading-normal min-w-full', description: 'text-center ' }">
<div class="flex justify-center gap-5 mt-5">
<div class="flex items-center gap-2 text-gray-300">
Expand Down
16 changes: 16 additions & 0 deletions docs/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,20 @@ export const collections = {
ogImageComponent: z.string().optional(),
}),
}),
recipes: defineCollection({
type: 'page',
source: {
path: '**/*.md',
cwd: resolve('content/recipes'),
prefix: '/recipes',
},
schema: z.object({
icon: z.string().optional(),
publishedAt: z.string().optional(),
updatedAt: z.string().optional(),
keywords: z.array(z.string()).optional(),
readTime: z.string(),
ogImageComponent: z.string().optional(),
}),
}),
}
16 changes: 16 additions & 0 deletions docs/content/learn-todo/0.mastering-meta/slack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
title: Mastering Meta in Vue & Nuxt
description: Learn how to effectively manage web crawlers in Vue and Nuxt applications to optimize SEO and protect your content.
navigation:
title: 'Slack'
publishedAt: 2024-11-03
updatedAt: 2024-11-03
readTime: 10 mins
keywords:
- vue crawler control
- web crawlers
- robots.txt
- sitemap.xml
---

todo
16 changes: 16 additions & 0 deletions docs/content/learn-todo/0.mastering-meta/twitter-cards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
title: Mastering Meta in Vue & Nuxt
description: Learn how to effectively manage web crawlers in Vue and Nuxt applications to optimize SEO and protect your content.
navigation:
title: 'Twitter - X Cards'
publishedAt: 2024-11-03
updatedAt: 2024-11-03
readTime: 10 mins
keywords:
- vue crawler control
- web crawlers
- robots.txt
- sitemap.xml
---

todo
10 changes: 10 additions & 0 deletions docs/content/learn-todo/3.routes-and-rendering/0.index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
title: Trailing Slashes in Vue & Nuxt
description: Learn why we might use a trailing slash, best practices for them and how to handle them in Nuxt.
navigation:
title: 'Routes and rendering'
publishedAt: 2024-10-25
updatedAt: 2024-10-25
icon: i-tabler-slashes
---

113 changes: 113 additions & 0 deletions docs/content/learn-todo/3.routes-and-rendering/3.trailing-slashes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
title: Complete Trailing Slashes Guide for Vue & Nuxt
description: Learn why trailing slashes matter (or don't), how to implement them correctly in Vue & Nuxt, and when you can ignore them entirely.
icon: i-tabler-slashes
navigation:
title: 'Trailing Slashes'
publishedAt: 2024-11-06
updatedAt: 2024-11-06
readTime: 8 mins
keywords:
- trailing slashes
- vue routing
- nuxt routes
- technical seo
---

## Introduction

A trailing slash is the "/" at the end of a URL. For example:
- With trailing slash: `/about/`
- Without trailing slash: `/about`

While trailing slashes don't directly impact SEO rankings, they can create technical challenges:

1. **Duplicate Content**: When both `/about` and `/about/` serve the same content without proper canonicalization, search engines have to choose which version to index. While Google is generally good at figuring this out, it's better to be explicit.

2. **Crawl Budget**: On large sites, having multiple URLs for the same content can waste your [crawl budget](/learn/controlling-crawlers#improve-organic-traffic).

3. **Analytics Accuracy**: Different URL formats can split your analytics data, making it harder to track page performance.

The solution is simple: pick one format and stick to it by [redirecting](/learn/controlling-crawlers/redirects) the other and set
[canonical URLs](/learn/controlling-crawlers/canonical-urls).


## Vue Router

By default, all routes in Vue Router are case-insensitive and match routes with or without trailing slashes. For example, `/users` will match:
- `/users`
- `/users/`
- `/Users/`

### Quick Implementation

Control trailing slash behavior through router configuration:

```ts
import { createRouter } from 'vue-router'

const router = createRouter({
strict: true,
})
```


## Quick Implementation

In Vue, you can control trailing slash behavior through router configuration:

::code-group

\```ts [Vue Router]
import { createRouter } from 'vue-router'

const router = createRouter({
// Make routes case-sensitive and enforce trailing slash rules
strict: true,
sensitive: true,
// your other config
})
\```

\```ts [Nuxt Config]
export default defineNuxtConfig({
// Remove trailing slashes (default)
router: {
trailingSlash: false
}
})
\```

::

### Router Options Explained

Vue Router provides two key options for URL matching:

- `strict: true` - Enforce trailing slash rules (e.g., `/users` won't match `/users/`)
- `sensitive: true` - Make routes case-sensitive (e.g., `/users` won't match `/Users`)

You can set these globally or per-route:

\```ts
const router = createRouter({
routes: [
{
path: '/users',
// Override global settings per-route
strict: true,
sensitive: true,
component: Users
}
]
})
\```

## Avoiding Duplicate Content

The main reason to care about trailing slashes is to avoid having the same content accessible at multiple URLs:

```bash
https://mysite.com/about/
https://mysite.com/about
https://mysite.com/About # with case-insensitive routing
Loading

0 comments on commit 8cc9253

Please sign in to comment.