Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions app/components/StarterCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@ const props = defineProps({
})

const template = computed(() => {
return props.starter.repo === 'nuxt/starter'
? (props.starter.branch === 'v4')
? ''
: `-- -t ${props.starter.branch}`
: `-- -t "${props.starter.repo}#${props.starter.branch}"`
return props.starter.default ? '' : `-- -t ${props.starter.slug}`
})

const command = computed(() => {
Expand Down
3 changes: 2 additions & 1 deletion nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ export default defineNuxtConfig({
routeRules: {
'/themes': { redirect: 'https://nuxt.com/templates' },
'/templates': { redirect: 'https://nuxt.com/templates' },
'/data/starters.json': { isr: 3600 },
},
compatibilityDate: '2025-07-28',
nitro: {
prerender: {
routes: ['/', '/data/starters.json'],
routes: ['/'],
},
},
vite: {
Expand Down
2 changes: 1 addition & 1 deletion server/routes/c/[slug].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ export default defineEventHandler(async (event) => {
body: 'Not found',
}
}
return sendRedirect(event, `https://codesandbox.io/s/github/${starter.repo}/tree/${starter.branch}/${starter.dir || ''}`, 302)
return sendRedirect(event, `https://codesandbox.io/s/github/${starter.repo}/tree/${starter.branch}`, 302)
})
72 changes: 24 additions & 48 deletions server/routes/data/starters.json.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,24 @@
export default defineEventHandler((): Starter[] => [
{
name: 'Nuxt 4',
slug: 'v4',
description: 'Minimal starter with a single app.vue.',
image: 'data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'34\' height=\'34\' viewBox=\'0 0 34 34\' fill=\'none\'%3E%3Cpath d=\'M19.0778 28.3333H31.6956C32.0993 28.3364 32.4968 28.2333 32.8482 28.0344C33.1996 27.8355 33.4926 27.5478 33.6978 27.2C33.8967 26.8554 34.0015 26.4645 34.0015 26.0667C34.0015 25.6688 33.8967 25.2779 33.6978 24.9333L25.1978 10.3511C24.9987 10.0063 24.7124 9.72003 24.3675 9.52108C24.0226 9.32213 23.6315 9.21751 23.2333 9.21777C22.8296 9.2147 22.4321 9.31778 22.0807 9.51669C21.7293 9.71559 21.4363 10.0033 21.2311 10.3511L19.0778 14.0911L14.8467 6.79999C14.6445 6.44959 14.3521 6.1597 14 5.96041C13.648 5.76112 13.249 5.65969 12.8445 5.66666C12.4471 5.67016 12.0574 5.77635 11.7132 5.97494C11.369 6.17352 11.0819 6.45774 10.88 6.79999L0.302233 24.9333C0.153235 25.1913 0.056538 25.476 0.0176701 25.7714C-0.0211977 26.0667 -0.00147386 26.3668 0.075714 26.6545C0.152902 26.9422 0.28604 27.2118 0.467516 27.4481C0.648992 27.6843 0.875247 27.8824 1.13334 28.0311C1.47334 28.22 1.8889 28.3333 2.30446 28.3333H10.2378C13.3733 28.3333 15.6778 26.9733 17.2645 24.2911L21.1556 17.6422L23.2333 14.0911L29.4667 24.7822H21.1556L19.0778 28.3333ZM10.0867 24.7822H4.57112L12.8445 10.54L17 17.6422L14.2045 22.4022C13.1467 24.14 11.9756 24.7822 10.0867 24.7822Z\' fill=\'%2300DC82\'/%3E%3C/svg%3E',
repo: 'nuxt/starter',
branch: 'v4',
docs: 'https://nuxt.com/docs/4.x/getting-started/installation',
},
{
name: 'Nuxt 3',
deprecated: true,
slug: 'v3',
description: 'Minimal starter with a single app.vue.',
image: 'data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'34\' height=\'34\' viewBox=\'0 0 34 34\' fill=\'none\'%3E%3Cpath d=\'M19.0778 28.3333H31.6956C32.0993 28.3364 32.4968 28.2333 32.8482 28.0344C33.1996 27.8355 33.4926 27.5478 33.6978 27.2C33.8967 26.8554 34.0015 26.4645 34.0015 26.0667C34.0015 25.6688 33.8967 25.2779 33.6978 24.9333L25.1978 10.3511C24.9987 10.0063 24.7124 9.72003 24.3675 9.52108C24.0226 9.32213 23.6315 9.21751 23.2333 9.21777C22.8296 9.2147 22.4321 9.31778 22.0807 9.51669C21.7293 9.71559 21.4363 10.0033 21.2311 10.3511L19.0778 14.0911L14.8467 6.79999C14.6445 6.44959 14.3521 6.1597 14 5.96041C13.648 5.76112 13.249 5.65969 12.8445 5.66666C12.4471 5.67016 12.0574 5.77635 11.7132 5.97494C11.369 6.17352 11.0819 6.45774 10.88 6.79999L0.302233 24.9333C0.153235 25.1913 0.056538 25.476 0.0176701 25.7714C-0.0211977 26.0667 -0.00147386 26.3668 0.075714 26.6545C0.152902 26.9422 0.28604 27.2118 0.467516 27.4481C0.648992 27.6843 0.875247 27.8824 1.13334 28.0311C1.47334 28.22 1.8889 28.3333 2.30446 28.3333H10.2378C13.3733 28.3333 15.6778 26.9733 17.2645 24.2911L21.1556 17.6422L23.2333 14.0911L29.4667 24.7822H21.1556L19.0778 28.3333ZM10.0867 24.7822H4.57112L12.8445 10.54L17 17.6422L14.2045 22.4022C13.1467 24.14 11.9756 24.7822 10.0867 24.7822Z\' fill=\'%2300DC82\'/%3E%3C/svg%3E',
repo: 'nuxt/starter',
branch: 'v3',
docs: 'https://nuxt.com/docs/3.x/getting-started/installation',
},
{
name: 'UI',
slug: 'ui',
description: 'Starter with Nuxt UI.',
image: 'data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'32\' height=\'32\' viewBox=\'0 0 20 20\'%3E%3Cpath fill=\'%2300DC82\' fill-rule=\'evenodd\' d=\'M10 2.5c-1.31 0-2.526.386-3.546 1.051a.75.75 0 0 1-.82-1.256A8 8 0 0 1 18 9a22.47 22.47 0 0 1-1.228 7.351a.75.75 0 1 1-1.417-.49A20.97 20.97 0 0 0 16.5 9A6.5 6.5 0 0 0 10 2.5ZM4.333 4.416a.75.75 0 0 1 .218 1.038A6.466 6.466 0 0 0 3.5 9a7.966 7.966 0 0 1-1.293 4.362a.75.75 0 0 1-1.257-.819A6.466 6.466 0 0 0 2 9c0-1.61.476-3.11 1.295-4.365a.75.75 0 0 1 1.038-.219ZM10 6.12a3 3 0 0 0-3.001 3.041a11.455 11.455 0 0 1-2.697 7.24a.75.75 0 0 1-1.148-.965A9.957 9.957 0 0 0 5.5 9c0-.028.002-.055.004-.082a4.5 4.5 0 0 1 8.996.084v.148l-.005.297a.75.75 0 1 1-1.5-.034c.003-.11.004-.219.005-.328a3 3 0 0 0-3-2.965Zm0 2.13a.75.75 0 0 1 .75.75c0 3.51-1.187 6.745-3.181 9.323a.75.75 0 1 1-1.186-.918A13.687 13.687 0 0 0 9.25 9a.75.75 0 0 1 .75-.75Zm3.529 3.698a.75.75 0 0 1 .584.885a18.883 18.883 0 0 1-2.257 5.84a.75.75 0 1 1-1.29-.764a17.386 17.386 0 0 0 2.078-5.377a.75.75 0 0 1 .885-.584Z\' clip-rule=\'evenodd\'/%3E%3C/svg%3E',
repo: 'nuxt/starter',
branch: 'ui',
docs: 'https://ui.nuxt.com',
},
{
name: 'Content',
slug: 'content',
description: 'Starter for a content-driven website.',
image: 'data:image/svg+xml,%3Csvg width=\'34\' height=\'34\' fill=\'none\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cpath d=\'M12 14h10a1.667 1.667 0 0 0 1.667-1.667v-10A1.667 1.667 0 0 0 22 .667H12a1.667 1.667 0 0 0-1.667 1.666v10A1.667 1.667 0 0 0 12 14Zm1.667-10h6.666v6.667h-6.666V4ZM32 10.667h-3.333a1.667 1.667 0 0 0 0 3.333H32a1.667 1.667 0 1 0 0-3.333Zm-3.333-3.334H32A1.667 1.667 0 1 0 32 4h-3.333a1.667 1.667 0 0 0 0 3.333ZM2 7.333h3.333a1.667 1.667 0 1 0 0-3.333H2a1.667 1.667 0 1 0 0 3.333ZM2 14h3.333a1.667 1.667 0 1 0 0-3.333H2A1.667 1.667 0 0 0 2 14Zm30 3.333H2a1.667 1.667 0 1 0 0 3.334h30a1.667 1.667 0 1 0 0-3.334ZM18.667 24H2a1.667 1.667 0 1 0 0 3.333h16.667a1.667 1.667 0 0 0 0-3.333Z\' fill=\'%2300BD6F\'/%3E%3C/svg%3E',
repo: 'nuxt/starter',
branch: 'content',
docs: 'https://content.nuxt.com',
},
{
name: 'Module',
slug: 'module',
description: 'Starter to create your first Nuxt module.',
image: 'data:image/svg+xml,%3Csvg width=\'34\' height=\'34\' fill=\'none\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cpath d=\'M29.117 9.283V9.15l-.1-.25a1.186 1.186 0 0 0-.117-.15 1.571 1.571 0 0 0-.15-.2l-.15-.117-.267-.133-12.5-7.717a1.667 1.667 0 0 0-1.766 0L1.667 8.3l-.15.133-.15.117c-.056.063-.106.13-.15.2a1.183 1.183 0 0 0-.117.15l-.1.25v.133c-.016.144-.016.29 0 .434v14.566a1.667 1.667 0 0 0 .783 1.417l12.5 7.717a.784.784 0 0 0 .25.1h.134c.282.09.584.09.866 0h.134a.784.784 0 0 0 .25-.1L28.333 25.7a1.667 1.667 0 0 0 .784-1.417V9.717c.016-.144.016-.29 0-.434ZM13.333 29.017 4.167 23.35V12.717l9.166 5.65v10.65ZM15 15.483 5.667 9.717 15 3.967l9.333 5.75L15 15.483Zm10.833 7.867-9.166 5.667v-10.65l9.166-5.65V23.35Z\' fill=\'%2300BD6F\'/%3E%3C/svg%3E',
repo: 'nuxt/starter',
branch: 'module',
docs: 'https://nuxt.com/docs/4.x/guide/going-further/modules',
},
])
export default defineEventHandler(async () => {
const templates: Starter[] = []

const files = await $fetch<Array<{ name: string, type: string, download_url?: string }>>('https://api.github.com/repos/maximepvrt/nuxt-starter/contents/templates?ref=update-template')

await Promise.all(files.map(async (file) => {
if (!file.download_url || file.type !== 'file' || !file.name.endsWith('.json')) {
return
}
const templateName = file.name.replace('.json', '')
const template = await $fetch(file.download_url, {
responseType: 'json',
}) as Starter
if (!template.deprecated) {
templates.push({ ...template, slug: templateName, tar: `https://codeload.github.com/${template.repo}/tar.gz/refs/heads/${template.branch}` })
}
}))

return templates.sort((a, b) => {
if (a.default && !b.default) return -1
if (!a.default && b.default) return 1
return 0
})
})
Comment on lines +1 to +24
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

No error handling for external GitHub API calls.

Both $fetch calls (directory listing on Line 4, individual template files on Line 11) can fail due to rate-limiting (unauthenticated GitHub API allows 60 req/hr), network errors, or malformed responses. A single failure inside Promise.all will reject the entire response with an unhandled error.

Consider:

  • Adding a try/catch around each inner fetch so one bad template doesn't take down the whole endpoint.
  • Using Promise.allSettled instead of Promise.all.
  • Authenticating the GitHub API request (e.g., via a server token) to raise the rate limit, especially given ISR revalidation every hour.
🧰 Tools
πŸͺ› GitHub Check: test

[failure] 15-15:
'slug' is specified more than once, so this usage will be overwritten.


[failure] 6-6:
Parameter 'file' implicitly has an 'any' type.


[failure] 6-6:
'files' is of type 'unknown'.


[failure] 15-15:
'slug' is specified more than once, so this usage will be overwritten.


[failure] 6-6:
Parameter 'file' implicitly has an 'any' type.


[failure] 6-6:
'files' is of type 'unknown'.

πŸ€– Prompt for AI Agents
In `@server/routes/data/starters.json.ts` around lines 1 - 24, Wrap the external
GitHub calls inside robust error handling: replace the Promise.all over files
with Promise.allSettled and for each entry in the map around the inner $fetch
(the call that produces template in the defineEventHandler) add a try/catch so
one failed template is skipped and logged (push only successful, non-deprecated
templates into the templates array); also wrap the initial $fetch that retrieves
files in its own try/catch and return an empty templates array or a safe
fallback on failure; finally, when calling $fetch to GitHub include an
Authorization header using a server-side GITHUB_TOKEN env var to increase rate
limits.

2 changes: 1 addition & 1 deletion server/routes/s/[slug].get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ export default defineEventHandler(async (event) => {
body: 'Not found',
}
}
return sendRedirect(event, `https://stackblitz.com/github/${starter.repo}/tree/${starter.branch}/${starter.dir || ''}`, 302)
return sendRedirect(event, `https://stackblitz.com/github/${starter.repo}/tree/${starter.branch}`, 302)
})
7 changes: 4 additions & 3 deletions shared/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
export interface Starter {
name: string
slug: string
default?: boolean
description: string
image?: string
tar: string
repo: string
branch: string
dir?: string
demo?: string
docs?: string
defaultDir?: string
url?: string
deprecated?: boolean
}