dev blog shell
This commit is contained in:
+38
-55
@@ -1,62 +1,45 @@
|
||||
---
|
||||
import { SITE_TITLE } from '../consts';
|
||||
const today = new Date();
|
||||
---
|
||||
|
||||
<footer>
|
||||
© {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>© {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>
|
||||
|
||||
@@ -10,7 +10,7 @@ const { date } = Astro.props;
|
||||
{
|
||||
date.toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
+305
-58
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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]}
|
||||
/>
|
||||
@@ -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">•</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
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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">← 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">•</span>
|
||||
<span class="meta-item dim">{readTime}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
updatedDate && (
|
||||
<>
|
||||
<span class="meta-dot">•</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
@@ -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> — long-form posts on application security,
|
||||
threat models, and post-incident write-ups.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Field notes</strong> — shorter, sharper takes on tools, libraries,
|
||||
and the trade-offs they impose.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Open source</strong> — 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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user