diff --git a/app/app.vue b/app/app.vue index 52b2164..c876d24 100755 --- a/app/app.vue +++ b/app/app.vue @@ -8,8 +8,6 @@ diff --git a/app/components/RichText/Link.vue b/app/components/RichText/Link.vue deleted file mode 100755 index 4714303..0000000 --- a/app/components/RichText/Link.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - diff --git a/app/components/RichText/NewLine.vue b/app/components/RichText/NewLine.vue deleted file mode 100755 index 1502375..0000000 --- a/app/components/RichText/NewLine.vue +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/app/components/RichText/Paragraph.vue b/app/components/RichText/Paragraph.vue deleted file mode 100755 index 5ec1196..0000000 --- a/app/components/RichText/Paragraph.vue +++ /dev/null @@ -1,11 +0,0 @@ - - - \ No newline at end of file diff --git a/app/components/RichText/Plain.vue b/app/components/RichText/Plain.vue deleted file mode 100755 index 162d9d3..0000000 --- a/app/components/RichText/Plain.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/components/RichText/RichText.vue b/app/components/RichText/RichText.vue deleted file mode 100755 index ed64451..0000000 --- a/app/components/RichText/RichText.vue +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/app/components/RichText/String.vue b/app/components/RichText/String.vue deleted file mode 100755 index 926b4e0..0000000 --- a/app/components/RichText/String.vue +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/app/components/RichText/Types.ts b/app/components/RichText/Types.ts deleted file mode 100755 index 3372f88..0000000 --- a/app/components/RichText/Types.ts +++ /dev/null @@ -1,30 +0,0 @@ -type RichTextBasis = { - type: string - content: string - css ?: string -} - -export type RichTextPlain = RichTextBasis & { - type: 'plain' -} - -export type RichTextParagraph = Omit & { - type: 'p' - children ?: RichText[] -} - -export type RichTextSpan = RichTextBasis & { - type: 'span' -} - -export type RichTextLink = RichTextBasis & { - type: 'a' - href: string - target ?: string -} - -export type RichTextNewLine = { - type: 'br' -} - -export type RichText = RichTextPlain | RichTextParagraph | RichTextSpan | RichTextLink | RichTextNewLine \ No newline at end of file diff --git a/app/components/Section/Booking.vue b/app/components/Section/Booking.vue index a715ed7..ca9d9fa 100755 --- a/app/components/Section/Booking.vue +++ b/app/components/Section/Booking.vue @@ -1,33 +1,44 @@ @@ -65,15 +76,16 @@ type Service = { } const intl = new Intl.NumberFormat( - 'de-DE', - { - style: 'currency', - currency: 'EUR', - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }) + 'de-DE', + { + style: 'currency', + currency: 'EUR', + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }, +) -const oneOff : Service[] = [ +const oneOff: Service[] = [ { title: 'Quick Check', price: { @@ -128,36 +140,21 @@ const oneOff : Service[] = [ }, ] -const faq = [ - { - header: 'Warum machst du keine Stundensätze?', - content: [ - 'Ich finde Stundensätze haben für beide Seiten nur Nachteile:', - 'Wenn ich schnell und gut arbeite, dann bekomme ich weniger Geld. Hab ich mal einen Knoten im Gehirn und brauche sehr lange, muss der Kunde mehr zahlen.', - 'Klar kann man sagen, dass sich das irgendwann ausgleichen könnte - aber so weit will ich es garnicht erst kommen lassen.' - ] - }, - { - header: 'Welche Themen bietest du für deine Schulungen an?', - content: [ - 'Sprachen: JavaScript, TypeScript, HTML, CSS', - 'Frameworks: Vue, Nuxt', - ] - }, - { - header: 'Wo finden die Schulungen statt?', - content: [ - 'Die Schulungen finden online statt. Normalerweise nutze ich dafür Google Meet, aber wenn du oder deine Firma eine andere Plattform wünschen und bereitstellen bin ich natürlich flexibel.', - 'Wenn sich deine Firma in der Nähe meines Wohnortes befindet - und damit meine ich "In einer Stunde mit der Straßenbahn zu erreichen", dann kann alles natürlich auch vor Ort stattfinden.', - ] - }, - { - header: 'Ich hab ein cooles Projekt! Aber kein Geld...', - content: [ - 'Tja.', - 'Ne, awas. Meld dich einfach trotzdem über meine E-Mail-Adresse und vielleicht finden wir eine Lösung.' - ] - } -] +const { data: faq } = await useAsyncData('faq_booking', () => queryCollection('faq').path('/snippets/faq/booking').first()) +const texts = generatePlainText<['title']>(faq.value?.body.value) +if (faq) { + useSchemaOrg({ + '@context': 'https://schema.org', + '@type': 'FAQPage', + 'mainEntity': texts.map(entity => ({ + '@type': 'Question', + 'name': entity.meta.title, + 'acceptedAnswer': { + '@type': 'Answer', + 'text': entity.text, + }, + })), + }) +} diff --git a/app/components/Section/Contact.vue b/app/components/Section/Contact.vue index 15a187f..56f51da 100644 --- a/app/components/Section/Contact.vue +++ b/app/components/Section/Contact.vue @@ -1,7 +1,7 @@ \ No newline at end of file + + + \ No newline at end of file diff --git a/app/utils/markdown.ts b/app/utils/markdown.ts new file mode 100644 index 0000000..adeaec8 --- /dev/null +++ b/app/utils/markdown.ts @@ -0,0 +1,27 @@ +import type { MinimalElement, MinimalNode } from '@nuxt/content' + +type TypedRecord = Record + +type PlainText = { + meta: TypedRecord + text: string +} + +const extractText = (element ?: MinimalNode) : string => { + if (!element) return '' + if (typeof element === 'string') return element + const [,, ...nodes] = element + return nodes?.map((el : MinimalNode) => typeof el === 'string' ? el : extractText(el)).join(' ') ?? '' +} + +export const generatePlainText = (body ?: MinimalNode[]) : PlainText[] => { + if (!body) return [] + return body.map>(part => { + const [, meta] = part as MinimalElement + return { + meta : meta as TypedRecord, + text: extractText(part).replace(/\n/g, ' ') + } + }) +} + diff --git a/app/utils/socials.ts b/app/utils/socials.ts index 8c5eb5e..8178280 100644 --- a/app/utils/socials.ts +++ b/app/utils/socials.ts @@ -18,12 +18,12 @@ export const socials = [ name: '@webfussel.de', 'aria-label': 'Externer Link: Bluesky Profil' }, - { - href: 'https://twitch.tv/webfussel', - icon: 'ph:twitch-logo-duotone', - name: 'webfussel', - 'aria-label': 'Externer Link: Twitch Kanal' - }, + // { + // href: 'https://twitch.tv/webfussel', + // icon: 'ph:twitch-logo-duotone', + // name: 'webfussel', + // 'aria-label': 'Externer Link: Twitch Kanal' + // }, { href: 'https://ko-fi.com/webfussel', icon: 'wf:kofi', diff --git a/content.config.ts b/content.config.ts index 9ea361f..bcb3581 100644 --- a/content.config.ts +++ b/content.config.ts @@ -10,13 +10,13 @@ export default defineContentConfig({ skills: defineCollection({ type: 'page', source: 'snippets/skills/*.md', + }), + + faq: defineCollection({ + type: 'page', + source: 'snippets/faq/*.md', schema: z.object({ - title: z.string(), - img: z.object({ - path: z.string(), - name: z.string(), - position: z.string(), - }), + rawbody: z.string(), }) }) } diff --git a/content/snippets/faq/booking.md b/content/snippets/faq/booking.md new file mode 100644 index 0000000..2af58b4 --- /dev/null +++ b/content/snippets/faq/booking.md @@ -0,0 +1,67 @@ +::spoiler +--- +title: "Warum machst du keine Stundensätze?" +--- +Ich finde, Stundensätze haben für beide Seiten nur Nachteile: + +Wenn ich schnell und gut arbeite, bekomme ich weniger Geld. Hab ich mal einen Knoten im Gehirn und brauche sehr lange, muss der Kunde mehr zahlen. + +Klar kann man sagen, dass sich das irgendwann ausgleichen könnte – aber so weit will ich es gar nicht erst kommen lassen. +:: + +::spoiler +--- +title: "Was bekomme ich denn für die 999 €?" +--- +Grundsätzlich kommt das natürlich immer darauf an, was du genau willst. Da spielen einige Faktoren eine Rolle. + +**Ein Beispiel für eine 999 € Seite wäre** + +--- +- Kein CMS +- Eine Section auf der Landingpage +- Impressum, Datenschutzerklärung +- Standarddesign +--- + +Je nachdem was du willst, kann sich das aber auch anders zusammensetzen. +Zu Beachten ist, dass es sich hier um reines HTML und CSS handelt. Keine Fancy Daten aus einem Backend, keine 3D-Animationen. +Große und gute Websites werden normalerweise über einen längeren Zeitraum entwickelt und kosten entsprechend auch mehr. + +Bei diesem Startangebot handelt es sich also einfach nur um eine kleine Online-Visitenkarte. +:: + +::spoiler +--- +title: "Welche Themen bietest du für deine Schulungen an?" +--- +
+Sprachen +
JavaScript, TypeScript, HTML, CSS +
+ +
+Frameworks +
Vue, Nuxt +
+:: + +::spoiler +--- +title: "Wo finden die Schulungen statt?" +--- +Die Schulungen finden online statt. Normalerweise nutze ich dafür Google Meet, aber wenn du oder deine Firma eine andere Plattform wünschen und bereitstellen bin ich natürlich flexibel. + +Wenn sich deine Firma in der Nähe meines Wohnortes befindet – und damit meine ich "In einer Stunde mit der Straßenbahn zu erreichen", dann kann alles natürlich auch vor Ort stattfinden. +:: + +::spoiler +--- +title: "Ich hab ein cooles Projekt! Aber kein Geld …" +--- +Tja. + +Ne, awas. Meld dich einfach trotzdem über meine E-Mail-Adresse und vielleicht finden wir eine Lösung. + +Du findest weitere Kontaktmöglichkeiten auf meiner [Kontakt-Seite ](/contact/){class="text inline-flex-row"}. +:: \ No newline at end of file diff --git a/content/snippets/faq/flatrate.md b/content/snippets/faq/flatrate.md new file mode 100644 index 0000000..adee048 --- /dev/null +++ b/content/snippets/faq/flatrate.md @@ -0,0 +1,53 @@ +::spoiler +--- +title: "Was ist eine Entwickler-Flatrate?" +--- +Die Entwickler-Flatrate ist ein Angebot, bei dem du für eine bestimmte Zeit eine bestimmte Menge an Leistungen erhältst. + +Sie wird auch oft als sogenannter "Retainer" bezeichnet und ist eine günstige Art, immer wieder anfallende Aufgaben auszulagern. +:: + +::spoiler +--- +title: "Wie läuft die Zusammenarbeit ab?" +--- +Nach einer ersten Analyse deines Projekts legen wir gemeinsam den Umfang und die monatlichen Aufgaben fest. Du kannst mich je nach Paket auf den vereinbarten Wegen jederzeit kontaktieren. + +Die Bearbeitung erfolgt innerhalb der vereinbarten Fristen. +:: + +::spoiler +--- +title: "Wie wird abgerechnet?" +--- +Du zahlst monatlich einen festen Betrag im Prepaid-Format unabhängig davon, wie viele Aufgaben tatsächlich anfallen. + +Dadurch kannst du dein Budget besser einplanen und Aufgaben verteilen. +:: + +::spoiler +--- +title: "Kann ich den Retainer jederzeit kündigen?" +--- +Ja klar. Du musst dabei nur die Mindestvertragslaufzeit beachten – die bezahlte Leistung steht dir weiterhin zu. + +Falls wir merken, dass wir für eine Zusammenarbeit mehr als ungeeignet sind, bekommst du dein Geld anteilig der verbleibenden Zeit zurück und verlierst den Anspruch auf Leistungen ab diesem Zeitpunkt. +:: + +::spoiler +--- +title: "Was passiert, wenn ich dich mal weniger brauche?" +--- +Der monatliche feste Betrag bleibt bestehen. Ähnlich wie bei einer Versicherung bezahlst du hier für den Fall, dass du mich brauchst, und erhältst dann entsprechend die vereinbarten Leistungen. + +Wenn du merkst, dass du mich nicht mehr brauchst, dann kannst du den Vertrag jederzeit kündigen und die Leistungen auslaufen lassen. +:: + +::spoiler +--- +title: "Für wen lohnt sich sowas überhaupt?" +--- +Vor allem Unternehmen, bei denen immer wieder Aufgaben anfallen, für die sich aber kein komplettes Projekt oder gar eine feste Stelle in der Firma lohnt, profitieren von dieser Art von Leistung. + +Sie ist ideal für eine langfristige und zuverlässige Zusammenarbeit. +:: \ No newline at end of file diff --git a/content/snippets/skills/1.see.md b/content/snippets/skills/1.see.md index fa0430c..0ea7a91 100644 --- a/content/snippets/skills/1.see.md +++ b/content/snippets/skills/1.see.md @@ -15,5 +15,5 @@ Diese Komponenten so zu entwickeln, dass sie wirklich flexibel sind und auch per **Diese Voraussicht, für den Fall der Fälle vorzusorgen:** **Die gibt's bei mir dazu.** -Fussel-Ehrenwort. +Fussel-Ehrenwort. :: \ No newline at end of file diff --git a/content/snippets/skills/2.edit.md b/content/snippets/skills/2.edit.md index 0037d6a..b5b195a 100644 --- a/content/snippets/skills/2.edit.md +++ b/content/snippets/skills/2.edit.md @@ -7,12 +7,12 @@ image: position: "right" --- -In vielen Fällen ist es natürlich praktisch, wenn du einfach Inhalte einer Seite auf das ändern kannst, was gerade aktuell ist, ohne auf andere angewiesen zu sein. Damit das reibungslos möglich ist, entwickle ich dir gerne eine Anwendung oder Homepage, deren Inhalte du komplett selbst auf dem neuesten Stand halten kannst – und zwar mit einem sogenannten CMS – ein Content Management System. +In vielen Fällen ist es natürlich praktisch, wenn du einfach Inhalte einer Seite auf das ändern kannst, was gerade aktuell ist, ohne auf andere angewiesen zu sein. Damit das reibungslos möglich ist, entwickle ich dir gerne eine Anwendung oder Homepage, deren Inhalte du komplett selbst auf dem neuesten Stand halten kannst – und zwar mit einem sogenannten CMS – ein Content Management System. -Für CMS setze ich in erster Linie auf die cloudbasierte Lösung [Storyblok ](https://www.storyblok.com){class="inline-flex-row text"}. Dies stellt für die Meisten eine kostenlose bis kostengünstige Lösung dar ohne viel technisches Wissen mitbringen zu müssen. +Für CMS setze ich in erster Linie auf die cloudbasierte Lösung [Storyblok ](https://www.storyblok.com){class="inline-flex-row text"}. Dies stellt für die Meisten eine kostenlose bis kostengünstige Lösung dar, ohne viel technisches Wissen mitbringen zu müssen. -Falls du aber nicht möchtest, dass deine Daten auf irgendeinem Fremden Server liegen – was ich durchaus verstehen kann! – dann gibt es auch die Möglichkeit mit [Strapi ](https://strapi.io){class="inline-flex-row text"} selbst das eigene CMS zu hosten. Das musst du dann aber allerdings selbst erledigen oder ich erledige das für dich für einen Aufpreis. +Falls du aber nicht möchtest, dass deine Daten auf irgendeinem Fremden Server liegen – was ich durchaus verstehen kann! – dann gibt es auch die Möglichkeit mit [Strapi ](https://strapi.io){class="inline-flex-row text"} selbst das eigene CMS zu hosten. Das musst du dann aber allerdings selbst erledigen oder ich erledige das für dich für einen Aufpreis. **Nie wieder jemand anderen fragen zu müssen, um deine Website auf dem neuesten Stand zu halten.** -Mit Fussel-Garantie. +Mit Fussel-Garantie. :: \ No newline at end of file diff --git a/content/snippets/skills/3.result.md b/content/snippets/skills/3.result.md index 02045db..bc2a49d 100644 --- a/content/snippets/skills/3.result.md +++ b/content/snippets/skills/3.result.md @@ -7,12 +7,12 @@ image: position: "left" --- -Grundsätzlich lässt sich das ganz einfach zusammenfassen: Dein persönlicher Webauftritt. +Grundsätzlich lässt sich das ganz einfach zusammenfassen: Dein persönlicher Webauftritt. -Ob du nun etwas kleineres brauchst, um ein paar Hobbies zu zeigen. Oder vielleicht etwas größeres, weil du dir ein eigenes Business aufbauen willst. Eventuell ein eigener Blog, eine komplette Applikation oder die Möglichkeit für andere Leute deine Daten bereit zu stellen. All das, das kann ich dir mit meinen Fähigkeiten und meiner Erfahrung bieten. +Ob du nun etwas Kleineres brauchst, um ein paar Hobbys zu zeigen. Oder vielleicht etwas Größeres, weil du dir ein eigenes Business aufbauen willst. Eventuell ein eigener Blog, eine komplette Applikation oder die Möglichkeit für andere Leute deine Daten bereitzustellen. All das, das kann ich dir mit meinen Fähigkeiten und meiner Erfahrung bieten. -Erkunde einfach meine [Referenzen ](references/){class="inline-flex-row text"} und dir wird auffallen, dass diese äußerst durchmischt und bunt sind. So individuell wie die Projekte meiner bisherigen Kunden wird auch dein Projekt behandelt. Und auch, wenn du glaubst, dass die Referenzen nicht zu deinem Projekt passen, werden wir deine ganz individuelle Lösung gemeinsam erarbeiten. +Erkunde einfach meine [Referenzen ](references/){class="inline-flex-row text"} und dir wird auffallen, dass diese äußerst durchmischt und bunt sind. So individuell wie die Projekte meiner bisherigen Kunden wird auch dein Projekt behandelt. Und auch, wenn du glaubst, dass die Referenzen nicht zu deinem Projekt passen, werden wir deine ganz individuelle Lösung gemeinsam erarbeiten. -**Denn jedes Projekt ist etwas eigenes und besonderes.** -Auch deins. +**Denn jedes Projekt ist etwas Eigenes und Besonderes.** +Auch deins. :: \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..7578ab9 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,47 @@ +version: '3' + +services: + wf4: + image: oven/bun:latest + container_name: wf4 + working_dir: /app + ports: + - "1337:3000" + volumes: + - wf4_data:/app + environment: + - NODE_ENV=production + command: > + sh -c " + # Install git and curl if not already installed + if ! command -v git &> /dev/null || ! command -v curl &> /dev/null; then + echo 'Installing required packages...' + apt-get update && apt-get install -y git curl + fi && + + # Clone repository if not already cloned + if [ ! -d /app/.git ]; then + echo 'Cloning repository...' + git clone https://git.webfussel.de/webfussel/wf4 /tmp/wf4 && + cp -r /tmp/wf4/. /app/ && + rm -rf /tmp/wf4 + fi && + + # Install dependencies and start application + echo 'Installing dependencies...' && + bun install && + echo 'Building application...' && + bun run build && + echo 'Starting application...' && + bun .output/server/index.mjs + " + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:1337"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +volumes: + wf4_data: diff --git a/package-lock.json b/package-lock.json index b36d5f2..35806ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3723,7 +3723,6 @@ }, "node_modules/@parcel/watcher-wasm/node_modules/napi-wasm": { "version": "1.1.0", - "dev": true, "inBundle": true, "license": "MIT" }, diff --git a/public/img/network/orell@1x.webp b/public/img/network/orell@1x.webp new file mode 100644 index 0000000..e47f07f Binary files /dev/null and b/public/img/network/orell@1x.webp differ diff --git a/public/img/network/orell@2x.webp b/public/img/network/orell@2x.webp new file mode 100644 index 0000000..2425726 Binary files /dev/null and b/public/img/network/orell@2x.webp differ diff --git a/public/img/network/orell@3x.webp b/public/img/network/orell@3x.webp new file mode 100644 index 0000000..c011a41 Binary files /dev/null and b/public/img/network/orell@3x.webp differ