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
8 changes: 8 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ const serve = async () => {
attachAnkifySessionProxy(app, server, ankifySessionValidate);

app.use('/templates', express.static(templateDir));
// Hashed Vite output under /assets/* is content-addressed and immutable.
app.use(
'/assets',
express.static(`${BUILD_DIR}/assets`, {
immutable: true,
maxAge: '1y',
})
);
app.use(express.static(BUILD_DIR));

// API Documentation
Expand Down
59 changes: 32 additions & 27 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -103,20 +103,22 @@
}
}
</script>
<!-- Hotjar Tracking Code for https://2anki.net -->
<!-- Hotjar Tracking Code for https://2anki.net (deferred until after load) -->
<script>
(function(h,o,t,j,a,r) {
h.hj=h.hj||function() {
const hjQueue=h.hj.q=h.hj.q||[];
hjQueue.push(arguments);
};
h._hjSettings={hjid: 2603407,hjsv: 6};
var headElement=o.getElementsByTagName('head')[0];
var scriptElement=o.createElement('script');
scriptElement.async=1;
scriptElement.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
headElement.appendChild(scriptElement);
})(globalThis,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
globalThis.addEventListener('load', function () {
(function (h, o, t, j) {
h.hj = h.hj || function () {
const hjQueue = h.hj.q = h.hj.q || [];
hjQueue.push(arguments);
};
h._hjSettings = { hjid: 2603407, hjsv: 6 };
var headElement = o.getElementsByTagName('head')[0];
var scriptElement = o.createElement('script');
scriptElement.async = 1;
scriptElement.src = t + h._hjSettings.hjid + j + h._hjSettings.hjsv;
headElement.appendChild(scriptElement);
})(globalThis, document, 'https://static.hotjar.com/c/hotjar-', '.js?sv=');
});
</script>

<!-- Legacy redirect (early + preserve URL) -->
Expand All @@ -135,30 +137,33 @@
</script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/bulma-switch.css">
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
onload="this.onload=null;this.rel='stylesheet'">
<noscript>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
</noscript>

<!-- Global site tag (gtag.js) - Google Analytics -->
<!-- Note: SRI not used for gtag.js as Google frequently updates this script,
<!-- Global site tag (gtag.js) - Google Analytics (deferred until after load) -->
<!-- Note: SRI not used for gtag.js as Google frequently updates this script,
which would break the integrity hash. Using secure loading with error handling instead. -->
<script>
(function() {
globalThis.dataLayer=globalThis.dataLayer||[];
globalThis.addEventListener('load', function () {
globalThis.dataLayer = globalThis.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
const script=document.createElement('script');
script.async=true;
script.src='https://www.googletagmanager.com/gtag/js?id=G-X6K69TY3EX';
script.onerror=function() {
const script = document.createElement('script');
script.async = true;
script.src = 'https://www.googletagmanager.com/gtag/js?id=G-X6K69TY3EX';
script.onerror = function () {
console.warn('Failed to load Google Analytics');
};
script.onload=function() {
gtag('js',new Date());
gtag('config','G-X6K69TY3EX',{anonymize_ip: true});
script.onload = function () {
gtag('js', new Date());
gtag('config', 'G-X6K69TY3EX', { anonymize_ip: true });
};
document.head.appendChild(script);
})();
});
</script>
</head>

Expand Down
12 changes: 10 additions & 2 deletions web/src/components/NavigationBar/NavigationBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,23 @@ function NavigationBar({ isLoggedIn }: Readonly<NavigationBarProps>) {
const [active, setActive] = useState(false);
const path = globalThis.location.pathname;
const theme = useTheme();
const logoSrc = theme === 'light' ? '/mascot/navbar-logo.png' : '/mascot/Notion 1.png';
const isLight = theme === 'light';
const logoSrc = isLight ? '/mascot/navbar-logo.png' : '/mascot/Notion 1.png';
const logoWidth = isLight ? 103 : 33;

const isResolved = isLoggedIn !== undefined;

return (
<nav className={styles.navbar} aria-label="main navigation">
<div className={styles.brand}>
<a className={styles.logoLink} href="/">
<img src={logoSrc} alt="2anki Logo" />
<img
src={logoSrc}
alt="2anki Logo"
width={logoWidth}
height={28}
fetchPriority="high"
/>
</a>
<button
type="button"
Expand Down
1 change: 1 addition & 0 deletions web/src/pages/WhatsNewPage/changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface ChangelogEntry {
}

export const changelog: ChangelogEntry[] = [
{ type: 'fix', title: 'The landing page paints faster on phones over slow connections', date: '2026-05-19' },
{ type: 'style', title: 'What\'s new page redesigned as a board — backlog, in progress, and shipped', date: '2026-05-19' },
{ type: 'feature', title: 'First upload — clearer no-cards message, one button to create an account and start a trial, and a quick first-visit tour', date: '2026-05-19' },
{ type: 'feature', title: 'Notion tables convert to flashcards — one row per card, column 1 on the front, column 2 on the back', date: '2026-05-19' },
Expand Down