Compare commits

..

2 commits

Author SHA1 Message Date
64d01a9b45
chore: sync .idea ide config 2025-07-21 22:23:34 +08:00
8033b18a64
feat: add basic working spa mode 2025-07-21 22:22:37 +08:00
8 changed files with 140 additions and 53 deletions

View file

@ -0,0 +1,21 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myValues">
<value>
<list size="7">
<item index="0" class="java.lang.String" itemvalue="nobr" />
<item index="1" class="java.lang.String" itemvalue="noembed" />
<item index="2" class="java.lang.String" itemvalue="comment" />
<item index="3" class="java.lang.String" itemvalue="noscript" />
<item index="4" class="java.lang.String" itemvalue="embed" />
<item index="5" class="java.lang.String" itemvalue="script" />
<item index="6" class="java.lang.String" itemvalue="oom-comments" />
</list>
</value>
</option>
<option name="myCustomValuesEnabled" value="true" />
</inspection_tool>
</profile>
</component>

25
.idea/jsonSchemas.xml generated Normal file
View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JsonSchemaMappingsProjectConfiguration">
<state>
<map>
<entry key="Azure Static Web Apps configuration file">
<value>
<SchemaInfo>
<option name="name" value="Azure Static Web Apps configuration file" />
<option name="relativePathToSchema" value="https://www.schemastore.org/staticwebapp.config.json" />
<option name="applicationDefined" value="true" />
<option name="patterns">
<list>
<Item>
<option name="path" value="staticwebapp.config.json" />
</Item>
</list>
</option>
</SchemaInfo>
</value>
</entry>
</map>
</state>
</component>
</project>

1
.idea/mercury.iml generated
View file

@ -10,5 +10,6 @@
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="n20171213" level="application" />
</component>
</module>

View file

@ -234,6 +234,9 @@ const domain = Astro.url.host
// Initialize with default config
initSearch();
// Re-initialize when Astro's view transitions occur, this provides fix for SPA navigation
document.addEventListener('astro:page-load', initSearch);
</script>}
<style>

View file

