dev blog shell

This commit is contained in:
Jason Ross
2026-04-29 13:26:30 -05:00
parent d3f8a75c87
commit 70c66552d7
19 changed files with 1541 additions and 482 deletions
+38 -55
View File
@@ -1,62 +1,45 @@
---
import { SITE_TITLE } from '../consts';
const today = new Date();
---
<footer>
&copy; {today.getFullYear()} Your name here. All rights reserved.
<div class="social-links">
<a href="https://m.webtoo.ls/@astro" target="_blank">
<span class="sr-only">Follow Astro on Mastodon</span>
<svg
viewBox="0 0 16 16"
aria-hidden="true"
width="32"
height="32"
astro-icon="social/mastodon"
><path
fill="currentColor"
d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"
></path></svg
>
</a>
<a href="https://twitter.com/astrodotbuild" target="_blank">
<span class="sr-only">Follow Astro on Twitter</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" astro-icon="social/twitter"
><path
fill="currentColor"
d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z"
></path></svg
>
</a>
<a href="https://github.com/withastro/astro" target="_blank">
<span class="sr-only">Go to Astro's GitHub repo</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" astro-icon="social/github"
><path
fill="currentColor"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
></path></svg
>
</a>
</div>
<footer class="site-footer">
<div class="container footer-inner">
<div>&copy; {today.getFullYear()} {SITE_TITLE} All rights reserved.</div>
</div>
</footer>
<style>
footer {
padding: 2em 1em 6em 1em;
background: linear-gradient(var(--gray-gradient)) no-repeat;
color: rgb(var(--gray));
text-align: center;
}
.social-links {
display: flex;
justify-content: center;
gap: 1em;
margin-top: 1em;
}
.social-links a {
text-decoration: none;
color: rgb(var(--gray));
}
.social-links a:hover {
color: rgb(var(--gray-dark));
}
.site-footer {
width: 100%;
margin-top: auto;
background: var(--panel);
border-top: 1px solid var(--border);
}
.footer-inner {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 2rem 1.5rem;
font-size: 0.875rem;
color: var(--dim);
}
@media (min-width: 768px) {
.footer-inner {
flex-direction: row;
padding: 2rem;
}
}
.footer-links {
display: flex;
gap: 1.5rem;
}
.footer-links a {
color: var(--dim);
transition: color 0.15s ease;
}
.footer-links a:hover {
color: var(--accent-bright);
}
</style>
+1 -1
View File
@@ -10,7 +10,7 @@ const { date } = Astro.props;
{
date.toLocaleDateString('en-us', {
year: 'numeric',
month: 'short',
month: 'long',
day: 'numeric',
})
}
+305 -58
View File
@@ -1,85 +1,332 @@
---
import { SITE_TITLE } from '../consts';
import { SITE_TITLE, NAV_LINKS, SOCIAL_LINKS } from '../consts';
import HeaderLink from './HeaderLink.astro';
const pathname = Astro.url.pathname.replace(/\/+$/, '') || '/';
---
<header>
<nav>
<h2><a href="/">{SITE_TITLE}</a></h2>
<div class="internal-links">
<HeaderLink href="/">Home</HeaderLink>
<HeaderLink href="/blog">Blog</HeaderLink>
<HeaderLink href="/about">About</HeaderLink>
</div>
<div class="social-links">
<a href="https://m.webtoo.ls/@astro" target="_blank">
<span class="sr-only">Follow Astro on Mastodon</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
><path
fill="currentColor"
d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"
></path></svg
<header class="site-header">
<!-- Top social bar -->
<div class="social-bar">
<div class="container social-bar-inner">
<a
href={SOCIAL_LINKS.github}
target="_blank"
rel="noopener noreferrer"
class="social-link"
aria-label="GitHub"
>
<svg
class="icon"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/>
<path d="M9 18c-4.51 2-5-2-7-2"/>
</svg>
<span class="social-text">GitHub</span>
</a>
<a href="https://twitter.com/astrodotbuild" target="_blank">
<span class="sr-only">Follow Astro on Twitter</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
><path
fill="currentColor"
d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z"
></path></svg
<a
href={SOCIAL_LINKS.linkedin}
target="_blank"
rel="noopener noreferrer"
class="social-link linkedin"
aria-label="LinkedIn"
>
<svg
class="icon"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"/>
<rect width="4" height="12" x="2" y="9"/>
<circle cx="4" cy="4" r="2"/>
</svg>
<span class="social-text">LinkedIn</span>
</a>
<a href="https://github.com/withastro/astro" target="_blank">
<span class="sr-only">Go to Astro's GitHub repo</span>
<svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
><path
fill="currentColor"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
></path></svg
<a
href={SOCIAL_LINKS.mastodon}
target="_blank"
rel="noopener noreferrer"
class="social-link"
aria-label="Mastodon"
>
<svg
class="icon"
width="14"
height="14"
viewBox="0 0 24 24"
fill="currentColor"
stroke="none"
aria-hidden="true"
>
<path d="M21.327 8.566c0-4.339-2.843-5.61-2.843-5.61-1.433-.658-3.894-.935-6.451-.956h-.063c-2.557.021-5.016.298-6.45.956 0 0-2.843 1.272-2.843 5.61 0 .993-.019 2.181.012 3.441.103 4.243.778 8.425 4.701 9.463 1.809.479 3.362.579 4.612.51 2.268-.126 3.541-.809 3.541-.809l-.075-1.646s-1.621.511-3.441.449c-1.804-.062-3.707-.194-3.999-2.409a4.523 4.523 0 0 1-.04-.621s1.77.433 4.014.536c1.372.063 2.658-.08 3.965-.236 2.506-.299 4.688-1.843 4.962-3.254.434-2.223.398-5.424.398-5.424zm-3.353 5.59h-2.081V9.057c0-1.075-.452-1.62-1.357-1.62-1 0-1.501.647-1.501 1.927v2.791h-2.069V9.364c0-1.28-.501-1.927-1.502-1.927-.905 0-1.357.546-1.357 1.62v5.099H6.026V8.903c0-1.074.273-1.927.823-2.558.566-.631 1.307-.955 2.228-.955 1.065 0 1.872.409 2.405 1.228l.518.869.519-.869c.533-.819 1.34-1.228 2.405-1.228.92 0 1.662.324 2.228.955.549.631.822 1.484.822 2.558v5.253z"/>
</svg>
<span class="social-text">Mastodon</span>
</a>
</div>
</nav>
</div>
<!-- Main nav bar -->
<div class="nav-bar">
<div class="container nav-inner">
<a href="/" class="brand" aria-label={SITE_TITLE}>
<span class="brand-mark" aria-hidden="true">_</span>
<span class="brand-text">{SITE_TITLE}</span>
</a>
<nav class="desktop-nav" aria-label="Primary">
{NAV_LINKS.map((link) => <HeaderLink href={link.path}>{link.name}</HeaderLink>)}
</nav>
<button
class="menu-toggle"
aria-label="Toggle menu"
aria-controls="mobile-nav"
aria-expanded="false"
type="button"
>
<svg
class="icon menu-open"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<line x1="4" x2="20" y1="6" y2="6"/>
<line x1="4" x2="20" y1="12" y2="12"/>
<line x1="4" x2="20" y1="18" y2="18"/>
</svg>
<svg
class="icon menu-close"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M18 6 6 18"/>
<path d="m6 6 12 12"/>
</svg>
</button>
</div>
<nav id="mobile-nav" class="mobile-nav" aria-label="Mobile" hidden>
{
NAV_LINKS.map((link) => {
const active =
link.path === '/'
? pathname === '/'
: pathname === link.path || pathname.startsWith(link.path + '/');
return (
<a href={link.path} class:list={['mobile-nav-link', { active }]}>
{link.name}
</a>
);
})
}
</nav>
</div>
</header>
<style>
header {
margin: 0;
padding: 0 1em;
background: white;
box-shadow: 0 2px 8px rgba(var(--black), 5%);
<script>
const toggle = document.querySelector<HTMLButtonElement>('.menu-toggle');
const mobileNav = document.getElementById('mobile-nav');
const header = document.querySelector('.site-header');
if (toggle && mobileNav && header) {
toggle.addEventListener('click', () => {
const open = header.classList.toggle('menu-open');
toggle.setAttribute('aria-expanded', String(open));
if (open) {
mobileNav.removeAttribute('hidden');
} else {
mobileNav.setAttribute('hidden', '');
}
});
}
h2 {
margin: 0;
font-size: 1em;
</script>
<style>
.site-header {
display: flex;
flex-direction: column;
background: var(--bg);
}
h2 a,
h2 a.active {
text-decoration: none;
.social-bar {
background: var(--panel);
border-bottom: 1px solid var(--border);
padding: 0.5rem 0;
font-size: 0.75rem;
}
nav {
@media (min-width: 640px) {
.social-bar {
font-size: 0.875rem;
}
}
.social-bar-inner {
display: flex;
justify-content: flex-end;
align-items: center;
justify-content: space-between;
gap: 1.25rem;
color: var(--muted);
}
nav a {
padding: 1em 0.5em;
color: var(--black);
border-bottom: 4px solid transparent;
text-decoration: none;
.social-link {
display: inline-flex;
align-items: center;
gap: 0.375rem;
color: var(--muted);
transition: color 0.15s ease;
}
nav a.active {
text-decoration: none;
border-bottom-color: var(--accent);
.social-link:hover {
color: var(--accent-bright);
}
.social-links,
.social-links a {
.social-link.linkedin:hover {
color: #0a66c2;
}
.social-text {
display: none;
}
@media (min-width: 640px) {
.social-text {
display: inline;
}
}
.nav-bar {
position: sticky;
top: 0;
z-index: 50;
background: rgba(10, 10, 10, 0.9);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border-bottom: 1px solid var(--border);
}
.nav-inner {
display: flex;
justify-content: space-between;
align-items: center;
height: 4rem;
}
@media (max-width: 720px) {
.social-links {
.brand {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 1.25rem;
font-weight: 700;
letter-spacing: -0.01em;
color: var(--text-strong);
}
.brand:hover {
color: var(--text-strong);
}
.brand-mark {
display: inline-flex;
justify-content: center;
align-items: center;
width: 2rem;
height: 2rem;
background: var(--accent);
color: #030712;
border-radius: 4px;
font-size: 1.125rem;
line-height: 1;
font-weight: 700;
}
.desktop-nav {
display: none;
gap: 2rem;
align-items: center;
}
@media (min-width: 768px) {
.desktop-nav {
display: flex;
}
}
.menu-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
background: transparent;
border: 0;
color: var(--muted);
cursor: pointer;
padding: 0.5rem;
}
.menu-toggle:hover {
color: var(--text-strong);
}
@media (min-width: 768px) {
.menu-toggle {
display: none;
}
}
.menu-toggle .menu-close {
display: none;
}
.site-header.menu-open .menu-toggle .menu-open {
display: none;
}
.site-header.menu-open .menu-toggle .menu-close {
display: inline-block;
}
.mobile-nav {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 0.5rem 1.5rem 1.5rem;
background: var(--panel);
border-top: 1px solid var(--border);
}
@media (min-width: 768px) {
.mobile-nav {
display: none !important;
}
}
.mobile-nav-link {
display: block;
padding: 0.625rem 0.5rem;
margin: 0 -0.5rem;
color: var(--muted);
font-weight: 500;
border-radius: 4px;
}
.mobile-nav-link:hover {
color: var(--text-strong);
background: #1f2937;
}
.mobile-nav-link.active {
color: var(--accent-bright);
background: #111827;
}
</style>
+18 -7
View File
@@ -4,21 +4,32 @@ import type { HTMLAttributes } from 'astro/types';
type Props = HTMLAttributes<'a'>;
const { href, class: className, ...props } = Astro.props;
const pathname = Astro.url.pathname.replace(import.meta.env.BASE_URL, '');
const subpath = pathname.match(/[^\/]+/g);
const isActive = href === pathname || href === '/' + (subpath?.[0] || '');
const pathname = Astro.url.pathname.replace(import.meta.env.BASE_URL, '').replace(/\/+$/, '') || '/';
const target = (typeof href === 'string' ? href : '').replace(/\/+$/, '') || '/';
const isActive = target === '/' ? pathname === '/' : pathname === target || pathname.startsWith(target + '/');
---
<a href={href} class:list={[className, { active: isActive }]} {...props}>
<a href={href} class:list={['header-link', className, { active: isActive }]} {...props}>
<slot />
</a>
<style>
a {
.header-link {
display: inline-block;
font-size: 0.875rem;
font-weight: 500;
color: var(--muted);
text-decoration: none;
transition: color 0.15s ease;
}
a.active {
font-weight: bolder;
.header-link:hover {
color: var(--accent-bright);
}
.header-link.active {
color: var(--accent-bright);
text-decoration: underline;
text-decoration-thickness: 2px;
text-underline-offset: 4px;
text-decoration-color: rgba(34, 211, 238, 0.5);
}
</style>
+44
View File
@@ -0,0 +1,44 @@
---
interface Props {
name:
| 'terminal'
| 'arrow-right'
| 'chevron-right'
| 'calendar'
| 'tag'
| 'github'
| 'mastodon';
size?: number;
class?: string;
}
const { name, size = 16, class: className } = Astro.props;
const paths: Record<string, string> = {
terminal: '<polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/>',
'arrow-right': '<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>',
'chevron-right': '<path d="m9 18 6-6-6-6"/>',
calendar:
'<path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/>',
tag:
'<path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"/><circle cx="7.5" cy="7.5" r=".5" fill="currentColor"/>',
github:
'<path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/>',
mastodon:
'<path d="M21.327 8.566c0-4.339-2.843-5.61-2.843-5.61-1.433-.658-3.894-.935-6.451-.956h-.063c-2.557.021-5.016.298-6.45.956 0 0-2.843 1.272-2.843 5.61 0 .993-.019 2.181.012 3.441.103 4.243.778 8.425 4.701 9.463 1.809.479 3.362.579 4.612.51 2.268-.126 3.541-.809 3.541-.809l-.075-1.646s-1.621.511-3.441.449c-1.804-.062-3.707-.194-3.999-2.409a4.523 4.523 0 0 1-.04-.621s1.77.433 4.014.536c1.372.063 2.658-.08 3.965-.236 2.506-.299 4.688-1.843 4.962-3.254.434-2.223.398-5.424.398-5.424zm-3.353 5.59h-2.081V9.057c0-1.075-.452-1.62-1.357-1.62-1 0-1.501.647-1.501 1.927v2.791h-2.069V9.364c0-1.28-.501-1.927-1.502-1.927-.905 0-1.357.546-1.357 1.62v5.099H6.026V8.903c0-1.074.273-1.927.823-2.558.566-.631 1.307-.955 2.228-.955 1.065 0 1.872.409 2.405 1.228l.518.869.519-.869c.533-.819 1.34-1.228 2.405-1.228.92 0 1.662.324 2.228.955.549.631.822 1.484.822 2.558v5.253z"/>',
};
---
<svg
class:list={['icon', className]}
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
set:html={paths[name]}
/>
+120
View File
@@ -0,0 +1,120 @@
---
import FormattedDate from './FormattedDate.astro';
import Icon from './Icon.astro';
interface Props {
href: string;
title: string;
date: Date;
readTime?: string;
excerpt?: string;
tags?: string[];
}
const { href, title, date, readTime, excerpt, tags = [] } = Astro.props;
---
<article class="post-card">
<div class="meta">
<span class="meta-item">
<Icon name="calendar" size={12} />
<FormattedDate date={date} />
</span>
{readTime && (
<>
<span class="meta-dot">&bull;</span>
<span class="meta-item dim">{readTime}</span>
</>
)}
</div>
<div class="card-body">
<h3 class="post-title">
<a href={href}>
<span class="hit-area"></span>
{title}
</a>
</h3>
{excerpt && <p class="excerpt">{excerpt}</p>}
</div>
{
tags.length > 0 && (
<div class="tag-row">
{tags.map((t) => (
<span class="tag">
<Icon name="tag" size={10} />
{t}
</span>
))}
</div>
)
}
</article>
<style>
.post-card {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
}
.meta {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.75rem;
color: var(--dim);
}
.meta-item {
display: inline-flex;
align-items: center;
gap: 0.375rem;
font-weight: 500;
}
.meta-item.dim {
color: var(--dim);
}
.meta-dot {
color: var(--dimmer);
}
.card-body {
max-width: 36rem;
}
.post-title {
font-size: 1.25rem;
font-weight: 700;
color: var(--text);
margin: 0.25rem 0 0;
transition: color 0.15s ease;
}
.post-card:hover .post-title {
color: var(--accent-bright);
}
.post-title a {
color: inherit;
}
.post-title a:hover {
color: inherit;
}
.hit-area {
position: absolute;
inset: 0;
}
.excerpt {
margin: 0.75rem 0 0;
color: var(--muted);
font-size: 0.9375rem;
line-height: 1.6;
}
.tag-row {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.25rem;
}
</style>
+32 -2
View File
@@ -1,5 +1,35 @@
// Place any global data in this file.
// You can import this data from anywhere in your site by using the `import` keyword.
export const SITE_TITLE = 'Astro Blog';
export const SITE_DESCRIPTION = 'Welcome to my website!';
export const SITE_TITLE = 'Jason Ross, SecDev.';
export const SITE_TAGLINE = 'Securing the web, one layer at a time.';
export const SITE_DESCRIPTION =
'Application security, high-performance backend systems, and resilient interfaces — by a security-focused full-stack web developer.';
export const SOCIAL_LINKS = {
github: 'https://github.com/JMR-dev',
linkedin: 'https://linkedin.com/in/jasonmichaelross',
mastodon: 'https://mastodon.social/@jason_ross_cyberdev',
};
export const NAV_LINKS = [
{ name: 'Home', path: '/' },
{ name: 'Articles', path: '/blog' },
{ name: 'Projects', path: '/projects' },
{ name: 'About', path: '/about' },
];
export const PROJECTS = [
{
name: 'AuthGuard',
description:
'A lightweight, memory-safe authentication middleware written in Rust for high-throughput microservices.',
url: 'https://github.com',
},
{
name: 'NetMonitor',
description:
'Real-time network traffic analysis and anomaly detection dashboard built with React, WebSockets, and Go.',
url: 'https://github.com',
},
];
+2
View File
@@ -14,6 +14,8 @@ const blog = defineCollection({
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.optional(image()),
tags: z.array(z.string()).default([]),
readTime: z.string().optional(),
}),
});
+3 -1
View File
@@ -1,8 +1,10 @@
---
title: 'First post'
description: 'Lorem ipsum dolor sit amet'
description: 'A practical first dispatch — what to expect from this blog and the threat models we''ll be unpacking over the coming months.'
pubDate: 'Jul 08 2022'
heroImage: '../../assets/blog-placeholder-3.jpg'
readTime: '4 min read'
tags: ['Security', 'Meta']
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
+2
View File
@@ -3,6 +3,8 @@ title: 'Markdown Style Guide'
description: 'Here is a sample of some basic Markdown syntax that can be used when writing Markdown content in Astro.'
pubDate: 'Jun 19 2024'
heroImage: '../../assets/blog-placeholder-1.jpg'
readTime: '5 min read'
tags: ['Markdown', 'Reference']
---
Here is a sample of some basic Markdown syntax that can be used when writing Markdown content in Astro.
+3 -1
View File
@@ -1,8 +1,10 @@
---
title: 'Second post'
description: 'Lorem ipsum dolor sit amet'
description: 'Notes from production: lessons learned wiring up authentication for a high-throughput service without breaking the SLO.'
pubDate: 'Jul 15 2022'
heroImage: '../../assets/blog-placeholder-4.jpg'
readTime: '6 min read'
tags: ['Auth', 'Backend']
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
+3 -1
View File
@@ -1,8 +1,10 @@
---
title: 'Third post'
description: 'Lorem ipsum dolor sit amet'
description: 'Hardening the front line — content security policy, trusted types, and the small wins that compound into real defense.'
pubDate: 'Jul 22 2022'
heroImage: '../../assets/blog-placeholder-2.jpg'
readTime: '7 min read'
tags: ['Frontend', 'CSP', 'Security']
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
+3 -1
View File
@@ -1,8 +1,10 @@
---
title: 'Using MDX'
description: 'Lorem ipsum dolor sit amet'
description: 'Mixing components into your prose: how MDX works in Astro and where it pulls its weight in a security-focused blog.'
pubDate: 'Jun 01 2024'
heroImage: '../../assets/blog-placeholder-5.jpg'
readTime: '6 min read'
tags: ['MDX', 'Astro']
---
This theme comes with the [@astrojs/mdx](https://docs.astro.build/en/guides/integrations-guide/mdx/) integration installed and configured in your `astro.config.mjs` config file. If you prefer not to use MDX, you can disable support by removing the integration from your config file.
+196 -71
View File
@@ -5,82 +5,207 @@ import BaseHead from '../components/BaseHead.astro';
import Footer from '../components/Footer.astro';
import FormattedDate from '../components/FormattedDate.astro';
import Header from '../components/Header.astro';
import Icon from '../components/Icon.astro';
type Props = CollectionEntry<'blog'>['data'];
const { title, description, pubDate, updatedDate, heroImage } = Astro.props;
const { title, description, pubDate, updatedDate, heroImage, tags = [], readTime } = Astro.props;
---
<html lang="en">
<head>
<BaseHead title={title} description={description} />
<style>
main {
width: calc(100% - 2em);
max-width: 100%;
margin: 0;
}
.hero-image {
width: 100%;
}
.hero-image img {
display: block;
margin: 0 auto;
border-radius: 12px;
box-shadow: var(--box-shadow);
}
.prose {
width: 720px;
max-width: calc(100% - 2em);
margin: auto;
padding: 1em;
color: rgb(var(--gray-dark));
}
.title {
margin-bottom: 1em;
padding: 1em 0;
text-align: center;
line-height: 1;
}
.title h1 {
margin: 0 0 0.5em 0;
}
.date {
margin-bottom: 0.5em;
color: rgb(var(--gray));
}
.last-updated-on {
font-style: italic;
}
</style>
</head>
<head>
<BaseHead title={title} description={description} />
</head>
<body>
<Header />
<main>
<article>
<div class="hero-image">
{heroImage && <Image width={1020} height={510} src={heroImage} alt="" />}
</div>
<div class="prose">
<div class="title">
<div class="date">
<FormattedDate date={pubDate} />
{
updatedDate && (
<div class="last-updated-on">
Last updated on <FormattedDate date={updatedDate} />
</div>
)
}
</div>
<h1>{title}</h1>
<hr />
</div>
<slot />
</div>
</article>
</main>
<Footer />
</body>
<body>
<Header />
<main class="post animate-in">
<article>
<header class="post-header">
<a href="/blog" class="back-link">&larr; All articles</a>
<div class="post-meta">
<span class="meta-item">
<Icon name="calendar" size={12} />
<FormattedDate date={pubDate} />
</span>
{
readTime && (
<>
<span class="meta-dot">&bull;</span>
<span class="meta-item dim">{readTime}</span>
</>
)
}
{
updatedDate && (
<>
<span class="meta-dot">&bull;</span>
<span class="meta-item dim">
Updated <FormattedDate date={updatedDate} />
</span>
</>
)
}
</div>
<h1 class="post-title">{title}</h1>
{description && <p class="post-lede">{description}</p>}
{
tags.length > 0 && (
<div class="tag-row">
{tags.map((t) => (
<span class="tag">
<Icon name="tag" size={10} />
{t}
</span>
))}
</div>
)
}
</header>
{
heroImage && (
<div class="hero-image">
<Image width={1020} height={510} src={heroImage} alt="" />
</div>
)
}
<div class="prose">
<slot />
</div>
</article>
</main>
<Footer />
</body>
</html>
<style>
.post {
display: flex;
flex-direction: column;
gap: 2rem;
max-width: 42rem;
}
.back-link {
display: inline-block;
font-size: 0.875rem;
color: var(--accent);
margin-bottom: 1.5rem;
}
.back-link:hover {
color: var(--accent-bright);
}
.post-header {
border-bottom: 1px solid var(--border);
padding-bottom: 2rem;
}
.post-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
font-size: 0.75rem;
color: var(--dim);
margin-bottom: 1rem;
}
.meta-item {
display: inline-flex;
align-items: center;
gap: 0.375rem;
font-weight: 500;
}
.meta-dot {
color: var(--dimmer);
}
.post-title {
font-size: 2.25rem;
font-weight: 800;
letter-spacing: -0.02em;
line-height: 1.15;
color: var(--text-strong);
margin: 0 0 0.75rem;
}
@media (min-width: 768px) {
.post-title {
font-size: 2.75rem;
}
}
.post-lede {
color: var(--muted);
font-size: 1.125rem;
line-height: 1.6;
margin: 0 0 1.25rem;
}
.tag-row {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
}
.hero-image {
margin: 0;
border: 1px solid var(--border);
border-radius: 4px;
overflow: hidden;
background: var(--panel);
}
.hero-image :global(img) {
display: block;
width: 100%;
height: auto;
border-radius: 0;
}
.prose {
font-size: 1.0625rem;
line-height: 1.75;
color: var(--text);
}
.prose :global(p) {
color: var(--muted);
margin: 0 0 1.25em;
}
.prose :global(h1),
.prose :global(h2),
.prose :global(h3),
.prose :global(h4) {
color: var(--text-strong);
margin-top: 2rem;
margin-bottom: 0.75rem;
}
.prose :global(h2) {
font-size: 1.5rem;
}
.prose :global(h3) {
font-size: 1.25rem;
}
.prose :global(a) {
color: var(--accent-bright);
text-decoration: underline;
text-decoration-color: rgba(34, 211, 238, 0.4);
text-underline-offset: 3px;
}
.prose :global(a:hover) {
color: var(--accent);
text-decoration-color: var(--accent);
}
.prose :global(ul),
.prose :global(ol) {
color: var(--muted);
padding-left: 1.5rem;
}
.prose :global(li) {
margin-bottom: 0.4em;
}
.prose :global(blockquote) {
color: var(--muted);
}
.prose :global(img) {
border: 1px solid var(--border);
}
</style>
+114 -57
View File
@@ -1,63 +1,120 @@
---
import AboutHeroImage from '../assets/blog-placeholder-about.jpg';
import Layout from '../layouts/BlogPost.astro';
import BaseHead from '../components/BaseHead.astro';
import Footer from '../components/Footer.astro';
import Header from '../components/Header.astro';
import Icon from '../components/Icon.astro';
import { SITE_TITLE, SOCIAL_LINKS } from '../consts';
---
<Layout
title="About Me"
description="Lorem ipsum dolor sit amet"
pubDate={new Date('August 08 2021')}
heroImage={AboutHeroImage}
>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo
viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam
adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus
et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus
vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque
sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
</p>
<!doctype html>
<html lang="en">
<head>
<BaseHead
title={`About · ${SITE_TITLE}`}
description="A security-focused full-stack web developer's manifesto."
/>
</head>
<body>
<Header />
<main class="about animate-in">
<header class="page-header">
<div class="badge-online">
<Icon name="terminal" size={14} />
<span>About</span>
</div>
<h1>The Manifesto</h1>
<p class="lede">
Software is only as trustworthy as the assumptions it ships with. I write
about the ones that fail under pressure.
</p>
</header>
<p>
Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non
tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non
blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna
porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis
massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc.
Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis
bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra
massa massa ultricies mi.
</p>
<section class="prose">
<p>
I'm a security-focused full-stack web developer. My day-to-day is split between
building production systems — auth, APIs, real-time pipelines — and breaking
them on purpose to find out where the seams are.
</p>
<p>
This site is where I write up what I've learned: zero-trust architecture,
hardening GraphQL surfaces, high-performance crypto with Rust and WebAssembly,
and the everyday discipline of <strong>defense in depth</strong>.
</p>
<h2>What you'll find here</h2>
<ul>
<li>
<strong>Disclosures</strong> &mdash; long-form posts on application security,
threat models, and post-incident write-ups.
</li>
<li>
<strong>Field notes</strong> &mdash; shorter, sharper takes on tools, libraries,
and the trade-offs they impose.
</li>
<li>
<strong>Open source</strong> &mdash; libraries and utilities I maintain in the
open. Issues and PRs welcome.
</li>
</ul>
<p>
Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl
suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet
nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae
turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem
dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat
semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus
vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum
facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam
vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla
urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
</p>
<h2>Get in touch</h2>
<p>
The best way to reach me is on
<a href={SOCIAL_LINKS.github} target="_blank" rel="noopener noreferrer">GitHub</a>
or
<a href={SOCIAL_LINKS.mastodon} target="_blank" rel="noopener noreferrer">Mastodon</a>.
For anything sensitive, ping me for a PGP key.
</p>
</section>
</main>
<Footer />
</body>
</html>
<p>
Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper
viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc
scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur
gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus
pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim
blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id
cursus metus aliquam eleifend mi.
</p>
<p>
Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta
nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam
tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci
ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar
proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
</p>
</Layout>
<style>
.about {
display: flex;
flex-direction: column;
gap: 2.5rem;
max-width: 42rem;
}
.page-header {
display: flex;
flex-direction: column;
gap: 1rem;
border-bottom: 1px solid var(--border);
padding-bottom: 1.5rem;
}
.page-header h1 {
font-size: 2.5rem;
font-weight: 800;
letter-spacing: -0.02em;
margin: 0;
}
.page-header .lede {
color: var(--muted);
font-size: 1.125rem;
margin: 0;
line-height: 1.6;
}
.prose {
color: var(--text);
line-height: 1.75;
}
.prose p {
color: var(--muted);
margin: 0 0 1.25em;
}
.prose h2 {
font-size: 1.375rem;
margin: 2rem 0 0.75rem;
color: var(--text-strong);
}
.prose ul {
margin: 0 0 1.25em;
padding-left: 1.25rem;
color: var(--muted);
}
.prose ul li {
margin-bottom: 0.5em;
}
</style>
+63 -101
View File
@@ -1,114 +1,76 @@
---
import { Image } from 'astro:assets';
import { getCollection } from 'astro:content';
import BaseHead from '../../components/BaseHead.astro';
import Footer from '../../components/Footer.astro';
import FormattedDate from '../../components/FormattedDate.astro';
import Header from '../../components/Header.astro';
import { SITE_DESCRIPTION, SITE_TITLE } from '../../consts';
import PostCard from '../../components/PostCard.astro';
import { SITE_TITLE } from '../../consts';
const posts = (await getCollection('blog')).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
---
<!doctype html>
<html lang="en">
<head>
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
<style>
main {
width: 960px;
}
ul {
display: flex;
flex-wrap: wrap;
gap: 2rem;
list-style-type: none;
margin: 0;
padding: 0;
}
ul li {
width: calc(50% - 1rem);
}
ul li * {
text-decoration: none;
transition: 0.2s ease;
}
ul li:first-child {
width: 100%;
margin-bottom: 1rem;
text-align: center;
}
ul li:first-child img {
width: 100%;
}
ul li:first-child .title {
font-size: 2.369rem;
}
ul li img {
margin-bottom: 0.5rem;
border-radius: 12px;
}
ul li a {
display: block;
}
.title {
margin: 0;
color: rgb(var(--black));
line-height: 1;
}
.date {
margin: 0;
color: rgb(var(--gray));
}
ul li a:hover h4,
ul li a:hover .date {
color: rgb(var(--accent));
}
ul a:hover img {
box-shadow: var(--box-shadow);
}
@media (max-width: 720px) {
ul {
gap: 0.5em;
}
ul li {
width: 100%;
text-align: center;
}
ul li:first-child {
margin-bottom: 0;
}
ul li:first-child .title {
font-size: 1.563em;
}
}
</style>
</head>
<body>
<Header />
<main>
<section>
<ul>
{
posts.map((post) => (
<li>
<a href={`/blog/${post.id}/`}>
{post.data.heroImage && (
<Image width={720} height={360} src={post.data.heroImage} alt="" />
)}
<h4 class="title">{post.data.title}</h4>
<p class="date">
<FormattedDate date={post.data.pubDate} />
</p>
</a>
</li>
))
}
</ul>
</section>
</main>
<Footer />
</body>
<head>
<BaseHead
title={`Articles · ${SITE_TITLE}`}
description="Disclosures, deep-dives, and field notes on application security and web infrastructure."
/>
</head>
<body>
<Header />
<main class="articles animate-in">
<header class="page-header">
<h1>Disclosures</h1>
<p class="lede">
Field notes on application security, performance, and resilient systems.
</p>
</header>
<div class="post-list">
{
posts.map((post) => (
<PostCard
href={`/blog/${post.id}/`}
title={post.data.title}
date={post.data.pubDate}
readTime={post.data.readTime}
excerpt={post.data.description}
tags={post.data.tags}
/>
))
}
</div>
</main>
<Footer />
</body>
</html>
<style>
.articles {
display: flex;
flex-direction: column;
gap: 3rem;
}
.page-header {
border-bottom: 1px solid var(--border);
padding-bottom: 1.5rem;
}
.page-header h1 {
font-size: 2.25rem;
font-weight: 800;
letter-spacing: -0.02em;
margin: 0 0 0.5rem;
}
.page-header .lede {
color: var(--muted);
font-size: 1.0625rem;
margin: 0;
}
.post-list {
display: grid;
grid-template-columns: 1fr;
gap: 3rem;
}
</style>
+198 -40
View File
@@ -1,49 +1,207 @@
---
import { getCollection } from 'astro:content';
import BaseHead from '../components/BaseHead.astro';
import Footer from '../components/Footer.astro';
import Header from '../components/Header.astro';
import { SITE_DESCRIPTION, SITE_TITLE } from '../consts';
import Icon from '../components/Icon.astro';
import PostCard from '../components/PostCard.astro';
import { SITE_TITLE, SITE_DESCRIPTION, PROJECTS } from '../consts';
const latestPosts = (await getCollection('blog'))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())
.slice(0, 3);
---
<!doctype html>
<html lang="en">
<head>
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
</head>
<body>
<Header />
<main>
<h1>🧑‍🚀 Hello, Astronaut!</h1>
<p>
Welcome to the official <a href="https://astro.build/">Astro</a> blog starter template. This template
serves as a lightweight, minimally-styled starting point for anyone looking to build a personal
website, blog, or portfolio with Astro.
</p>
<p>
This template comes with a few integrations already configured in your
<code>astro.config.mjs</code> file. You can customize your setup with
<a href="https://astro.build/integrations">Astro Integrations</a> to add tools like Tailwind,
React, or Vue to your project.
</p>
<p>Here are a few ideas on how to get started with the template:</p>
<ul>
<li>Edit this page in <code>src/pages/index.astro</code></li>
<li>Edit the site header items in <code>src/components/Header.astro</code></li>
<li>Add your name to the footer in <code>src/components/Footer.astro</code></li>
<li>Check out the included blog posts in <code>src/content/blog/</code></li>
<li>Customize the blog post page layout in <code>src/layouts/BlogPost.astro</code></li>
</ul>
<p>
Have fun! If you get stuck, remember to
<a href="https://docs.astro.build/">read the docs</a>
or <a href="https://astro.build/chat">join us on Discord</a> to ask questions.
</p>
<p>
Looking for a blog template with a bit more personality? Check out
<a href="https://github.com/Charca/astro-blog-template">astro-blog-template</a>
by <a href="https://twitter.com/Charca">Maxi Ferreira</a>.
</p>
</main>
<Footer />
</body>
<head>
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
</head>
<body>
<Header />
<main class="home animate-in">
<!-- Hero -->
<section class="hero">
<div class="badge-online">
<Icon name="terminal" size={14} />
<span>System Online</span>
</div>
<h1 class="hero-title">
Securing the web,<br />
<span class="accent">one layer at a time.</span>
</h1>
<p class="hero-lede">
Hi, I'm a security-focused full-stack web developer. I write about application
security, high-performance backend systems, and building resilient interfaces.
</p>
<div class="hero-actions">
<a href="/about" class="btn-primary">
Read my manifesto
<Icon name="arrow-right" size={16} />
</a>
</div>
</section>
<!-- Latest Articles -->
<section class="latest">
<div class="section-heading">
<h2>Latest Articles</h2>
<a href="/blog" class="view-all">
View all <Icon name="chevron-right" size={14} />
</a>
</div>
<div class="post-list">
{
latestPosts.map((post) => (
<PostCard
href={`/blog/${post.id}/`}
title={post.data.title}
date={post.data.pubDate}
readTime={post.data.readTime}
excerpt={post.data.description}
tags={post.data.tags}
/>
))
}
</div>
</section>
<!-- Open Source -->
<section class="projects">
<div class="section-heading">
<h2>Open Source</h2>
</div>
<div class="project-grid">
{
PROJECTS.map((p) => (
<a href={p.url} target="_blank" rel="noopener noreferrer" class="project-card">
<div class="project-card-head">
<h3>{p.name}</h3>
<Icon name="arrow-right" size={16} class="project-arrow" />
</div>
<p>{p.description}</p>
</a>
))
}
</div>
</section>
</main>
<Footer />
</body>
</html>
<style>
.home {
display: flex;
flex-direction: column;
gap: 4rem;
}
@media (min-width: 768px) {
.home {
gap: 6rem;
}
}
.hero {
align-items: flex-start;
display: flex;
flex-direction: column;
gap: 1.5rem;
max-width: 42rem;
padding-top: 1rem;
}
@media (min-width: 768px) {
.hero {
align-items: flex-start;
padding-top: 2.5rem;
}
}
.hero-title {
font-size: 2.25rem;
line-height: 1.1;
margin: 0;
font-weight: 800;
letter-spacing: -0.02em;
color: var(--text-strong);
}
@media (min-width: 640px) {
.hero-title {
font-size: 3rem;
}
}
.hero-title .accent {
color: var(--accent-bright);
}
.hero-lede {
font-size: 1.25rem;
color: var(--muted);
line-height: 1.6;
font-weight: 300;
margin: 0;
}
.hero-actions {
padding-top: 0.5rem;
}
.post-list {
display: grid;
grid-template-columns: 1fr;
gap: 3rem;
}
.project-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
@media (min-width: 640px) {
.project-grid {
grid-template-columns: 1fr 1fr;
}
}
.project-card {
display: flex;
flex-direction: column;
padding: 1.25rem;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 4px;
color: inherit;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.project-card:hover {
border-color: rgba(6, 182, 212, 0.5);
box-shadow: var(--accent-glow-soft);
color: inherit;
}
.project-card-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.project-card h3 {
margin: 0;
font-size: 1.125rem;
color: var(--text);
font-weight: 600;
transition: color 0.15s ease;
}
.project-card:hover h3 {
color: var(--accent-bright);
}
.project-arrow {
color: var(--dimmer);
transition: color 0.15s ease, transform 0.2s ease;
}
.project-card:hover .project-arrow {
color: var(--accent-bright);
transform: rotate(-45deg);
}
.project-card p {
margin: 0;
font-size: 0.875rem;
color: var(--muted);
line-height: 1.6;
}
</style>
+120
View File
@@ -0,0 +1,120 @@
---
import BaseHead from '../components/BaseHead.astro';
import Footer from '../components/Footer.astro';
import Header from '../components/Header.astro';
import Icon from '../components/Icon.astro';
import { PROJECTS, SITE_TITLE } from '../consts';
---
<!doctype html>
<html lang="en">
<head>
<BaseHead
title={`Projects · ${SITE_TITLE}`}
description="Open source projects, tools, and experiments."
/>
</head>
<body>
<Header />
<main class="projects-page animate-in">
<header class="page-header">
<h1>Open Source</h1>
<p class="lede">
A small collection of tools and experiments — most live on
<a href="https://github.com" target="_blank" rel="noopener noreferrer">GitHub</a>.
</p>
</header>
<div class="project-grid">
{
PROJECTS.map((p) => (
<a href={p.url} target="_blank" rel="noopener noreferrer" class="project-card">
<div class="project-card-head">
<h3>{p.name}</h3>
<Icon name="arrow-right" size={16} class="project-arrow" />
</div>
<p>{p.description}</p>
</a>
))
}
</div>
</main>
<Footer />
</body>
</html>
<style>
.projects-page {
display: flex;
flex-direction: column;
gap: 2.5rem;
}
.page-header {
border-bottom: 1px solid var(--border);
padding-bottom: 1.5rem;
}
.page-header h1 {
font-size: 2.25rem;
font-weight: 800;
letter-spacing: -0.02em;
margin: 0 0 0.5rem;
}
.page-header .lede {
color: var(--muted);
font-size: 1.0625rem;
margin: 0;
}
.project-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
@media (min-width: 640px) {
.project-grid {
grid-template-columns: 1fr 1fr;
}
}
.project-card {
display: flex;
flex-direction: column;
padding: 1.25rem;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 4px;
color: inherit;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.project-card:hover {
border-color: rgba(6, 182, 212, 0.5);
box-shadow: var(--accent-glow-soft);
}
.project-card-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.project-card h3 {
margin: 0;
font-size: 1.125rem;
color: var(--text);
font-weight: 600;
transition: color 0.15s ease;
}
.project-card:hover h3 {
color: var(--accent-bright);
}
.project-arrow {
color: var(--dimmer);
transition: color 0.15s ease, transform 0.2s ease;
}
.project-card:hover .project-arrow {
color: var(--accent-bright);
transform: rotate(-45deg);
}
.project-card p {
margin: 0;
font-size: 0.875rem;
color: var(--muted);
line-height: 1.6;
}
</style>
+276 -86
View File
@@ -1,125 +1,295 @@
/*
The CSS in this style tag is based off of Bear Blog's default CSS.
https://github.com/HermanMartinus/bearblog/blob/297026a877bc2ab2b3bdfbd6b9f7961c350917dd/templates/styles/blog/default.css
License MIT: https://github.com/HermanMartinus/bearblog/blob/master/LICENSE.md
*/
SecDev. theme — dark, security-focused.
Palette inspired by the React/Tailwind template:
- bg: #0a0a0a
- panel: #111
- border: #1f2937 (gray-800)
- text: #f3f4f6 (gray-100)
- muted: #9ca3af (gray-400)
- dim: #6b7280 (gray-500)
- accent: #06b6d4 (cyan-500) / #22d3ee (cyan-400)
- secondary: #10b981 (emerald-500) / #34d399 (emerald-400)
*/
:root {
--accent: #2337ff;
--accent-dark: #000d8a;
--black: 15, 18, 25;
--gray: 96, 115, 159;
--gray-light: 229, 233, 240;
--gray-dark: 34, 41, 57;
--gray-gradient: rgba(var(--gray-light), 50%), #fff;
--box-shadow:
0 2px 6px rgba(var(--gray), 25%), 0 8px 24px rgba(var(--gray), 33%),
0 16px 32px rgba(var(--gray), 33%);
--bg: #0a0a0a;
--panel: #111111;
--panel-2: #0d0d0d;
--border: #1f2937;
--border-soft: #111827;
--text: #f3f4f6;
--text-strong: #ffffff;
--muted: #9ca3af;
--dim: #6b7280;
--dimmer: #4b5563;
--accent: #06b6d4;
--accent-bright: #22d3ee;
--emerald: #10b981;
--emerald-bright: #34d399;
--accent-glow: 0 0 15px rgba(6, 182, 212, 0.3);
--accent-glow-soft: 0 0 15px rgba(6, 182, 212, 0.1);
--font-sans:
var(--font-atkinson), ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
background: var(--bg);
color-scheme: dark;
}
body {
font-family: var(--font-atkinson);
margin: 0;
padding: 0;
text-align: left;
background: linear-gradient(var(--gray-gradient)) no-repeat;
background-size: 100% 600px;
min-height: 100vh;
display: flex;
flex-direction: column;
font-family: var(--font-sans);
font-size: 16px;
line-height: 1.6;
color: var(--text);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
word-wrap: break-word;
overflow-wrap: break-word;
color: rgb(var(--gray-dark));
font-size: 20px;
line-height: 1.7;
}
::selection {
background: rgba(6, 182, 212, 0.3);
color: #a5f3fc;
}
main {
width: 720px;
max-width: calc(100% - 2em);
margin: auto;
padding: 3em 1em;
flex: 1 1 auto;
width: 100%;
max-width: 56rem; /* matches max-w-4xl */
margin: 0 auto;
padding: 3rem 1.5rem;
}
@media (min-width: 1024px) {
main {
padding: 4rem 2rem;
}
}
a {
color: var(--accent-bright);
text-decoration: none;
transition: color 0.15s ease;
}
a:hover {
color: var(--accent);
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0 0 0.5rem 0;
color: rgb(var(--black));
color: var(--text-strong);
margin: 0 0 0.75rem 0;
line-height: 1.2;
}
h1 {
font-size: 3.052em;
}
h2 {
font-size: 2.441em;
}
h3 {
font-size: 1.953em;
}
h4 {
font-size: 1.563em;
}
h5 {
font-size: 1.25em;
}
strong,
b {
letter-spacing: -0.01em;
font-weight: 700;
}
a {
color: var(--accent);
h1 {
font-size: 2.25rem;
}
a:hover {
color: var(--accent);
h2 {
font-size: 1.5rem;
}
h3 {
font-size: 1.25rem;
}
h4 {
font-size: 1.125rem;
}
p {
margin-bottom: 1em;
margin: 0 0 1em 0;
}
.prose p {
margin-bottom: 2em;
}
textarea {
width: 100%;
font-size: 16px;
}
input {
font-size: 16px;
}
table {
width: 100%;
strong,
b {
color: var(--text-strong);
font-weight: 700;
}
img {
max-width: 100%;
height: auto;
border-radius: 8px;
border-radius: 4px;
}
hr {
border: none;
border-top: 1px solid var(--border);
margin: 2rem 0;
}
code {
padding: 2px 5px;
background-color: rgb(var(--gray-light));
border-radius: 2px;
font-family: var(--font-mono);
font-size: 0.9em;
padding: 0.15em 0.4em;
background: #0f1720;
color: #a5f3fc;
border-radius: 3px;
border: 1px solid var(--border);
}
pre {
padding: 1.5em;
border-radius: 8px;
font-family: var(--font-mono);
padding: 1.25em;
border-radius: 4px;
overflow-x: auto;
background: #0f1720 !important;
border: 1px solid var(--border);
}
pre > code {
all: unset;
font-family: var(--font-mono);
}
blockquote {
border-left: 4px solid var(--accent);
padding: 0 0 0 20px;
border-left: 3px solid var(--accent);
padding: 0.25em 0 0.25em 1em;
margin: 1.25em 0;
color: var(--muted);
background: rgba(6, 182, 212, 0.04);
}
table {
width: 100%;
border-collapse: collapse;
margin: 1em 0;
}
th,
td {
border-bottom: 1px solid var(--border);
padding: 0.5em 0.75em;
text-align: left;
}
textarea,
input {
font-family: inherit;
font-size: 1rem;
}
/* ---------- Reusable utility / component classes ---------- */
.container {
width: 100%;
max-width: 56rem;
margin: 0 auto;
padding: 0 1.5rem;
}
@media (min-width: 1024px) {
.container {
padding: 0 2rem;
}
}
.btn-primary {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: var(--accent);
color: #030712;
padding: 0.625rem 1.25rem;
border-radius: 2px;
font-size: 0.875rem;
font-weight: 700;
box-shadow: var(--accent-glow);
transition: background 0.15s ease;
text-decoration: none;
}
.btn-primary:hover {
background: var(--accent-bright);
color: #030712;
}
.section-heading {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 1rem;
border-bottom: 1px solid var(--border);
padding-bottom: 1rem;
margin-bottom: 2rem;
}
.section-heading h2 {
margin: 0;
font-size: 1.333em;
font-size: 1.5rem;
color: var(--text-strong);
letter-spacing: -0.01em;
}
hr {
border: none;
border-top: 1px solid rgb(var(--gray-light));
.section-heading .view-all {
font-size: 0.875rem;
color: var(--accent);
display: inline-flex;
align-items: center;
gap: 0.125rem;
font-weight: 500;
}
@media (max-width: 720px) {
body {
font-size: 18px;
}
main {
padding: 1em;
}
.section-heading .view-all:hover {
color: var(--accent-bright);
}
.tag {
display: inline-flex;
align-items: center;
gap: 0.25rem;
background: rgba(16, 185, 129, 0.1);
color: var(--emerald-bright);
border: 1px solid rgba(16, 185, 129, 0.2);
padding: 0.2rem 0.5rem;
border-radius: 3px;
font-size: 0.75rem;
font-weight: 500;
font-family: var(--font-mono);
}
.tag svg {
color: rgba(16, 185, 129, 0.7);
}
.badge-online {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
background: rgba(16, 185, 129, 0.1);
color: var(--emerald-bright);
border: 1px solid rgba(16, 185, 129, 0.2);
font-size: 0.875rem;
font-weight: 500;
font-family: var(--font-mono);
}
.muted {
color: var(--muted);
}
.dim {
color: var(--dim);
}
.icon {
display: inline-block;
vertical-align: middle;
flex-shrink: 0;
}
.sr-only {
@@ -130,12 +300,32 @@ hr {
height: 1px;
width: 1px;
overflow: hidden;
/* IE6, IE7 - a 0 height clip, off to the bottom right of the visible 1px box */
clip: rect(1px 1px 1px 1px);
/* maybe deprecated but we need to support legacy browsers */
clip: rect(1px, 1px, 1px, 1px);
/* modern browsers, clip-path works inwards from each corner */
clip-path: inset(50%);
/* added line to stop words getting smushed together (as they go onto separate lines and some screen readers do not understand line feeds as a space */
white-space: nowrap;
}
/* ---------- Animations ---------- */
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(0.5rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-in {
animation: fade-in-up 0.7s ease-out both;
}
/* ---------- Responsive type ---------- */
@media (max-width: 640px) {
h1 {
font-size: 1.875rem;
}
main {
padding: 2rem 1rem;
}
}