commit dce2819468e1900362a5a29a069606c08cf93f0b Author: Valentin Date: Fri May 3 18:22:13 2024 +0200 first commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..47768da --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +DIRECTUS_API_TOKEN= +URL= +DIRECTUS_URL= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..142f292 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Nuxt dev/build outputs +.output +.data +.nuxt +.nitro +.cache +dist +public/api +public/imgs + +# Node dependencies +node_modules + +# Logs +logs +*.log + +# Misc +.DS_Store +.fleet +.idea + +# Local env files +.env +.env.* +!.env.example diff --git a/README.md b/README.md new file mode 100644 index 0000000..109f70e --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# Nuxt Static Site Generation Boilerplate + +Generate static site with content from [Directus CMS](https://directus.io/). + +Use the [Directus MariaDB boilerplate]() to launch the CMS locally. + +The files are retrieved and cached from Directus on build time. + +## Develop + +Install the dependencies : + +```bash +npm install +``` + +Create the `.env` file from `.env.example` + +Work with + +```bash +npm run dev +``` + +Build the site in `.output/public` directory + +```bash +npm run generate --prerender +``` + +Preview the freshly built site with + +```bash +npx serve .output/public +``` + +## Deploy + +Deploy on a Debian VPS with the [Deploy CDCN]() script. diff --git a/app.vue b/app.vue new file mode 100644 index 0000000..9864f76 --- /dev/null +++ b/app.vue @@ -0,0 +1,27 @@ + + + diff --git a/composables/useFetchGlobalData.js b/composables/useFetchGlobalData.js new file mode 100644 index 0000000..0f087a9 --- /dev/null +++ b/composables/useFetchGlobalData.js @@ -0,0 +1,11 @@ +export const useFetchGlobalData = async () => { + const globalData = useState('globalData', () => {}) + + await callOnce(async () => { + globalData.value = await $fetch(`/api/items/global`) + globalData.value = globalData.value.data + }) + return { + globalData + } +} \ No newline at end of file diff --git a/error.vue b/error.vue new file mode 100644 index 0000000..77c7d9b --- /dev/null +++ b/error.vue @@ -0,0 +1,15 @@ + + + diff --git a/nuxt.config.ts b/nuxt.config.ts new file mode 100644 index 0000000..2fd3adb --- /dev/null +++ b/nuxt.config.ts @@ -0,0 +1,47 @@ +// nitro hook to get Directus files working on ssg +// https://github.com/codepie-io/nuxt3-dynamic-routes/blob/main/nuxt.config.ts +// + ssg homemade caching to not retrieve all the files each generation + +import { crawlImages } from './ssg_hooks/crawlImages.js' +import { cacheImages } from './ssg_hooks/cacheImages.js' + +export default defineNuxtConfig({ + devtools: { enabled: true }, + modules: [ + '@nuxtjs/seo', + ], + runtimeConfig: { + apiURL: process.env.DIRECTUS_URL, + apiToken: process.env.DIRECTUS_API_TOKEN + }, + nitro: { + hooks: { + async 'prerender:routes'(routes) { + await crawlImages(routes); + }, + }, + prerender: { + routes: [ + '/api/items/global', + ] + }, + }, + hooks: { + 'nitro:build:public-assets': async () => { + const imageSizes = [ + { small: 750 }, + { large: 1920 }, + ]; + await cacheImages(imageSizes); + } + }, + app: { + pageTransition: { name: '', mode: '' } // define in the app.vue css + }, + site: { + url: process.env.URL, + defaultLocale: 'fr', + name: '', + description: '' + }, +}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..ba3590d --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "nuxt-app", + "private": true, + "type": "module", + "scripts": { + "build": "nuxt build", + "dev": "nuxt dev", + "generate": "nuxt generate", + "preview": "nuxt preview", + "postinstall": "nuxt prepare" + }, + "dependencies": { + "@directus/sdk": "^15.1.0", + "sharp": "^0.33.3", + "vue": "^3.4.15", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@nuxtjs/seo": "^2.0.0-rc.8", + "nuxt": "^3.11.2", + "sass": "^1.71.0" + } +} diff --git a/pages/contact.vue b/pages/contact.vue new file mode 100644 index 0000000..c94debf --- /dev/null +++ b/pages/contact.vue @@ -0,0 +1,73 @@ + + + + + \ No newline at end of file diff --git a/pages/galerie.vue b/pages/galerie.vue new file mode 100644 index 0000000..b34b5c2 --- /dev/null +++ b/pages/galerie.vue @@ -0,0 +1,28 @@ + + + \ No newline at end of file diff --git a/pages/index.vue b/pages/index.vue new file mode 100644 index 0000000..83c76d3 --- /dev/null +++ b/pages/index.vue @@ -0,0 +1,95 @@ + + + + + \ No newline at end of file diff --git a/pages/magasin.vue b/pages/magasin.vue new file mode 100644 index 0000000..84ec7d2 --- /dev/null +++ b/pages/magasin.vue @@ -0,0 +1,35 @@ + + + \ No newline at end of file diff --git a/server/api/[...].ts b/server/api/[...].ts new file mode 100644 index 0000000..359273e --- /dev/null +++ b/server/api/[...].ts @@ -0,0 +1,18 @@ +// The BEST way to proxy your API in Nuxt +// by Alexander Lichter +// https://www.youtube.com/watch?v=J4E5uYz5AY8 +// to run as static `npm run generate --prerender` + +import { joinURL } from 'ufo' + +export default defineEventHandler(async (event) => { + const proxyUrl = useRuntimeConfig().apiURL + + if (event.path.startsWith('/api')) { + const path = event.path.replace(/^\/api\//, '') + + const target = joinURL(proxyUrl, path) + + return proxyRequest(event, target, { headers: { Authorization: `Bearer ${useRuntimeConfig().apiToken}`}}) + } +}) \ No newline at end of file diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..b9ed69c --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../.nuxt/tsconfig.server.json" +} diff --git a/ssg_hooks/cacheImages.js b/ssg_hooks/cacheImages.js new file mode 100644 index 0000000..1072884 --- /dev/null +++ b/ssg_hooks/cacheImages.js @@ -0,0 +1,69 @@ +import fs from 'fs'; +import path from 'path'; +import { promisify } from 'util'; + +import { resizeImages } from '../ssg_hooks/resizeImages.js' + +export async function cacheImages(imageSizes) { + const sourceFolder = './.output/public/api/assets'; + const destinationFolder = './public/api/assets'; + + if (!fs.existsSync(destinationFolder)) fs.mkdirSync(destinationFolder, { recursive: true }); + + const readdir = promisify(fs.readdir); + const stat = promisify(fs.stat); + const copyFile = promisify(fs.copyFile); + + async function directoryExists(directoryPath) { + try { + const stats = await fs.promises.stat(directoryPath); + return stats.isDirectory(); + } catch (error) { + if (error.code === 'ENOENT') { + return false; + } else { + throw error; + } + } + } + + async function copyFilesIfNotExist(sourceFolder, destinationFolder) { + try { + const exists = await directoryExists(sourceFolder); + if (!exists) { + console.log(`Source folder '${sourceFolder}' does not exist.`); + return; + } + + const files = await readdir(sourceFolder); + for (const file of files) { + const sourceFilePath = path.join(sourceFolder, file); + const destinationFilePath = path.join(destinationFolder, file); + const sourceFileStat = await stat(sourceFilePath); + + if (sourceFileStat.isFile()) { + try { + await stat(destinationFilePath); + } catch (error) { + if (error.code === 'ENOENT') { + await copyFile(sourceFilePath, destinationFilePath); + console.log(`Copied '${file}' to '${destinationFolder}'.`); + } else { + throw error; + } + } + } + } + console.log('Files copied successfully.'); + + console.log('Start images resizing.'); + + await resizeImages(imageSizes); + + } catch (error) { + console.error('Error:', error); + } + } + + copyFilesIfNotExist(sourceFolder, destinationFolder); +} \ No newline at end of file diff --git a/ssg_hooks/crawlImages.js b/ssg_hooks/crawlImages.js new file mode 100644 index 0000000..9039860 --- /dev/null +++ b/ssg_hooks/crawlImages.js @@ -0,0 +1,35 @@ +import { createDirectus, staticToken, rest, readFiles } from '@directus/sdk'; +import fs from 'fs'; + +export async function crawlImages(routes) { + const client = createDirectus(process.env.DIRECTUS_URL) + .with(staticToken(process.env.DIRECTUS_API_TOKEN)) + .with(rest()); + + const directusFiles = await client.request( + readFiles({ + query: { + filter: { + type: { + _eq: 'image/', + }, + }, + }, + }) + ); + + for (let image of directusFiles) { + if (image.type != "image/heic") { + const fileExists = async (filePath) => !!(await fs.promises.access(filePath, fs.constants.F_OK).then(() => true).catch(() => false)); + + const filePath = `./public/api/assets/${image.id}.webp`; + fileExists(filePath) + .then(exists => { + if (!exists) { + routes.add(`/api/assets/${image.id}.webp`); + } + }) + .catch(error => console.error('Error:', error)); + } + } +} diff --git a/ssg_hooks/resizeImages.js b/ssg_hooks/resizeImages.js new file mode 100644 index 0000000..b0cec08 --- /dev/null +++ b/ssg_hooks/resizeImages.js @@ -0,0 +1,49 @@ +import fs from 'fs'; +import path from 'path'; +import sharp from 'sharp'; +import { promisify } from 'util'; + +export async function resizeImages(sizes) { + const stat = promisify(fs.stat); + const copyFile = promisify(fs.copyFile); + + const sourceFolder = './public/api/assets'; + const outputFolder = './.output/public/imgs'; + const sizeCacheFolder = './public/imgs'; + + for (const size of sizes) { + const key = Object.keys(size)[0]; + const sizeFolder = `${outputFolder}/${key}`; + if (!fs.existsSync(sizeFolder)) fs.mkdirSync(sizeFolder, { recursive: true }); + const cacheSizeFolder = `${sizeCacheFolder}/${key}`; + if (!fs.existsSync(cacheSizeFolder)) fs.mkdirSync(cacheSizeFolder, { recursive: true }); + } + + const files = fs.readdirSync(sourceFolder); + + for (const file of files) { + const filePath = `${sourceFolder}/${file}`; + const image = sharp(filePath); + + for (const size of sizes) { + const key = Object.keys(size)[0]; + const destinationFile = path.join(sizeCacheFolder, key, file); + try { + const destinationFileStat = await stat(destinationFile); + } catch (error) { + if (error.code === 'ENOENT') { + const width = parseInt(size[key]); + await image.clone().resize({ width }).toFile(destinationFile); + await copyFile(destinationFile, path.join(outputFolder, key, file)); + } else { + throw error; + } + } + + } + } + + fs.rmSync('./.output/public/api/assets', { recursive: true, force: true }); + + console.log('Images resized and cached successfully.'); +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a746f2a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,4 @@ +{ + // https://nuxt.com/docs/guide/concepts/typescript + "extends": "./.nuxt/tsconfig.json" +}