Compare commits

...

11 Commits

Author SHA1 Message Date
matthieu42morin e7b8dc721a script to create blogpost
Playwright Tests / test (push) Waiting to run Details
Vercel Preview Deployment / Deploy-Preview (push) Has been cancelled Details
Publish to Netlify / publish (push) Has been cancelled Details
2024-04-29 15:45:46 +02:00
matthieu42morin 62726a6a40 disable anon comments 2024-04-29 15:45:33 +02:00
matthieu42morin 02d4ca7898 TODO add support for added icons, list in app.html 2024-04-29 15:45:18 +02:00
matthieu42morin 910b62cd1b urara fix from Sevichecc 2024-04-29 15:44:44 +02:00
matthieu42morin b687c154b0 gitea workflows 2024-04-29 15:43:19 +02:00
matthieu42morin f90270bc24 fix site scrolling 2024-04-29 15:42:50 +02:00
matthieu42morin b0b463bea6 update err list, styles 2024-04-29 15:42:29 +02:00
matthieu42morin 4e64c1b2f1 fonts, schema 2024-04-29 15:42:11 +02:00
matthieu42morin 6e29c4f9e5 projs 2024-04-29 15:41:45 +02:00
matthieu42morin 25f2766ec2 upgrade configs 2024-04-29 15:40:37 +02:00
matthieu42morin 608045ef6c update deps 2024-04-29 15:39:55 +02:00
16 changed files with 439 additions and 257 deletions

View File

@ -0,0 +1,27 @@
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm install -g pnpm && pnpm install
- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps
- name: Run Playwright tests
run: pnpm exec playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

View File

@ -0,0 +1,33 @@
name: 'Publish to Netlify'
on:
push:
branches: [ prod, deploy ]
pull_request:
branches: [ prod, deploy ]
jobs:
publish:
timeout-minutes: 10
runs-on: debian-latest
steps:
- uses: actions/checkout@v3
name: Checkout
- name: Build
run: |
npm --version && node --version
npm ci --no-update-notifier
npm run build
- uses: https://github.com/nwtgck/actions-netlify@v2.0
name: Deploy
with:
publish-dir: './dist'
production-deploy: true
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Deployed from Gitea Action"
enable-commit-comment: false
enable-pull-request-comment: false
overwrites-pull-request-comment: true
enable-github-deployment: false
env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
timeout-minutes: 1

View File

@ -90,6 +90,7 @@
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^6.5.2", "@fortawesome/fontawesome-free": "^6.5.2",
"@sentry/sveltekit": "^7.112.2", "@sentry/sveltekit": "^7.112.2",
"@sveltejs/adapter-cloudflare": "^4.4.0",
"@yushijinhun/three-minifier-rollup": "^0.4.0", "@yushijinhun/three-minifier-rollup": "^0.4.0",
"vite-plugin-tailwind-purgecss": "^0.3.3", "vite-plugin-tailwind-purgecss": "^0.3.3",
"vitest": "^1.5.2" "vitest": "^1.5.2"

View File

@ -1,18 +1,30 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head prefix="og: https://ogp.me/ns#"> <head prefix="og: https://ogp.me/ns#">
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="generator" content="gh:importantimport/urara" /> <meta name="generator" content="gh:importantimport/urara" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="apple-touch-icon" sizes="180x180" href="%sveltekit.assets%/assets/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="%sveltekit.assets%/assets/apple-touch-icon.png" />
<link rel="icon" href="%sveltekit.assets%/assets/favicon@192.png" /> <link rel="icon" href="%sveltekit.assets%/assets/maskable@192.png" />
<link rel="manifest" crossorigin="use-credentials" href="/manifest.webmanifest" /> <link rel="manifest" crossorigin="use-credentials" href="/manifest.webmanifest" />
<link rel="alternate" type="application/feed+json" href="/feed.json" /> <link rel="alternate" type="application/feed+json" href="/feed.json" />
<link rel="alternate" type="application/atom+xml" href="/atom.xml" /> <link rel="alternate" type="application/atom+xml" href="/atom.xml" />
<link rel="sitemap" type="application/xml" href="/sitemap.xml" /> <link rel="sitemap" type="application/xml" href="/sitemap.xml" />
%sveltekit.head% %sveltekit.head%
</head> </head>
<body itemscope itemtype="https://schema.org/WebPage"> <body itemscope itemtype="https://schema.org/WebPage" data-sveltekit-prefetch>
<div style="display: contents">%sveltekit.body%</div> <script>
</body> const themeLocalStorageKey = 'theme'
if (
!!localStorage.getItem(themeLocalStorageKey)
? localStorage.getItem(themeLocalStorageKey) === 'dark'
: window.matchMedia('(prefers-color-scheme: dark)').matches
) {
document.body.classList.add('dark')
} else {
document.body.classList.add('light')
}
</script>
<div style="display: contents">%sveltekit.body%</div>
</body>
</html> </html>

