feat: ship blog platform admin and deploy stack
This commit is contained in:
@@ -11,6 +11,12 @@ interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
siteSettings?: SiteSettings;
|
||||
canonical?: string;
|
||||
noindex?: boolean;
|
||||
ogImage?: string;
|
||||
ogType?: string;
|
||||
twitterCard?: 'summary' | 'summary_large_image';
|
||||
jsonLd?: Record<string, unknown> | Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
const props = Astro.props;
|
||||
@@ -28,6 +34,59 @@ if (!props.siteSettings) {
|
||||
|
||||
const title = props.title || siteSettings.siteTitle;
|
||||
const description = props.description || siteSettings.siteDescription;
|
||||
const siteUrl = siteSettings.siteUrl.replace(/\/$/, '');
|
||||
const defaultCanonical = `${siteUrl}${Astro.url.pathname}`;
|
||||
const canonical = props.canonical
|
||||
? /^https?:\/\//i.test(props.canonical)
|
||||
? props.canonical
|
||||
: `${siteUrl}${props.canonical.startsWith('/') ? props.canonical : `/${props.canonical}`}`
|
||||
: defaultCanonical;
|
||||
const resolvedOgImage = props.ogImage || siteSettings.seo.defaultOgImage;
|
||||
const ogImage = resolvedOgImage
|
||||
? /^https?:\/\//i.test(resolvedOgImage)
|
||||
? resolvedOgImage
|
||||
: `${siteUrl}${resolvedOgImage.startsWith('/') ? resolvedOgImage : `/${resolvedOgImage}`}`
|
||||
: undefined;
|
||||
const ogType = props.ogType || 'website';
|
||||
const twitterCard = props.twitterCard || (ogImage ? 'summary_large_image' : 'summary');
|
||||
const defaultJsonLdObjects = [
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebSite',
|
||||
name: siteSettings.siteName,
|
||||
alternateName: siteSettings.siteShortName,
|
||||
url: siteUrl,
|
||||
description,
|
||||
inLanguage: locale,
|
||||
potentialAction: {
|
||||
'@type': 'SearchAction',
|
||||
target: `${siteUrl}/search?q={search_term_string}`,
|
||||
'query-input': 'required name=search_term_string',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Person',
|
||||
name: siteSettings.ownerName,
|
||||
url: siteUrl,
|
||||
image: siteSettings.ownerAvatarUrl || undefined,
|
||||
jobTitle: siteSettings.ownerTitle,
|
||||
description: siteSettings.ownerBio || description,
|
||||
sameAs: [
|
||||
siteSettings.social.github,
|
||||
siteSettings.social.twitter,
|
||||
].filter(Boolean),
|
||||
},
|
||||
].filter((item) => item.name || item.url);
|
||||
const mergedJsonLdObjects = [
|
||||
...defaultJsonLdObjects,
|
||||
...(props.jsonLd === undefined
|
||||
? []
|
||||
: Array.isArray(props.jsonLd)
|
||||
? props.jsonLd
|
||||
: [props.jsonLd]),
|
||||
];
|
||||
const jsonLd = mergedJsonLdObjects.length ? JSON.stringify(mergedJsonLdObjects) : undefined;
|
||||
const i18nPayload = JSON.stringify({ locale, messages });
|
||||
---
|
||||
|
||||
@@ -37,8 +96,27 @@ const i18nPayload = JSON.stringify({ locale, messages });
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content={description} />
|
||||
<meta name="robots" content={props.noindex ? 'noindex, nofollow' : 'index, follow'} />
|
||||
<link rel="canonical" href={canonical} />
|
||||
<meta property="og:site_name" content={siteSettings.siteName} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:type" content={ogType} />
|
||||
<meta property="og:url" content={canonical} />
|
||||
<meta name="twitter:card" content={twitterCard} />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
{siteSettings.seo.defaultTwitterHandle && (
|
||||
<meta name="twitter:site" content={siteSettings.seo.defaultTwitterHandle} />
|
||||
)}
|
||||
{siteSettings.seo.defaultTwitterHandle && (
|
||||
<meta name="twitter:creator" content={siteSettings.seo.defaultTwitterHandle} />
|
||||
)}
|
||||
{ogImage && <meta property="og:image" content={ogImage} />}
|
||||
{ogImage && <meta name="twitter:image" content={ogImage} />}
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<title>{title}</title>
|
||||
{jsonLd && <script type="application/ld+json" set:html={jsonLd}></script>}
|
||||
|
||||
<style is:inline>
|
||||
:root {
|
||||
@@ -57,7 +135,7 @@ const i18nPayload = JSON.stringify({ locale, messages });
|
||||
--text: #0f172a;
|
||||
--text-rgb: 15 23 42;
|
||||
--text-secondary: #475569;
|
||||
--text-tertiary: #7c8aa0;
|
||||
--text-tertiary: #5f6f86;
|
||||
--terminal-text: #0f172a;
|
||||
--title-color: #0f172a;
|
||||
--button-text: #0f172a;
|
||||
@@ -103,7 +181,7 @@ const i18nPayload = JSON.stringify({ locale, messages });
|
||||
--text: #e6e6e6;
|
||||
--text-rgb: 230 230 230;
|
||||
--text-secondary: #d1d5db;
|
||||
--text-tertiary: #6b7280;
|
||||
--text-tertiary: #9ca3af;
|
||||
--terminal-text: #e6e6e6;
|
||||
--title-color: #ffffff;
|
||||
--button-text: #e6e6e6;
|
||||
@@ -141,7 +219,7 @@ const i18nPayload = JSON.stringify({ locale, messages });
|
||||
--text: #e6e6e6;
|
||||
--text-rgb: 230 230 230;
|
||||
--text-secondary: #d1d5db;
|
||||
--text-tertiary: #6b7280;
|
||||
--text-tertiary: #9ca3af;
|
||||
--terminal-text: #e6e6e6;
|
||||
--title-color: #ffffff;
|
||||
--button-text: #e6e6e6;
|
||||
@@ -195,6 +273,78 @@ const i18nPayload = JSON.stringify({ locale, messages });
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script is:inline>
|
||||
(function() {
|
||||
function renderPrompt(el, nextCommand, typingMode) {
|
||||
const id = el.getAttribute('data-id');
|
||||
const cmdEl = document.getElementById('cmd-' + id);
|
||||
const cursorEl = document.getElementById('cursor-' + id);
|
||||
|
||||
if (!cmdEl || !cursorEl) return;
|
||||
|
||||
const command = String(nextCommand || '');
|
||||
const typing = typingMode === true || typingMode === 'true';
|
||||
const renderSeq = String((Number(el.getAttribute('data-render-seq') || '0') || 0) + 1);
|
||||
|
||||
el.setAttribute('data-command', command);
|
||||
el.setAttribute('data-render-seq', renderSeq);
|
||||
cmdEl.textContent = '';
|
||||
cursorEl.style.animation = 'none';
|
||||
cursorEl.style.opacity = '1';
|
||||
|
||||
if (!typing) {
|
||||
cmdEl.textContent = command;
|
||||
cursorEl.style.animation = 'blink 1s infinite';
|
||||
return;
|
||||
}
|
||||
|
||||
let index = 0;
|
||||
|
||||
function typeChar() {
|
||||
if (el.getAttribute('data-render-seq') !== renderSeq) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (index < command.length) {
|
||||
cmdEl.textContent += command.charAt(index);
|
||||
index += 1;
|
||||
setTimeout(typeChar, 42 + Math.random() * 22);
|
||||
} else {
|
||||
cursorEl.style.animation = 'blink 1s infinite';
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(typeChar, 120);
|
||||
}
|
||||
|
||||
if (!window.__termiCommandPrompt) {
|
||||
window.__termiCommandPrompt = {
|
||||
set(promptId, command, options = {}) {
|
||||
if (!promptId) return;
|
||||
const el = document.querySelector(`[data-prompt-id="${promptId}"]`);
|
||||
if (!el) return;
|
||||
renderPrompt(el, command, options.typing ?? true);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function mountPrompts() {
|
||||
const prompts = document.querySelectorAll('[data-command]:not([data-command-mounted])');
|
||||
|
||||
prompts.forEach(function(el) {
|
||||
el.setAttribute('data-command-mounted', 'true');
|
||||
renderPrompt(el, el.getAttribute('data-command') || '', el.getAttribute('data-typing') === 'true');
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', mountPrompts, { once: true });
|
||||
} else {
|
||||
mountPrompts();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script is:inline>
|
||||
(function() {
|
||||
const STORAGE_KEY = 'theme';
|
||||
@@ -325,7 +475,7 @@ const i18nPayload = JSON.stringify({ locale, messages });
|
||||
</main>
|
||||
|
||||
<Footer siteSettings={siteSettings} />
|
||||
<BackToTop client:idle />
|
||||
<BackToTop client:load />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user