Dockerized the application, fixed fonts, improved SEO, upgraded to Nuxt 3.8.2

This commit is contained in:
Andrew Illarionov 2023-11-25 19:52:25 +03:00
parent 2bd1e4a512
commit 4939d24f62
28 changed files with 3011 additions and 16621 deletions

8
.dockerignore Normal file
View File

@ -0,0 +1,8 @@
# .dockerignore
/.git
/.nuxt
/.output
/node_modules
.gitignore
README.md

View File

@ -1 +1 @@
[{"D:\\Software\\Development\\Websites\\enderman.ch\\index\\assets\\styles\\list-types.scss":"1","D:\\Software\\Development\\Websites\\enderman.ch\\index\\assets\\styles\\transitions.scss":"2","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\Logo.vue":"3","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\LinkButton.vue":"4","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\TransitionY.vue":"5","D:\\Software\\Development\\Websites\\enderman.ch\\index\\app.vue":"6","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\EMail.vue":"7","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\Navigation.vue":"8","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\Separator.vue":"9","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\Flooter.vue":"10","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\Portal.vue":"11","D:\\Software\\Development\\Websites\\enderman.ch\\index\\pages\\about.vue":"12","D:\\Software\\Development\\Websites\\enderman.ch\\index\\pages\\index.vue":"13","D:\\Software\\Development\\Websites\\enderman.ch\\index\\pages\\projects.vue":"14","D:\\Software\\Development\\Websites\\enderman.ch\\index\\pages\\social.vue":"15","D:\\Software\\Development\\Websites\\enderman.ch\\index\\layouts\\Card.vue":"16"},{"size":389,"mtime":1700689887460,"hashOfConfig":"17"},{"size":165,"mtime":1700688268778,"hashOfConfig":"17"},{"size":1113,"mtime":1700251498711,"hashOfConfig":"17"},{"size":986,"mtime":1700324714794,"hashOfConfig":"17"},{"size":1513,"mtime":1700430002840,"hashOfConfig":"17"},{"size":1517,"mtime":1700692384277,"hashOfConfig":"17"},{"size":804,"mtime":1700688952880,"hashOfConfig":"17"},{"size":1252,"mtime":1700694015567,"hashOfConfig":"17"},{"size":300,"mtime":1700689033070,"hashOfConfig":"17"},{"size":1853,"mtime":1700508715727,"hashOfConfig":"17"},{"size":16907,"mtime":1700748650426,"hashOfConfig":"17"},{"size":2135,"mtime":1700689265787,"hashOfConfig":"17"},{"size":3117,"mtime":1700689271383,"hashOfConfig":"17"},{"size":2304,"mtime":1700689265769,"hashOfConfig":"17"},{"size":3232,"mtime":1700689265781,"hashOfConfig":"17"},{"size":1623,"mtime":1700748530633,"hashOfConfig":"17"},"5tgxr3"]
[{"D:\\Software\\Development\\Websites\\enderman.ch\\index\\assets\\styles\\transitions.scss":"1","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\Logo.vue":"2","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\LinkButton.vue":"3","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\TransitionY.vue":"4","D:\\Software\\Development\\Websites\\enderman.ch\\index\\app.vue":"5","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\EMail.vue":"6","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\Navigation.vue":"7","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\Separator.vue":"8","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\Flooter.vue":"9","D:\\Software\\Development\\Websites\\enderman.ch\\index\\components\\Portal.vue":"10","D:\\Software\\Development\\Websites\\enderman.ch\\index\\pages\\about.vue":"11","D:\\Software\\Development\\Websites\\enderman.ch\\index\\pages\\index.vue":"12","D:\\Software\\Development\\Websites\\enderman.ch\\index\\pages\\projects.vue":"13","D:\\Software\\Development\\Websites\\enderman.ch\\index\\pages\\social.vue":"14","D:\\Software\\Development\\Websites\\enderman.ch\\index\\layouts\\Card.vue":"15"},{"size":165,"mtime":1700688268778,"hashOfConfig":"16"},{"size":1113,"mtime":1700251498711,"hashOfConfig":"16"},{"size":986,"mtime":1700324714794,"hashOfConfig":"16"},{"size":1513,"mtime":1700430002840,"hashOfConfig":"16"},{"size":1550,"mtime":1700850776284,"hashOfConfig":"16"},{"size":804,"mtime":1700688952880,"hashOfConfig":"16"},{"size":1252,"mtime":1700694015567,"hashOfConfig":"16"},{"size":300,"mtime":1700689033070,"hashOfConfig":"16"},{"size":1877,"mtime":1700929377808,"hashOfConfig":"16"},{"size":16907,"mtime":1700919582401,"hashOfConfig":"16"},{"size":2135,"mtime":1700689265787,"hashOfConfig":"16"},{"size":3117,"mtime":1700689271383,"hashOfConfig":"16"},{"size":2304,"mtime":1700689265769,"hashOfConfig":"16"},{"size":3232,"mtime":1700689265781,"hashOfConfig":"16"},{"size":1623,"mtime":1700748530633,"hashOfConfig":"16"},"5tgxr3"]