View File

@ -45,8 +45,8 @@ h6 {
@font-face { @font-face {
font-family: 'Orbitron-Variable'; font-family: 'Orbitron-Variable';
src: src:
url('assets/fonts/Orbitron-VariableFont_wght.woff') format('woff'), url('/assets/fonts/Orbitron-VariableFont_wght.woff') format('woff'),
url('assets/fonts/Orbitron-VariableFont_wght.ttf') format('truetype'); url('/assets/fonts/Orbitron-VariableFont_wght.ttf') format('truetype');
font-weight: 300 900; font-weight: 300 900;
font-display: swap; font-display: swap;
font-style: normal; font-style: normal;
@ -55,8 +55,8 @@ h6 {
@font-face { @font-face {
font-family: 'RobotoMono-Italic-Variable'; font-family: 'RobotoMono-Italic-Variable';
src: src:
url('assets/fonts/RobotoMono-Italic-VariableFont_wght.woff') format('woff'), url('/assets/fonts/RobotoMono-Italic-VariableFont_wght.woff') format('woff'),
url('assets/fonts/RobotoMono-Italic-VariableFont_wght.ttf') format('truetype'); url('/assets/fonts/RobotoMono-Italic-VariableFont_wght.ttf') format('truetype');
font-weight: 300 900; font-weight: 300 900;
font-display: swap; font-display: swap;
font-style: normal; font-style: normal;
@ -65,8 +65,8 @@ h6 {
@font-face { @font-face {
font-family: 'RobotoMono-VariableFont'; font-family: 'RobotoMono-VariableFont';
src: src:
url('assets/fonts/RobotoMono-VariableFont_wght.woff') format('woff'), url('/assets/fonts/RobotoMono-VariableFont_wght.woff') format('woff'),
url('assets/fonts/RobotoMono-VariableFont_wght.ttf') format('truetype'); url('/assets/fonts/RobotoMono-VariableFont_wght.ttf') format('truetype');
font-weight: 300 900; font-weight: 300 900;
font-display: swap; font-display: swap;
font-style: italic; font-style: italic;

View File

@ -2,43 +2,98 @@ import type { Icon } from '$lib/types/icon'
import { site } from '$lib/config/site' import { site } from '$lib/config/site'
export const favicon: Icon = { export const favicon: Icon = {
src: site.protocol + site.domain + '/favicon.png', src: site.protocol + site.domain + '/favicon.png',
sizes: '48x48', sizes: '48x48',
type: 'image/png' type: 'image/png'
} }
export const any: { [key: number]: Icon } = { export const any: { [key: number]: Icon } = {
180: { 180: {
src: site.protocol + site.domain + '/assets/any@180.png', src: site.protocol + site.domain + '/assets/any@180.png',
sizes: '180x180', sizes: '180x180',
type: 'image/png' type: 'image/png'
}, },
192: { 192: {
src: site.protocol + site.domain + '/assets/any@192.png', src: site.protocol + site.domain + '/assets/any@192.png',
sizes: '192x192', sizes: '192x192',
type: 'image/png' type: 'image/png'
}, },
512: { 512: {
src: site.protocol + site.domain + '/assets/any@512.png', src: site.protocol + site.domain + '/assets/any@512.png',
sizes: '512x512', sizes: '512x512',
type: 'image/png' type: 'image/png'
} }
} }
export const maskable: { [key: number]: Icon } = { export const maskable: { [key: number]: Icon } = {
180: { 180: {
src: site.protocol + site.domain + '/assets/maskable@180.png', src: site.protocol + site.domain + '/assets/maskable@180.png',
sizes: '180x180', sizes: '180x180',
type: 'image/png' type: 'image/png'
}, },
192: { 192: {
src: site.protocol + site.domain + '/assets/maskable@192.png', src: site.protocol + site.domain + '/assets/maskable@192.png',
sizes: '192x192', sizes: '192x192',
type: 'image/png' type: 'image/png'
}, },
512: { 512: {
src: site.protocol + site.domain + '/assets/maskable@512.png', src: site.protocol + site.domain + '/assets/maskable@512.png',
sizes: '512x512', sizes: '512x512',
type: 'image/png' type: 'image/png'
} }
} }
// <link rel="apple-touch-icon" sizes="57x57" href="/apple-icon-57x57.png">
// <link rel="apple-touch-icon" sizes="60x60" href="/apple-icon-60x60.png">
// <link rel="apple-touch-icon" sizes="72x72" href="/apple-icon-72x72.png">
// <link rel="apple-touch-icon" sizes="76x76" href="/apple-icon-76x76.png">
// <link rel="apple-touch-icon" sizes="114x114" href="/apple-icon-114x114.png">
// <link rel="apple-touch-icon" sizes="120x120" href="/apple-icon-120x120.png">
// <link rel="apple-touch-icon" sizes="144x144" href="/apple-icon-144x144.png">
// <link rel="apple-touch-icon" sizes="152x152" href="/apple-icon-152x152.png">
// <link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png">
// <link rel="icon" type="image/png" sizes="192x192" href="/android-icon-192x192.png">
// <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
// <link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
// <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
// <link rel="manifest" href="/manifest.json">
// <meta name="msapplication-TileColor" content="#ffffff">
// <meta name="msapplication-TileImage" content="/ms-icon-144x144.png">
// <meta name="theme-color" content="#ffffff">
// {
// "src": "\/android-icon-36x36.png",
// "sizes": "36x36",
// "type": "image\/png",
// "density": "0.75"
// },
// {
// "src": "\/android-icon-48x48.png",
// "sizes": "48x48",
// "type": "image\/png",
// "density": "1.0"
// },
// {
// "src": "\/android-icon-72x72.png",
// "sizes": "72x72",
// "type": "image\/png",
// "density": "1.5"
// },
// {
// "src": "\/android-icon-96x96.png",
// "sizes": "96x96",
// "type": "image\/png",
// "density": "2.0"
// },
// {
// "src": "\/android-icon-144x144.png",
// "sizes": "144x144",
// "type": "image\/png",
// "density": "3.0"
// },
// {
// "src": "\/android-icon-192x192.png",
// "sizes": "192x192",
// "type": "image\/png",
// "density": "4.0"
// }

View File

@ -9,7 +9,7 @@ export const post: PostConfig = {
sortBy: 'created', // sort by: created / updated sortBy: 'created', // sort by: created / updated
sortDir: 'down', // sort order: up / down sortDir: 'down', // sort order: up / down
form: true, // enable comments: true / false form: true, // enable comments: true / false
commentParade: true // enable anonymous comments: true / false commentParade: false // enable anonymous comments: true / false
} }
} }
} }

View File

@ -3,7 +3,7 @@
</script> </script>
{#if $page.status === 404} {#if $page.status === 404}
<div class="flex flex-col items-center m-4 py-8"> <div class="flex flex-col items-center m-4 py-8 space-y-8">
<div <div
class="relative mb-[3.33vh] flex h-24 w-24 md:h-64 md:w-64 items-center justify-center bg-surface-800" class="relative mb-[3.33vh] flex h-24 w-24 md:h-64 md:w-64 items-center justify-center bg-surface-800"
> >
@ -15,13 +15,18 @@
<h1 class="h1 absolute leading-[5rem] text-white">404</h1> <h1 class="h1 absolute leading-[5rem] text-white">404</h1>
</div> </div>
<h2 class="h4 mb-2"> <h4 class="flex text-center justify-center w-1/2">
? My God, what are you doing here, this page doesn't exist and so on and so on. ? My God, what are you doing here, this page doesn't exist and so on and so on.
</h2> </h4>
<p> <p>
This site either never existed or just suddenly committed disappearance from this This site either never existed or just suddenly committed disappearance from this
universe. universe after I changed something.
</p> </p>
<h5>Maybe try one of these links?</h5>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/projects">Projects</a></li>
</ul>
</div> </div>
{:else} {:else}
<h2>{$page.status}</h2> <h2>{$page.status}</h2>
@ -32,8 +37,7 @@
<p><strong>Sorry!</strong> A grave error has occured. Maybe try one of these links?</p> <p><strong>Sorry!</strong> A grave error has occured. Maybe try one of these links?</p>
<ul> <ul>
<li><a href="/">Home</a></li> <li><a class="link link-accent" href="/">Home</a></li>
<li><a href="/blog">Blog</a></li> <li><a class="link link-accent" href="/projects">Projects</a></li>
<li><a href="/projects">Projects</a></li>
</ul> </ul>
{/if} {/if}

View File

@ -1,5 +1,5 @@
import type { LayoutLoad } from './$types' import type { LayoutLoad } from './$types'
export const prerender = false export const prerender = true
export const trailingSlash = 'always' export const trailingSlash = 'always'
export const load: LayoutLoad = async ({ url, fetch }) => ({ export const load: LayoutLoad = async ({ url, fetch }) => ({
path: url.pathname, path: url.pathname,

View File

@ -21,6 +21,23 @@
storedTitle.set('') storedTitle.set('')
function handleTagClick(tag) {
if (tags.includes(tag)) {
tags = tags.filter(tagName => tagName !== tag);
} else {
tags = [...tags, tag];
}
scrollToRecentFeed();
}
function scrollToRecentFeed() {
const recentFeedSection = document.getElementById('recent-feed');
if (recentFeedSection) {
recentFeedSection.scrollIntoView({ behavior: 'smooth' });
}
}
$: storedPosts.subscribe(storedPosts => (allPosts = storedPosts.filter(post => !post.flags?.includes('unlisted')))) $: storedPosts.subscribe(storedPosts => (allPosts = storedPosts.filter(post => !post.flags?.includes('unlisted'))))
$: storedTags.subscribe(storedTags => (allTags = storedTags as string[])) $: storedTags.subscribe(storedTags => (allTags = storedTags as string[]))
@ -29,13 +46,9 @@
$: if (tags) { $: if (tags) {
posts = !tags ? allPosts : allPosts.filter(post => tags.every(tag => post.tags?.includes(tag))) posts = !tags ? allPosts : allPosts.filter(post => tags.every(tag => post.tags?.includes(tag)))
if (browser) { if (loaded && browser) {
const newUrl = tags.length > 0 ? `/?tags=${tags.toString()}#recent-feed` : '/#recent-feed'; const newUrl = `/?tags=${tags.toString()}`;
history.pushState(null, '', newUrl); history.pushState(null, '', newUrl);
const recentFeedSection = document.getElementById('recent-feed');
if (recentFeedSection) {
recentFeedSection.scrollIntoView({ behavior: 'smooth' });
}
} }
} }
@ -79,17 +92,7 @@
{#each allTags as tag} {#each allTags as tag}
<button <button
id={tag} id={tag}
on:click|preventDefault={() => { on:click={() => handleTagClick(tag)}
if (tags.includes(tag)) {
tags = tags.filter(tagName => tagName != tag);
} else {
tags = [...tags, tag];
}
const recentFeedSection = document.getElementById('recent-feed');
if (recentFeedSection) {
recentFeedSection.scrollIntoView({ behavior: 'smooth' });
}
}}
class:!btn-secondary={tags.includes(tag)} class:!btn-secondary={tags.includes(tag)}
class:shadow-lg={tags.includes(tag)} class:shadow-lg={tags.includes(tag)}
class="btn btn-sm btn-ghost normal-case border-dotted border-base-content/20 border-2 mt-4 mb-8 xl:m-0 whitespace-nowrap"> class="btn btn-sm btn-ghost normal-case border-dotted border-base-content/20 border-2 mt-4 mb-8 xl:m-0 whitespace-nowrap">

View File

@ -0,0 +1 @@
export const prerender = true

View File

@ -1,6 +1,9 @@
// svelte adapter // svelte adapter
import adapterAuto from '@sveltejs/adapter-auto'
import adapterNode from '@sveltejs/adapter-node' import adapterNode from '@sveltejs/adapter-node'
import adapterVercel from '@sveltejs/adapter-vercel'
import adapterNetlify from '@sveltejs/adapter-netlify'
import adapterCloudflare from '@sveltejs/adapter-cloudflare'
import adapterStatic from '@sveltejs/adapter-static' import adapterStatic from '@sveltejs/adapter-static'
// svelte preprocessor // svelte preprocessor
import { mdsvex } from 'mdsvex' import { mdsvex } from 'mdsvex'
@ -8,51 +11,59 @@ import mdsvexConfig from './mdsvex.config.js'
// import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' // import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
import preprocess from 'svelte-preprocess' import preprocess from 'svelte-preprocess'
const adapter = { function getAdapter() {
auto: adapterAuto(), if (Object.keys(process.env).some(key => key.includes('VERCEL'))) {
node: adapterNode(), return adapterVercel()
static: adapterStatic({ } else if (Object.keys(process.env).some(key => key.includes('NETLIFY'))) {
pages: 'build', return adapterNetlify()
assets: 'build', } else if (Object.keys(process.env).some(key => key.includes('CF_PAGES'))) {
fallback: undefined return adapterCloudflare()
}) } else {
return process.env.ADAPTER === 'node'
? adapterNode({ out: 'build' })
: adapterStatic({
pages: 'build',
assets: 'build',
fallback: undefined
})
}
} }
/** @type {import("@svletejs/kit".Config)} */ /** @type {import("@svletejs/kit".Config)} */
export default { export default {
extensions: ['.svelte', ...(mdsvexConfig.extensions || [])], extensions: ['.svelte', ...(mdsvexConfig.extensions || [])],
preprocess: [preprocess({ postcss: true }), mdsvex(mdsvexConfig) /*, vitePreprocess()*/], preprocess: [preprocess({ postcss: true }), mdsvex(mdsvexConfig) /*, vitePreprocess()*/],
vitePlugin: { vitePlugin: {
inspector: true inspector: true
},
kit: {
adapter: process.env.ADAPTER
? adapter[process.env.ADAPTER.toLowerCase()]
: Object.keys(process.env).some(key => ['VERCEL', 'NETLIFY'].includes(key))
? adapter['auto']
: adapter['static'],
alias: {
$lib: './src/lib',
$root: './',
$src: './src',
$routes: './src/routes',
$content: './content'
}, },
csrf: { kit: {
checkOrigin: process.env.NODE_ENV === 'development' ? false : true adapter: getAdapter(),
}, alias: {
prerender: { $lib: './src/lib',
crawl: true, $root: './',
handleMissingId: 'warn', $src: './src',
handleHttpError: details => { $routes: './src/routes',
// Handle blog trying to prerender relative links that it can't $content: './content'
if (details.status == 404 && details.path.startsWith('/blog' && '/projects') && details.referenceType == 'linked') { },
console.warn(`PRERENDER ignored route ${details.path}`) csrf: {
return checkOrigin: process.env.NODE_ENV === 'development' ? false : true
} },
prerender: {
crawl: true,
handleMissingId: 'warn',
handleHttpError: ({ status, path, referrer, referenceType, message }) => {
// Handle blog trying to prerender relative links that it can't
if (
(status == 404 && path.startsWith('/blog')) ||
path.startsWith('/projects') ||
(path.startsWith('/') && referenceType == 'linked')
) {
console.warn(`PRERENDER ignored route ${path}`)
return
}
throw new Error(`${details.status} ${details.path} from ${details.referrer}`) throw new Error(`${status} ${path} from ${referrer}, ~~~~~~~~~ message: ${message}~~~~~~~~~`)
} }
}
} }
}
} }

View File

@ -1,14 +1,28 @@
{ {
"extends": "./.svelte-kit/tsconfig.json", "extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"allowJs": true, "outDir": "build",
"checkJs": true, "allowJs": true,
"esModuleInterop": true, "checkJs": true,
"forceConsistentCasingInFileNames": false, "esModuleInterop": true,
"resolveJsonModule": true, "forceConsistentCasingInFileNames": true,
"skipLibCheck": true, "resolveJsonModule": true,
"sourceMap": true, "skipLibCheck": true,
"strict": true, "sourceMap": true,
"types": ["vite/client", "vite-plugin-pwa/client"] "strict": true,
} "types": ["vite/client"]
},
"include": [
"./types/**/$types.d.ts",
"./svelte.config.js",
"./vite.config.js",
"./vite.config.ts",
"./src/**/*.js",
"./src/**/*.ts",
"./src/**/*.svelte",
"./tests/**/*.js",
"./tests/**/*.ts",
"./tests/**/*.svelte"
],
"exclude": ["node_modules", "build/**/*", "src/lib/template/**/*", "src/generated/**/*"]
} }

216
urara.ts
View File

@ -9,160 +9,132 @@ import chokidar from 'chokidar'
import chalk from 'chalk' import chalk from 'chalk'
const config = { const config = {
extensions: { extensions: ['md'],
posts: ['md'], catch: ['ENOENT', 'EEXIST']
images: ['jpg', 'png', 'webp', 'avif']
},
images: [''],
catch: ['ENOENT', 'EEXIST']
} }
const check = (ext: string) => (config.extensions.posts.includes(ext) ? 'src/routes' : 'static') const check = (ext: string) => (config.extensions.includes(ext) ? 'src/routes' : 'static')
const log = (color: string, msg: string, dest?: string | Error) => const log = (color: string, msg: string, dest?: string | Error) =>
console.log( console.log(
chalk.dim(new Date().toLocaleTimeString() + ' ') + chalk.dim(new Date().toLocaleTimeString() + ' ') +
chalk.magentaBright.bold('[urara] ') + chalk.magentaBright.bold('[urara] ') +
chalk[color](msg + ' ') + chalk[color](msg + ' ') +
chalk.dim(dest ?? '') chalk.dim(dest ?? '')
) )
const error = (err: { code: string; message: unknown }) => { const error = (err: { code: string; message: unknown }) => {
if (config.catch.includes(err.code)) { if (config.catch.includes(err.code)) {
console.log( console.log(
chalk.dim(new Date().toLocaleTimeString() + ' ') + chalk.dim(new Date().toLocaleTimeString() + ' ') +
chalk.redBright.bold('[urara] ') + chalk.redBright.bold('[urara] ') +
chalk.red('error ') + chalk.red('error ') +
chalk.dim(err.message) chalk.dim(err.message)
) )
} else { } else {
throw err throw err
} }
} }
const cpFile = (src: string, { stat = 'copy', dest = path.join(check(path.parse(src).ext.slice(1)), src.slice(6)) } = {}) => const cpFile = (src: string, { stat = 'copy', dest = path.join(check(path.parse(src).ext.slice(1)), src.slice(6)) } = {}) =>
config.extensions.images.includes(path.parse(src).ext.slice(1)) fs
? fs
.copyFile(src, path.join('src/static', src.slice(6)))
.then(() => fs.copyFile(src, path.join('static', src.slice(6))))
.then(() => log('green', `${stat} file`, dest))
.catch(error)
: fs
.copyFile(src, dest) .copyFile(src, dest)
.then(() => log('green', `${stat} file`, dest)) .then(() => log('green', `${stat} file`, dest))
.catch(error) .catch(error)
const rmFile = (src: string, { dest = path.join(check(path.parse(src).ext.slice(1)), src.slice(6)) } = {}) => const rmFile = (src: string, { dest = path.join(check(path.parse(src).ext.slice(1)), src.slice(6)) } = {}) =>
config.extensions.images.includes(path.parse(src).ext.slice(1)) fs
? fs
.rm(path.join('src/static', src.slice(6)))
.then(() => fs.rm(path.join('static', src.slice(6))))
.then(() => log('yellow', 'remove file', dest))
.catch(error)
: fs
.rm(dest) .rm(dest)
.then(() => log('yellow', 'remove file', dest)) .then(() => log('yellow', 'remove file', dest))
.catch(error) .catch(error)
const cpDir = (src: string) => const cpDir = (src: string) =>
fs.readdir(src, { withFileTypes: true }).then(files => fs.readdir(src, { withFileTypes: true }).then(files =>
files.forEach(file => { files.forEach(file => {
const dest = path.join(src, file.name) const dest = path.join(src, file.name)
if (file.isDirectory()) { if (file.isDirectory()) {
mkDir(dest) mkDir(dest)
cpDir(dest) cpDir(dest)
} else if (file.name.startsWith('.')) { } else if (file.name.startsWith('.')) {
log('cyan', 'ignore file', dest) log('cyan', 'ignore file', dest)
} else { } else {
cpFile(dest) cpFile(dest)
} }
}) })
) )
const mkDir = ( const mkDir = (src: string, { dest = [path.join('src/routes', src.slice(6)), path.join('static', src.slice(6))] } = {}) => {
src: string, dest.forEach(path =>
{ fs
dest = [path.join('src/routes', src.slice(6)), path.join('static', src.slice(6)), path.join('src/static', src.slice(6))] .mkdir(path)
} = {} .then(() => log('green', 'make dir', path))
) => { .catch(error)
dest.forEach(path => )
fs
.mkdir(path)
.then(() => log('green', 'make dir', path))
.catch(error)
)
} }
const rmDir = ( const rmDir = (src: string, { dest = [path.join('src/routes', src.slice(6)), path.join('static', src.slice(6))] } = {}) => {
src: string, dest.forEach(path =>
{ fs
dest = [path.join('src/routes', src.slice(6)), path.join('static', src.slice(6)), path.join('src/static', src.slice(6))] .rm(path, { force: true, recursive: true })
} = {} .then(() => log('yellow', 'remove dir', path))
) => { .catch(error)
dest.forEach(path => )
fs
.rm(path, { force: true, recursive: true })
.then(() => log('yellow', 'remove dir', path))
.catch(error)
)
} }
const cleanDir = (src: string) => const cleanDir = (src: string) =>
fs.readdir(src, { withFileTypes: true }).then(files => { fs.readdir(src, { withFileTypes: true }).then(files => {
files.forEach(file => { files.forEach(file => {
const dest = path.join(src, file.name) const dest = path.join(src, file.name)
file.isDirectory() ? rmDir(dest) : file.name.startsWith('.') ? log('cyan', 'ignore file', dest) : rmFile(dest) file.isDirectory() ? rmDir(dest) : file.name.startsWith('.') ? log('cyan', 'ignore file', dest) : rmFile(dest)
})
}) })
})
const build = () => { const build = () => {
mkDir('static', { dest: ['static'] }) mkDir('static', { dest: ['static'] })
mkDir('src/static', { dest: ['src/static'] }) cpDir('urara')
cpDir('urara')
} }
const clean = () => { const clean = () => {
cleanDir('urara') cleanDir('urara')
rmDir('static', { dest: ['static'] }) rmDir('static', { dest: ['static'] })
rmDir('src/static', { dest: ['src/static'] })
} }
switch (process.argv[2]) { switch (process.argv[2]) {
case 'watch': case 'watch':
{ {
const watcher = chokidar.watch('urara', { const watcher = chokidar.watch('urara', {
ignored: (file: string) => path.basename(file).startsWith('.') ignored: (file: string) => path.basename(file).startsWith('.')
}) })
watcher watcher
.on('add', file => cpFile(file)) .on('add', file => cpFile(file))
.on('change', file => cpFile(file, { stat: 'update' })) .on('change', file => cpFile(file, { stat: 'update' }))
.on('unlink', file => rmFile(file)) .on('unlink', file => rmFile(file))
.on('addDir', dir => mkDir(dir)) .on('addDir', dir => mkDir(dir))
.on('unlinkDir', dir => rmDir(dir)) .on('unlinkDir', dir => rmDir(dir))
.on('error', error => log('red', 'error', error)) .on('error', error => log('red', 'error', error))
.on('ready', () => log('cyan', 'copy complete. ready for changes')) .on('ready', () => log('cyan', 'copy complete. ready for changes'))
process process
.on('SIGINT', () => { .on('SIGINT', () => {
log('red', 'sigint') log('red', 'sigint')
clean() clean()
watcher?.close() watcher?.close()
}) })
.on('SIGTERM', () => { .on('SIGTERM', () => {
log('red', 'sigterm') log('red', 'sigterm')
watcher?.close() watcher?.close()
}) })
.on('exit', () => { .on('exit', () => {
log('red', 'exit') log('red', 'exit')
}) })
} }
break break
case 'build': case 'build':
build() build()
break break
case 'clean': case 'clean':
clean() clean()
break break
default: default:
log('red', 'error', 'invalid arguments') log('red', 'error', 'invalid arguments')
break break
} }

49
utils/create-blog.sh Normal file
View File

@ -0,0 +1,49 @@
#!/bin/bash
BASE_PATH="./user/blogs"
# Check if a directory name is provided
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <directory-name>"
exit 1
fi
# Get the current date in ISO 8601 format with timezone
CURRENT_DATE=$(date "+%Y-%m-%dT%H:%M:%S.000")
TIMEZONE_FORMATTED=$(date "+%z" | sed 's/\([0-9][0-9]\)\([0-9][0-9]\)/\1:\2/')
# Every time you enter pnpm run createblog YourDirName,
# a folder will be created for you based on the following configuration,
# along with a default content for index.md.
# You are free to edit the information as needed.
DIRECTORY="$BASE_PATH/$1"
MD_TEMPLATE="---
title:
description:
summary:
published: '$CURRENT_DATE$TIMEZONE_FORMATTED'
updated: '$CURRENT_DATE$TIMEZONE_FORMATTED'
cover: ./cover.jpg
coverCaption: Photo by
coverStyle: 'IN'
series_tag:
series_title:
tag:
- [svelte-QWER]
---"
# Ensure base path exists
if [ ! -d "$BASE_PATH" ]; then
mkdir -p "$BASE_PATH"
fi
# Check if directory exists
if [ -d "$DIRECTORY" ]; then
echo "Directory $DIRECTORY already exists!"
else
mkdir "$DIRECTORY"
echo "$MD_TEMPLATE" > "$DIRECTORY/index.md"
echo "Directory $DIRECTORY and index.md created successfully!"
fi

View File

@ -91,8 +91,8 @@ export default defineConfig({
manifest: false, manifest: false,
scope: '/', scope: '/',
workbox: { workbox: {
globPatterns: ['posts.json', '**/*.{js,css,html,svg,ico,png,webp,avif}'], globPatterns: ['robots.txt', 'posts.json', '**/*.{js,css,html,svg,ico,png,webp,avif}', 'prerendered/**/*.html'],
globIgnores: ['**/sw*', '**/workbox-*'] globIgnores: ['**/sw*', '**/workbox-*', '*.xml', 'feed.json', 'tags.json']
} }
}) })
] ]