Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ const SKIP_PATTERNS = ['/write', '/search'];
function priorityFor(path) {
if (path === '/' || path === '') return 1.0;
if (path === '/blog' || path === '/topics') return 0.9;
if (path === '/playground' || path === '/community' || path === '/contribute') return 0.8;
if (path === '/playground' || path === '/community' || path === '/contribute' || path === '/why')
return 0.8;
if (path.startsWith('/blog/')) return 0.7;
if (path.startsWith('/topics/') || path.startsWith('/tags/')) return 0.6;
if (path.startsWith('/authors/')) return 0.6;
Expand Down Expand Up @@ -163,7 +164,9 @@ export default defineConfig({
output: 'static',

build: {
inlineStylesheets: 'always',
// 'auto' inlines only small styles; the shared design-system sheet is emitted
// as one cacheable /_astro/*.css instead of being duplicated into every page.
inlineStylesheets: 'auto',
// Flat files (blog/x.html) instead of blog/x/index.html, so URLs stay clean
// with no trailing slash (pairs with trailingSlash: 'never'). Avoids
// Cloudflare's directory-style 308 redirect that appended the slash.
Expand Down
67 changes: 31 additions & 36 deletions functions/api/create-pr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,26 @@ async function gh(
return res.status === 204 ? {} : ((await res.json()) as Record<string, unknown>);
}

async function fileExistsOnMain(
owner: string,
name: string,
path: string,
token: string,
): Promise<boolean> {
const res = await fetch(
`https://api.github.com/repos/${owner}/${name}/contents/${encodeURI(path)}?ref=main`,
{
headers: {
accept: 'application/vnd.github+json',
authorization: `Bearer ${token}`,
'user-agent': UA,
'x-github-api-version': '2022-11-28',
},
},
);
return res.ok;
}

export async function onRequestPost(context: { request: Request; env: Env }): Promise<Response> {
const { request, env } = context;

Expand Down Expand Up @@ -121,48 +141,23 @@ export async function onRequestPost(context: { request: Request; env: Env }): Pr

// A brand-new post must not silently overwrite an existing one at the same slug.
// Edits (loaded via the portal's "Open existing post") are meant to, so skip then.
if (!isEdit) {
const existing = await fetch(
`https://api.github.com/repos/${owner}/${name}/contents/${encodeURI(
`src/content/posts/${slug}/index.mdx`,
)}?ref=main`,
{
headers: {
accept: 'application/vnd.github+json',
authorization: `Bearer ${token}`,
'user-agent': UA,
'x-github-api-version': '2022-11-28',
},
},
if (
!isEdit &&
(await fileExistsOnMain(owner, name, `src/content/posts/${slug}/index.mdx`, token))
) {
return json(
{ error: 'A post with this URL already exists. Change the URL slug and try again.' },
409,
);
if (existing.ok) {
return json(
{ error: 'A post with this URL already exists. Change the URL slug and try again.' },
409,
);
}
}

// A newly registered author must not overwrite an existing profile at the same handle.
const authorFile = files.find((f) => f.path.startsWith('src/content/authors/'));
if (authorFile) {
const existing = await fetch(
`https://api.github.com/repos/${owner}/${name}/contents/${encodeURI(authorFile.path)}?ref=main`,
{
headers: {
accept: 'application/vnd.github+json',
authorization: `Bearer ${token}`,
'user-agent': UA,
'x-github-api-version': '2022-11-28',
},
},
if (authorFile && (await fileExistsOnMain(owner, name, authorFile.path, token))) {
return json(
{ error: 'That author handle is already taken. Pick another handle and try again.' },
409,
);
if (existing.ok) {
return json(
{ error: 'That author handle is already taken. Pick another handle and try again.' },
409,
);
}
}

const ref = (await gh(`/repos/${owner}/${name}/git/ref/heads/main`, token)) as {
Expand Down
17 changes: 17 additions & 0 deletions public/_headers
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
X-Frame-Options: SAMEORIGIN
Permissions-Policy: camera=(), microphone=(), geolocation=()

/_astro/*
Cache-Control: public, max-age=31536000, immutable

/_pagefind/*
Cache-Control: public, max-age=3600

/og/*
Cache-Control: public, max-age=86400

/og-default.png
Cache-Control: public, max-age=86400
1 change: 0 additions & 1 deletion public/robots.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@ Allow: /
Disallow: /api/

Sitemap: https://mlsystems.dev/sitemap-index.xml
Host: https://mlsystems.dev
61 changes: 61 additions & 0 deletions src/components/AnalyticsConsent.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
interface Props {
measurementId?: string;
}
const { measurementId } = Astro.props;
---

{
measurementId && (
<aside class="analytics-consent" aria-label="Analytics consent" data-consent-banner hidden>
<button
type="button"
class="analytics-consent-close"
aria-label="Dismiss analytics prompt"
data-consent-choice="declined"
>
x
</button>
<div>
<div class="analytics-consent-title">Privacy preferences</div>
<p>We use optional analytics to understand readership and improve the site.</p>
</div>
<div class="analytics-consent-actions">
<button type="button" class="btn btn-primary" data-consent-choice="accepted">
Accept
</button>
<button type="button" class="btn" data-consent-choice="declined">
Decline
</button>
</div>
</aside>
)
}

<script>
const STORAGE_KEY = 'mlsystems-analytics-consent';
const banner = document.querySelector<HTMLElement>('[data-consent-banner]');

if (banner) {
let stored: string | null = null;
try {
stored = localStorage.getItem(STORAGE_KEY);
} catch {}

if (stored !== 'accepted' && stored !== 'declined') {
banner.hidden = false;
banner.querySelectorAll<HTMLButtonElement>('[data-consent-choice]').forEach((btn) => {
btn.addEventListener('click', () => {
const choice = btn.dataset.consentChoice as 'accepted' | 'declined';
try {
localStorage.setItem(STORAGE_KEY, choice);
} catch {}
window.gtag?.('consent', 'update', {
analytics_storage: choice === 'accepted' ? 'granted' : 'denied',
});
banner.hidden = true;
});
});
}
}
</script>
69 changes: 0 additions & 69 deletions src/components/AnalyticsConsent.tsx

This file was deleted.

73 changes: 70 additions & 3 deletions src/components/ArticleActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ export default function ArticleActions({
}) {
const [liked, setLiked] = useState(false);
const [count, setCount] = useState<number | null>(null);
const [copied, setCopied] = useState(false);

const citation = `${author.split(' ').reverse().join(', ')}. "${title.split(':')[0]}." ${SITE.domain}, ${date}.`;

const copyCitation = async () => {
try {
await navigator.clipboard.writeText(`${citation} ${window.location.href}`);
setCopied(true);
setTimeout(() => setCopied(false), 1400);
} catch {
/* clipboard blocked */
}
};

useEffect(() => {
const key = `mlsys-liked-${slug}`;
Expand Down Expand Up @@ -116,9 +129,63 @@ export default function ArticleActions({
↗ Share
</button>
</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--ink-3)' }}>
Cite as: {author.split(' ').reverse().join(', ')}. &quot;{title.split(':')[0]}.&quot;{' '}
{SITE.domain}, {date}.
<div
style={{
fontFamily: 'var(--font-mono)',
fontSize: 12,
color: 'var(--ink-3)',
display: 'inline-flex',
alignItems: 'center',
gap: 6,
flexWrap: 'wrap',
}}
>
<span>Cite as: {citation}</span>
<button
type="button"
onClick={copyCitation}
aria-label={copied ? 'Citation copied' : 'Copy citation with link'}
title="Copy citation with link"
style={{
background: 'none',
border: 'none',
padding: 2,
cursor: 'pointer',
display: 'inline-flex',
color: copied ? 'var(--accent)' : 'inherit',
}}
>
{copied ? (
<svg
viewBox="0 0 24 24"
width="14"
height="14"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M20 6 9 17l-5-5" />
</svg>
) : (
<svg
viewBox="0 0 24 24"
width="14"
height="14"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<rect x="9" y="9" width="13" height="13" rx="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</svg>
)}
</button>
</div>
</div>
);
Expand Down
1 change: 0 additions & 1 deletion src/components/BlogFilter.astro
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ const { activeTopic, allCount, topicCounts } = Astro.props;
const topic = link.dataset.topic!;
const newUrl = topic === 'all' ? '/blog' : `/blog?topic=${topic}`;
history.pushState(null, '', newUrl);
// Update active chip
document
.querySelectorAll<HTMLAnchorElement>('[data-blog-filter] a[data-topic]')
.forEach((a) => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Footer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const socialIcons = (['discord', 'github', 'twitter', 'linkedin'] as const).map(

<div class="footer-bottom">
<span>© {currentYear} {SITE.name}</span>
<span>Made by the community, for the community.</span>
<a href="/why" class="footer-why">Made by the community, for the community.</a>
</div>
</div>
</footer>
1 change: 0 additions & 1 deletion src/components/IconLinkCard.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
---
// Icon + label + action link card. Shared by About and Contact.
// Surface + hover-lift come from the global .card / .card--interactive classes.
interface Props {
href?: string | null;
Expand Down
Loading
Loading