36
Dockerfile Normal file
View File

@ -0,0 +1,36 @@
ARG NODE_ENV=development
ARG HOST=localhost
ARG PORT=3000
FROM node:18-alpine as build
RUN apk update && apk upgrade
# Create application directory
WORKDIR /app
# Copy package files to the application directory and install dependencies
COPY package* ./
RUN npm install
# Copy everything else from the project
COPY . ./
RUN npm run build
FROM node:18-alpine as production
RUN apk update && apk upgrade && apk add dumb-init && adduser -D nuxt
# Set non-root user
USER nuxt
# Set working directory
WORKDIR /opt/index
COPY --chown=nuxt:nuxt --from=build /app/.output ./
# Expose the listening port
EXPOSE ${PORT}
# Set Nitro variables and run the application
# https://go.enderman.ch/vws4k
ENV HOST=${HOST} PORT=${PORT} NODE_ENV=${NODE_ENV}
CMD ["dumb-init", "node", "/opt/index/server/index.mjs"]

13
app.vue
View File

@ -1,23 +1,27 @@
<script setup lang="ts">
import { useQuasar } from 'quasar'
import { useRuntimeConfig } from '#imports'
const config = useRuntimeConfig()
const quasar = useQuasar()
useHead({
titleTemplate: (chunk) => {
return !chunk || chunk === 'Enderman' ? 'Enderman' : `${chunk} Enderman`
return !chunk || chunk === config.public.websiteName
? config.public.websiteName
: `${chunk} Enderman`
},
})
</script>
<template>
<NuxtLoadingIndicator color="purple" />
<div
id="ender-layout"
class="d-flex flex-column align-items-center pt-sm-5 h-100 animate__animated-sm animate__delay-1-5s animate__fadeInDown"
>
<NuxtLayout name="card">
<template #header> </template>
<template #footer>
<ClientOnly>
<template #fallback> </template>
@ -39,7 +43,10 @@ useHead({
</NuxtLayout>
</div>
<Portal layout="#ender-layout" animate randomize fade />
<ClientOnly>
<template #fallback> </template>
<Portal layout="#ender-layout" animate randomize fade />
</ClientOnly>
<Transition
appear

View File

@ -1,22 +0,0 @@
@charset "UTF-8";
@counter-style do {
system: cyclic;
symbols: "💻" "📌" "👻" "📷";
suffix: " ";
}
@counter-style enjoy {
system: cyclic;
symbols: "💻" "📌" "👻" "📷" "♟" "⛄";
suffix: " ";
}
.list-style-type-none {
list-style-type: none;
}
.list-style-type-do {
list-style-type: do;
}
.list-style-type-enjoy {
list-style-type: enjoy;
}
/*# sourceMappingURL=list-types.css.map */

View File

@ -1 +0,0 @@
{"version":3,"sourceRoot":"","sources":["list-types.scss"],"names":[],"mappings":";AAAA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAIA;EACE;;AAGF;EACE;;AAGF;EACE","file":"list-types.css"}

View File

@ -10,6 +10,12 @@
suffix: " ";
}
@counter-style faq {
system: cyclic;
symbols: "\2753" "\1F4A1";
suffix: " ";
}
.list-style-type {
&-none {
list-style-type: none;
@ -22,4 +28,8 @@
&-enjoy {
list-style-type: enjoy;
}
&-faq {
list-style-type: faq;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,21 +1,20 @@
// Fonts.
@import '@fontsource/Lato/index.css';
@import '@fontsource/Alexandria/700.css';
$grid-breakpoints: (
xs: 0,
sm: 601px,
md: 996px,
lg: 1281px,
xl: 1921px,
xxl: 2561px
// Libraries.
@use '@fortawesome/fontawesome-svg-core/styles.css' as fontawesome;
@use 'bootstrap' with (
$grid-breakpoints: (
xs: 0,
sm: 601px,
md: 996px,
lg: 1281px,
xl: 1921px,
xxl: 2561px
)
);
@use 'animate.css';
// Bootstrap & animations.
@import '../../node_modules/bootstrap/scss/bootstrap';
@import '../../node_modules/animate.css/animate';
@import 'transitions';
@import 'list-types';
// Modules.
@use 'transitions';
@use 'lists';
// CSS Variables.
:root {
@ -33,18 +32,25 @@ html {
body {
display: flex;
flex-direction: column;
position: sticky;
background-color: rgb(6 25 28);
background-image: url('~/assets/images/sky.png');
background-attachment: fixed;
background-blend-mode: multiply;
background-size: cover;
background-repeat: no-repeat;
image-rendering: pixelated;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: white;
font-family: Lato, sans-serif;
font-size: 18px;
height: 100%;
}
@ -78,6 +84,7 @@ body {
.alex {
font-family: Alexandria, sans-serif;
font-weight: 700;
}
.smooth-glide {
@ -94,17 +101,21 @@ body {
min-height: 100%;
}
.link {
%link {
color: inherit;
text-decoration: none;
transition: 0.3s ease;
}
.link {
@extend %link;
&:hover {
color: cornflowerblue;
}
&-hi {
@extend .link;
@extend %link;
color: cornflowerblue;
@ -113,7 +124,7 @@ body {
}
&-force-dark {
@extend .link;
@extend %link;
color: cornflowerblue;
@ -210,7 +221,7 @@ body {
}
// Dynamic classes.
@include media-breakpoint-up(sm) {
@include bootstrap.media-breakpoint-up(sm) {
#ender-app {
height: 100%;
}
@ -247,22 +258,23 @@ body {
}
}
@include media-breakpoint-up(md) {
@include bootstrap.media-breakpoint-up(md) {
/* TODO: Fill it up :) */
}
@include media-breakpoint-up(lg) {
@include bootstrap.media-breakpoint-up(lg) {
.dimensions {
width: 60%;
}
}
@include media-breakpoint-up(xl) {
@include bootstrap.media-breakpoint-up(xl) {
.dimensions {
width: 40%;
}
}
@include media-breakpoint-up(xxl) {
@include bootstrap.media-breakpoint-up(xxl) {
.dimensions {
width: 30%;
}

View File

@ -1,9 +0,0 @@
.page-enter-active, .page-leave-active {
transition: all 0.4s;
}
.page-enter-from, .page-leave-to {
opacity: 0;
filter: blur(1rem);
}
/*# sourceMappingURL=transitions.css.map */

View File

@ -1 +0,0 @@
{"version":3,"sourceRoot":"","sources":["transitions.scss"],"names":[],"mappings":"AACE;EAEE;;AAGF;EAEE;EACA","file":"transitions.css"}

View File

@ -6,8 +6,10 @@ const props = defineProps({
})
const config = useRuntimeConfig()
const currentYear = new Date().getFullYear()
const version = config.public.version
const buildDate = config.public.buildDate
const currentYear = new Date().getFullYear()
const mailTemplate = `I've just found a bug on https://enderman.ch and would like to report it.%0D%0A%0D%0AWebsite version: ${
config.public.version
}%0D%0ABuild date: ${
@ -35,8 +37,8 @@ const mailTemplate = `I've just found a bug on https://enderman.ch and would lik
<a class="link-hi-force-dark" href="https://enderman.ch">Enderman</a>. All
rights reserved.
<sub>
β{{ config.public.version ? config.public.version : '?.?.?' }} ({{
config.public.buildDate ? config.public.buildDate : '1970-01-01'
β{{ version ? version : '?.?.?' }} ({{
buildDate ? buildDate : '1970-01-01'
}})
</sub>
</span>

View File

@ -11,7 +11,20 @@ const pageStore = usePageStore()
const { pages } = storeToRefs(pageStore)
const links = toRaw(pages.value).filter((page) => page.path !== '/')
const icons = [aboutIcon, projectIcon, socialIcon]
const icons = [
{
src: aboutIcon,
alt: 'Information',
},
{
src: projectIcon,
alt: 'Blocks',
},
{
src: socialIcon,
alt: 'Users',
},
]
</script>
<template>
@ -31,8 +44,8 @@ const icons = [aboutIcon, projectIcon, socialIcon]
<LinkButton
:path="page.path"
:name="page.name"
:icon="icons[index]"
alt="Info"
:icon="icons[index].src"
:alt="icons[index].alt"
/>
</li>
</ul>

View File

@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<button type="button" data-theme-toggle aria-label="Change to light theme">
Change to light theme
</button>
</template>

15
docker-compose.yml Normal file
View File

@ -0,0 +1,15 @@
version: "3"
services:
index:
container_name: frontend-index
image: endermanch/index:latest
environment:
- NODE_ENV=production
- PORT=3000
- HOST=0.0.0.0
build:
context: .
ports:
- "127.0.0.1:3670:3000"
restart: unless-stopped

View File

@ -13,9 +13,6 @@ const { pages } = storeToRefs(pageStore)
const swipe = useSwipe(card, {
passive: true,
onSwipe: (_touch: TouchEvent) => {
console.log(card.value?.offsetWidth, swipe.lengthX.value)
},
onSwipeEnd: (_touch: TouchEvent, _direction: UseSwipeDirection) => {
const currentPage = pages.value.find(
(page) => page.path === route.fullPath,
@ -25,21 +22,18 @@ const swipe = useSwipe(card, {
if (
card.value?.offsetWidth &&
Math.abs(swipe.lengthX.value) / card.value?.offsetWidth >= 0.5
Math.abs(swipe.lengthX.value) > 1.5 * Math.abs(swipe.lengthY.value) &&
Math.abs(swipe.lengthX.value) / card.value?.offsetWidth >= 0.1
) {
if (swipe.lengthX.value > 0) {
router.push(
toRaw(pages.value)[
currentIndex - 1 < 0
router.push(
toRaw(pages.value)[
swipe.lengthX.value > 0
? (currentIndex + 1) % pages.value.length
: currentIndex - 1 < 0
? pages.value.length - (currentIndex + 1)
: currentIndex - 1
].path,
)
} else {
router.push(
toRaw(pages.value)[(currentIndex + 1) % pages.value.length].path,
)
}
].path,
)
}
},
})

View File

@ -1,6 +1,10 @@
import eslint from 'vite-plugin-eslint'
import packageJSON from './package.json'
const websiteName = "Enderman's Website"
const websiteURL = 'https://enderman.ch'
const buildDate = new Date().toISOString().split('T')[0]
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
app: {
@ -41,32 +45,126 @@ export default defineNuxtConfig({
},
],
},
css: [
'~/assets/styles/main.min.css',
'@fortawesome/fontawesome-svg-core/styles.css',
],
css: ['~/assets/styles/main.scss'],
plugins: [],
modules: [
'nuxt-font-loader',
'@nuxt/content',
[
'@nuxtjs/eslint-module',
{ fix: true, failOnError: true, lintOnStart: false },
],
'@pinia/nuxt',
'@nuxt/content',
'nuxt-quasar-ui',
['@nuxtjs/eslint-module', { failOnError: false, lintOnStart: false }],
['@nuxtjs/stylelint-module', { failOnError: true, lintOnStart: false }],
'@nuxtjs/color-mode',
'@nuxtjs/google-fonts',
'nuxt-simple-sitemap',
'nuxt-simple-robots',
'nuxt-link-checker',
'nuxt-og-image',
],
nitro: {
prerender: {
crawlLinks: true,
autoSubfolderIndex: true,
failOnError: true,
routes: ['/robots.txt', '/sitemap.xml'],
},
},
quasar: {
lang: 'en-US',
},
googleFonts: {
download: true,
families: {
Lato: true,
Alexandria: true,
},
},
site: {
url: websiteURL,
name: websiteName,
},
routeRules: {
'/': {
sitemap: {
changefreq: 'yearly',
priority: 1,
},
},
'/about': {
sitemap: {
changefreq: 'yearly',
priority: 0.8,
},
},
'/projects': {
sitemap: {
changefreq: 'monthly',
priority: 0.8,
},
},
'/social': {
sitemap: {
changefreq: 'yearly',
priority: 0.8,
},
},
},
sitemap: {
cacheMaxAgeSeconds: 360,
exclude: [],
credits: false,
xslColumns: [
{ label: 'URL', width: '50%' },
{ label: 'Last Modified', select: 'sitemap:lastmod', width: '25%' },
{ label: 'Priority', select: 'sitemap:priority', width: '12.5%' },
{
label: 'Change Frequency',
select: 'sitemap:changefreq',
width: '12.5%',
},
],
// defaults: { ← Defaults override route rules...
// changefreq: 'monthly',
// priority: 0.7,
// lastmod: buildDate,
// },
},
robots: {
blockNonSeoBots: true,
credits: false,
},
linkChecker: {
failOnError: true,
report: {
html: true,
markdown: true,
},
},
typescript: {
typeCheck: true,
},
runtimeConfig: {
public: {
version: JSON.stringify(packageJSON.version).slice(1, -1),
buildDate: new Date().toISOString().split('T')[0],
buildDate,
websiteName,
websiteURL,
},
},
/* vite: {
build: {
rollupOptions: {
output: {
manualChunks(id: string) {
if (id.includes('node_modules')) {
return id
.toString()
.split('node_modules/')[1]
.split('/')[0]
.toString()
}
},
},
},
},
}, */
})

3382
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,48 +17,48 @@
"@nuxt/content": "^2.9.0",
"@nuxt/devtools": "latest",
"@nuxt/types": "^2.17.2",
"@nuxtjs/color-mode": "^3.3.2",
"@nuxtjs/eslint-config-typescript": "^12.1.0",
"@nuxtjs/eslint-module": "^4.1.0",
"@nuxtjs/google-fonts": "^3.1.0",
"@nuxtjs/stylelint-module": "^5.1.0",
"@pinia/nuxt": "^0.5.1",
"@quasar/extras": "^1.16.8",
"@typescript-eslint/parser": "^6.10.0",
"caniuse-lite": "^1.0.30001564",
"csso": "^5.0.5",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-nuxt": "^4.0.0",
"eslint-plugin-prettier": "^5.0.1",
"nuxt": "^3.8.1",
"nuxt-font-loader": "^2.3.4",
"nuxt-link-checker": "^2.1.10",
"nuxt-og-image": "^2.2.4",
"nuxt-quasar-ui": "^2.0.6",
"prettier": "^3.0.3",
"nuxt-simple-robots": "^3.1.9",
"nuxt-simple-sitemap": "^4.1.6",
"pinia": "^2.1.7",
"prettier": "^3.1.0",
"quasar": "^2.14.0",
"sass": "^1.69.5",
"stylelint": "^15.11.0",
"stylelint-config-recommended-vue": "^1.5.0",
"stylelint-config-standard-scss": "^11.1.0",
"typescript": "^5.2.2",
"typescript": "^5.3.2",
"vite-plugin-require": "^0.0.3",
"vue": "^3.3.8",
"vue-router": "^4.2.5",
"vue-tsc": "^1.8.22"
},
"dependencies": {
"@fontsource/alexandria": "^5.0.8",
"@fontsource/jetbrains-mono": "^5.0.17",
"@fontsource/lato": "^5.0.17",
"@fortawesome/fontawesome": "^1.1.8",
"@fortawesome/fontawesome-free": "^6.4.2",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-brands-svg-icons": "^6.4.2",
"@fortawesome/free-regular-svg-icons": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/vue-fontawesome": "^3.0.5",
"@quasar/extras": "^1.16.8",
"animate.css": "^4.1.1",
"bootstrap": "^5.3.2",
"caniuse-lite": "^1.0.30001564",
"humps": "^2.0.1",
"pinia": "^2.1.7",
"quasar": "^2.14.0"
"bootstrap": "^5.3.2"
},
"version": "0.1.1"
}

View File

@ -37,6 +37,8 @@ useHead({
},
],
})
defineRobotMeta()
</script>
<template>
@ -73,5 +75,15 @@ useHead({
doesn't help with adhering to deadlines at all. However, you can always
check them out!
</p>
<h5 class="alex">FAQ</h5>
<ul class="list-style-type-faq">
<li><strong>Why are you called Endermanch?</strong></li>
<li>
There's no particular reason to it. When I was rebranding my YouTube
channel from Ender's Show to Enderman, the short URL was taken. I went
thinking for quite some time, and then concluded that adding «ch» at the
end sounded pretty good. I was 14 at the time of making that decision.
</li>
</ul>
</section>
</template>

View File

@ -1,8 +1,11 @@
<script setup lang="ts">
import { useRuntimeConfig } from '#imports'
const config = useRuntimeConfig()
const meta = {
title: 'Enderman',
description:
'A software engineer, a malware enthusiast and, most importantly, a weird tall creature.',
'A software engineer, a malware enthusiast and, most importantly, a weird tall creature. I have over 300K subscribers on YouTube and over 20K followers on Twitter.',
image: 'https://enderman.ch/images/logo.png',
url: 'https://enderman.ch/',
}
@ -22,7 +25,7 @@ useSeoMeta({
})
useHead({
title: 'Enderman',
title: config.public.websiteName ? config.public.websiteName : 'Enderman',
htmlAttrs: {
lang: 'en',
},
@ -34,6 +37,8 @@ useHead({
},
],
})
defineRobotMeta()
</script>
<template>

View File

@ -64,6 +64,8 @@ useHead({
},
],
})
defineRobotMeta()
</script>
<template>

View File

@ -84,6 +84,8 @@ useHead({
},
],
})
defineRobotMeta()
</script>
<template>

View File

@ -1,13 +1,10 @@
import { library, config } from '@fortawesome/fontawesome-svg-core'
import { far } from '@fortawesome/free-regular-svg-icons'
import { fab } from '@fortawesome/free-brands-svg-icons'
import { fas } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
// Add icon packs.
library.add(far, fab, fas)
library.add(fab)
// Let Nuxt manage CSS.
config.autoAddCss = false

View File

@ -1,3 +0,0 @@
User-agent: *
Disallow: /images
Sitemap: https://enderman.ch/sitemap.xml

View File

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http:www.w3.org/1999/xhtml">
<url>
<loc>https://enderman.ch/</loc>
<lastmod>2023-11-19</lastmod>
<changefreq>never</changefreq>
<priority>1</priority>
</url>
<url>
<loc>https://enderman.ch/about</loc>
<lastmod>2023-11-19</lastmod>
<changefreq>yearly</changefreq>
<priority>0.6</priority>
</url>
<url>
<loc>https://enderman.ch/projects</loc>
<lastmod>2023-11-19</lastmod>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://enderman.ch/socials</loc>
<lastmod>2023-11-19</lastmod>
<changefreq>yearly</changefreq>
<priority>0.75</priority>
</url>
</urlset>