Migrate to astro

This commit is contained in:
2026-06-15 13:13:57 +03:00
parent 1a844c57df
commit 8c4c4af56a
81 changed files with 6200 additions and 5069 deletions

View File

@@ -1,4 +0,0 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored
View File

@@ -1,11 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

239
README.md
View File

@@ -1,166 +1,115 @@
# Astroship - Astro SAAS Starter Website Template
# mariosant.dev
Astroship is a free starter astro website template for saas, startups, marketing websites, landing pages & blogs. Built with Astro & TailwindCSS.
Personal site and writing log for [Marios Antonoudiou](https://mariosant.dev) — AI product engineer.
This Free Template is sponsored by [Web3Templates](https://web3templates.com)
Migrated from a Nuxt 4 + `@nuxt/content` setup to **Astro 5** + **Tailwind CSS 4**, preserving every URL and the dark-mode design of the original.
## Live Demo
## Stack
**[https://astroship.web3templates.com/](https://astroship.web3templates.com/)**
- [Astro 5](https://astro.build) — static site generation
- [Tailwind CSS 4](https://tailwindcss.com) — utility CSS
- [astro-icon](https://github.com/natemoo-re/astro-icon) — Iconify icons
- [astro-seo](https://github.com/jonasmerlin/astro-seo) — `<SEO>` component
- [@astrojs/sitemap](https://docs.astro.build/en/guides/integrations-guide/sitemap/) — sitemap
- [@astrojs/mdx](https://docs.astro.build/en/guides/integrations-guide/mdx/) — MDX content
- [Inter Variable](https://fontsource.org/fonts/inter) via `@fontsource-variable/inter`
**[Download Astroship Template](https://web3templates.com/templates/astroship-starter-website-template-for-astro)**
## Upgrade to Astroship Pro Version
**[https://astroship-pro.web3templates.com/](https://astroship-pro.web3templates.com/)**
**[Purchase Astroship Pro — $49](https://web3templates.com/templates/astroship-pro-astro-saas-website-template)**
<!-- prettier-ignore -->
| Feature | Free Version | Pro Version |
| --- | ------ | --- |
| Astro v3 | ✅ | ✅ |
| Content Collections | ✅ | ✅ |
| Tailwind CSS | ✅ | ✅ |
| Mobile Responsive | ✅ | ✅ |
| Working Contact Page | ✅ | ✅ |
| Pro Layouts & Features | ❌ | ✅ |
| Blog with Pagination | ❌ | ✅ |
| View Transitions | ❌ | ✅ |
| Advanced Homepage Design | ❌ | ✅ |
| Features Page | ❌ | ✅ |
| Integrations Page | ❌ | ✅ |
| Elegant 404 Page | ❌ | ✅ |
| 6 Months Support| ❌ | ✅ |
| Free Updates | ✅ | ✅ |
| License | GPL-2.0 | Commercial |
| &nbsp; | &nbsp;| &nbsp;|
| Pricing| Free|**$49**|
| &nbsp; | [Deploy for free](https://vercel.com/new/surjithctly/clone?demo-description=Starter%20template%20for%20startups%2C%20marketing%20websites%20%26%20blogs%20built%20with%20Astro%20and%20TailwindCSS.&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F5dB0dDqBr1BfvIoNOmffVB%2F784984a8d3fe5e3db123e7c655166046%2Fastroship_-_Tony_Sullivan.jpg&demo-title=Astroship&demo-url=https%3A%2F%2Fastroship.web3templates.com%2F&from=templates&project-name=Astroship&repository-name=astroship&repository-url=https%3A%2F%2Fgithub.com%2Fsurjithctly%2Fastroship&skippable-integrations=1) | [Purchase Pro](https://web3templates.com/templates/astroship-pro-astro-saas-website-template) |
<a href="https://web3templates.com/templates/astroship-pro-astro-saas-website-template">
<img width="160" alt="Upgrade to Pro" src="https://user-images.githubusercontent.com/1884712/199181300-37c2128e-d033-4145-a906-16fa5263a53b.png">
</a>
## Deploy this template
You can instantly clone this to your GitHub and deploy the site by clicking the below buttons to deploy to your chosen providers!
Click here to deploy on Vercel:
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fsurjithctly%2Fastroship&project-name=astroship&repository-name=astroship&demo-title=Astroship%20-%20Astro%20Starter%20Template&demo-description=Astroship%20is%20a%20starter%20template%20for%20startups%2C%20marketing%20websites%20%26%20landing%20pages.%20Built%20with%20Astro%2C%20TailwindCSS&demo-url=https%3A%2F%2Fastroship.web3templates.com%2F&demo-image=https%3A%2F%2Fuser-images.githubusercontent.com%2F1884712%2F200831799-10ef2456-a02e-4068-b580-4b5326f0b33b.png)
Click here to deploy on Netlify:
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/surjithctly/astroship)
## Preview
![image](https://user-images.githubusercontent.com/1884712/200831799-10ef2456-a02e-4068-b580-4b5326f0b33b.png)
## Pro Version Preview
![preview](https://github.com/surjithctly/astroship/assets/1884712/25665c02-d2a7-43dc-89b2-34a8ae37ade9)
### Pagespeed Score
[![pagespeed](https://user-images.githubusercontent.com/1884712/210250214-7aa98167-7993-4b90-8138-326b8fa0c223.png)](https://pagespeed.web.dev/report?url=https%3A%2F%2Fastroship.web3templates.com%2F)
## Installation
If you are reading this on github, you can click on the "Use this template" button above to create a new repository from astroship to your account. Then you can do a `git clone` to clone it to your local system.
Alternatively, you can clone the project directly from this repo to your local system.
### 1. Clone the repo
## Scripts
```bash
git clone https://github.com/surjithctly/astroship.git myProjectName
# or
git clone https://github.com/surjithctly/astroship.git .
pnpm install # install dependencies
pnpm dev # start the dev server at http://localhost:4321
pnpm build # build the static site to ./dist
pnpm preview # preview the built site locally
```
The `.` will clone it to the current directory so make sure you are inside your project folder first.
### 2. Install Dependencies
```bash
npm install
# or
yarn install
# or (recommended)
pnpm install
```
### 3. Start development Server
```bash
npm run dev
# or
yarn dev
# or (recommended)
pnpm dev
```
### Preview & Build
```bash
npm run preview
npm run build
# or
yarn preview
yarn build
# or (recommended)
pnpm preview
pnpm build
```
We recommend using [pnpm](https://pnpm.io/) to save disk space on your computer.
### Other Commands
```bash
pnpm astro ...
pnpm astro add
pnpm astro --help
```
## Project Structure
Inside of your Astro project, you'll see the following folders and files:
## Project structure
```
/
├── public/
── ...
├── src/
│ ├── components/
│ └── ...
│ ├── layouts/
│ └── ...
── pages/
└── ...
── package.json
src/
├── components/ # Astro components
── home/ # Home page sections
│ ├── button.astro # Button primitive
│ ├── container.astro # UContainer equivalent
├── footer.astro
│ ├── link.astro # ULink equivalent
├── previous-role.astro
── project.astro
└── top-nav.astro
── content/
│ └── articles/ # Markdown articles
├── content.config.ts # Content collection schema
├── layouts/
│ └── Layout.astro # Single layout (default + content)
├── pages/
│ ├── 404.astro
│ ├── index.astro
│ └── articles/
│ ├── index.astro
│ └── [...slug].astro
├── styles/
│ └── global.css
└── utils/
└── date.ts # "15th of June 2026" formatter
public/ # Static assets (avatar, logos, favicon)
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
## Authoring an article
Any static assets, like images, can be placed in the `public/` directory.
1. Drop a new file in `src/content/articles/`, e.g. `2026-07-01-my-post.md`.
2. Add frontmatter that matches the schema in `src/content.config.ts`:
```yaml
---
title: My Post
date: 2026-07-01
coverImage:
author: Photographer Name
authorUrl: https://unsplash.com/@photographer # optional
url: https://images.unsplash.com/photo-...
---
```
3. Write the body in Markdown.
4. The new article will appear at `/articles/2026-07-01-my-post` and on the `/articles` index automatically.
## TailwindCSS
## Deployment
TailwindCSS is already configured in this repo, so you can start using it without any installation.
The site is fully static. Build with `pnpm build` and deploy the `dist/` directory to any static host (Vercel, Netlify, Cloudflare Pages, GitHub Pages, etc.).
## Credits
### Build settings (most hosts)
[Hero Illustration](https://www.figma.com/community/file/1108400791662599811) by [Streamline](https://www.streamlinehq.com/)
| Setting | Value |
|---|---|
| Build command | `pnpm build` |
| Output directory | `dist` |
| Node version | 20+ |
## 👀 Want to learn more?
### Recommended cache headers
Feel free to check out [Astro Docs](https://docs.astro.build) or jump into our [Discord Chat](https://web3templates.com/discord).
For `/_astro/*` (hashed bundles):
[![Built with Astro](https://astro.badg.es/v1/built-with-astro.svg)](https://astro.build)
```
Cache-Control: public, max-age=31536000, immutable
```
For `/articles/*` (HTML, sitemap-referenced):
```
Cache-Control: public, max-age=3600, must-revalidate
```
Configure these via your host's preferred mechanism (`vercel.json`, `_headers` for Netlify, `_headers` for Cloudflare Pages, etc.).
### SEO surface
- `sitemap-index.xml` + `sitemap-0.xml` — auto-generated by `@astrojs/sitemap`, all 13 routes
- `robots.txt` — points at the sitemap
- `rss.xml` — feed of all articles
- JSON-LD `Person` schema on the home page
- JSON-LD `Article` + `BreadcrumbList` schema on each article page
- Open Graph + Twitter Card meta on every page
- Canonical URLs on every page
- `<link rel="alternate" type="application/rss+xml">` autodiscovery
### Open Graph image
The default OG image is `public/mariosant.webp` (a 512×512 avatar). For best display on Twitter/LinkedIn (1200×630 recommended), drop a `public/og-image.png` and update `src/utils/seo.ts`'s default or pass it explicitly to the `Layout`.

View File

@@ -4,9 +4,8 @@ import mdx from "@astrojs/mdx";
import sitemap from "@astrojs/sitemap";
import icon from "astro-icon";
// https://astro.build/config
export default defineConfig({
site: "https://astroship.web3templates.com",
site: "https://mariosant.dev",
integrations: [mdx(), sitemap(), icon()],
vite: {
plugins: [tailwindcss()],

View File

@@ -1,7 +1,7 @@
{
"name": "astroship",
"name": "mariosant.dev",
"type": "module",
"version": "3.0.0",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "astro dev",
@@ -12,17 +12,17 @@
},
"dependencies": {
"@astrojs/mdx": "^4.2.0",
"@astrojs/rss": "^4.0.11",
"@astrojs/sitemap": "^3.2.1",
"@fontsource-variable/bricolage-grotesque": "^5.2.5",
"@fontsource-variable/inter": "^5.2.5",
"@iconify-json/bx": "^1.2.2",
"@iconify-json/heroicons": "^1.2.1",
"@iconify-json/hugeicons": "^1.2.29",
"@iconify-json/mynaui": "^1.2.5",
"@iconify-json/simple-icons": "^1.2.28",
"@iconify-json/uil": "^1.2.3",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.0.14",
"astro": "^5.5.2",
"astro-icon": "^1.1.5",
"astro-navbar": "^2.3.9",
"astro-seo": "^0.8.4",
"sharp": "^0.33.5",
"tailwindcss": "^4.0.14"

7967
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

BIN
public/agentic-ui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 KiB

BIN
public/celonis-logo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

@@ -1,13 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 36 36">
<path fill="#000" d="M22.25 4h-8.5a1 1 0 0 0-.96.73l-5.54 19.4a.5.5 0 0 0 .62.62l5.05-1.44a2 2 0 0 0 1.38-1.4l3.22-11.66a.5.5 0 0 1 .96 0l3.22 11.67a2 2 0 0 0 1.38 1.39l5.05 1.44a.5.5 0 0 0 .62-.62l-5.54-19.4a1 1 0 0 0-.96-.73Z"/>
<path fill="url(#gradient)" d="M18 28a7.63 7.63 0 0 1-5-2c-1.4 2.1-.35 4.35.6 5.55.14.17.41.07.47-.15.44-1.8 2.93-1.22 2.93.6 0 2.28.87 3.4 1.72 3.81.34.16.59-.2.49-.56-.31-1.05-.29-2.46 1.29-3.25 3-1.5 3.17-4.83 2.5-6-.67.67-2.6 2-5 2Z"/>
<defs>
<linearGradient id="gradient" x1="16" x2="16" y1="32" y2="24" gradientUnits="userSpaceOnUse">
<stop stop-color="#000"/>
<stop offset="1" stop-color="#000" stop-opacity="0"/>
</linearGradient>
</defs>
<style>
@media (prefers-color-scheme:dark){:root{filter:invert(100%)}}
</style>
</svg>

Before

Width:  |  Height:  |  Size: 873 B

BIN
public/lateralus-logo.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
public/lenses-logo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
public/mariosant.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -1,4 +1,4 @@
User-agent: *
Allow: /
Sitemap: http://astroship.web3templates.com/sitemap-index.xml
Sitemap: https://mariosant.dev/sitemap-index.xml

BIN
public/thechatshop-logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -0,0 +1,75 @@
---
import { Icon } from "astro-icon/components";
export type ButtonVariant = "primary" | "neutral" | "outline" | "ghost";
export type ButtonSize = "md" | "xl";
export interface Props {
href?: string;
to?: string;
target?: string;
rel?: string;
type?: "button" | "submit" | "reset";
variant?: ButtonVariant;
size?: ButtonSize;
icon?: string;
block?: boolean;
class?: string;
}
const {
href,
to,
target,
rel,
type = "button",
variant = "neutral",
size = "md",
icon,
block = false,
class: className = "",
} = Astro.props;
const VARIANT_CLASSES: Record<ButtonVariant, string> = {
primary:
"bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-300 dark:text-slate-900 dark:hover:bg-blue-200",
neutral:
"bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-700 dark:text-white dark:hover:bg-slate-600",
outline:
"border border-gray-200 text-slate-900 hover:bg-gray-50 dark:border-slate-600 dark:text-white dark:hover:bg-slate-800",
ghost:
"text-slate-900 hover:bg-gray-100 dark:text-white dark:hover:bg-slate-800",
};
const SIZE_CLASSES: Record<ButtonSize, string> = {
md: "px-3 py-2 text-sm",
xl: "px-4 py-3 text-base",
};
const baseClasses = [
"inline-flex items-center justify-center gap-2 rounded-md font-semibold whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 dark:focus-visible:ring-offset-slate-900",
block ? "w-full" : "",
SIZE_CLASSES[size],
VARIANT_CLASSES[variant],
className,
]
.filter(Boolean)
.join(" ");
const linkProps = { href: href ?? to, target, rel };
const isLink = Boolean(href || to);
---
{
isLink ? (
<a {...linkProps} class={baseClasses}>
{icon && <Icon name={icon} class="size-5 shrink-0" />}
<slot />
</a>
) : (
<button type={type} class={baseClasses}>
{icon && <Icon name={icon} class="size-5 shrink-0" />}
<slot />
</button>
)
}

View File

@@ -1,127 +0,0 @@
---
import Button from "./ui/button.astro";
---
<!-- To make this contact form work, create your free access key from https://web3forms.com/
Then you will get all form submissions in your email inbox. -->
<form
action="https://api.web3forms.com/submit"
method="POST"
id="form"
class="needs-validation"
novalidate>
<input type="hidden" name="access_key" value="YOUR_ACCESS_KEY_HERE" />
<!-- Create your free access key from https://web3forms.com/ -->
<input type="checkbox" class="hidden" style="display:none" name="botcheck" />
<div class="mb-5">
<input
type="text"
placeholder="Full Name"
required
class="w-full px-4 py-3 border-2 placeholder:text-gray-800 rounded-md outline-hidden focus:ring-4 border-gray-300 focus:border-gray-600 ring-gray-100"
name="name"
/>
<div class="empty-feedback invalid-feedback text-red-400 text-sm mt-1">
Please provide your full name.
</div>
</div>
<div class="mb-5">
<label for="email_address" class="sr-only">Email Address</label><input
id="email_address"
type="email"
placeholder="Email Address"
name="email"
required
class="w-full px-4 py-3 border-2 placeholder:text-gray-800 rounded-md outline-hidden focus:ring-4 border-gray-300 focus:border-gray-600 ring-gray-100"
/>
<div class="empty-feedback text-red-400 text-sm mt-1">
Please provide your email address.
</div>
<div class="invalid-feedback text-red-400 text-sm mt-1">
Please provide a valid email address.
</div>
</div>
<div class="mb-3">
<textarea
name="message"
required
placeholder="Your Message"
class="w-full px-4 py-3 border-2 placeholder:text-gray-800 rounded-md outline-hidden h-36 focus:ring-4 border-gray-300 focus:border-gray-600 ring-gray-100"
></textarea>
<div class="empty-feedback invalid-feedback text-red-400 text-sm mt-1">
Please enter your message.
</div>
</div>
<Button type="submit" size="lg" block>Send Message</Button>
<div id="result" class="mt-3 text-center"></div>
</form>
<style>
.invalid-feedback,
.empty-feedback {
display: none;
}
.was-validated :placeholder-shown:invalid ~ .empty-feedback {
display: block;
}
.was-validated :not(:placeholder-shown):invalid ~ .invalid-feedback {
display: block;
}
.is-invalid,
.was-validated :invalid {
border-color: #dc3545;
}
</style>
<script is:inline>
const form = document.getElementById("form");
const result = document.getElementById("result");
form.addEventListener("submit", function (e) {
e.preventDefault();
form.classList.add("was-validated");
if (!form.checkValidity()) {
form.querySelectorAll(":invalid")[0].focus();
return;
}
const formData = new FormData(form);
const object = Object.fromEntries(formData);
const json = JSON.stringify(object);
result.innerHTML = "Sending...";
fetch("https://api.web3forms.com/submit", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: json,
})
.then(async (response) => {
let json = await response.json();
if (response.status == 200) {
result.classList.add("text-green-500");
result.innerHTML = json.message;
} else {
console.log(response);
result.classList.add("text-red-500");
result.innerHTML = json.message;
}
})
.catch((error) => {
console.log(error);
result.innerHTML = "Something went wrong!";
})
.then(function () {
form.reset();
form.classList.remove("was-validated");
setTimeout(() => {
result.style.display = "none";
}, 5000);
});
});
</script>

View File

@@ -1,7 +1,16 @@
---
const { class: className } = Astro.props;
type Gap = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12;
export interface Props {
as?: string;
class?: string;
gap?: Gap;
}
const { as: Tag = "div", class: className = "", gap = 0 } = Astro.props;
const gapClass = gap === 0 ? "" : `gap-${gap}`;
---
<div class:list={["max-w-(--breakpoint-xl) mx-auto px-5", className]}>
<Tag class:list={["flex flex-col p-4 sm:p-6 lg:p-8 max-w-3xl mx-auto w-full", gapClass, className]}>
<slot />
</div>
</Tag>

View File

@@ -1,17 +0,0 @@
---
import Link from "./ui/link.astro";
---
<div
class="bg-black p-8 md:px-20 md:py-20 mt-20 mx-auto max-w-5xl rounded-lg flex flex-col items-center text-center">
<h2 class="text-white text-4xl md:text-6xl tracking-tight">
Build faster websites.
</h2>
<p class="text-slate-400 mt-4 text-lg md:text-xl">
Pull content from anywhere and serve it fast with Astro's next-gen island
architecture.
</p>
<div class="flex mt-5">
<Link href="#" style="inverted">Get Started</Link>
</div>
</div>

View File

@@ -1,69 +0,0 @@
---
// @ts-ignore
import { Icon } from "astro-icon/components";
const features = [
{
title: "Bring Your Own Framework",
description:
"Build your site using React, Svelte, Vue, Preact, web components, or just plain ol' HTML + JavaScript.",
icon: "bx:bxs-briefcase",
},
{
title: "100% Static HTML, No JS",
description:
"Astro renders your entire page to static HTML, removing all JavaScript from your final build by default.",
icon: "bx:bxs-window-alt",
},
{
title: "On-Demand Components",
description:
"Need some JS? Astro can automatically hydrate interactive components when they become visible on the page. ",
icon: "bx:bxs-data",
},
{
title: "Broad Integration",
description:
"Astro supports TypeScript, Scoped CSS, CSS Modules, Sass, Tailwind, Markdown, MDX, and any other npm packages.",
icon: "bx:bxs-bot",
},
{
title: "SEO Enabled",
description:
"Automatic sitemaps, RSS feeds, pagination and collections take the pain out of SEO and syndication. It just works!",
icon: "bx:bxs-file-find",
},
{
title: "Community",
description:
"Astro is an open source project powered by hundreds of contributors making thousands of individual contributions.",
icon: "bx:bxs-user",
},
];
---
<div class="mt-16 md:mt-0">
<h2 class="text-4xl lg:text-5xl font-bold lg:tracking-tight">
Everything you need to start a website
</h2>
<p class="text-lg mt-4 text-slate-600">
Astro comes batteries included. It takes the best parts of state-of-the-art
tools and adds its own innovations.
</p>
</div>
<div class="grid sm:grid-cols-2 md:grid-cols-3 mt-16 gap-16">
{
features.map((item) => (
<div class="flex gap-4 items-start">
<div class="mt-1 bg-black rounded-full p-2 w-8 h-8 shrink-0">
<Icon class="text-white" name={item.icon} />
</div>
<div>
<h3 class="font-semibold text-lg">{item.title}</h3>{" "}
<p class="text-slate-500 mt-2 leading-relaxed">{item.description}</p>
</div>
</div>
))
}
</div>

View File

@@ -1,19 +1,9 @@
<footer class="my-20">
<p class="text-center text-sm text-slate-500">
Copyright © {new Date().getFullYear()} Astroship. All rights reserved.
</p>
<!--
Can we ask you a favor 🙏
Please keep this backlink on your website if possible.
or Purchase a commercial license from https://web3templates.com
-->
<p class="text-center text-xs text-slate-500 mt-1">
Made by <a
href="https://web3templates.com"
target="_blank"
rel="noopener"
class="hover:underline">
Web3Templates
</a>
</p>
---
const currentYear = new Date().getFullYear();
---
<footer class="flex flex-col gap-2 p-4 sm:p-6 lg:p-8 max-w-3xl mx-auto w-full items-center justify-center">
<div class="text-sm text-gray-400">
© Marios Antonoudiou {currentYear} - 🏛️ Athens, Greece
</div>
</footer>

View File

@@ -1,53 +0,0 @@
---
import { Picture } from "astro:assets";
import heroImage from "@/assets/hero.png";
import Link from "@/components/ui/link.astro";
import { Icon } from "astro-icon/components";
---
<main
class="grid lg:grid-cols-2 place-items-center pt-16 pb-8 md:pt-12 md:pb-24">
<div class="py-6 md:order-1 hidden md:block">
<Picture
src={heroImage}
alt="Astronaut in the air"
widths={[200, 400, 600]}
sizes="(max-width: 800px) 100vw, 620px"
loading="eager"
format="avif"
/>
</div>
<div>
<h1
class="text-5xl lg:text-6xl xl:text-7xl font-bold lg:tracking-tight xl:tracking-tighter">
Marketing website done with Astro
</h1>
<p class="text-lg mt-4 text-slate-600 max-w-xl">
Astroship is a starter template for startups, marketing websites & landing
pages.<wbr /> Built with Astro.build and TailwindCSS. You can quickly create
any website with this starter.
</p>
<div class="mt-6 flex flex-col sm:flex-row gap-3">
<Link
href="#"
href="https://web3templates.com/templates/astroship-starter-website-template-for-astro"
target="_blank"
class="flex gap-1 items-center justify-center"
rel="noopener">
<Icon class="text-white w-5 h-5" name="bx:bxs-cloud-download" />
Download for Free
</Link>
<Link
size="lg"
style="outline"
rel="noopener"
href="https://github.com/surjithctly/astroship"
class="flex gap-1 items-center justify-center"
target="_blank">
<Icon class="text-black w-4 h-4" name="bx:bxl-github" />
GitHub Repo
</Link>
</div>
</div>
</main>

View File

@@ -0,0 +1,56 @@
---
import PreviousRole from "@/components/previous-role.astro";
---
<section class="flex flex-col gap-5 p-4 sm:p-6 lg:p-8 max-w-3xl mx-auto w-full">
<div class="text-sm text-slate-500 dark:text-slate-400">Been building with</div>
<PreviousRole
company="Lateralus Ventures"
logo="/lateralus-logo.jpeg"
href="https://lateralus.ventures"
>
Led frontend development for Ask Chief, shaping AI product experiences with scalable interfaces
that could evolve alongside intelligent workflows.
</PreviousRole>
<PreviousRole
company="Celonis"
logo="/celonis-logo.webp"
href="https://celonis.com"
>
Built analytical product interfaces for OCPM, alongside shared component systems, build pipelines,
and platform foundations that helped teams move faster together.
</PreviousRole>
<PreviousRole
company="Lenses.io"
logo="/lenses-logo.webp"
href="https://lenses.io"
>
<ul class="list-disc list-inside space-y-1">
<li>Engineered product interfaces for SQL Processors and Data Catalog.</li>
<li>Architected the shared components library.</li>
<li>Worked on developer tooling, including the CLI.</li>
</ul>
</PreviousRole>
<PreviousRole company="The Chat Shop" logo="/thechatshop-logo.jpg">
<ul class="list-disc list-inside space-y-1">
<li>Spearheaded end-to-end processing architecture.</li>
<li>Established the company's streaming data infrastructure.</li>
<li>Built the operational foundations for reliable delivery and automation.</li>
</ul>
</PreviousRole>
<PreviousRole
company="Commversion"
logo="/commversion-logo.webp"
href="https://commversion.com"
>
<ul class="list-disc list-inside space-y-1">
<li>Developed the customer delivery platform and its public-facing infrastructure.</li>
<li>Designed and implemented the initial InstantConnect system.</li>
</ul>
</PreviousRole>
</section>

View File

@@ -0,0 +1,50 @@
---
import Button from "@/components/button.astro";
---
<section class="flex flex-col gap-5 p-4 sm:p-6 lg:p-8 max-w-3xl mx-auto w-full">
<div class="text-sm text-slate-500 dark:text-slate-400">Let's connect</div>
<div class="flex gap-4 overflow-x-auto">
<Button
variant="neutral"
icon="mynaui:calendar"
href="https://cal.com/mariosant/30min"
target="_blank"
rel="noopener noreferrer">
Calendar
</Button>
<Button
variant="neutral"
icon="mynaui:envelope"
href="mailto:mariosant@sent.com"
target="_blank"
rel="noopener noreferrer">
Email
</Button>
<Button
variant="neutral"
icon="mynaui:brand-github"
href="https://github.com/mariosant"
target="_blank"
rel="noopener noreferrer">
Github
</Button>
<Button
variant="neutral"
icon="mynaui:brand-linkedin"
href="https://www.linkedin.com/in/mariosant/"
target="_blank"
rel="noopener noreferrer">
Linkedin
</Button>
<Button
variant="neutral"
icon="hugeicons:bluesky"
href="https://bsky.app/profile/mariosant.bsky.social"
target="_blank"
rel="noopener noreferrer">
Bluesky
</Button>
</div>
</section>

View File

@@ -0,0 +1,71 @@
---
import Button from "@/components/button.astro";
import Link from "@/components/link.astro";
---
<header class="flex flex-col md:gap-12 gap-5 p-4 sm:p-6 lg:p-8 max-w-3xl mx-auto w-full">
<div class="flex items-center justify-between">
<a href="/" aria-label="Home">
<img
alt="Marios Antonoudiou"
src="/mariosant.webp"
width="64"
height="64"
class="w-16 h-16 rounded-full border border-gray-200 dark:border-gray-800 dark:bg-slate-300 block"
loading="eager"
/>
</a>
<div class="flex gap-3 items-center justify-end">
<Link
href="/"
variant="default"
class="text-slate-600 dark:text-slate-400 font-semibold"
activeClass="text-slate-800 dark:text-slate-200 underline"
isActive={Astro.url.pathname === "/"}
>
Home
</Link>
<Link
href="/articles"
variant="default"
class="text-slate-600 dark:text-slate-400 font-semibold"
activeClass="text-slate-800 dark:text-slate-200 underline"
isActive={Astro.url.pathname.startsWith("/articles")}
>
Articles
</Link>
</div>
</div>
<h1 class="md:text-xl text-lg font-semibold">
<Link href="mailto:mariosant@sent.com" variant="primary">Marios Antonoudiou</Link>, AI product engineer.<br />
Building AI-powered products that feel simple, useful, and ready for real users.
</h1>
<p class="text-slate-800 dark:text-slate-200">
I design and ship <span class="font-semibold">AI-enabled product experiences</span> across
frontend architecture, interaction design, and product workflows. My focus is turning complex
systems, data, and model capabilities into software people can actually
<span class="font-semibold">understand, trust, and use</span>.
</p>
<div class="flex gap-4 overflow-x-auto">
<Button
size="xl"
variant="primary"
icon="mynaui:calendar"
href="https://cal.com/mariosant/30min"
target="_blank"
rel="noopener noreferrer"
>
Arrange a call
</Button>
<Button
size="xl"
variant="neutral"
icon="mynaui:envelope"
href="mailto:mariosant@sent.com"
target="_blank"
rel="noopener noreferrer"
>
Send a message
</Button>
</div>
</header>

View File

@@ -0,0 +1,56 @@
---
import { getCollection } from "astro:content";
import { formatDoMMMMYYYY } from "@/utils/date";
const articles = (await getCollection("articles")).sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime(),
);
const article = articles[0];
if (!article) {
throw new Error("No articles found in the 'articles' content collection.");
}
const cover = article.data.coverImage;
const coverUrl = cover.url.replace("w=1287&h=600", "w=300&h=300");
const body = (article.body ?? "").trim();
const excerpt = body.length > 150 ? `${body.slice(0, 150)}…` : body;
---
<section class="p-4 sm:p-6 lg:p-8 max-w-3xl mx-auto w-full">
<span class="text-sm text-slate-500 dark:text-slate-400 flex flex-row gap-1 items-center">
Latest article
</span>
<a
href={`/articles/${article.id}`}
class="block mt-4 p-4 rounded-lg border border-gray-200 dark:border-slate-600 hover:border-gray-300 dark:hover:border-slate-500 transition-colors"
>
<div class="flex gap-6 items-start flex-row-reverse">
{
cover.url && (
<img
class="w-24 h-24 object-cover rounded-lg shrink-0"
src={coverUrl}
alt={article.data.title}
loading="lazy"
/>
)
}
<div class="flex flex-col gap-2 min-w-0">
<h3 class="font-semibold text-lg text-slate-900 dark:text-white">
{article.data.title}
</h3>
{excerpt && (
<p class="text-sm text-slate-600 dark:text-slate-400 line-clamp-3">
{excerpt}
</p>
)}
<time class="text-xs text-slate-500 dark:text-slate-400">
{formatDoMMMMYYYY(article.data.date)}
</time>
</div>
</div>
</a>
</section>

View File

@@ -0,0 +1,42 @@
---
import Project from "@/components/project.astro";
---
<section class="flex flex-col gap-5 p-4 sm:p-6 lg:p-8 max-w-3xl mx-auto w-full">
<div class="text-sm text-slate-500 dark:text-slate-400">Personal projects</div>
<div class="grid grid-cols-1 gap-5 md:grid-cols-3">
<Project
title="Ghostwriter"
image="https://app.ghostwriter.rocks/_ipx/_/ghostwriter-logo.png"
url="https://ghostwriter.rocks"
>
<Fragment slot="description">
Creative post-generation for Strava activities with a lightweight, playful product feel.
</Fragment>
<Fragment slot="subtitle">Compatible with Strava</Fragment>
</Project>
<Project
title="Places for Zendesk"
image="https://990141.apps.zdusercontent.com/990141/assets/1701544308-8c83d0f02afd0b3d7342e5f19304c4b4/logo.png"
url="https://getplaces.co"
>
<Fragment slot="description">
Location-aware lead capture designed to connect digital conversations to real-world businesses.
</Fragment>
<Fragment slot="subtitle">Built for Zendesk marketplace</Fragment>
</Project>
<Project
title="SneakPeek"
image="https://cdn.livechat-files.com/api/file/developers/img/applications/449B6QFGg/icons/AecAyk1Gg-960x960.png"
url="https://www.livechat.com/marketplace/apps/sneakpeek"
>
<Fragment slot="description">
Turn plain links into richer, more informative chat experiences.
</Fragment>
<Fragment slot="subtitle">Built for LiveChat marketplace</Fragment>
</Project>
</div>
</section>

45
src/components/link.astro Normal file
View File

@@ -0,0 +1,45 @@
---
export type LinkVariant = "default" | "primary" | "muted";
export interface Props {
href: string;
class?: string;
variant?: LinkVariant;
activeClass?: string;
isActive?: boolean;
target?: string;
rel?: string;
}
const {
href,
class: className = "",
variant = "default",
activeClass,
isActive = false,
target,
rel,
} = Astro.props;
const VARIANT_CLASSES: Record<LinkVariant, string> = {
default:
"text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white",
primary:
"text-blue-500 dark:text-blue-300 hover:underline hover:underline-offset-4",
muted:
"text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white",
};
const baseClasses = [
"underline-offset-4 transition-colors",
isActive && activeClass ? activeClass : "",
VARIANT_CLASSES[variant],
className,
]
.filter(Boolean)
.join(" ");
---
<a href={href} class={baseClasses} target={target} rel={rel}>
<slot />
</a>

View File

@@ -1,18 +0,0 @@
---
// @ts-ignore
import { Icon } from "astro-icon/components";
---
<div class="mt-24">
<h2 class="text-center text-slate-500">Works with your technologies</h2>
<div class="flex gap-8 md:gap-20 items-center justify-center mt-10 flex-wrap">
<Icon class="size-8 md:size-12" name="simple-icons:react" />
<Icon class="size-8 md:size-12" name="simple-icons:svelte" />
<Icon class="size-8 md:size-12" name="simple-icons:astro" />
<Icon class="size-8 md:size-16" name="simple-icons:tailwindcss" />
<Icon class="size-8 md:size-16" name="simple-icons:alpinedotjs" />
<Icon class="size-8 md:size-12" name="simple-icons:vercel" />
</div>
</div>

View File

@@ -1,47 +0,0 @@
---
import { Dropdown as DropdownContainer, DropdownItems } from "astro-navbar";
const { title, lastItem, children } = Astro.props;
---
<li class="relative">
<DropdownContainer class="group">
<button
class="flex items-center gap-1 w-full lg:w-auto lg:px-3 py-2 text-gray-600 hover:text-gray-900">
<span>{title}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
class="w-3 h-3 mt-0.5 group-open:rotate-180">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path>
</svg>
</button>
<DropdownItems>
<div
class:list={[
"lg:absolute w-full lg:w-48 z-10",
lastItem
? "lg:right-0 origin-top-right"
: "lg:left-0 origin-top-left",
]}>
<div
class="px-3 lg:py-2 lg:bg-white lg:rounded-md lg:shadow-sm lg:border flex flex-col">
{
children.map((item) => (
<a
href={item.path}
class="py-1 text-gray-600 hover:text-gray-900">
{item.title}
</a>
))
}
</div>
</div>
</DropdownItems>
</DropdownContainer>
</li>

View File

@@ -1,99 +0,0 @@
---
import Container from "@/components/container.astro";
import Link from "@/components/ui/link.astro";
import Dropdown from "./dropdown.astro";
import { Astronav, MenuItems, MenuIcon } from "astro-navbar";
const menuitems = [
{
title: "Features",
path: "#",
children: [
{ title: "Action", path: "/" },
{ title: "Another action", path: "#" },
{ title: "Dropdown Submenu", path: "#" },
{ title: "404 Page", path: "/404" },
],
},
{
title: "Pricing",
path: "/pricing",
},
{
title: "About",
path: "/about",
},
{
title: "Blog",
path: "/blog",
},
{
title: "Contact",
path: "/contact",
},
{
title: "Pro Version",
badge: true,
path: "https://astroship-pro.web3templates.com/",
},
];
---
<Container>
<header class="flex flex-col lg:flex-row justify-between items-center my-5">
<Astronav>
<div class="flex w-full lg:w-auto items-center justify-between">
<a href="/" class="text-lg"
><span class="font-bold text-slate-800">Astro</span><span
class="text-slate-500">ship</span
>
</a>
<div class="block lg:hidden">
<MenuIcon class="w-4 h-4 text-gray-800" />
</div>
</div>
<MenuItems class="hidden w-full lg:w-auto mt-2 lg:flex lg:mt-0">
<ul class="flex flex-col lg:flex-row lg:gap-3">
{
menuitems.map((item, index) => (
<>
{item.children && (
<Dropdown
title={item.title}
children={item.children}
lastItem={index === menuitems.length - 1}
/>
)}
{!item.children && (
<li>
<a
href={item.path}
class="flex lg:px-3 py-2 items-center text-gray-600 hover:text-gray-900">
<span> {item.title}</span>
{item.badge && (
<span class="ml-1 px-2 py-0.5 text-[10px] animate-pulse font-semibold uppercase text-white bg-indigo-600 rounded-full">
New
</span>
)}
</a>
</li>
)}
</>
))
}
</ul>
<div class="lg:hidden flex items-center mt-3 gap-4">
<Link href="#" style="muted" block size="md">Log in</Link>
<Link href="#" size="md" block>Sign up</Link>
</div>
</MenuItems>
</Astronav>
<div>
<div class="hidden lg:flex items-center gap-4">
<a href="#">Log in</a>
<Link href="#" size="md">Sign up</Link>
</div>
</div>
</header>
</Container>

View File

@@ -0,0 +1,52 @@
---
export interface Props {
logo: string;
href?: string;
company: string;
}
const { logo, href, company } = Astro.props;
---
<div class="flex gap-5">
{
href ? (
<a href={href} target="_blank" rel="noopener noreferrer" class="block shrink-0">
<img
src={logo}
alt={company}
class="block rounded min-w-[50px] w-[50px] h-[50px] object-contain bg-white"
loading="lazy"
/>
</a>
) : (
<img
src={logo}
alt={company}
class="block rounded min-w-[50px] w-[50px] h-[50px] object-contain bg-white shrink-0"
loading="lazy"
/>
)
}
<div class="flex flex-col justify-center gap-1">
{
href ? (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
class="md:text-xl text-lg font-semibold hover:underline hover:underline-offset-4 text-slate-900 dark:text-white"
>
{company}
</a>
) : (
<span class="md:text-xl text-lg font-semibold text-slate-900 dark:text-white">
{company}
</span>
)
}
<div class="text-sm text-slate-500 dark:text-slate-400 lg:max-w-lg">
<slot />
</div>
</div>
</div>

View File

@@ -1,45 +0,0 @@
---
import { Tick } from "@/components/ui/icons";
import Link from "@/components/ui/link.astro";
const { plan } = Astro.props;
---
<div>
<div
class="flex flex-col w-full order-first lg:order-none border-2 border-[#D8DEE9] border-opacity-50 py-5 px-6 rounded-md">
<div class="text-center">
<h4 class="text-lg font-medium text-gray-400">{plan.name}</h4><p
class="mt-3 text-4xl font-bold text-black md:text-4xl">
{
plan.price && typeof plan.price === "object"
? plan.price.monthly
: plan.price
}
</p>
<!-- {
plan.price.original && (
<p class="mt-1 text-xl font-medium text-gray-400 line-through md:text-2xl">
{plan.price.original}
</p>
)
} -->
</div><ul class="grid mt-8 text-left gap-y-4">
{
plan.features.map((item) => (
<li class="flex items-start gap-3 text-gray-800">
<Tick class="w-6 h-6" />
<span>{item}</span>
</li>
))
}
</ul><div class="flex mt-8">
<Link
href={plan.button.link || "#"}
block
style={plan.popular ? "primary" : "outline"}>
{plan.button.text || "Get Started"}
</Link>
</div>
</div>
</div>

View File

@@ -0,0 +1,38 @@
---
export interface Props {
title: string;
image: string;
url: string;
}
const { title, image, url } = Astro.props;
---
<div class="flex overflow-auto gap-x-5 md:gap-2 items-center md:items-start md:flex-col md:border md:rounded-lg md:border-gray-200 md:p-4 md:dark:border-slate-600">
<a href={url} target="_blank" rel="noopener noreferrer" class="shrink-0">
<img
src={image}
alt={title}
class="block min-w-[50px] w-[50px] md:w-16 h-[50px] md:h-16 object-contain bg-white rounded"
loading="lazy"
/>
</a>
<div class="grid row-span-3 gap-2 min-w-0">
<div>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
class="font-semibold hover:underline hover:underline-offset-4 text-slate-900 dark:text-white"
>
{title}
</a>
<div class="text-sm text-slate-500 dark:text-slate-400 md:text-xs">
<slot name="subtitle" />
</div>
</div>
<div class="text-sm text-slate-600 dark:text-slate-400 hidden md:block">
<slot name="description">Lorem ipsum dorcet sit amet</slot>
</div>
</div>
</div>

View File

@@ -1,12 +0,0 @@
---
const { align = "center" } = Astro.props;
---
<div class:list={["mt-16", align === "center" && "text-center"]}>
<h1 class="text-4xl lg:text-5xl font-bold lg:tracking-tight">
<slot name="title">Title</slot>
</h1>
<p class="text-lg mt-4 text-slate-600">
<slot name="desc">Some description goes here</slot>
</p>
</div>

View File

@@ -0,0 +1,45 @@
---
import Link from "@/components/link.astro";
const path = Astro.url.pathname;
const isHome = path === "/" || path === "";
const isArticles = path === "/articles" || path.startsWith("/articles/");
---
<div class="sticky top-0 z-10 border-b border-gray-200 dark:border-slate-600 bg-white/85 dark:bg-slate-800/90 backdrop-blur-sm">
<nav class="grid grid-cols-2 items-center gap-3 !py-2 max-w-3xl mx-auto w-full p-4 sm:p-6 lg:p-8">
<Link href="/" class="flex gap-3 items-center" variant="default">
<img
alt="Marios Antonoudiou"
src="/mariosant.webp"
width="40"
height="40"
class="w-10 h-10 rounded-full border border-gray-200 dark:border-gray-800 dark:bg-slate-300 block"
loading="eager"
/>
<div class="font-bold text-slate-800 dark:text-white">
Marios Antonoudiou
</div>
</Link>
<div class="flex gap-4 items-center justify-end text-slate-600 dark:text-slate-400">
<Link
href="/"
variant="default"
class="font-semibold text-sm"
activeClass="text-slate-900 dark:text-slate-200 underline"
isActive={isHome}
>
Home
</Link>
<Link
href="/articles"
variant="default"
class="font-semibold text-sm"
activeClass="text-slate-900 dark:text-slate-200 underline"
isActive={isArticles}
>
Articles
</Link>
</div>
</nav>
</div>

View File

@@ -1,40 +0,0 @@
---
interface Props {
size?: "md" | "lg";
block?: boolean;
style?: "outline" | "primary" | "inverted";
class?: string;
[x: string]: any;
}
const {
size = "md",
style = "primary",
block,
class: className,
...rest
} = Astro.props;
const sizes = {
md: "px-5 py-2.5",
lg: "px-6 py-3",
};
const styles = {
outline: "border-2 border-black hover:bg-black text-black hover:text-white",
primary:
"bg-black text-white hover:bg-slate-900 border-2 border-transparent",
};
---
<button
{...rest}
class:list={[
"rounded-sm text-center transition focus-visible:ring-2 ring-offset-2 ring-gray-200",
block && "w-full",
sizes[size],
styles[style],
className,
]}
><slot />
</button>

View File

@@ -1 +0,0 @@
export { default as Tick } from "./tick.astro";

View File

@@ -1,16 +0,0 @@
---
const { class: className } = Astro.props;
---
<svg
class={className}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
><circle cx="12" cy="12" r="9" fill="currentColor" fill-opacity=".16"
></circle><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.75 12a8.25 8.25 0 0 1 11.916-7.393.75.75 0 1 0 .668-1.343A9.713 9.713 0 0 0 12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75c0-.366-.02-.727-.06-1.082a.75.75 0 1 0-1.49.164A8.25 8.25 0 1 1 3.75 12Zm17.78-6.47a.75.75 0 0 0-1.06-1.06L12 12.94l-2.47-2.47a.75.75 0 0 0-1.06 1.06l3 3a.75.75 0 0 0 1.06 0l9-9Z"
fill="currentColor"></path>
</svg>

View File

@@ -1,44 +0,0 @@
---
interface Props {
href: string;
size?: "md" | "lg";
block?: boolean;
style?: "outline" | "primary" | "inverted" | "muted";
class?: string;
[x: string]: any;
}
const {
href,
block,
size = "lg",
style = "primary",
class: className,
...rest
} = Astro.props;
const sizes = {
lg: "px-5 py-2.5",
md: "px-4 py-2",
};
const styles = {
outline: "bg-white border-2 border-black hover:bg-gray-100 text-black ",
primary: "bg-black text-white hover:bg-gray-800 border-2 border-transparent",
inverted: "bg-white text-black border-2 border-transparent",
muted: "bg-gray-100 hover:bg-gray-200 border-2 border-transparent",
};
---
<a
href={href}
{...rest}
class:list={[
"rounded-sm text-center transition focus-visible:ring-2 ring-offset-2 ring-gray-200",
block && "w-full",
sizes[size],
styles[style],
className,
]}
><slot />
</a>

18
src/content.config.ts Normal file
View File

@@ -0,0 +1,18 @@
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
const articles = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/articles" }),
schema: z.object({
title: z.string(),
date: z.coerce.date(),
description: z.string().optional(),
coverImage: z.object({
author: z.string(),
authorUrl: z.string().url().optional(),
url: z.string(),
}),
}),
});
export const collections = { articles };

View File

@@ -0,0 +1,34 @@
---
title: Beyond Web Frameworks - Unveiling the Real Factors that Shape Successful Web Development
date: 2023-12-01
coverImage:
author: Kaleidiko
authorUrl: https://unsplash.com/@kaleidico
url: "https://images.unsplash.com/photo-1532622785990-d2c36a76f5a6?q=80&h=600&w=1287&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
---
In the ever-evolving landscape of web development, discussions around the best web frameworks often dominate the discourse. However, focusing solely on the choice of a web framework can be misleading. In this article, we will explore the key factors that truly matter in web development and argue that web frameworks, while important, are not the sole determinants of success.
## Project Structure Matters
While web frameworks provide a foundation, the project structure is the architectural blueprint that defines how components and modules interact. A well-organized project structure promotes code maintainability, scalability, and collaboration. Regardless of the web framework chosen, a thoughtful project structure ensures that the codebase remains coherent and adaptable over time.
## Reusability Matters
One of the fundamental principles of efficient development is code reuse. The ability to create reusable components and modules transcends the choice of a specific web framework. A carefully designed architecture and coding practices foster reusability, allowing developers to leverage existing code across projects and streamline the development process. This emphasizes the importance of a holistic approach beyond the confines of any single framework.
## Tooling Matters
Web development involves a plethora of tools, ranging from version control systems to build tools and testing frameworks. The effectiveness of these tools can significantly impact the development workflow and overall productivity. Regardless of the chosen web framework, a robust set of tools is crucial for tasks such as code collaboration, debugging, and deployment. Emphasizing tooling ensures a seamless development experience and accelerates the delivery of high-quality software.
## Intuitiveness Matters
The learning curve associated with a web framework can influence development speed and the onboarding process for new team members. Intuitive frameworks empower developers to quickly grasp concepts and become productive, minimizing the time spent on overcoming steep learning curves. An intuitive framework promotes a positive development experience, fostering creativity and innovation within the team.
## Team Adoption Matters
Team dynamics play a pivotal role in the success of any web development project. Even the most powerful web framework may fall short if it lacks team adoption. A unified understanding and agreement on the chosen tools and practices within the team ensure consistency and collaboration. The collective expertise and experience of the team contribute significantly to the project's success, irrespective of the web framework in use.
## Conclusion
While web frameworks undoubtedly play a crucial role in web development, it is essential to recognize that success hinges on a combination of factors. Project structure, reusability, tooling, intuitiveness, and team adoption collectively shape the development process and determine the project's outcome. Developers and teams should prioritize a comprehensive approach that extends beyond the realm of web frameworks, focusing on the broader aspects that contribute to a successful and sustainable development journey.

View File

@@ -0,0 +1,36 @@
---
title: The Ripple Effect - The Importance of Impact as a Senior Software Engineer
date: 2023-12-18
coverImage:
author: Sami Matias
authorUrl: https://unsplash.com/@samimatias
url: "https://images.unsplash.com/photo-1581284943246-0eb528155992?q=80&w=1287&auto=format&fit=crop&ixlib=rb-4.0.3&h=600"
---
As a senior software engineer, your role extends far beyond writing lines of code and solving complex technical challenges. One of the key responsibilities you carry is the ability to make a lasting impact on your entire team. Your influence can shape the work environment, foster collaboration, and contribute to the overall success of the project. Let's delve into why having a positive impact is not just a personal achievement but a crucial aspect of effective senior leadership in the world of software development.
## Knowledge Sharing and Mentorship
Oh, imagine that! It turns out that as a senior software engineer, your primary role is to share knowledge and mentor others. Who would have thought that the key to success goes beyond just writing code? The impact we make by guiding and supporting our teammates in their professional journey is truly remarkable. It's not just about the code we produce; it's about the knowledge we impart and the mentorship we provide that creates a resilient and empowered team. Let's continue to be surprised by the magic that happens when collaboration and learning take center stage in our roles! 🌟
## Team Morale and Motivation
In the realm of senior software engineering, it's crucial to recognize that your team will naturally follow your lead. As a senior engineer, you must exemplify the qualities you wish to see in your coworkers. Leading by example involves maintaining a positive attitude, demonstrating a strong work ethic, and motivating your team members. By setting the standard for enthusiasm and dedication, you create an environment where everyone is inspired to give their best and overcome challenges with a sense of purpose. Remember, your actions as a leader speak louder than words, and a motivated team begins with your own unwavering commitment.
## Effective Communication
Communication is the backbone of any successful project. As a senior software engineer, your ability to communicate complex technical concepts in a clear and concise manner is paramount. Enhancing team communication channels ensures that ideas are exchanged seamlessly, preventing misunderstandings and fostering a harmonious working environment.
## Setting Standards and Best Practices
Your experience equips you to set coding standards and best practices that elevate the overall quality of the codebase. Establishing a robust foundation for development ensures consistency, scalability, and maintainability. This, in turn, reduces technical debt and facilitates smoother collaboration among team members.
## Problem-Solving and Decision-Making
Senior software engineers are often called upon to tackle challenging problems and make critical decisions. Your ability to approach problem-solving with a strategic mindset and make informed decisions positively impacts the project's trajectory. This leadership is crucial for steering the team towards success, even in the face of adversity.
## Career Development and Growth
Investing in the professional development of your team members is a key aspect of senior leadership. By recognizing and nurturing individual strengths, you contribute to the growth of your team members' careers. This not only benefits the individuals but also enhances the overall skill set of the team.
In conclusion, the impact of a senior software engineer extends well beyond the lines of code they write. It encompasses mentorship, communication, setting standards, and fostering a positive team culture. Recognizing the significance of these contributions is essential for creating a dynamic and successful development environment. As a senior engineer, embrace the opportunity to lead with impact, leaving a lasting legacy on both your team and the projects you undertake.

View File

@@ -0,0 +1,46 @@
---
title: Choosing the Right Web UI Framework for Seamless Team Collaboration
date: 2024-01-01
coverImage:
author: Geraldo Stanislas
authorUrl: https://unsplash.com/@pixelsucker
url: "https://images.unsplash.com/photo-1500293333195-9d3fe158b9e4?q=80&w=1287&auto=format&fit=crop&ixlib=rb-4.0.3&h=600"
---
In the fast-paced world of web development, choosing the right UI framework is a critical decision that can significantly impact the success of a project. When working in a team, the stakes are even higher, as effective collaboration and streamlined workflows are paramount. In this article, we'll explore the key qualities to seek in a web UI framework to ensure a smooth and productive team experience.
## Comprehensive Documentation
Comprehensive and well-maintained documentation acts as the foundation for successful collaboration. A framework with clear, detailed documentation helps team members understand its features, components, and best practices, reducing the learning curve and minimizing the chances of misunderstandings.
## Community Support
A vibrant community can be a lifesaver when facing challenges or seeking best practices. Choosing a framework with an active community ensures that your team has access to a wealth of resources, support forums, and shared experiences, fostering a collaborative and dynamic development environment.
## Scalability
Scalability is a key consideration for any project. The chosen framework should be able to handle the growth of your application without sacrificing performance or introducing unnecessary complexity. This scalability ensures that your team can seamlessly adapt to evolving project requirements.
## Modularity
Modularity in a framework allows for the independent development of components, promoting collaboration among team members. Each member can focus on specific modules, reducing conflicts and making it easier to maintain and update the codebase.
## Performance
Performance is not just a user experience concern; it's also crucial for team productivity. A fast-loading framework with smooth interactions minimizes frustration and waiting times, allowing your team to focus on building features instead of battling performance bottlenecks.
## Responsiveness
With the diversity of devices in use today, responsiveness is non-negotiable. A framework that supports responsive design ensures that your application looks and functions well on various screen sizes, from desktop monitors to mobile devices, catering to a broad user base.
## Accessibility (A11Y)
Prioritizing accessibility standards (e.g., WAI-ARIA) ensures that your web application is usable by people with disabilities. A framework that embeds accessibility principles supports your team in delivering inclusive and user-friendly interfaces.
## Testing and Debugging Tools
Robust testing and debugging tools are invaluable for maintaining code quality and catching issues early in the development process. A framework that provides these tools makes it easier for your team to collaborate on identifying and resolving issues efficiently.
## Conclusion
Choosing the right web UI framework is a strategic decision that directly influences the success of your team's projects. By prioritizing qualities such as comprehensive documentation, community support, scalability, modularity, customization, performance, cross-browser compatibility, responsiveness, accessibility, and robust testing tools, your team can build a solid foundation for effective collaboration and successful web development projects. As you embark on your next project, keep these considerations in mind to empower your team and deliver outstanding user experiences.

View File

@@ -0,0 +1,30 @@
---
title: Unit and Integration Tests in Frontend Development
date: 2024-01-18
coverImage:
author: Marek Piwnicki
authorUrl: https://unsplash.com/@marekpiwnicki
url: "https://images.unsplash.com/photo-1704741253008-7b0a5ed52c12?q=80&h=600&w=1287&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
---
Testing stands as a formidable pillar, ensuring the reliability and stability of applications. As developers, the choices we make regarding testing methodologies can significantly impact the quality of our code. In this exploration of the differences between unit and integration tests in frontend development, we emphasize that even a modest amount of testing is better than none at all.
## Complex Dependencies in Frontend Components
Frontend components often have intricate dependencies, ranging from external APIs to state management libraries. Unit testing, which aims to isolate and evaluate individual units of code in isolation, can be challenging in this context. Complex dependencies may require extensive mocking, making unit tests harder to write and maintain.
## Simulating Real-world Scenarios with Integration Testing
Integration testing steps in to address the limitations of unit testing in the frontend realm. By testing the collaboration between various components and services, integration tests provide a more holistic view of an application's behavior. This becomes crucial in simulating real-world scenarios, including slow network conditions and delayed user responses, that might not be adequately captured in unit tests.
## The Need for Speed: Unit Tests vs. Integration Tests
When it comes to speed, unit tests take the lead. The isolation of smaller units like utility functions and state management for testing means that unit tests can be executed swiftly. This quick turnaround time is crucial in a development environment where feedback loops need to be fast. For smaller units, where dependencies can be effectively managed, unit tests are the preferred choice.
## Running Unit Tests on a Headless Browser
For frontend unit tests, running them on a headless browser provides a more realistic environment compared to a Node.js simulation. A headless browser environment closely mirrors the conditions a user's browser experiences, ensuring that the tests are more representative of real-world scenarios. This approach contributes to the reliability of unit tests and enhances their value in ensuring frontend functionality.
## Conclusion
In the dynamic landscape of frontend development, the choice between unit and integration testing is not an either-or proposition but rather a strategic decision based on the specific needs of the project. While unit tests excel in speed and precision for smaller units, integration tests provide a more comprehensive evaluation of an application's behavior in complex scenarios. Striking the right balance between these testing methodologies is key to delivering a frontend that not only functions as intended but also performs robustly in diverse real-world conditions.

View File

@@ -0,0 +1,42 @@
---
title: "Exploring the Power of Nuxt: Part 1"
date: 2024-02-13
coverImage:
author: SpaceX
authorUrl: https://unsplash.com/@spacex
url: "https://images.unsplash.com/photo-1516849841032-87cbac4d88f7?q=80&h=600&w=1287&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
---
Nuxt a powerhouse framework designed to streamline the development process while offering a plethora of features to empower developers. In this comprehensive overview, we'll delve into the core aspects of Nuxt, highlighting its key features and exploring its practical applications.
## Nuxt: Simplifying Web Development
At its core, Nuxt is engineered to provide an intuitive and performant web development experience with a strong emphasis on Developer Experience (DX). With Nuxt, creating full-stack web applications and websites with Vue.js becomes a breeze. Its intuitive nature and extensive feature set make it an ideal choice for both beginners and seasoned developers alike.
### Getting Started Made Easy
Launching a Nuxt project is as simple as executing the `nuxi init` command. This command serves as your gateway to a guided setup process, where you'll configure essential project parameters such as the project name, Nuxt options, preferred UI framework, TypeScript integration, linter settings, and testing framework preferences. This streamlined approach ensures that developers can kickstart their projects with minimal friction.
## Comprehensive Solutions with Nuxt
Nuxt transcends the realm of a traditional frontend framework by offering an end-to-end solution that encompasses server-side rendering (SSR) capabilities out of the box. Unlike other frameworks that require extensive server configuration, Nuxt comes equipped with built-in SSR capabilities, eliminating the need for manual setup. Moreover, Nuxt introduces Nitro, a powerful server engine that unlocks new possibilities for full-stack development.
## Harnessing the Power of TypeScript
With Nuxt, harnessing the power of TypeScript has never been easier. The framework boasts zero-config TypeScript support, enabling developers to write type-safe code without the steep learning curve typically associated with TypeScript integration. Thanks to its auto-generated types and seamless integration with `tsconfig.json`, Nuxt empowers developers to embrace type safety without sacrificing productivity.
## Enhancing Developer Productivity
Nuxt goes the extra mile in enhancing developer productivity with its arsenal of developer-friendly tools. From hot module replacement (HMR) in development to optimized performance in production, Nuxt ensures a seamless development experience at every stage of the project lifecycle. Additionally, its modular architecture facilitates easy extension through custom plugins and third-party integrations, further enhancing its flexibility and adaptability.
## Unparalleled Performance Out of the Box
Performance is paramount in the realm of web development, and Nuxt delivers on this front with blazing-fast performance out of the box. By optimizing factors such as initial page load times, SEO capabilities, and device compatibility, Nuxt empowers developers to craft lightning-fast web experiences that cater to a diverse range of users.
## Extensibility through Plugins and Modules
One of Nuxt's defining features is its robust plugin and module system, which allows developers to extend the framework's functionality with ease. Whether integrating custom features or incorporating third-party services, Nuxt's modular architecture offers unparalleled flexibility, enabling developers to tailor their projects to specific requirements effortlessly.
## Unlocking the Potential of Nuxt
In conclusion, Nuxt stands as a testament to the power of simplicity and versatility in web development. With its intuitive interface, comprehensive feature set, and unparalleled performance, Nuxt empowers developers to unlock their full potential and bring their web projects to life with unparalleled ease and efficiency. Whether you're a novice exploring the world of web development or a seasoned pro seeking to streamline your workflow, Nuxt promises to be your ultimate ally in the journey towards web development excellence.

View File

@@ -0,0 +1,55 @@
---
title: Efficient Browser Analytics with the Beacon API
date: 2024-03-17
coverImage:
author: Jigar Panchal
authorUrl: https://unsplash.com/@brave4_heart
url: "https://images.unsplash.com/photo-1710781944947-7cd4a381499f?q=80&h=600&w=1287&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
---
Tracking user interactions and behaviors is crucial for the continuous improvement of web applications. However, efficiently managing analytics data without impacting the user experience can be challenging. This is where the Beacon API steps in as a powerful tool for web developers. This blog post explores how `sendBeacon()` can be utilized to send analytics data effectively, ensuring minimal disruption to the user experience.
### What is `sendBeacon()`?
The `sendBeacon()` method is a web API that allows developers to send a small amount of data to a server asynchronously, especially useful when sending analytics data as the page unloads. Unlike traditional AJAX calls, `sendBeacon()` is designed to handle data sending without delaying the page's unload process, making it ideal for capturing end-of-session data.
### Key Benefits of Using `sendBeacon()`
1. **Reliability on Page Unload**: It ensures data is sent even if the user navigates away from the page or closes the tab.
2. **Asynchronous Execution**: Executes without blocking the window's unload event, thus not impacting the browsing experience.
3. **Low Overhead**: Designed to be minimalistic and efficient, perfect for sending small bursts of data like analytics.
### How to Implement `sendBeacon()` in Your Projects
Implementing `sendBeacon()` is straightforward. Heres a basic example to get you started:
```javascript
window.addEventListener("unload", logData, false);
function logData() {
const data = { userAction: "page_exit", timestamp: Date.now() };
const beaconUrl = "https://example.com/analytics";
navigator.sendBeacon(beaconUrl, JSON.stringify(data));
}
```
In this example, we attach an event listener to the window's `unload` event. When the user leaves the page, the `logData` function is triggered, sending a JSON string containing user action and timestamp to a specified URL via `sendBeacon()`.
### Best Practices for Using `sendBeacon()`
- **Data Size**: Keep the data payload small as `sendBeacon()` is designed for lighter loads.
- **Endpoint Handling**: Ensure that the server endpoint is configured to handle POST requests efficiently, as `sendBeacon()` sends data using the HTTP POST method.
- **Compatibility Check**: While most modern browsers support `sendBeacon()`, its wise to implement a fallback mechanism for older browsers.
### Real-world Use Cases
- **E-commerce Tracking**: Capture user actions like adding items to a cart or completing a purchase as they navigate away from a page.
- **Session Time Tracking**: Record the duration of a user session accurately by sending data on page unload.
- **Form Interaction Analytics**: Log interactions in forms, such as field entries and selections, to understand user behavior better.
### Conclusion
The `sendBeacon()` method is a robust solution for sending analytics data efficiently without compromising the user experience. By integrating `sendBeacon()` into your web applications, you can gather valuable insights into user interactions while maintaining a smooth and responsive interface. Start experimenting with `sendBeacon()` today and unlock the potential of efficient browser-based analytics.
By leveraging the capabilities of `sendBeacon()`, developers can ensure that critical data is captured and transmitted effectively, even in the fleeting moments as users exit a web page. This makes `sendBeacon()` an indispensable tool in the arsenal of modern web development.

View File

@@ -0,0 +1,50 @@
---
title: "Exploring the Power of Nuxt: Part 2"
date: 2025-03-15
coverImage:
author: Behnam Norouzi
authorUrl: https://unsplash.com/@behy_studio
url: "https://images.unsplash.com/photo-1739542233673-572b6f1f9934?q=80&h=600&w=1287&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
---
# Exploring the Power of Nuxt: Part 2
In the first part of our *Exploring the Power of Nuxt* series ([read it here](https://mariosant.dev/articles/2024-02-13-exploring-nuxt-pt-1)), we introduced Nuxt as a powerful Vue framework designed for performance, flexibility, and developer experience. In this second installment, well dive deeper into how Nuxt empowers developers to build scalable full-stack applications, leverage configurable auto-imports to manage complexity, and solve caching challenges effectively.
## Full-Stack Applications with Nuxt
Nuxt is not just a frontend framework—its a full-stack solution. With Nuxts powerful Nitro engine, developers can seamlessly build server-side logic alongside their client-side applications. Features like API routes, middleware, and database connectors allow Nuxt to handle backend operations, making it an excellent choice for full-stack development. Some key benefits include:
- **Server-Side Rendering (SSR)**: Improves performance and SEO by rendering pages on the server.
- **API Routes**: Define backend endpoints within your Nuxt app without needing an external server.
- **Database Integration**: Connect to databases via Nitros built-in support for ORM tools like Prisma or Drizzle.
- **Edge and Serverless Deployments**: Deploy efficiently to platforms like Vercel, Netlify, or Cloudflare Workers.
This holistic approach enables teams to build and deploy full-stack applications with minimal overhead.
## Scaling with Configurable Auto-Imports
As applications grow, managing imports can become tedious and error-prone. Nuxts configurable auto-imports streamline this process by automatically registering commonly used functions and components. This makes the codebase cleaner and easier to maintain. Some advantages include:
- **Reduced Boilerplate**: No need to manually import composables, utilities, or Vue components.
- **Custom Auto-Imports**: Define project-specific utilities that are auto-registered across the app.
- **Scoped Imports**: Enable or disable auto-imports for specific directories to maintain structure.
By leveraging auto-imports effectively, teams can introduce new units of functionality without disrupting existing code, making large-scale applications more maintainable.
## Caching Management for Hard Problems
Caching is a crucial aspect of optimizing performance, especially for applications dealing with high traffic or dynamic content. Nuxt provides several mechanisms to handle caching efficiently:
- **Nitro Storage Layer**: Offers built-in key-value storage for caching API responses or computed data.
- **Middleware Caching**: Cache expensive computations or API calls at the server level to reduce redundant processing.
- **CDN and Edge Caching**: Deploy Nuxt apps to edge networks to serve content faster.
- **Fine-Grained Control**: Configure caching policies per route or API to optimize different parts of the application.
By implementing the right caching strategies, developers can significantly improve app performance, reduce load times, and handle scalability challenges more effectively.
## Go out and build something!
Nuxt is more than just a Vue framework—its a robust full-stack solution that streamlines development and scalability. From its built-in server-side capabilities to auto-import configurations and advanced caching strategies, Nuxt equips developers with powerful tools to solve complex problems efficiently.
Stay tuned for the next part of our series, where well explore Nuxts ecosystem and how it integrates with modern development workflows!

View File

@@ -0,0 +1,39 @@
---
title: Smooth Scrolling in Agentic Chat Interfaces
date: 2025-09-15
coverImage:
author: mariosant
url: "/agentic-ui.png"
---
When building conversational UIs, small interaction details can make a huge difference in how natural the experience feels. Recently, while working on an agentic chat interface, I ran into a deceptively simple requirement:
Whenever the user submits a question, the viewport should scroll to that question, position it neatly at the top, and then let the user read the agents response at their own pace.
It sounds straightforward, but the implementation had a few subtle challenges.
## The Core Challenge
Most chat apps scroll new messages to the bottom, where they stack endlessly. For this interface, I wanted something more intentional:
* The users own question should appear at the top of the viewport.
* There should be extra breathing room below it so the agents reply flows in naturally.
* That extra whitespace shouldnt stick around forever - otherwise the layout starts looking broken.
## Creating the Right Spacing
To achieve the temporary space effect, I leaned on modern CSS units like viewport height (vh) and dynamic CSS variables. The idea was simple: whenever the user sends a question, create some space at the bottom of the chat, which effectively pushes their question to the top of the screen.
From a user perspective, this feels intentional: their message takes center stage, while the agents answer appears in a comfortable, readable position just below.
## Knowing When to Remove It
The real trick was deciding when to get rid of that artificial space. Leaving it there permanently made the UI look odd, as if something was missing below the messages.
This is where the Intersection Observer API came into play. By watching a small invisible element placed in that whitespace, the system could detect when the user scrolled and the space was no longer visible. At that moment, the extra space was removed - restoring the chat to a normal flow.
## Why This Matters
This kind of interaction is easy to overlook, but it shapes how people experience a conversational product. Instead of feeling like a chat feed bolted onto a webpage, the interface feels deliberate and respectful of the users focus.
By combining viewport-relative layout techniques with scroll-aware cleanup logic, I was able to create a chat UI that feels smooth, intentional, and more human.

View File

@@ -0,0 +1,50 @@
---
title: "Your Codebase is an Asset: How to Govern AI Tooling"
date: 2026-02-24
coverImage:
author: Marii Siia
authorUrl: https://unsplash.com/@mariisiia
url: "https://images.unsplash.com/photo-1585481127583-96d53aaac9fa?q=80&w=1287&h=600&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
---
The current era of software development feels a bit like a gold rush. Every week, a new AI-powered IDE extension or CLI tool promises to double our velocity and delete our "boilerplate" woes. But in the rush to automate the mundane, its easy to forget a fundamental truth: **Your codebase is a high-value business asset.**
If we treat our code like an asset, we have to treat the tools that touch it like high-stakes infrastructure. Here is how teams should be thinking about the integration of AI into their workflows.
### 1. The Asset Mindset
A codebase isn't just a collection of text files; it is the crystallized intellectual property of a company. It represents thousands of hours of architectural decisions, security hardening, and domain-specific logic.
When we introduce AI tools, we often treat them as "fancy auto-complete." However, if a tool influences the structure or logic of your asset, that tool is now a stakeholder in your technical debt. Regardless of whether a human or an LLM wrote a block of code, the business value—and the long-term maintenance cost—remains the same.
> **Key Takeaway:** Tooling should never dictate the quality of the asset. If the AI produces "working" code that violates your architectural standards, it isn't saving you time; it's charging you interest on future debt.
### 2. The Liability of the "Black Box"
One of the most significant risks currently facing engineering teams is the reliance on **closed-source AI tools.** While the convenience of a polished, proprietary UI is tempting, these tools often function as a liability for several reasons:
* **Data Sovereignty:** Where is your code going? If you are pumping proprietary logic into a closed model, you may be inadvertently training a competitors future assistant or violating compliance (GDPR, SOC2, etc.).
* **Vendor Lock-in:** If your team becomes dependent on a proprietary feature that changes its pricing or shuts down, your workflow is compromised.
* **Lack of Auditability:** With closed-source models, you cant truly know *why* a certain suggestion was made or if it includes licensed code that could create legal friction later.
Transitioning toward open-weights models or self-hosted instances isn't just for the paranoid - its a strategy for protecting the integrity of your asset.
### 3. AI Adoption is a Team Sport
A common pitfall is the "Individual Rogue" approach, where one developer uses Tool A, another uses Tool B, and a third is pasting snippets into a browser-based chat. This fragmentation is a nightmare for consistency.
**AI tooling should be a team-level decision.** Just as you wouldnt allow a single developer to unilaterally switch the entire project from TypeScript to Go on a whim, the choice of AI assistant should be standardized. This ensures:
1. **Uniform Security:** Everyone is using a tool that has cleared the companys security hurdles.
2. **Shared Context:** The team can develop "prompt libraries" or custom configurations that work for the specific nuances of your project.
### 4. Enforcing Conventions (No Exceptions)
AI tools are notorious for hallucinating patterns or reverting to "generic" coding styles that might not align with your team's specific conventions.
If your codebase uses a specific pattern for state management or a particular way of handling errors, the AI needs to follow suit—not the other way around.
* **Linting is still King:** AI-generated code must pass the same CI/CD checks as human code.
* **Peer Review is Mandatory:** "The AI wrote it" is never a valid excuse during a PR review. If anything, AI-generated code requires *more* scrutiny to ensure it hasn't introduced subtle logic "hallucinations" that look correct at a glance but fail in edge cases.
### Final Thoughts
AI is a powerful lever, but a lever is only useful if its resting on a solid fulcrum. That fulcrum is your codebase. By treating your code as a precious asset and your AI tools as potentially volatile contributors, you can harness the speed of the future without compromising the stability of the present.
**Don't let your tools own your code. Own your tools.**

View File

@@ -0,0 +1,80 @@
---
title: "The AI-Native Team: From Solo Pilots to Command Fleets"
date: 2026-04-06
coverImage:
author: Curated Lifestyle
authorUrl: https://unsplash.com/@curatedlifestyle
url: "https://images.unsplash.com/photo-1522071820081-009f0129c71c?q=80&w=1287&h=600&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
---
Moving from using AI as a personal "secret weapon" to integrating it as a core team asset is the most significant workflow shift since the adoption of Git. It's the transition from being a solo pilot to commanding a fleet.
Here is how to structure your team's transition to an AI-native development cycle, utilizing the latest 2026 frontier models and automation-first strategies.
---
## 1. The Strategy: Multi-Model Tiering (The Agent Orchestra)
In 2026, the most efficient teams don't use one model for everything. They use an **Orchestrator-Worker** pattern. This "Agent Orchestra" balances high-level reasoning with lightning-fast execution.
### The 2026 Model Stack
- **The Architect (Frontier Reasoning):** Models like **GPT-5.4 Pro** or **Claude 4.6 Opus** handle the "thinking." They analyze system architecture, plan multi-file refactors, and verify logic.
- **The Workers (High-Velocity Execution):** Models like **Gemini 3.1 Flash** or **GPT-5.4 Standard** handle the "doing." They generate boilerplate, write unit tests, and handle routine styling.
### The Cost-Reduction Edge
By tiering models, teams slash API costs by **6080%**. Instead of paying "Pro" prices for a simple CSS fix, your orchestrator delegates the grunt work to a "Flash" sub-agent. You only pay for high-tier reasoning when the complexity actually demands it.
---
## 2. From Prompts to Commands: Automating the Mundane
The biggest mistake teams make is relying on "Shared Prompts." Prompts are inconsistent and prone to human error. Modern teams use **Commands**—the automation of repetitive tasks into repeatable, scripted actions.
A command is an AI-powered macro that combines a specific model, a set of context rules, and a defined output.
- **`/boilerplate-feature [name]`:** Instead of a prompt, this command triggers a sub-agent to create the directory, the component, the test file, and the Storybook entry, all following your team's exact specs.
- **`/logic-audit`:** A command that runs a reasoning model (like Claude 4.6) over a PR to find edge cases, rather than just "reviewing" it.
- **`/doc-sync`:** Automatically updates the `README.md` and internal Notion docs whenever a specific API folder changes.
---
## 3. Shared Skill Files: The Team's Collective Brain
To make commands work, the AI needs to know *how* your team builds. This is where **Shared Skill Files** (e.g., `.cursorrules`, `.ai-skills`, or `.clinerules`) come in.
These files are committed to your repository and act as the "instruction manual" for every AI agent that touches your code.
### Benefits of Shared Skill Files:
- **Governance at Scale:** "Always use TypeScript 5.4 features," or "Never use barrel imports." The AI learns these rules once and applies them to every command.
- **Instant Onboarding:** A new hire doesn't need to learn your naming conventions through trial and error; the AI agents—guided by the skill files—enforce them automatically.
- **Consistency as a Service:** The code stops looking like it was written by five different people and starts looking like it was written by one highly disciplined entity.
---
## 4. Workflow Evolution: The "Commander" Role
The daily grind for a developer changes from "writing lines" to **orchestrating intent.**
### The New Dev Loop:
1. **Plan:** Use an Architect model to map out a feature.
2. **Execute:** Run a custom **Command** (e.g., `/scaffold-api`) to spawn sub-agents.
3. **Review:** Use a secondary Reviewer agent to verify the Flash-model's output against your Skill Files.
4. **Final Polish:** The human dev handles the high-level edge cases and final integration.
### Parallel Execution
Because sub-agents are cheap and fast, a lead developer can manage three streams of work simultaneously: one agent refactoring the database layer, one building the UI, and one generating the integration suite.
---
## 5. Conclusion: The Competitive Advantage
Moving to a team-based AI workflow isn't just about typing faster. It's about building a **predictable software factory.** By replacing flaky prompts with **automated commands** and leveraging **multi-model tiering**, you reduce costs, eliminate "review fatigue," and ship features at a velocity that solo AI usage simply cannot match.
Is your team currently stuck in the "copy-paste prompt" phase, or have you started committing AI automation directly to your repo?
How has the shift to specialized sub-agents changed your team's perspective on the role of a "Senior" developer?

View File

@@ -0,0 +1,57 @@
---
title: "When the Model Goes Dark: The Real Cost of Closed-Source AI"
date: 2026-06-15
coverImage:
author: Patrick Szalewicz
authorUrl: https://unsplash.com/@fachinformatiker
url: "https://images.unsplash.com/photo-1564313677573-481bd55237ad?q=80&w=1287&h=600&auto=format&fit=crop"
---
Three days. That's how long it took for a flagship closed-source AI model to go from public release to global blackout. A leading frontier lab launched what it called its most capable model ever. By the end of the same week, a regulatory directive had pulled the plug for *every* customer on the planet, paying enterprises, internal staff, everyone. Sessions errored out. New queries were silently rerouted to older, less capable fallback models. The model wasn't deprecated. It was simply unreachable, by order of someone who doesn't know your roadmap.
If your stomach dropped reading that, you already understand the thesis.
## The New Shape of Vendor Lock-In
For two decades, vendor lock-in was a *cost* problem. Switching databases, cloud providers, or CRM platforms was painful and expensive, but technically possible on a sane timeline. "Vendor lock-in" meant migration friction.
Closed-source AI introduced a different shape: **availability lock-in**. The same week, a major cloud provider restricted its own employees from using a peer lab's newest model over data-retention concerns. Earlier this year, that lab was publicly designated a "supply chain risk" by a defense agency and cut off from an entire government vertical overnight. These aren't pricing changes. They're access changes, and your team has zero say in them.
> **Key Takeaway:** Lock-in used to mean "expensive to leave." In the closed-source AI era, it increasingly means "the vendor may not be there tomorrow."
As I argued in [Your Codebase is an Asset](/articles/2026-02-24-codebase-is-asset), if a tool touches your codebase it's a stakeholder in your technical debt. Last week proved the corollary: that tool is also a stakeholder in your *uptime*.
## The Bill Keeps Going Up
The economics are shifting under anyone who built production workflows on closed APIs. The newest frontier-tier model launched at roughly **double the per-token price** of the previous flagship, and burned through subscription quota at about twice the rate. Independent testers drained a top-tier subscription in under nine minutes. The interactive tier's flat rate was always an implicit subsidy; on the new model, the subsidy no longer covers a working session.
And then today, on the day this post goes live, that same vendor cut the subsidy further. Agent workflows, headless scripts, CI integrations, and third-party tools authenticated through subscription credentials no longer draw from the same usage pool. They consume a small per-user credit ($20 to $200 a month depending on plan), billed at full API rates. Once that credit runs out, automated requests simply stop. No automatic overflow, no rollover, no team pooling. For teams running production automation on subscription credentials, this is the day a CI pipeline started failing silently at 2 a.m.
It's the third such billing change in five months. The "cheap subscription" era was a customer-acquisition tactic, not a pricing model. The math caught up, the subsidy is being clawed back, and the workflows you built on top became the bill.
## The Hidden Costs You Can't See on the Invoice
The line items above are bad enough. The things that don't show up on the invoice are arguably worse.
**Data retention as a feature gate.** The same top-tier model ships with mandatory 30-day data retention, which effectively excludes any European team operating under GDPR data-minimization, any healthcare or legal workflow with zero-retention contracts, and any organization that has promised its customers that prompts are not stored. A leading open-source CEO put it bluntly: this policy "delegated a lot of European companies to the permanent underclass." The same week, a major hyperscaler restricted internal use of the model over exactly these data concerns. If they don't trust it for their own staff, that's a signal.
**The alignment tax.** The model card itself documents that the model silently downgrades its own responses through prompt modification and steering vectors, for certain categories of prompt (anything related to building frontier AI systems), *without telling the user*. You don't have a hallucination problem. You have a transparency problem, baked into the platform.
These are not bugs. They are policy. And policy can change, unilaterally, with an email.
## The Hedge: Build Like Any Single Model Can Vanish Tomorrow
- **Routing layer first, model second.** Your production code should never `import` a vendor. Abstract the model behind a thin interface so swapping providers is a config change, not a sprint.
- **Keep an open-weights fallback.** Frontier-class open models (M3, and the local-inference ecosystem that matured this year) are competitive enough to serve as same-day failover. They are immune to export controls, retention mandates, and pricing whiplash because they run on your hardware.
- **Tier by risk, not by hype.** Reserve the closed frontier model for tasks where its marginal capability genuinely matters, a shorter list than vendor marketing suggests. A 12% quality lift on a CSV parser is not worth a single point of vendor dependency.
- **Re-read your contracts.** Yesterday's "flat rate" is today's metered credit. Plan for the next reprice now, not the morning it ships.
The teams that survive the next wave of AI consolidation won't be the ones with the best prompt libraries for a single provider. They'll be the ones who treated the model as a *substitution* rather than a *platform*.
## Final Thoughts
Closed-source AI isn't going away, and frontier models will keep delivering real capability gains. But the assumption that you're entering a stable, long-term commercial relationship when you build on top of one is no longer defensible. You're entering a relationship whose terms can change at the speed of a regulatory directive, an IPO, or a quarter-end margin call.
> **Don't let your tools own your code. Don't let one vendor own your runtime either.**
What does your team's AI exit plan look like? And if the honest answer is "we don't have one", what's the smallest step you could take this week to change that?

View File

@@ -1,37 +0,0 @@
---
draft: false
title: "The Complete Guide to Full Stack Web Development"
snippet: "Ornare cum cursus laoreet sagittis nunc fusce posuere per euismod dis vehicula a, semper fames lacus maecenas dictumst pulvinar neque enim non potenti. Torquent hac sociosqu eleifend potenti."
image: {
src: "https://images.unsplash.com/photo-1593720213428-28a5b9e94613?&fit=crop&w=430&h=240",
alt: "full stack web development"
}
publishDate: "2022-11-08 11:39"
category: "Tutorials"
author: "Janette Lynch"
tags: [webdev, tailwindcss, frontend]
---
Lorem ipsum dolor sit amet consectetur adipiscing elit euismod rutrum, consequat fringilla ultricies nullam curae mollis semper conubia viverra, orci aenean dapibus pharetra nec tortor tellus cubilia. Ullamcorper mi lectus eu malesuada tempor massa praesent magna mattis posuere, lobortis vulputate ut duis magnis parturient habitant nibh id tristique, quis suspendisse donec nisl penatibus sem non feugiat taciti. Mollis per ridiculus integer cursus semper vestibulum fermentum penatibus cubilia blandit scelerisque, tempus platea leo posuere ac pharetra volutpat aliquet euismod id ullamcorper lobortis, urna est magna mus rhoncus massa curae libero praesent eget. Mattis malesuada vestibulum quis ac nam phasellus suscipit facilisis libero diam posuere, cursus massa vehicula neque imperdiet tincidunt dui egestas lacinia mollis aliquet orci, nisl curabitur dapibus litora dis cum nostra montes ligula praesent. Facilisi aliquam convallis molestie tempor blandit ultricies bibendum parturient cubilia quam, porttitor morbi torquent tempus taciti nec faucibus elementum phasellus, quis inceptos vestibulum gravida augue potenti eget nunc maecenas. Tempor facilisis ligula volutpat habitant consequat inceptos orci per potenti blandit platea, mus sapien eget vel libero vestibulum augue cubilia ut ultrices fringilla lectus, imperdiet pellentesque cum ridiculus convallis sollicitudin nisl interdum semper felis.
Ornare cum cursus laoreet sagittis nunc fusce posuere per euismod dis vehicula a, semper fames lacus maecenas dictumst pulvinar neque enim non potenti. Torquent hac sociosqu eleifend potenti augue nulla vivamus senectus odio, quisque curabitur enim consequat class sociis feugiat ullamcorper, felis dis imperdiet cubilia commodo sed massa phasellus. Viverra purus mus nisi condimentum dui vehicula facilisis turpis, habitant nascetur lectus tempor quisque habitasse urna scelerisque, nibh nullam vestibulum luctus aenean mollis metus. Suscipit gravida duis nec aliquet natoque molestie a ridiculus scelerisque cum, justo cursus sapien sodales purus dignissim vel facilisi magnis, inceptos rutrum ut integer auctor commodo sollicitudin fames et. Faucibus ligula nibh sagittis mauris auctor posuere habitant, scelerisque phasellus accumsan egestas gravida viverra nam, sed etiam eleifend proin massa dictumst. Porttitor risus luctus per aenean tellus primis fringilla vitae fames lacinia mauris metus, nec pulvinar quisque commodo sodales ac nibh natoque phasellus semper placerat. Lectus aenean potenti leo sollicitudin tristique eros quam ligula, vestibulum diam consequat enim torquent nec tempus, blandit viverra dapibus eleifend dis nunc nascetur.
## Sodales hendrerit malesuada et vestibulum
- Luctus euismod pretium nisi et, est dui enim.
- Curae eget inceptos malesuada, fermentum class.
- Porttitor vestibulum aliquam porta feugiat velit, potenti eu placerat.
- Ligula lacus tempus ac porta, vel litora.
Torquent non nisi lacinia faucibus nibh tortor taciti commodo porttitor, mus hendrerit id leo scelerisque mollis habitasse orci tristique aptent, lacus at molestie cubilia facilisis porta accumsan condimentum. Metus lacus suscipit porttitor integer facilisi torquent, nostra nulla platea at natoque varius venenatis, id quam pharetra aliquam leo. Dictum orci himenaeos quam mi fusce lacinia maecenas ac magna eleifend laoreet, vivamus enim curabitur ullamcorper est ultrices convallis suscipit nascetur. Ornare fames pretium ante ac eget nisi tellus vivamus, convallis mauris sapien imperdiet sollicitudin aliquet taciti quam, lacinia tempor primis magna iaculis at eu. Est facilisi proin risus eleifend orci torquent ultricies platea, quisque nullam vel porttitor euismod sociis non, maecenas sociosqu interdum arcu sed pharetra potenti. Aliquet risus tempus hendrerit sapien tellus eget cursus enim etiam dui, lobortis nostra pellentesque odio posuere morbi ad neque senectus arcu eu, turpis proin ac felis purus fames magnis dis dignissim.
Orci volutpat augue viverra scelerisque dictumst ut condimentum vivamus, accumsan cum sem sollicitudin aliquet vehicula porta pretium placerat, malesuada euismod primis cubilia rutrum tempus parturient. Urna mauris in nibh morbi hendrerit vulputate condimentum, iaculis consequat porttitor dui dis euismod eros, arcu elementum venenatis varius lectus nisi. Nibh arcu ultrices semper morbi quam aptent quisque porta posuere iaculis, vestibulum cum vitae primis varius natoque conubia eu. Placerat sociis sagittis sociosqu morbi purus lobortis convallis, bibendum tortor ridiculus orci habitasse viverra dictum, quis rutrum fusce potenti volutpat vehicula. Curae porta inceptos lectus mus urna litora semper aliquam libero rutrum sem dui maecenas ligula quis, eget risus non imperdiet cum morbi magnis suspendisse etiam augue porttitor placerat facilisi hendrerit. Et eleifend eget augue duis fringilla sagittis erat est habitasse commodo tristique quisque pretium, suspendisse imperdiet inceptos mollis blandit magna mus elementum molestie sed vestibulum. Euismod morbi hendrerit suscipit felis ornare libero ligula, mus tortor urna interdum blandit nisi netus posuere, purus fermentum magnis nam primis nulla.
## Elementum nisi urna cursus nisl quam ante tristique blandit ultricies eget
Netus at rutrum taciti vestibulum molestie conubia semper class potenti lobortis, hendrerit donec vitae ad libero natoque parturient litora congue. Torquent rhoncus odio cursus iaculis molestie arcu leo condimentum accumsan, laoreet congue duis libero justo tortor commodo fusce, massa eros hac euismod netus sodales mi magnis. Aenean nullam sollicitudin ad velit nulla venenatis suspendisse iaculis, aliquet senectus mollis aptent fringilla volutpat nascetur, nec urna vehicula lacinia neque augue orci. Suspendisse et eleifend convallis sollicitudin posuere diam turpis gravida congue ultrices, laoreet ultricies dapibus proin facilisis magna class praesent fusce. Mus morbi magnis ultricies sed turpis ultrices tempus tortor bibendum, netus nulla viverra torquent malesuada ridiculus tempor. Parturient sociosqu erat ullamcorper gravida natoque varius, etiam habitant augue praesent per curabitur iaculis, donec pellentesque cursus suscipit aliquet. Congue curae cursus scelerisque pellentesque quis fusce arcu eros dictumst luctus ridiculus nisl viverra, turpis class faucibus phasellus feugiat eleifend fringilla orci tristique habitasse conubia quam. Habitasse montes congue sodales rutrum cras torquent cursus auctor condimentum imperdiet egestas nascetur, platea tincidunt ut sollicitudin purus libero lobortis ad nisi diam quam.
Suspendisse et eleifend convallis sollicitudin posuere diam turpis gravida congue ultrices, laoreet ultricies dapibus proin facilisis magna class praesent fusce. Mus morbi magnis ultricies sed turpis ultrices tempus tortor bibendum, netus nulla viverra torquent malesuada ridiculus tempor. Parturient sociosqu erat ullamcorper gravida natoque varius, etiam habitant augue praesent per curabitur iaculis, donec pellentesque cursus suscipit aliquet. Congue curae cursus scelerisque pellentesque quis fusce arcu eros dictumst luctus ridiculus nisl viverra, turpis class faucibus phasellus feugiat eleifend fringilla orci tristique habitasse conubia quam. Habitasse montes congue sodales rutrum cras torquent cursus auctor condimentum imperdiet egestas nascetur.

View File

@@ -1,37 +0,0 @@
---
draft: false
title: " Introduction to the Essential Data Structures & Algorithms"
snippet: "Ornare cum cursus laoreet sagittis nunc fusce posuere per euismod dis vehicula a, semper fames lacus maecenas dictumst pulvinar neque enim non potenti. Torquent hac sociosqu eleifend potenti."
image: {
src: "https://images.unsplash.com/photo-1627163439134-7a8c47e08208?&fit=crop&w=430&h=240",
alt: "data structures & algorithms"
}
publishDate: "2022-11-09 16:39"
category: "Courses"
author: "Marcell Ziemann"
tags: [webdev, tailwindcss, frontend]
---
Lorem ipsum dolor sit amet consectetur adipiscing elit euismod rutrum, consequat fringilla ultricies nullam curae mollis semper conubia viverra, orci aenean dapibus pharetra nec tortor tellus cubilia. Ullamcorper mi lectus eu malesuada tempor massa praesent magna mattis posuere, lobortis vulputate ut duis magnis parturient habitant nibh id tristique, quis suspendisse donec nisl penatibus sem non feugiat taciti. Mollis per ridiculus integer cursus semper vestibulum fermentum penatibus cubilia blandit scelerisque, tempus platea leo posuere ac pharetra volutpat aliquet euismod id ullamcorper lobortis, urna est magna mus rhoncus massa curae libero praesent eget. Mattis malesuada vestibulum quis ac nam phasellus suscipit facilisis libero diam posuere, cursus massa vehicula neque imperdiet tincidunt dui egestas lacinia mollis aliquet orci, nisl curabitur dapibus litora dis cum nostra montes ligula praesent. Facilisi aliquam convallis molestie tempor blandit ultricies bibendum parturient cubilia quam, porttitor morbi torquent tempus taciti nec faucibus elementum phasellus, quis inceptos vestibulum gravida augue potenti eget nunc maecenas. Tempor facilisis ligula volutpat habitant consequat inceptos orci per potenti blandit platea, mus sapien eget vel libero vestibulum augue cubilia ut ultrices fringilla lectus, imperdiet pellentesque cum ridiculus convallis sollicitudin nisl interdum semper felis.
Ornare cum cursus laoreet sagittis nunc fusce posuere per euismod dis vehicula a, semper fames lacus maecenas dictumst pulvinar neque enim non potenti. Torquent hac sociosqu eleifend potenti augue nulla vivamus senectus odio, quisque curabitur enim consequat class sociis feugiat ullamcorper, felis dis imperdiet cubilia commodo sed massa phasellus. Viverra purus mus nisi condimentum dui vehicula facilisis turpis, habitant nascetur lectus tempor quisque habitasse urna scelerisque, nibh nullam vestibulum luctus aenean mollis metus. Suscipit gravida duis nec aliquet natoque molestie a ridiculus scelerisque cum, justo cursus sapien sodales purus dignissim vel facilisi magnis, inceptos rutrum ut integer auctor commodo sollicitudin fames et. Faucibus ligula nibh sagittis mauris auctor posuere habitant, scelerisque phasellus accumsan egestas gravida viverra nam, sed etiam eleifend proin massa dictumst. Porttitor risus luctus per aenean tellus primis fringilla vitae fames lacinia mauris metus, nec pulvinar quisque commodo sodales ac nibh natoque phasellus semper placerat. Lectus aenean potenti leo sollicitudin tristique eros quam ligula, vestibulum diam consequat enim torquent nec tempus, blandit viverra dapibus eleifend dis nunc nascetur.
## Sodales hendrerit malesuada et vestibulum
- Luctus euismod pretium nisi et, est dui enim.
- Curae eget inceptos malesuada, fermentum class.
- Porttitor vestibulum aliquam porta feugiat velit, potenti eu placerat.
- Ligula lacus tempus ac porta, vel litora.
Torquent non nisi lacinia faucibus nibh tortor taciti commodo porttitor, mus hendrerit id leo scelerisque mollis habitasse orci tristique aptent, lacus at molestie cubilia facilisis porta accumsan condimentum. Metus lacus suscipit porttitor integer facilisi torquent, nostra nulla platea at natoque varius venenatis, id quam pharetra aliquam leo. Dictum orci himenaeos quam mi fusce lacinia maecenas ac magna eleifend laoreet, vivamus enim curabitur ullamcorper est ultrices convallis suscipit nascetur. Ornare fames pretium ante ac eget nisi tellus vivamus, convallis mauris sapien imperdiet sollicitudin aliquet taciti quam, lacinia tempor primis magna iaculis at eu. Est facilisi proin risus eleifend orci torquent ultricies platea, quisque nullam vel porttitor euismod sociis non, maecenas sociosqu interdum arcu sed pharetra potenti. Aliquet risus tempus hendrerit sapien tellus eget cursus enim etiam dui, lobortis nostra pellentesque odio posuere morbi ad neque senectus arcu eu, turpis proin ac felis purus fames magnis dis dignissim.
Orci volutpat augue viverra scelerisque dictumst ut condimentum vivamus, accumsan cum sem sollicitudin aliquet vehicula porta pretium placerat, malesuada euismod primis cubilia rutrum tempus parturient. Urna mauris in nibh morbi hendrerit vulputate condimentum, iaculis consequat porttitor dui dis euismod eros, arcu elementum venenatis varius lectus nisi. Nibh arcu ultrices semper morbi quam aptent quisque porta posuere iaculis, vestibulum cum vitae primis varius natoque conubia eu. Placerat sociis sagittis sociosqu morbi purus lobortis convallis, bibendum tortor ridiculus orci habitasse viverra dictum, quis rutrum fusce potenti volutpat vehicula. Curae porta inceptos lectus mus urna litora semper aliquam libero rutrum sem dui maecenas ligula quis, eget risus non imperdiet cum morbi magnis suspendisse etiam augue porttitor placerat facilisi hendrerit. Et eleifend eget augue duis fringilla sagittis erat est habitasse commodo tristique quisque pretium, suspendisse imperdiet inceptos mollis blandit magna mus elementum molestie sed vestibulum. Euismod morbi hendrerit suscipit felis ornare libero ligula, mus tortor urna interdum blandit nisi netus posuere, purus fermentum magnis nam primis nulla.
## Elementum nisi urna cursus nisl quam ante tristique blandit ultricies eget
Netus at rutrum taciti vestibulum molestie conubia semper class potenti lobortis, hendrerit donec vitae ad libero natoque parturient litora congue. Torquent rhoncus odio cursus iaculis molestie arcu leo condimentum accumsan, laoreet congue duis libero justo tortor commodo fusce, massa eros hac euismod netus sodales mi magnis. Aenean nullam sollicitudin ad velit nulla venenatis suspendisse iaculis, aliquet senectus mollis aptent fringilla volutpat nascetur, nec urna vehicula lacinia neque augue orci. Suspendisse et eleifend convallis sollicitudin posuere diam turpis gravida congue ultrices, laoreet ultricies dapibus proin facilisis magna class praesent fusce. Mus morbi magnis ultricies sed turpis ultrices tempus tortor bibendum, netus nulla viverra torquent malesuada ridiculus tempor. Parturient sociosqu erat ullamcorper gravida natoque varius, etiam habitant augue praesent per curabitur iaculis, donec pellentesque cursus suscipit aliquet. Congue curae cursus scelerisque pellentesque quis fusce arcu eros dictumst luctus ridiculus nisl viverra, turpis class faucibus phasellus feugiat eleifend fringilla orci tristique habitasse conubia quam. Habitasse montes congue sodales rutrum cras torquent cursus auctor condimentum imperdiet egestas nascetur, platea tincidunt ut sollicitudin purus libero lobortis ad nisi diam quam.
Suspendisse et eleifend convallis sollicitudin posuere diam turpis gravida congue ultrices, laoreet ultricies dapibus proin facilisis magna class praesent fusce. Mus morbi magnis ultricies sed turpis ultrices tempus tortor bibendum, netus nulla viverra torquent malesuada ridiculus tempor. Parturient sociosqu erat ullamcorper gravida natoque varius, etiam habitant augue praesent per curabitur iaculis, donec pellentesque cursus suscipit aliquet. Congue curae cursus scelerisque pellentesque quis fusce arcu eros dictumst luctus ridiculus nisl viverra, turpis class faucibus phasellus feugiat eleifend fringilla orci tristique habitasse conubia quam. Habitasse montes congue sodales rutrum cras torquent cursus auctor condimentum imperdiet egestas nascetur.

View File

@@ -1,44 +0,0 @@
---
draft: false
title: "How to become a Frontend Master"
snippet: "Ornare cum cursus laoreet sagittis nunc fusce posuere per euismod dis vehicula a, semper fames lacus maecenas dictumst pulvinar neque enim non potenti. Torquent hac sociosqu eleifend potenti."
image: {
src: "https://images.unsplash.com/photo-1667372393119-3d4c48d07fc9?&fit=crop&w=430&h=240",
alt: "frontend master"
}
publishDate: "2022-11-07 15:39"
category: "Tutorials"
author: "Connor Lopez"
tags: [astro, tailwindcss, frontend]
---
Whatever you do, it's always beneficial to have the right tools at your disposal. I love working remotely and am a big advocate of doing remote software development. Therefore, I always strive to have the best equipment available to be as productive as possible. Writing posts like this constantly takes a lot of time. Luckily iVanky helped me out and sponsored this post so that I can concentrate on writing. I recently had the chance to test out one of their hottest products, a dual USB-C Docking Station that allows me to connect both my wide-screen monitors to my MacBook with Dual 4K@60Hz display connectivity. It also supports up to 96W laptop charging, which is awesome. If you are in a situation like me and want to upgrade your equipment, check out this and their other products! And now comes the article:
Whether you are new to programming or already an experienced developer. In this industry, learning new concepts and languages/frameworks is
mandatory to keep up with the rapid changes. Take for example React - open-sourced by Facebook just a shy 4 years ago it already became the number one choice for JavaScript devs around the globe. But also Vue and Angular, of course, have their legitimate follower-base. And then there is Svelte, and universal frameworks like Next.js or Nuxt.js, and Gatsby, and Gridsome, and Quasar, and and and. If you want to shine as an expert JavaScript developer you should at least have some experience in different frameworks and libraries - besides doing your homework with good, old JS.
To help you become Frontend Masters, I have collected 9 different projects, each with a distinct topic and a different JavaScript framework or library as a tech stack that you can build and add to your portfolio. Remember, nothing helps you more than actually building stuff so go ahead, sharpen your mind and make this happen!
## Dictum integer fusce ac ridiculus
Lorem ipsum dolor sit amet consectetur adipiscing elit euismod rutrum, consequat fringilla ultricies nullam curae mollis semper conubia viverra, orci aenean dapibus pharetra nec tortor tellus cubilia. Ullamcorper mi lectus eu malesuada tempor massa praesent magna mattis posuere, lobortis vulputate ut duis magnis parturient habitant nibh id tristique, quis suspendisse donec nisl penatibus sem non feugiat taciti. Mollis per ridiculus integer cursus semper vestibulum fermentum penatibus cubilia blandit scelerisque, tempus platea leo posuere ac pharetra volutpat aliquet euismod id ullamcorper lobortis, urna est magna mus rhoncus massa curae libero praesent eget. Mattis malesuada vestibulum quis ac nam phasellus suscipit facilisis libero diam posuere, cursus massa vehicula neque imperdiet tincidunt dui egestas lacinia mollis aliquet orci, nisl curabitur dapibus litora dis cum nostra montes ligula praesent. Facilisi aliquam convallis molestie tempor blandit ultricies bibendum parturient cubilia quam, porttitor morbi torquent tempus taciti nec faucibus elementum phasellus, quis inceptos vestibulum gravida augue potenti eget nunc maecenas. Tempor facilisis ligula volutpat habitant consequat inceptos orci per potenti blandit platea, mus sapien eget vel libero vestibulum augue cubilia ut ultrices fringilla lectus, imperdiet pellentesque cum ridiculus convallis sollicitudin nisl interdum semper felis.
Ornare cum cursus laoreet sagittis nunc fusce posuere per euismod dis vehicula a, semper fames lacus maecenas dictumst pulvinar neque enim non potenti. Torquent hac sociosqu eleifend potenti augue nulla vivamus senectus odio, quisque curabitur enim consequat class sociis feugiat ullamcorper, felis dis imperdiet cubilia commodo sed massa phasellus. Viverra purus mus nisi condimentum dui vehicula facilisis turpis, habitant nascetur lectus tempor quisque habitasse urna scelerisque, nibh nullam vestibulum luctus aenean mollis metus. Suscipit gravida duis nec aliquet natoque molestie a ridiculus scelerisque cum, justo cursus sapien sodales purus dignissim vel facilisi magnis, inceptos rutrum ut integer auctor commodo sollicitudin fames et. Faucibus ligula nibh sagittis mauris auctor posuere habitant, scelerisque phasellus accumsan egestas gravida viverra nam, sed etiam eleifend proin massa dictumst. Porttitor risus luctus per aenean tellus primis fringilla vitae fames lacinia mauris metus, nec pulvinar quisque commodo sodales ac nibh natoque phasellus semper placerat. Lectus aenean potenti leo sollicitudin tristique eros quam ligula, vestibulum diam consequat enim torquent nec tempus, blandit viverra dapibus eleifend dis nunc nascetur.
## Sodales hendrerit malesuada et vestibulum
- Luctus euismod pretium nisi et, est dui enim.
- Curae eget inceptos malesuada, fermentum class.
- Porttitor vestibulum aliquam porta feugiat velit, potenti eu placerat.
- Ligula lacus tempus ac porta, vel litora.
Torquent non nisi lacinia faucibus nibh tortor taciti commodo porttitor, mus hendrerit id leo scelerisque mollis habitasse orci tristique aptent, lacus at molestie cubilia facilisis porta accumsan condimentum. Metus lacus suscipit porttitor integer facilisi torquent, nostra nulla platea at natoque varius venenatis, id quam pharetra aliquam leo. Dictum orci himenaeos quam mi fusce lacinia maecenas ac magna eleifend laoreet, vivamus enim curabitur ullamcorper est ultrices convallis suscipit nascetur. Ornare fames pretium ante ac eget nisi tellus vivamus, convallis mauris sapien imperdiet sollicitudin aliquet taciti quam, lacinia tempor primis magna iaculis at eu. Est facilisi proin risus eleifend orci torquent ultricies platea, quisque nullam vel porttitor euismod sociis non, maecenas sociosqu interdum arcu sed pharetra potenti. Aliquet risus tempus hendrerit sapien tellus eget cursus enim etiam dui, lobortis nostra pellentesque odio posuere morbi ad neque senectus arcu eu, turpis proin ac felis purus fames magnis dis dignissim.
Orci volutpat augue viverra scelerisque dictumst ut condimentum vivamus, accumsan cum sem sollicitudin aliquet vehicula porta pretium placerat, malesuada euismod primis cubilia rutrum tempus parturient. Urna mauris in nibh morbi hendrerit vulputate condimentum, iaculis consequat porttitor dui dis euismod eros, arcu elementum venenatis varius lectus nisi. Nibh arcu ultrices semper morbi quam aptent quisque porta posuere iaculis, vestibulum cum vitae primis varius natoque conubia eu. Placerat sociis sagittis sociosqu morbi purus lobortis convallis, bibendum tortor ridiculus orci habitasse viverra dictum, quis rutrum fusce potenti volutpat vehicula. Curae porta inceptos lectus mus urna litora semper aliquam libero rutrum sem dui maecenas ligula quis, eget risus non imperdiet cum morbi magnis suspendisse etiam augue porttitor placerat facilisi hendrerit. Et eleifend eget augue duis fringilla sagittis erat est habitasse commodo tristique quisque pretium, suspendisse imperdiet inceptos mollis blandit magna mus elementum molestie sed vestibulum. Euismod morbi hendrerit suscipit felis ornare libero ligula, mus tortor urna interdum blandit nisi netus posuere, purus fermentum magnis nam primis nulla.
## Elementum nisi urna cursus nisl quam ante tristique blandit ultricies eget
Netus at rutrum taciti vestibulum molestie conubia semper class potenti lobortis, hendrerit donec vitae ad libero natoque parturient litora congue. Torquent rhoncus odio cursus iaculis molestie arcu leo condimentum accumsan, laoreet congue duis libero justo tortor commodo fusce, massa eros hac euismod netus sodales mi magnis. Aenean nullam sollicitudin ad velit nulla venenatis suspendisse iaculis, aliquet senectus mollis aptent fringilla volutpat nascetur, nec urna vehicula lacinia neque augue orci. Suspendisse et eleifend convallis sollicitudin posuere diam turpis gravida congue ultrices, laoreet ultricies dapibus proin facilisis magna class praesent fusce. Mus morbi magnis ultricies sed turpis ultrices tempus tortor bibendum, netus nulla viverra torquent malesuada ridiculus tempor. Parturient sociosqu erat ullamcorper gravida natoque varius, etiam habitant augue praesent per curabitur iaculis, donec pellentesque cursus suscipit aliquet. Congue curae cursus scelerisque pellentesque quis fusce arcu eros dictumst luctus ridiculus nisl viverra, turpis class faucibus phasellus feugiat eleifend fringilla orci tristique habitasse conubia quam. Habitasse montes congue sodales rutrum cras torquent cursus auctor condimentum imperdiet egestas nascetur, platea tincidunt ut sollicitudin purus libero lobortis ad nisi diam quam.

View File

@@ -1,194 +0,0 @@
---
draft: false
title: "Typography Example Post"
snippet: "Sint sit cillum pariatur eiusmod nulla pariatur ipsum. Sit laborum anim qui mollit tempor pariatur nisi minim dolor. Aliquip et adipisicing sit sit fugiat"
publishDate: "2022-11-05 15:36"
image:
{
src: "https://images.unsplash.com/photo-1542393545-10f5cde2c810?&fit=crop&w=430&h=240",
alt: "typography",
}
category: "Technology"
author: "Charles North"
tags: [mdx, astro, blog]
---
import Button from "@/components/ui/button.astro";
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
## <a name="Headings"></a>Headings
Sint sit cillum pariatur eiusmod nulla pariatur ipsum. Sit laborum anim qui mollit tempor pariatur nisi minim dolor. Aliquip et adipisicing sit sit fugiat commodo id sunt. Nostrud enim ad commodo incididunt cupidatat in ullamco ullamco Lorem cupidatat velit enim et Lorem. Ut laborum cillum laboris fugiat culpa sint irure do reprehenderit culpa occaecat. Exercitation esse mollit tempor magna aliqua in occaecat aliquip veniam reprehenderit nisi dolor in laboris dolore velit.
## Heading two
Aute officia nulla deserunt do deserunt cillum velit magna. Officia veniam culpa anim minim dolore labore pariatur voluptate id ad est duis quis velit dolor pariatur enim. Incididunt enim excepteur do veniam consequat culpa do voluptate dolor fugiat ad adipisicing sit. Labore officia est adipisicing dolore proident eiusmod exercitation deserunt ullamco anim do occaecat velit. Elit dolor consectetur proident sunt aliquip est do tempor quis aliqua culpa aute. Duis in tempor exercitation pariatur et adipisicing mollit irure tempor ut enim esse commodo laboris proident. Do excepteur laborum anim esse aliquip eu sit id Lorem incididunt elit irure ea nulla dolor et. Nulla amet fugiat qui minim deserunt enim eu cupidatat aute officia do velit ea reprehenderit.
### Heading three
Voluptate cupidatat cillum elit quis ipsum eu voluptate fugiat consectetur enim. Quis ut voluptate culpa ex anim aute consectetur dolore proident voluptate exercitation eiusmod. Esse in do anim magna minim culpa sint. Adipisicing ipsum consectetur proident ullamco magna sit amet aliqua aute fugiat laborum exercitation duis et.
#### Heading four
Commodo fugiat aliqua minim quis pariatur mollit id tempor. Non occaecat minim esse enim aliqua adipisicing nostrud duis consequat eu adipisicing qui. Minim aliquip sit excepteur ipsum consequat laborum pariatur excepteur. Veniam fugiat et amet ad elit anim laborum duis mollit occaecat et et ipsum et reprehenderit. Occaecat aliquip dolore adipisicing sint labore occaecat officia fugiat. Quis adipisicing exercitation exercitation eu amet est laboris sunt nostrud ipsum reprehenderit ullamco. Enim sint ut consectetur id anim aute voluptate exercitation mollit dolore magna magna est Lorem. Ut adipisicing adipisicing aliqua ullamco voluptate labore nisi tempor esse magna incididunt.
##### Heading five
Veniam enim esse amet veniam deserunt laboris amet enim consequat. Minim nostrud deserunt cillum consectetur commodo eu enim nostrud ullamco occaecat excepteur. Aliquip et ut est commodo enim dolor amet sint excepteur. Amet ad laboris laborum deserunt sint sunt aliqua commodo ex duis deserunt enim est ex labore ut. Duis incididunt velit adipisicing non incididunt adipisicing adipisicing. Ad irure duis nisi tempor eu dolor fugiat magna et consequat tempor eu ex dolore. Mollit esse nisi qui culpa ut nisi ex proident culpa cupidatat cillum culpa occaecat anim. Ut officia sit ea nisi ea excepteur nostrud ipsum et nulla.
###### Heading six
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
[[Top]](#top)
## <a name="Paragraphs"></a>Paragraphs
Incididunt ex adipisicing ea ullamco consectetur in voluptate proident fugiat tempor deserunt reprehenderit ullamco id dolore laborum. Do laboris laboris minim incididunt qui consectetur exercitation adipisicing dolore et magna consequat magna anim sunt. Officia fugiat Lorem sunt pariatur incididunt Lorem reprehenderit proident irure. Dolore ipsum aliqua mollit ad officia fugiat sit eu aliquip cupidatat ipsum duis laborum laborum fugiat esse. Voluptate anim ex dolore deserunt ea ex eiusmod irure. Occaecat excepteur aliqua exercitation aliquip dolor esse eu eu.
Officia dolore laborum aute incididunt commodo nisi velit est est elit et dolore elit exercitation. Enim aliquip magna id ipsum aliquip consectetur ad nulla quis. Incididunt pariatur dolor consectetur cillum enim velit cupidatat laborum quis ex.
Officia irure in non voluptate adipisicing sit amet tempor duis dolore deserunt enim ut. Reprehenderit incididunt in ad anim et deserunt deserunt Lorem laborum quis. Enim aute anim labore proident laboris voluptate elit excepteur in. Ex labore nulla velit officia ullamco Lorem Lorem id do. Dolore ullamco ipsum magna dolor pariatur voluptate ipsum id occaecat ipsum. Dolore tempor quis duis commodo quis quis enim.
[[Top]](#top)
## <a name="Blockquotes"></a>Blockquotes
Ad nisi laborum aute cupidatat magna deserunt eu id laboris id. Aliquip nulla cupidatat sint ex Lorem mollit laborum dolor amet est ut esse aute. Nostrud ex consequat id incididunt proident ipsum minim duis aliqua ut ex et ad quis. Laborum sint esse cillum anim nulla cillum consectetur aliqua sit. Nisi excepteur cillum labore amet excepteur commodo enim occaecat consequat ipsum proident exercitation duis id in.
> Ipsum et cupidatat mollit exercitation enim duis sunt irure aliqua reprehenderit mollit. Pariatur Lorem pariatur laboris do culpa do elit irure. Eiusmod amet nulla voluptate velit culpa et aliqua ad reprehenderit sit ut.
Labore ea magna Lorem consequat aliquip consectetur cillum duis dolore. Et veniam dolor qui incididunt minim amet laboris sit. Dolore ad esse commodo et dolore amet est velit ut nisi ea. Excepteur ea nulla commodo dolore anim dolore adipisicing eiusmod labore id enim esse quis mollit deserunt est. Minim ea culpa voluptate nostrud commodo proident in duis aliquip minim.
> Qui est sit et reprehenderit aute est esse enim aliqua id aliquip ea anim. Pariatur sint reprehenderit mollit velit voluptate enim consectetur sint enim. Quis exercitation proident elit non id qui culpa dolore esse aliquip consequat.
Ipsum excepteur cupidatat sunt minim ad eiusmod tempor sit.
> Deserunt excepteur adipisicing culpa pariatur cillum laboris ullamco nisi fugiat cillum officia. In cupidatat nulla aliquip tempor ad Lorem Lorem quis voluptate officia consectetur pariatur ex in est duis. Mollit id esse est elit exercitation voluptate nostrud nisi laborum magna dolore dolore tempor in est consectetur.
Adipisicing voluptate ipsum culpa voluptate id aute laboris labore esse fugiat veniam ullamco occaecat do ut. Tempor et esse reprehenderit veniam proident ipsum irure sit ullamco et labore ea excepteur nulla labore ut. Ex aute minim quis tempor in eu id id irure ea nostrud dolor esse.
[[Top]](#top)
## <a name="Lists"></a>Lists
### Ordered List
1. Longan
2. Lychee
3. Excepteur ad cupidatat do elit laborum amet cillum reprehenderit consequat quis.
Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip aliquip velit pariatur dolore.
4. Marionberry
5. Melon
- Cantaloupe
- Honeydew
- Watermelon
6. Miracle fruit
7. Mulberry
### Unordered List
- Olive
- Orange
- Blood orange
- Clementine
- Papaya
- Ut aute ipsum occaecat nisi culpa Lorem id occaecat cupidatat id id magna laboris ad duis. Fugiat cillum dolore veniam nostrud proident sint consectetur eiusmod irure adipisicing.
- Passionfruit
[[Top]](#top)
## <a name="Horizontal"></a>Horizontal rule
In dolore velit aliquip labore mollit minim tempor veniam eu veniam ad in sint aliquip mollit mollit. Ex occaecat non deserunt elit laborum sunt tempor sint consequat culpa culpa qui sit. Irure ad commodo eu voluptate mollit cillum cupidatat veniam proident amet minim reprehenderit.
---
In laboris eiusmod reprehenderit aliquip sit proident occaecat. Non sit labore anim elit veniam Lorem minim commodo eiusmod irure do minim nisi. Dolor amet cillum excepteur consequat sint non sint.
[[Top]](#top)
## <a name="Table"></a>Table
Duis sunt ut pariatur reprehenderit mollit mollit magna dolore in pariatur nulla commodo sit dolor ad fugiat. Laboris amet ea occaecat duis eu enim exercitation deserunt ea laborum occaecat reprehenderit. Et incididunt dolor commodo consequat mollit nisi proident non pariatur in et incididunt id. Eu ut et Lorem ea ex magna minim ipsum ipsum do.
| Table Heading 1 | Table Heading 2 | Center align | Right align | Table Heading 5 |
| :-------------- | :-------------- | :----------: | ----------: | :-------------- |
| Item 1 | Item 2 | Item 3 | Item 4 | Item 5 |
| Item 1 | Item 2 | Item 3 | Item 4 | Item 5 |
| Item 1 | Item 2 | Item 3 | Item 4 | Item 5 |
| Item 1 | Item 2 | Item 3 | Item 4 | Item 5 |
| Item 1 | Item 2 | Item 3 | Item 4 | Item 5 |
Minim id consequat adipisicing cupidatat laborum culpa veniam non consectetur et duis pariatur reprehenderit eu ex consectetur. Sunt nisi qui eiusmod ut cillum laborum Lorem officia aliquip laboris ullamco nostrud laboris non irure laboris. Cillum dolore labore Lorem deserunt mollit voluptate esse incididunt ex dolor.
[[Top]](#top)
## <a name="Code"></a>Code
### Inline code
Ad amet irure est magna id mollit Lorem in do duis enim. Excepteur velit nisi magna ea pariatur pariatur ullamco fugiat deserunt sint non sint. Duis duis est `code in text` velit velit aute culpa ex quis pariatur pariatur laborum aute pariatur duis tempor sunt ad. Irure magna voluptate dolore consectetur consectetur irure esse. Anim magna `<strong>in culpa qui officia</strong>` dolor eiusmod esse amet aute cupidatat aliqua do id voluptate cupidatat reprehenderit amet labore deserunt.
### Highlighted
Et fugiat ad nisi amet magna labore do cillum fugiat occaecat cillum Lorem proident. In sint dolor ullamco ad do adipisicing amet id excepteur Lorem aliquip sit irure veniam laborum duis cillum. Aliqua occaecat minim cillum deserunt magna sunt laboris do do irure ea nostrud consequat ut voluptate ex.
```go
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
```
Ex amet id ex aliquip id do laborum excepteur exercitation elit sint commodo occaecat nostrud est. Nostrud pariatur esse veniam laborum non sint magna sit laboris minim in id. Aliqua pariatur pariatur excepteur adipisicing irure culpa consequat commodo et ex id ad.
[[Top]](#top)
## <a name="Inline"></a>Inline elements
Sint ea anim ipsum ad commodo cupidatat do **exercitation** incididunt et minim ad labore sunt. Minim deserunt labore laboris velit nulla incididunt ipsum nulla. Ullamco ad laborum ea qui et anim in laboris exercitation tempor sit officia laborum reprehenderit culpa velit quis. **Consequat commodo** reprehenderit duis [irure](#!) esse esse exercitation minim enim Lorem dolore duis irure. Nisi Lorem reprehenderit ea amet excepteur dolor excepteur magna labore proident voluptate ipsum. Reprehenderit ex esse deserunt aliqua ea officia mollit Lorem nulla magna enim. Et ad ipsum labore enim ipsum **cupidatat consequat**. Commodo non ea cupidatat magna deserunt dolore ipsum velit nulla elit veniam nulla eiusmod proident officia.
![Super wide](https://images.unsplash.com/photo-1471128466710-c26ff0d26143?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8fHx8fHx8MTY2MDc4MTk3Mw&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080)
_Proident sit veniam in est proident officia adipisicing_ ea tempor cillum non cillum velit deserunt. Voluptate laborum incididunt sit consectetur Lorem irure incididunt voluptate nostrud. Commodo ut eiusmod tempor cupidatat esse enim minim ex anim consequat. Mollit sint culpa qui laboris quis consectetur ad sint esse. Amet anim anim minim ullamco et duis non irure. Sit tempor adipisicing ea laboris `culpa ex duis sint` anim aute reprehenderit id eu ea. Aute [excepteur proident](#!) Lorem minim adipisicing nostrud mollit ad ut voluptate do nulla esse occaecat aliqua sint anim.
![Not so big](https://placekitten.com/480/400)
Incididunt in culpa cupidatat mollit cillum qui proident sit. In cillum aliquip incididunt voluptate magna amet cupidatat cillum pariatur sint aliqua est _enim **anim** voluptate_. Magna aliquip proident incididunt id duis pariatur eiusmod incididunt commodo culpa dolore sit. Culpa do nostrud elit ad exercitation anim pariatur non minim nisi **adipisicing sunt _officia_**. Do deserunt magna mollit Lorem commodo ipsum do cupidatat mollit enim ut elit veniam ea voluptate.
Reprehenderit non eu quis in ad elit esse qui aute id [incididunt](#!) dolore cillum. Esse laboris consequat dolor anim exercitation tempor aliqua deserunt velit magna laboris. Culpa culpa minim duis amet mollit do quis amet commodo nulla irure.
[[Top]](#top)
## MDX
```js
---
publishDate: 'Aug 02 2022'
title: 'Markdown elements demo post'
---
import Logo from "@/components/ui/button.astro";
## MDX
<Button>Click</Button>
```
<div>
<Button>Click Me</Button>
</div>
[[Top]](#top)

View File

@@ -1,39 +0,0 @@
// 1. Import utilities from `astro:content`
import { z, defineCollection } from 'astro:content';
// 2. Define your collection(s)
const blogCollection = defineCollection({
schema: z.object({
draft: z.boolean(),
title: z.string(),
snippet: z.string(),
image: z.object({
src: z.string(),
alt: z.string(),
}),
publishDate: z.string().transform(str => new Date(str)),
author: z.string().default('Astroship'),
category: z.string(),
tags: z.array(z.string()),
}),
});
const teamCollection = defineCollection({
schema: z.object({
draft: z.boolean(),
name: z.string(),
title: z.string(),
avatar: z.object({
src: z.string(),
alt: z.string(),
}),
publishDate: z.string().transform(str => new Date(str)),
}),
});
// 3. Export a single `collections` object to register your collection(s)
// This key should match your collection directory name in "src/content"
export const collections = {
'blog': blogCollection,
'team': teamCollection,
};

View File

@@ -1,10 +0,0 @@
---
draft: false
name: "Janette Lynch"
title: "Senior Director"
avatar: {
src: "https://images.unsplash.com/photo-1580489944761-15a19d654956?&fit=crop&w=280",
alt: "Janette Lynch"
}
publishDate: "2022-11-07 15:39"
---

View File

@@ -1,10 +0,0 @@
---
draft: false
name: "Marcell Ziemann"
title: "Principal Strategist"
avatar: {
src: "https://images.unsplash.com/photo-1633332755192-727a05c4013d?&fit=crop&w=280",
alt: "Marcell Ziemann"
}
publishDate: "2022-11-08 15:39"
---

View File

@@ -1,10 +0,0 @@
---
draft: false
name: "Robert Palmer"
title: "Marketing Engineer"
avatar: {
src: "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?&fit=crop&w=280",
alt: "Robert Palmer"
}
publishDate: "2022-11-09 15:39"
---

View File

@@ -1,49 +0,0 @@
---
import Container from "@/components/container.astro";
import { getFormattedDate } from "@/utils/all";
import Layout from "./Layout.astro";
const { frontmatter } = Astro.props;
---
<Layout title={frontmatter.title}>
<Container>
<div class="mx-auto max-w-[735px] mt-14">
<span class="text-blue-400 uppercase tracking-wider text-sm font-medium">
{frontmatter.category}
</span>
<h1
class="text-4xl lg:text-5xl font-bold lg:tracking-tight mt-1 lg:leading-tight">
{frontmatter.title}
</h1>
<div class="flex gap-2 mt-3 items-center flex-wrap md:flex-nowrap">
<span class="text-gray-400">
{frontmatter.author}
</span>
<span class="text-gray-400">•</span>
<time class="text-gray-400" datetime={frontmatter.publishDate}>
{getFormattedDate(frontmatter.publishDate)}
</time>
<span class="text-gray-400 hidden md:block">•</span>
<div class="w-full md:w-auto flex flex-wrap gap-3">
{
frontmatter.tags.map((tag) => (
<span class="text-sm text-gray-500">#{tag}</span>
))
}
</div>
</div>
</div>
<div class="mx-auto prose prose-lg mt-6">
<slot />
</div>
<div class="text-center mt-8">
<a
href="/blog"
class="bg-gray-100 px-5 py-3 rounded-md hover:bg-gray-200 transition"
>← Back to Blog</a
>
</div>
</Container>
</Layout>

View File

@@ -1,27 +1,40 @@
---
import { SEO } from "astro-seo";
import Footer from "@/components/footer.astro";
import Navbar from "@/components/navbar/navbar.astro";
import "@fontsource-variable/inter/index.css";
import "@fontsource-variable/bricolage-grotesque";
import "../styles/global.css";
import TopNav from "@/components/top-nav.astro";
import Footer from "@/components/footer.astro";
import { TWITTER_HANDLE, SITE_URL } from "@/utils/seo";
export interface Props {
title: string;
title?: string;
description?: string;
ogImage?: string;
ogImageAlt?: string;
ogType?: "website" | "article" | "profile";
jsonLd?: unknown;
rssHref?: string;
withNav?: boolean;
withFooter?: boolean;
}
const canonicalURL = new URL(Astro.url.pathname, Astro.site).toString();
const {
title = "Marios Antonoudiou — AI Product Engineer",
description = "AI product engineer building AI-powered products that feel simple, useful, and ready for real users.",
ogImage = "/mariosant.webp",
ogImageAlt = "Marios Antonoudiou",
ogType = "website",
jsonLd,
rssHref = "/rss.xml",
withNav = true,
withFooter = true,
} = Astro.props;
const resolvedImageWithDomain = new URL(
"/opengraph.jpg",
Astro.site
).toString();
const { title } = Astro.props;
const makeTitle = title
? title + " | " + "Astroship"
: "Astroship - Starter Template for Astro with Tailwind CSS";
const canonical = new URL(Astro.url.pathname, Astro.site).toString();
const resolvedOgImage = new URL(ogImage, Astro.site).toString();
const fullTitle = title.includes("Marios") ? title : `${title} — Marios Antonoudiou`;
const jsonLdBlocks = Array.isArray(jsonLd) ? jsonLd : jsonLd ? [jsonLd] : [];
const jsonLdJson = jsonLdBlocks.map((block) => JSON.stringify(block));
---
<!doctype html>
@@ -29,42 +42,53 @@ const makeTitle = title
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="alternate" type="application/rss+xml" title="Marios Antonoudiou" href={new URL(rssHref, Astro.site).toString()} />
<meta name="generator" content={Astro.generator} />
<!-- <link rel="preload" as="image" href={src} alt="Hero" /> -->
<script is:inline>
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
document.documentElement.classList.add("dark");
}
</script>
<SEO
title={makeTitle}
description="Astroship is a starter website template for Astro built with TailwindCSS."
canonical={canonicalURL}
title={fullTitle}
description={description}
canonical={canonical}
twitter={{
creator: "@surjithctly",
site: "@web3templates",
card: "summary_large_image",
site: TWITTER_HANDLE,
creator: TWITTER_HANDLE,
}}
openGraph={{
basic: {
url: canonicalURL,
type: "website",
title: `Astroship - Starter Template for Astro`,
image: resolvedImageWithDomain,
url: canonical,
type: ogType,
title: fullTitle,
image: resolvedOgImage,
},
image: {
alt: "Astroship Homepage Screenshot",
alt: ogImageAlt,
},
optional: {
description,
site_name: "Marios Antonoudiou",
locale: "en_US",
},
}}
/>
{
jsonLdJson.map((json) => (
<script type="application/ld+json" set:html={json} />
))
}
</head>
<body>
<Navbar />
<slot />
<Footer />
<style is:global>
/* Improve Page speed */
/* https://css-tricks.com/almanac/properties/c/content-visibility/ */
img {
content-visibility: auto;
}
</style>
<body class="bg-white dark:bg-slate-900 selection:bg-blue-100 dark:selection:bg-slate-600 text-slate-900 dark:text-white antialiased min-h-screen flex flex-col">
{withNav && <TopNav />}
<main class="flex-1">
<slot />
</main>
{withFooter && <Footer />}
</body>
</html>

View File

@@ -1,15 +1,12 @@
---
import Container from "@/components/container.astro";
import Layout from "@/layouts/Layout.astro";
import Link from "@/components/link.astro";
---
<Layout title="404 Not Found">
<Container>
<div class="min-h-[calc(100vh-16rem)] flex items-center justify-center">
<div class="mt-16 text-center">
<h1 class="text-4xl lg:text-5xl font-bold lg:tracking-tight">404</h1>
<p class="text-lg mt-4 text-slate-600">Page not found.</p>
</div>
</div>
</Container>
<Layout title="Page not found" description="The page you're looking for doesn't exist.">
<section class="flex flex-col items-center justify-center gap-4 p-4 sm:p-6 lg:p-8 max-w-3xl mx-auto w-full min-h-[50vh] text-center">
<h1 class="text-3xl md:text-4xl font-bold text-slate-900 dark:text-white">404</h1>
<p class="text-slate-600 dark:text-slate-400">Page not found.</p>
<Link href="/" variant="primary">← Back home</Link>
</section>
</Layout>

View File

@@ -1,57 +0,0 @@
---
import { getCollection } from "astro:content";
import { Picture } from "astro:assets";
import Layout from "@/layouts/Layout.astro";
import Container from "@/components/container.astro";
import Sectionhead from "@/components/sectionhead.astro";
// Filter team entries with 'draft: false' & date before current date
const publishedTeamMembers = await getCollection("team", ({ data }) => {
return !data.draft && data.publishDate < new Date();
});
---
<Layout title="About">
<Container>
<Sectionhead>
<Fragment slot="title">About</Fragment>
<Fragment slot="desc">We are a small passionate team.</Fragment>
</Sectionhead>
<div class="flex flex-col gap-3 mx-auto max-w-4xl mt-16">
<h2 class="font-bold text-3xl text-gray-800">
Empowering the world with Astro.
</h2>
<p class="text-lg leading-relaxed text-slate-500">
We're a multi-cultural team from around the world! We come from diverse
backgrounds, bringing different personalities, experiences and skills to
the job. This is what makes our team so special.
</p>
</div>
<div class="grid md:grid-cols-3 gap-10 mx-auto max-w-4xl mt-12">
{
publishedTeamMembers.map((teamMemberEntry) => (
<div class="group">
<div class="w-full aspect-square">
<Picture
src={teamMemberEntry.data.avatar.src}
alt={teamMemberEntry.data.avatar.alt}
sizes="(max-width: 800px) 100vw, 400px"
width={400}
height={400}
class="w-full rounded-sm transition group-hover:-translate-y-1 group-hover:shadow-xl bg-white object-cover object-center aspect-square"
/>
</div>
<div class="mt-4 text-center">
<h2 class="text-lg text-gray-800">{teamMemberEntry.data.name}</h2>
<h3 class="text-sm text-slate-500">
{teamMemberEntry.data.title}
</h3>
</div>
</div>
))
}
</div>
</Container>
</Layout>

View File

@@ -0,0 +1,125 @@
---
import { getCollection, render } from "astro:content";
import Layout from "@/layouts/Layout.astro";
import Link from "@/components/link.astro";
import Button from "@/components/button.astro";
import { formatDoMMMMYYYY } from "@/utils/date";
import { articleJsonLd, breadcrumbJsonLd, deriveDescription } from "@/utils/seo";
export async function getStaticPaths() {
const articles = await getCollection("articles");
return articles.map((entry) => ({
params: { slug: entry.id },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content } = await render(entry);
const cover = entry.data.coverImage;
const showCover = Boolean(cover?.url);
const description = entry.data.description ?? deriveDescription(entry.body ?? "");
---
<Layout
title={entry.data.title}
description={description}
ogType="article"
ogImage={cover.url}
ogImageAlt={entry.data.title}
jsonLd={[articleJsonLd(entry), breadcrumbJsonLd(entry)]}
>
<article class="flex flex-col gap-3 max-w-3xl p-4 sm:p-6 lg:p-8 mx-auto w-full">
<Link
href="/articles"
variant="default"
class="text-sm text-slate-500 dark:text-slate-400 flex flex-row gap-1 items-center w-fit"
>
← Back to articles
</Link>
<div class="text-sm text-slate-500 dark:text-slate-400 mt-6">
{formatDoMMMMYYYY(entry.data.date)}
</div>
<h1 class="text-3xl md:text-4xl font-bold text-slate-900 dark:text-white">
{entry.data.title}
</h1>
</article>
{
showCover && (
<figure class="hidden md:block max-w-3xl p-4 sm:p-6 lg:p-8 mx-auto w-full">
<img
src={cover.url}
alt={entry.data.title}
height="1000"
width="1700"
class="rounded-lg w-full h-auto"
loading="lazy"
/>
{cover.authorUrl && (
<a
href={cover.authorUrl}
target="_blank"
rel="noopener noreferrer"
class="block text-xs text-slate-500 dark:text-slate-400 italic font-serif hover:underline p-4 sm:p-6 lg:p-8"
>
Photo by {cover.author}
</a>
)}
</figure>
)
}
<article class="flex flex-col gap-3 prose dark:prose-invert !pt-0 max-w-3xl p-4 sm:p-6 lg:p-8 mx-auto w-full">
<Content />
</article>
<div class="max-w-3xl p-4 sm:p-6 lg:p-8 mx-auto w-full">
<hr class="border border-slate-100 dark:border-slate-700" />
</div>
<section class="flex flex-col gap-5 p-4 sm:p-6 lg:p-8 max-w-3xl mx-auto w-full">
<h2 class="md:text-xl text-lg font-semibold">
<Link href="mailto:mariosant@sent.com" variant="primary">Marios Antonoudiou</Link>, AI product engineer.<br />
Building AI-powered products that feel simple, useful, and ready for real users.
</h2>
<p class="text-slate-800 dark:text-slate-200">
I design and ship <span class="font-semibold">AI-enabled product experiences</span> across
frontend architecture, interaction design, and product workflows. My focus is turning complex
systems, data, and model capabilities into software people can actually
<span class="font-semibold">understand, trust, and use</span>.
</p>
<div class="flex gap-4 overflow-x-auto">
<Button
size="xl"
variant="primary"
icon="mynaui:calendar"
href="https://cal.com/mariosant/30min"
target="_blank"
rel="noopener noreferrer"
>
Arrange a call
</Button>
<Button
size="xl"
variant="neutral"
icon="mynaui:envelope"
href="mailto:mariosant@sent.com"
target="_blank"
rel="noopener noreferrer"
>
Send a message
</Button>
</div>
</section>
</Layout>
<style is:global>
article.prose h2 {
font-weight: bold !important;
}
article.prose h2 a {
font-weight: bold !important;
}
</style>

View File

@@ -0,0 +1,58 @@
---
import { getCollection } from "astro:content";
import Layout from "@/layouts/Layout.astro";
import { formatDoMMMMYYYY } from "@/utils/date";
import { articleListJsonLd } from "@/utils/seo";
const articles = (await getCollection("articles")).sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime(),
);
---
<Layout
title="Articles"
description="Long-form writing on AI product engineering, frontend architecture, and shipping software people can trust."
jsonLd={articleListJsonLd(articles)}
>
<section class="prose dark:prose-invert p-4 sm:p-6 lg:p-8 max-w-3xl mx-auto w-full">
<h1 class="font-bold text-3xl md:text-4xl">Articles</h1>
</section>
<section class="grid grid-cols-12 gap-8 max-w-3xl p-4 sm:p-6 lg:p-8 mx-auto w-full">
{
articles.map((article) => (
<article class="col-span-12 md:col-span-6 border border-gray-200 dark:border-slate-600 rounded-lg overflow-hidden !p-0">
<a href={`/articles/${article.id}`} class="block cursor-pointer">
<img
class="w-full h-48 object-cover"
src={article.data.coverImage.url}
alt={article.data.title}
loading="lazy"
/>
</a>
<div class="grid gap-4 p-4 md:p-6">
<div class="w-full">
<a
href={`/articles/${article.id}`}
class="font-semibold line-clamp-2 min-h-12 hover:underline text-slate-900 dark:text-white"
>
{article.data.title}
</a>
<div class="text-sm text-slate-500 dark:text-slate-400">
{formatDoMMMMYYYY(article.data.date)}
</div>
</div>
<div class="w-full">
<a
href={`/articles/${article.id}`}
class="inline-flex items-center px-3 py-2 text-sm font-semibold rounded-md bg-slate-100 dark:bg-slate-700 text-slate-900 dark:text-white hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors"
>
Read article
</a>
</div>
</div>
</article>
))
}
</section>
</Layout>

View File

@@ -1,73 +0,0 @@
---
import { getCollection } from "astro:content";
import { Picture } from "astro:assets";
import Layout from "@/layouts/Layout.astro";
import Container from "@/components/container.astro";
import Sectionhead from "@/components/sectionhead.astro";
// Filter blog entries with 'draft: false' & date before current date
const publishedBlogEntries = await getCollection("blog", ({ data }) => {
return !data.draft && data.publishDate < new Date();
});
// Sort content entries by publication date
publishedBlogEntries.sort(function (a, b) {
return b.data.publishDate.valueOf() - a.data.publishDate.valueOf();
});
---
<Layout title="Blog">
<Container>
<Sectionhead>
<Fragment slot="title">Our Blog</Fragment>
<Fragment slot="desc">
We write about building startups and thoughts going on our mind.
</Fragment>
</Sectionhead>
<main class="mt-16">
<ul class="grid gap-16 max-w-4xl mx-auto">
{
publishedBlogEntries.map((blogPostEntry, index) => (
<li>
<a href={`/blog/${blogPostEntry.slug}`}>
<div class="grid md:grid-cols-2 gap-5 md:gap-10 items-center">
<Picture
src={blogPostEntry.data.image.src}
alt={blogPostEntry.data.image.alt}
sizes="(max-width: 800px) 100vw, 800px"
width={800}
height={600}
loading={index <= 2 ? "eager" : "lazy"}
decoding={index <= 2 ? "sync" : "async"}
class="w-full rounded-md object-cover object-center bg-white"
/>
<div>
<span class="text-blue-400 uppercase tracking-wider text-sm font-medium">
{blogPostEntry.data.category}
</span>
<h2 class="text-3xl font-semibold leading-snug tracking-tight mt-1 ">
{blogPostEntry.data.title}
</h2>
<div class="flex gap-2 mt-3">
<span class="text-gray-400">
{blogPostEntry.data.author}
</span>
<span class="text-gray-400">• </span>
<time
class="text-gray-400"
datetime={blogPostEntry.data.publishDate.toISOString()}>
{blogPostEntry.data.publishDate.toDateString()}
</time>
</div>
</div>
</div>
</a>
</li>
))
}
</ul>
</main>
</Container>
</Layout>

View File

@@ -1,62 +0,0 @@
---
import { getCollection } from "astro:content";
import Layout from "@/layouts/Layout.astro";
import Container from "@/components/container.astro";
// Generate a new path for every collection entry
export async function getStaticPaths() {
const blogEntries = await getCollection("blog");
return blogEntries.map((entry) => ({
params: { slug: entry.slug },
props: { entry },
}));
}
// Get the entry directly from the prop on render
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<Layout title={entry.data.title}>
<Container>
<div class="mx-auto max-w-3xl mt-14">
<span class="text-blue-400 uppercase tracking-wider text-sm font-medium">
{entry.data.category}
</span>
<h1
class="text-4xl lg:text-5xl font-bold lg:tracking-tight mt-1 lg:leading-tight">
{entry.data.title}
</h1>
<div class="flex gap-2 mt-3 items-center flex-wrap md:flex-nowrap">
<span class="text-gray-400">
{entry.data.author}
</span>
<span class="text-gray-400">•</span>
<time
class="text-gray-400"
datetime={entry.data.publishDate.toISOString()}>
{entry.data.publishDate.toDateString()}
</time>
<span class="text-gray-400 hidden md:block">•</span>
<div class="w-full md:w-auto flex flex-wrap gap-3">
{
entry.data.tags.map((tag) => (
<span class="text-sm text-gray-500">#{tag}</span>
))
}
</div>
</div>
</div>
<div class="mx-auto prose prose-lg mt-6 max-w-3xl">
<Content />
</div>
<div class="text-center mt-8">
<a
href="/blog"
class="bg-gray-100 px-5 py-3 rounded-md hover:bg-gray-200 transition"
>← Back to Blog</a
>
</div>
</Container>
</Layout>

View File

@@ -1,44 +0,0 @@
---
import Contactform from "@/components/contactform.astro";
import Container from "@/components/container.astro";
import Sectionhead from "@/components/sectionhead.astro";
import Layout from "@/layouts/Layout.astro";
import { Icon } from "astro-icon/components";
---
<Layout title="Contact">
<Container>
<Sectionhead>
<Fragment slot="title">Contact</Fragment>
<Fragment slot="desc">We are a here to help.</Fragment>
</Sectionhead>
<div class="grid md:grid-cols-2 gap-10 mx-auto max-w-4xl mt-16">
<div>
<h2 class="font-medium text-2xl text-gray-800">Contact Astroship</h2>
<p class="text-lg leading-relaxed text-slate-500 mt-3">
Have something to say? We are here to help. Fill up the form or send
email or call phone.
</p>
<div class="mt-5">
<div class="flex items-center mt-2 space-x-2 text-gray-600">
<Icon class="text-gray-400 w-4 h-4" name="uil:map-marker" />
<span>1734 Sanfransico, CA 93063</span>
</div><div class="flex items-center mt-2 space-x-2 text-gray-600">
<Icon class="text-gray-400 w-4 h-4" name="uil:envelope" /><a
href="mailto:hello@astroshipstarter.com"
>hello@astroshipstarter.com</a
>
</div><div class="flex items-center mt-2 space-x-2 text-gray-600">
<Icon class="text-gray-400 w-4 h-4" name="uil:phone" /><a
href="tel:+1 (987) 4587 899">+1 (987) 4587 899</a
>
</div>
</div>
</div>
<div>
<Contactform />
</div>
</div>
</Container>
</Layout>

View File

@@ -1,17 +1,22 @@
---
import Container from "@/components/container.astro";
import Cta from "@/components/cta.astro";
import Features from "@/components/features.astro";
import Hero from "@/components/hero.astro";
import Logos from "@/components/logos.astro";
import Layout from "@/layouts/Layout.astro";
import HomeIntroduction from "@/components/home/introduction.astro";
import HomeLatestArticle from "@/components/home/latest-article.astro";
import HomeBeenWorkingWith from "@/components/home/been-working-with.astro";
import HomePersonalProjects from "@/components/home/personal-projects.astro";
import HomeConnect from "@/components/home/connect.astro";
import { personJsonLd } from "@/utils/seo";
---
<Layout title="">
<Container>
<Hero />
<Features />
<Logos />
<Cta />
</Container>
<Layout
title="Marios Antonoudiou — AI Product Engineer"
description="AI product engineer building AI-powered products that feel simple, useful, and ready for real users."
withNav={false}
jsonLd={personJsonLd()}
>
<HomeIntroduction />
<HomeLatestArticle />
<HomeBeenWorkingWith />
<HomePersonalProjects />
<HomeConnect />
</Layout>

View File

@@ -1,81 +0,0 @@
---
import Layout from "@/layouts/Layout.astro";
import Container from "@/components/container.astro";
import Sectionhead from "@/components/sectionhead.astro";
import PricingCard from "@/components/pricing.astro";
const pricing = [
{
name: "Personal",
price: "Free",
popular: false,
features: [
"Lifetime free",
"Up to 3 users",
"Unlimited Pages",
"Astro Sub domain",
"Basic Integrations",
"Community Support",
],
button: {
text: "Get Started",
link: "/",
},
},
{
name: "Startup",
price: {
monthly: "$19",
annual: "$16",
discount: "10%",
original: "$24",
},
popular: true,
features: [
"All Free Features",
"Up to 20 users",
"20 Custom domains",
"Unlimited Collaborators",
"Advanced Integrations",
"Priority Support",
],
button: {
text: "Get Started",
link: "#",
},
},
{
name: "Enterprise",
price: "Custom",
popular: false,
features: [
"All Pro Features",
"Unlimited Custom domains",
"99.99% Uptime SLA",
"SAML & SSO Integration",
"Dedicated Account Manager",
"24/7 Phone Support",
],
button: {
text: "Contact us",
link: "/contact",
},
},
];
---
<Layout title="Pricing">
<Container>
<Sectionhead>
<Fragment slot="title">Pricing</Fragment>
<Fragment slot="desc">
Simple & Predictable pricing. No Surprises.
</Fragment>
</Sectionhead>
<div
class="grid md:grid-cols-3 gap-10 mx-auto max-w-(--breakpoint-lg) mt-12">
{pricing.map((item) => <PricingCard plan={item} />)}
</div>
</Container>
</Layout>

34
src/pages/rss.xml.ts Normal file
View File

@@ -0,0 +1,34 @@
import rss from "@astrojs/rss";
import { getCollection } from "astro:content";
import { SITE_URL } from "@/utils/seo";
import { deriveDescription } from "@/utils/seo";
import type { APIContext } from "astro";
export async function GET(context: APIContext) {
const articles = (await getCollection("articles")).sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime(),
);
return rss({
title: "Marios Antonoudiou",
description:
"Long-form writing on AI product engineering, frontend architecture, and shipping software people can trust.",
site: context.site ?? SITE_URL,
items: articles.map((entry) => ({
title: entry.data.title,
pubDate: entry.data.date,
description: entry.data.description ?? deriveDescription(entry.body ?? ""),
link: `/articles/${entry.id}/`,
content: undefined,
author: "mariosant@sent.com (Marios Antonoudiou)",
categories: [],
customData: `<enclosure url="${new URL(entry.data.coverImage.url, context.site ?? SITE_URL).toString()}" type="image/jpeg" />`,
})),
customData: `<language>en-us</language>`,
stylesheet: false,
xmlns: {
atom: "http://www.w3.org/2005/Atom",
content: "http://purl.org/rss/1.0/modules/content/",
},
});
}

View File

@@ -1,22 +1,15 @@
@import 'tailwindcss';
@custom-variant dark (&:where(.dark, .dark *));
@plugin '@tailwindcss/typography';
@theme {
--font-sans:
Bricolage Grotesque Variable, Inter Variable, Inter, ui-sans-serif,
system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
Inter Variable, Inter, ui-sans-serif, system-ui, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,

View File

@@ -1,9 +0,0 @@
/** */
export const getFormattedDate = (date) =>
date
? new Date(date).toLocaleDateString("en-us", {
year: "numeric",
month: "short",
day: "numeric",
})
: "";

26
src/utils/date.ts Normal file
View File

@@ -0,0 +1,26 @@
const ORDINAL_SUFFIX = ["th", "st", "nd", "rd"] as const;
const pad2 = (n: number): string => (n < 10 ? `0${n}` : `${n}`);
const ordinal = (day: number): string => {
const v = day % 100;
const suffix =
v >= 11 && v <= 13
? "th"
: (ORDINAL_SUFFIX[day % 10] ?? "th");
return `${day}${suffix}`;
};
export const formatDoMMMMYYYY = (input: string | Date): string => {
const d = typeof input === "string" ? new Date(input) : input;
if (Number.isNaN(d.getTime())) return "";
const day = d.getDate();
const month = d.toLocaleDateString("en-GB", { month: "long" });
const year = d.getFullYear();
return `${ordinal(day)} of ${month} ${year}`;
};
export const isoDate = (input: string | Date): string => {
const d = typeof input === "string" ? new Date(input) : input;
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
};

128
src/utils/seo.ts Normal file
View File

@@ -0,0 +1,128 @@
import type { CollectionEntry } from "astro:content";
export const SITE_URL = "https://mariosant.dev";
export const TWITTER_HANDLE = "@marios_ant";
export const AUTHOR_NAME = "Marios Antonoudiou";
export const AUTHOR_JOB_TITLE = "AI Product Engineer";
export const SOCIALS = {
github: "https://github.com/mariosant",
linkedin: "https://www.linkedin.com/in/mariosant/",
bluesky: "https://bsky.app/profile/mariosant.bsky.social",
email: "mailto:mariosant@sent.com",
calendar: "https://cal.com/mariosant/30min",
} as const;
export const PERSON_ID = `${SITE_URL}/#person`;
export const AUTHOR_DESCRIPTION =
"AI product engineer building AI-powered products that feel simple, useful, and ready for real users.";
export const stripMarkdown = (body: string): string =>
body
.replace(/```[\s\S]*?```/g, " ")
.replace(/`[^`]*`/g, " ")
.replace(/^#{1,6}\s+.*$/gm, "")
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
.replace(/!\[[^\]]*\]\([^)]+\)/g, "")
.replace(/^>\s*/gm, "")
.replace(/^\s*[-*+]\s+/gm, "")
.replace(/^\s*\d+\.\s+/gm, "")
.replace(/[*_~]+/g, "")
.replace(/\s+/g, " ")
.trim();
export const deriveDescription = (body: string, max = 155): string => {
const plain = stripMarkdown(body);
if (plain.length <= max) return plain;
const cut = plain.slice(0, max);
const lastSpace = cut.lastIndexOf(" ");
const safeCut = lastSpace > 80 ? cut.slice(0, lastSpace) : cut;
return `${safeCut.trimEnd()}`;
};
type ArticleEntry = CollectionEntry<"articles">;
export const personJsonLd = (): string =>
JSON.stringify({
"@context": "https://schema.org",
"@type": "Person",
"@id": PERSON_ID,
name: AUTHOR_NAME,
jobTitle: AUTHOR_JOB_TITLE,
url: SITE_URL,
image: `${SITE_URL}/mariosant.webp`,
description: AUTHOR_DESCRIPTION,
sameAs: [SOCIALS.github, SOCIALS.linkedin, SOCIALS.bluesky],
});
export const articleJsonLd = (entry: ArticleEntry): string => {
const description = entry.data.description ?? deriveDescription(entry.body ?? "");
return JSON.stringify({
"@context": "https://schema.org",
"@type": "Article",
headline: entry.data.title,
description,
image: entry.data.coverImage.url,
datePublished: entry.data.date.toISOString(),
author: {
"@id": PERSON_ID,
"@type": "Person",
name: AUTHOR_NAME,
},
publisher: {
"@id": PERSON_ID,
"@type": "Person",
name: AUTHOR_NAME,
},
mainEntityOfPage: {
"@type": "WebPage",
"@id": `${SITE_URL}/articles/${entry.id}`,
},
});
};
export const breadcrumbJsonLd = (entry: ArticleEntry): string =>
JSON.stringify({
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Home",
item: SITE_URL,
},
{
"@type": "ListItem",
position: 2,
name: "Articles",
item: `${SITE_URL}/articles`,
},
{
"@type": "ListItem",
position: 3,
name: entry.data.title,
item: `${SITE_URL}/articles/${entry.id}`,
},
],
});
export const articleListJsonLd = (entries: readonly ArticleEntry[]): string =>
JSON.stringify({
"@context": "https://schema.org",
"@type": "ItemList",
name: "Articles by Marios Antonoudiou",
itemListElement: entries.map((entry, index) => ({
"@type": "ListItem",
position: index + 1,
url: `${SITE_URL}/articles/${entry.id}`,
name: entry.data.title,
})),
});
export const renderJsonLd = (data: unknown): string => {
if (Array.isArray(data)) {
return data.map((item) => JSON.stringify(item)).join("\n");
}
return JSON.stringify(data);
};