@ -8,13 +8,26 @@ const ArtalkInstanceDomain = siteConfig.comments.artalk.instanceDomain
<div id="comments" data-path={Astro.url.pathname} data-server={ArtalkInstanceDomain}></div>
<script>
import Artalk from "artalk";
const atkElement = document.querySelector('#comments');
Artalk.init({
el: '#comments',
pageKey: atkElement?.getAttribute('data-path') || window.location.pathname,
server: `https://${atkElement?.getAttribute('data-server')}`,
darkMode: "auto",
versionCheck: false
function initArtalk() {
const atkElement = document.querySelector('#comments');
if (!atkElement) return;
Artalk.init({
el: '#comments',
pageKey: atkElement?.getAttribute('data-path') || window.location.pathname,
server: `https://${atkElement?.getAttribute('data-server')}`,
darkMode: "auto",
versionCheck: false
});
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', initArtalk);
// Re-initialize on view transitions, fix issues with Astro's ClientRouter
document.addEventListener('astro:page-load', () => {
initArtalk();
});
</script>
</div>

View file

@ -54,25 +54,34 @@ const { encryptedData, iv } = encrypt(content, password);
</style>
<script>
// Client-side decryption logic
document.addEventListener('DOMContentLoaded', () => {
// Define the function that sets up the decryption logic
function setupDecryption() {
const containers = document.querySelectorAll('.encrypted-content');
containers.forEach(container => {
const encryptedData = container.getAttribute('data-encrypted');
const iv = container.getAttribute('data-iv');
const passwordInput = container.querySelector('.decrypt-password');
const decryptButton = container.querySelector('.decrypt-button');
const contentContainer = container.querySelector('.content-container');
const passwordForm = container.querySelector('.password-form');
const passwordInput = container.querySelector<HTMLInputElement>('.decrypt-password');
const decryptButton = container.querySelector<HTMLButtonElement>('.decrypt-button');
const contentContainer = container.querySelector<HTMLDivElement>('.content-container');
const passwordForm = container.querySelector<HTMLDivElement>('.password-form');
if (!encryptedData || !iv || !passwordInput || !decryptButton || !contentContainer || !passwordForm) {
console.error('Missing required elements for decryption');
return;
}
// Skip if already initialized to prevent duplicate event listeners
if (container.getAttribute("initialized") === 'true') {
return;
}
decryptButton.addEventListener('click', async () => {
try {
const password = passwordInput.value;
if (!password) return;
const content = await decrypt(encryptedData, iv, password);
contentContainer.innerHTML = content;
contentContainer.innerHTML = await decrypt(encryptedData, iv, password);
contentContainer.classList.remove('hidden');
passwordForm.classList.add('hidden');
} catch (error) {
@ -82,52 +91,61 @@ const { encryptedData, iv } = encrypt(content, password);
});
// Allow pressing Enter to decrypt
passwordInput.addEventListener('keydown', (e) => {
passwordInput.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Enter') {
decryptButton.click();
}
});
// Mark as initialized to prevent duplicate event listeners
container.setAttribute("initialized", 'true');
});
}
async function decrypt(encryptedData, iv, password) {
// Convert base64 to array buffer
const encryptedBytes = Uint8Array.from(atob(encryptedData), c => c.charCodeAt(0));
const ivBytes = Uint8Array.from(atob(iv), c => c.charCodeAt(0));
async function decrypt(encryptedData: string, iv: string, password: string): Promise<string> {
// Convert base64 to array buffer
const encryptedBytes = Uint8Array.from(atob(encryptedData), c => c.charCodeAt(0));
const ivBytes = Uint8Array.from(atob(iv), c => c.charCodeAt(0));
// Derive key from password
const encoder = new TextEncoder();
const passwordBytes = encoder.encode(password);
const keyMaterial = await window.crypto.subtle.importKey(
'raw',
passwordBytes,
{ name: 'PBKDF2' },
false,
['deriveBits', 'deriveKey']
);
// Derive key from password
const encoder = new TextEncoder();
const passwordBytes = encoder.encode(password);
const keyMaterial = await window.crypto.subtle.importKey(
'raw',
passwordBytes,
{ name: 'PBKDF2' },
false,
['deriveBits', 'deriveKey']
);
const key = await window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: ivBytes,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['decrypt']
);
const key = await window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: ivBytes,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['decrypt']
);
// Decrypt
const decryptedBytes = await window.crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: ivBytes },
key,
encryptedBytes
);
// Decrypt
const decryptedBytes = await window.crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: ivBytes },
key,
encryptedBytes
);
// Convert array buffer to text
const decoder = new TextDecoder();
return decoder.decode(decryptedBytes);
}
});
// Convert array buffer to text
const decoder = new TextDecoder();
return decoder.decode(decryptedBytes);
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', setupDecryption);
// Re-initialize when Astro's view transitions occur, this provides fix for SPA navigation
document.addEventListener('astro:page-load', setupDecryption);
</script>

View file

@ -9,6 +9,7 @@ export const siteConfig = {
email: 'hi@mercury.info',
},
// features
spa: false, // enable single page application mode, this will enable navigation (with fade transitions) without reloading the page, and enable client-side routing
noClientJavaScript: false, // disable client-side javascript, this will:
// 1. disable most built-in client-side javascript from rendering (protected content component and umami still needs javascript to function, sorry)
// 2. the full text search will be redirected to a search engine

View file

@ -19,6 +19,9 @@ interface Props {
ogImage?: string;
}
import { ClientRouter } from "astro:transitions";
const spaEnabled = siteConfig.spa
const noscript = siteConfig.noClientJavaScript
const statisticsEnabled = siteConfig.siteAnalytics.enabled
@ -44,6 +47,8 @@ const { title = pageTitle, author = siteConfig.defaultAuthor.name,description =
<meta name="generator" content={Astro.generator} />
<Meta title={pageTitle} author={author} description={description} ogImage={ogImage} />
<title>{pageTitle}</title>
{spaEnabled && <ClientRouter fallback="animate" />}
<!--transitional animation is broken in firefox though-->
</head>
<body>
<main>