feat: implement forms, SEO, sitemap, and other infrastructure improvements
Some checks failed
Build and Deploy to Storj / build-and-deploy (push) Failing after 0s
Some checks failed
Build and Deploy to Storj / build-and-deploy (push) Failing after 0s
This commit is contained in:
parent
e9af3a9c27
commit
e68db9c26c
14 changed files with 791 additions and 102 deletions
24
public/manifest.json
Normal file
24
public/manifest.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "Scott Scoble",
|
||||||
|
"short_name": "Scott Scoble",
|
||||||
|
"description": "Human Cultivation Expert for the AI Age",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#faf8f5",
|
||||||
|
"theme_color": "#3d5a3e",
|
||||||
|
"orientation": "portrait-primary",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
19
public/robots.txt
Normal file
19
public/robots.txt
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# robots.txt for scott.scoble.me
|
||||||
|
# https://scott.scoble.me
|
||||||
|
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
# Sitemap
|
||||||
|
Sitemap: https://scott.scoble.me/sitemap.xml
|
||||||
|
|
||||||
|
# Disallow admin pages (if any)
|
||||||
|
Disallow: /admin/
|
||||||
|
Disallow: /api/
|
||||||
|
|
||||||
|
# Disallow private user data
|
||||||
|
Disallow: /private/
|
||||||
|
Disallow: /checkout/
|
||||||
|
|
||||||
|
# Crawl delay (optional)
|
||||||
|
Crawl-delay: 1
|
||||||
|
|
@ -9,11 +9,13 @@ const currentYear = new Date().getFullYear();
|
||||||
<div class="footer-brand">
|
<div class="footer-brand">
|
||||||
<span class="footer-logo">Scott Scoble</span>
|
<span class="footer-logo">Scott Scoble</span>
|
||||||
<p class="footer-tagline">The human cultivation expert for the AI age.</p>
|
<p class="footer-tagline">The human cultivation expert for the AI age.</p>
|
||||||
|
<p class="footer-email">📧 <a href="mailto:scott@scoble.me">scott@scoble.me</a></p>
|
||||||
|
|
||||||
<div class="footer-social">
|
<div class="footer-social">
|
||||||
<a href="https://linkedin.com/in/scottscoble" target="_blank" rel="noopener" aria-label="LinkedIn">
|
<a href="https://linkedin.com/in/scottscoble" target="_blank" rel="noopener" aria-label="LinkedIn">
|
||||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://twitter.com/scottscoble" target="_blank" rel="noopener" aria-label="X/Twitter">
|
<a href="https://x.com/scottscoble" target="_blank" rel="noopener" aria-label="X/Twitter">
|
||||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
|
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/sscoble" target="_blank" rel="noopener" aria-label="GitHub">
|
<a href="https://github.com/sscoble" target="_blank" rel="noopener" aria-label="GitHub">
|
||||||
|
|
@ -22,6 +24,16 @@ const currentYear = new Date().getFullYear();
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-newsletter">
|
||||||
|
<h4>Stay in the loop</h4>
|
||||||
|
<p>Get updates on new talks, workshops, and writings.</p>
|
||||||
|
<form class="newsletter-form">
|
||||||
|
<input type="email" placeholder="Your email" required />
|
||||||
|
<button type="submit">Subscribe</button>
|
||||||
|
</form>
|
||||||
|
<p class="newsletter-note">No spam. Unsubscribe anytime.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<div class="footer-col">
|
<div class="footer-col">
|
||||||
<h4>Pages</h4>
|
<h4>Pages</h4>
|
||||||
|
|
@ -61,8 +73,8 @@ const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
.footer-main {
|
.footer-main {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2fr 1fr;
|
grid-template-columns: 1.5fr 1.5fr 1fr;
|
||||||
gap: 4rem;
|
gap: 3rem;
|
||||||
padding-bottom: 3rem;
|
padding-bottom: 3rem;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
|
|
@ -83,6 +95,21 @@ const currentYear = new Date().getFullYear();
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer-email {
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-email a {
|
||||||
|
color: var(--color-amber);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-email a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.footer-social {
|
.footer-social {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
@ -97,9 +124,71 @@ const currentYear = new Date().getFullYear();
|
||||||
color: var(--color-amber);
|
color: var(--color-amber);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer-newsletter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-newsletter h4 {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
margin: 0 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-newsletter p {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter-form input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.625rem 0.875rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
color: white;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter-form input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter-form button {
|
||||||
|
padding: 0.625rem 1.25rem;
|
||||||
|
background: var(--color-amber);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter-form button:hover {
|
||||||
|
background: var(--color-amber-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.newsletter-note {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.footer-links {
|
.footer-links {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 3rem;
|
gap: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-col {
|
.footer-col {
|
||||||
|
|
@ -136,6 +225,12 @@ const currentYear = new Date().getFullYear();
|
||||||
color: rgba(255, 255, 255, 0.5);
|
color: rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.footer-main {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.footer-main {
|
.footer-main {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|
|
||||||
293
src/components/Form/SharedForm.astro
Normal file
293
src/components/Form/SharedForm.astro
Normal file
|
|
@ -0,0 +1,293 @@
|
||||||
|
---
|
||||||
|
import { useState } from 'astro:collection-utils';
|
||||||
|
|
||||||
|
interface FormProps {
|
||||||
|
type: 'speaking' | 'contact' | 'waitlist';
|
||||||
|
onSubmit?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type } = Astro.props;
|
||||||
|
|
||||||
|
// Formspree endpoint - Scott will update with his Formspree form ID
|
||||||
|
const FORMSPREE_ENDPOINT = import.meta.env.PUBLIC_FORMSPREE_ENDPOINT || '';
|
||||||
|
|
||||||
|
// Contact form topic routing
|
||||||
|
const TOPIC_EMAILS = {
|
||||||
|
speaking: 'scott@scoble.me',
|
||||||
|
consulting: 'scott@scoble.me',
|
||||||
|
general: 'scott@scoble.me',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Form field definitions based on type
|
||||||
|
const getFormFields = () => {
|
||||||
|
switch (type) {
|
||||||
|
case 'speaking':
|
||||||
|
return [
|
||||||
|
{ name: 'name', label: 'Name *', type: 'text', required: true },
|
||||||
|
{ name: 'email', label: 'Email *', type: 'email', required: true },
|
||||||
|
{ name: 'event_date', label: 'Event Date', type: 'date', required: false },
|
||||||
|
{ name: 'event_type', label: 'Event Type', type: 'select', required: false, options: [
|
||||||
|
{ value: '', label: 'Select...' },
|
||||||
|
{ value: 'conference', label: 'Conference' },
|
||||||
|
{ value: 'corporate', label: 'Corporate Event' },
|
||||||
|
{ value: 'workshop', label: 'Workshop' },
|
||||||
|
{ value: 'other', label: 'Other' },
|
||||||
|
] },
|
||||||
|
{ name: 'message', label: 'Message *', type: 'textarea', required: true, rows: 4 },
|
||||||
|
];
|
||||||
|
case 'contact':
|
||||||
|
return [
|
||||||
|
{ name: 'name', label: 'Name *', type: 'text', required: true },
|
||||||
|
{ name: 'email', label: 'Email *', type: 'email', required: true },
|
||||||
|
{ name: 'topic', label: 'Topic', type: 'select', required: false, options: [
|
||||||
|
{ value: '', label: 'Select...' },
|
||||||
|
{ value: 'speaking', label: 'Speaking Inquiry' },
|
||||||
|
{ value: 'consulting', label: 'Consulting Inquiry' },
|
||||||
|
{ value: 'general', label: 'General' },
|
||||||
|
] },
|
||||||
|
{ name: 'message', label: 'Message *', type: 'textarea', required: true, rows: 5 },
|
||||||
|
];
|
||||||
|
case 'waitlist':
|
||||||
|
return [
|
||||||
|
{ name: 'email', label: 'Email *', type: 'email', required: true },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isWaiting = useState(false);
|
||||||
|
const isSuccess = useState(false);
|
||||||
|
const errorMessage = useState('');
|
||||||
|
|
||||||
|
const handleSubmit = async (e: any) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (isWaiting.value) return;
|
||||||
|
|
||||||
|
isWaiting.value = true;
|
||||||
|
isSuccess.value = false;
|
||||||
|
errorMessage.value = '';
|
||||||
|
|
||||||
|
const form = e.target as HTMLFormElement;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
// Add form type for routing
|
||||||
|
formData.append('form_type', type);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (FORMSPREE_ENDPOINT) {
|
||||||
|
const response = await fetch(FORMSPREE_ENDPOINT, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.error?.message || 'Form submission failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isSuccess.value = true;
|
||||||
|
|
||||||
|
// Track success event for Umami
|
||||||
|
if (typeof window !== 'undefined' && window.umami) {
|
||||||
|
window.umami.track('form_submitted', {
|
||||||
|
type: type,
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
errorMessage.value = error instanceof Error ? error.message : 'An error occurred';
|
||||||
|
} finally {
|
||||||
|
isWaiting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for URL query param for contact form topic pre-selection
|
||||||
|
const topicFromUrl = typeof window !== 'undefined' && type === 'contact'
|
||||||
|
? new URL(window.location.href).searchParams.get('topic')
|
||||||
|
: null;
|
||||||
|
---
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
data-form-type={type}
|
||||||
|
novalidate
|
||||||
|
{ ...Astro.attrs }
|
||||||
|
>
|
||||||
|
{import.meta.env.PUBLIC_FORMSPREE_HONEYPOT && (
|
||||||
|
<div style="position: absolute; left: -9999px;">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name={import.meta.env.PUBLIC_FORMSPREE_HONEYPOT}
|
||||||
|
tabIndex={-1}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isSuccess ? (
|
||||||
|
<div class="form-success">
|
||||||
|
<h4>Thank you!</h4>
|
||||||
|
<p>Your message has been sent. I'll get back to you shortly.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{getFormFields().map((field) => (
|
||||||
|
<div class="form-group">
|
||||||
|
<label htmlFor={field.name}>
|
||||||
|
{field.label}
|
||||||
|
{field.required && <span class="required">*</span>}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{field.type === 'select' ? (
|
||||||
|
<select
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
required={field.required}
|
||||||
|
defaultValue={topicFromUrl || ''}
|
||||||
|
>
|
||||||
|
{field.options?.map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
) : field.type === 'textarea' ? (
|
||||||
|
<textarea
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
rows={field.rows || 4}
|
||||||
|
required={field.required}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type={field.type}
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
required={field.required}
|
||||||
|
{...(field.type === 'date' && { min: new Date().toISOString().split('T')[0] })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{field.type === 'email' && (
|
||||||
|
<p class="hint">We'll keep your email private and never share it.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button type="submit" disabled={isWaiting}>
|
||||||
|
{isWaiting ? (
|
||||||
|
<span class="spinner" aria-hidden="true"></span>
|
||||||
|
) : null}
|
||||||
|
{type === 'waitlist' ? 'Join Waitlist' : 'Send Message'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{errorMessage && (
|
||||||
|
<p class="form-error" role="alert">
|
||||||
|
{errorMessage}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.form-success) {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
background: var(--color-green);
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.form-success h4) {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.form-group) {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.form-group label) {
|
||||||
|
display: block;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.375rem;
|
||||||
|
color: var(--color-brown-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.form-group label .required) {
|
||||||
|
color: var(--color-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.form-group input),
|
||||||
|
:global(.form-group select),
|
||||||
|
:global(.form-group textarea) {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.625rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.form-group input:focus),
|
||||||
|
:global(.form-group select:focus),
|
||||||
|
:global(.form-group textarea:focus) {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.form-group .hint) {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin: 0.25rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.form-group button) {
|
||||||
|
background: var(--color-amber);
|
||||||
|
color: white;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.form-group button:hover) {
|
||||||
|
background: var(--color-amber-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.form-group button:disabled) {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.spinner) {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid white;
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.75s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.form-error) {
|
||||||
|
background: #dc2626;
|
||||||
|
color: white;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 1rem;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
223
src/components/SEO.astro
Normal file
223
src/components/SEO.astro
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
---
|
||||||
|
import { useState } from 'astro:collection-utils';
|
||||||
|
|
||||||
|
interface SEOProps {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
ogImage?: string;
|
||||||
|
ogType?: 'website' | 'article' | 'book' | 'profile';
|
||||||
|
url?: string;
|
||||||
|
pageType?: 'home' | 'speaking' | 'books' | 'consulting' | 'contact' | 'blog';
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
ogImage = '/social-card.png',
|
||||||
|
ogType = 'website',
|
||||||
|
url = Astro.url.pathname,
|
||||||
|
pageType = 'home',
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const siteUrl = 'https://scott.scoble.me';
|
||||||
|
const fullTitle = title.includes('Scott Scoble') ? title : `${title} — Scott Scoble`;
|
||||||
|
const imageUrl = new URL(ogImage, siteUrl).href;
|
||||||
|
const pageUrl = new URL(url, siteUrl).href;
|
||||||
|
---
|
||||||
|
|
||||||
|
<meta name="title" content={fullTitle} />
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
<meta name="author" content="Scott Scoble" />
|
||||||
|
|
||||||
|
<meta property="og:title" content={fullTitle} />
|
||||||
|
<meta property="og:description" content={description} />
|
||||||
|
<meta property="og:type" content={ogType} />
|
||||||
|
<meta property="og:url" content={pageUrl} />
|
||||||
|
<meta property="og:image" content={imageUrl} />
|
||||||
|
<meta property="og:site_name" content="Scott Scoble" />
|
||||||
|
<meta property="og:locale" content="en_US" />
|
||||||
|
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content={fullTitle} />
|
||||||
|
<meta name="twitter:description" content={description} />
|
||||||
|
<meta name="twitter:image" content={imageUrl} />
|
||||||
|
<meta name="twitter:site" content="@scottscoble" />
|
||||||
|
|
||||||
|
<link rel="canonical" href={pageUrl} />
|
||||||
|
|
||||||
|
<JsonLd pageType={pageType} />
|
||||||
|
|
||||||
|
<Style is:global>
|
||||||
|
:root {
|
||||||
|
--color-brown: #4a4438;
|
||||||
|
--color-brown-dark: #2d2a24;
|
||||||
|
--color-green: #3d5a3e;
|
||||||
|
--color-cream: #faf8f5;
|
||||||
|
--color-amber: #c8952e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base theme color */
|
||||||
|
meta[name="theme-color"][content] {
|
||||||
|
content: var(--color-green);
|
||||||
|
}
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
---
|
||||||
|
function JsonLd({ pageType }: { pageType: SEOProps['pageType'] }) {
|
||||||
|
const person = {
|
||||||
|
'@type': 'Person',
|
||||||
|
'@id': `${siteUrl}#person`,
|
||||||
|
name: 'Scott Scoble',
|
||||||
|
givenName: 'Scott',
|
||||||
|
familyName: 'Scoble',
|
||||||
|
jobTitle: 'Human Cultivation Expert',
|
||||||
|
url: siteUrl,
|
||||||
|
email: 'mailto:scott@scoble.me',
|
||||||
|
sameAs: [
|
||||||
|
'https://linkedin.com/in/scottscoble',
|
||||||
|
'https://x.com/scottscoble',
|
||||||
|
'https://github.com/sscoble',
|
||||||
|
'https://amzn.com/author/scottscoble',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const website = {
|
||||||
|
'@type': 'WebSite',
|
||||||
|
'@id': `${siteUrl}#website`,
|
||||||
|
name: 'Scott Scoble',
|
||||||
|
description: 'Human Cultivation Expert for the AI Age',
|
||||||
|
url: siteUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (pageType) {
|
||||||
|
case 'home':
|
||||||
|
return (
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{JSON.stringify([person, website], null, 2)}
|
||||||
|
</script>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'speaking':
|
||||||
|
return (
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{JSON.stringify(
|
||||||
|
{
|
||||||
|
'@type': 'Person',
|
||||||
|
'@id': `${siteUrl}#speaker`,
|
||||||
|
name: 'Scott Scoble',
|
||||||
|
jobTitle: 'Keynote Speaker',
|
||||||
|
description: 'Human Cultivation Expert speaking on AI, leadership, and team cultivation',
|
||||||
|
url: siteUrl,
|
||||||
|
sameAs: person.sameAs,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}
|
||||||
|
</script>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'books':
|
||||||
|
return (
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{JSON.stringify(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
'@type': 'Book',
|
||||||
|
name: 'The Soil Beneath the Code',
|
||||||
|
author: 'Scott Scoble',
|
||||||
|
datePublished: '2023',
|
||||||
|
bookFormat: 'EBook, Hardcover',
|
||||||
|
isbn: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Book',
|
||||||
|
name: 'A Walk in Mentoring',
|
||||||
|
author: 'Scott Scoble',
|
||||||
|
datePublished: '2024',
|
||||||
|
bookFormat: 'EBook, Paperback',
|
||||||
|
isbn: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Book',
|
||||||
|
name: 'Dark Provenance',
|
||||||
|
author: 'Scott Scoble',
|
||||||
|
datePublished: '2026',
|
||||||
|
bookFormat: 'EBook, Hardcover',
|
||||||
|
isbn: '',
|
||||||
|
position: 'Forthcoming',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}
|
||||||
|
</script>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'consulting':
|
||||||
|
return (
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{JSON.stringify(
|
||||||
|
{
|
||||||
|
'@type': 'Service',
|
||||||
|
name: 'AI Team Consulting',
|
||||||
|
description: 'Helping leaders build AI systems that cultivate humans instead of strip-mining them',
|
||||||
|
provider: {
|
||||||
|
'@type': 'Person',
|
||||||
|
name: 'Scott Scoble',
|
||||||
|
},
|
||||||
|
serviceType: 'Professional Consulting',
|
||||||
|
areaServed: 'Worldwide',
|
||||||
|
offering: [
|
||||||
|
{
|
||||||
|
'@type': 'Offering',
|
||||||
|
name: 'Keynote Speaking',
|
||||||
|
description: '45-60 minute keynotes on AI, leadership, and team cultivation',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Offering',
|
||||||
|
name: 'Workshops',
|
||||||
|
description: 'Half-day or full-day workshops on cultivation practices',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}
|
||||||
|
</script>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'contact':
|
||||||
|
return (
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{JSON.stringify(
|
||||||
|
{
|
||||||
|
'@type': 'ContactPoint',
|
||||||
|
telephone: '+1-555-000-0000',
|
||||||
|
email: 'scott@scoble.me',
|
||||||
|
contactType: 'Customer Service',
|
||||||
|
availableLanguage: 'English',
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}
|
||||||
|
</script>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'blog':
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{JSON.stringify(
|
||||||
|
{
|
||||||
|
'@type': 'Blog',
|
||||||
|
name: 'Scott Scoble Blog',
|
||||||
|
description: 'Thoughts on AI, leadership, and human cultivation',
|
||||||
|
url: `${siteUrl}/blog`,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}
|
||||||
|
</script>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import '../styles/global.css';
|
import '../styles/global.css';
|
||||||
import Nav from '../components/Nav.astro';
|
import Nav from '../components/Nav.astro';
|
||||||
import Footer from '../components/Footer.astro';
|
import Footer from '../components/Footer.astro';
|
||||||
|
import SEO from '../components/SEO.astro';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -10,6 +11,7 @@ export interface Props {
|
||||||
ogImage?: string;
|
ogImage?: string;
|
||||||
ogType?: string;
|
ogType?: string;
|
||||||
canonicalUrl?: string;
|
canonicalUrl?: string;
|
||||||
|
pageType?: 'home' | 'speaking' | 'books' | 'consulting' | 'contact' | 'blog';
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -18,10 +20,10 @@ const {
|
||||||
ogImage = '/social-card.png',
|
ogImage = '/social-card.png',
|
||||||
ogType = 'website',
|
ogType = 'website',
|
||||||
canonicalUrl,
|
canonicalUrl,
|
||||||
|
pageType = 'home',
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
const siteTitle = 'Scott Scoble';
|
const siteTitle = 'Scott Scoble';
|
||||||
const fullTitle = title.includes(siteTitle) ? title : `${title} — ${siteTitle}`;
|
|
||||||
const siteUrl = 'https://scott.scoble.me';
|
const siteUrl = 'https://scott.scoble.me';
|
||||||
const currentPath = canonicalUrl || Astro.url.pathname;
|
const currentPath = canonicalUrl || Astro.url.pathname;
|
||||||
---
|
---
|
||||||
|
|
@ -37,35 +39,18 @@ const currentPath = canonicalUrl || Astro.url.pathname;
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
|
||||||
<!-- Primary Meta -->
|
<SEO
|
||||||
<title>{fullTitle}</title>
|
title={title}
|
||||||
<meta name="description" content={description} />
|
description={description}
|
||||||
<meta name="author" content="Scott Scoble" />
|
ogImage={ogImage}
|
||||||
|
ogType={ogType}
|
||||||
<!-- Open Graph -->
|
canonicalUrl={canonicalUrl}
|
||||||
<meta property="og:title" content={fullTitle} />
|
pageType={pageType}
|
||||||
<meta property="og:description" content={description} />
|
/>
|
||||||
<meta property="og:type" content={ogType} />
|
|
||||||
<meta property="og:url" content={new URL(currentPath, siteUrl).href} />
|
|
||||||
<meta property="og:image" content={new URL(ogImage, siteUrl).href} />
|
|
||||||
<meta property="og:site_name" content="Scott Scoble" />
|
|
||||||
<meta property="og:locale" content="en_US" />
|
|
||||||
|
|
||||||
<!-- Twitter Card -->
|
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
|
||||||
<meta name="twitter:title" content={fullTitle} />
|
|
||||||
<meta name="twitter:description" content={description} />
|
|
||||||
<meta name="twitter:image" content={new URL(ogImage, siteUrl).href} />
|
|
||||||
|
|
||||||
<!-- Canonical -->
|
|
||||||
<link rel="canonical" href={new URL(currentPath, siteUrl).href} />
|
|
||||||
|
|
||||||
<!-- RSS -->
|
<!-- RSS -->
|
||||||
<link rel="alternate" type="application/rss+xml" title="Scott Scoble Blog" href="/rss.xml" />
|
<link rel="alternate" type="application/rss+xml" title="Scott Scoble Blog" href="/rss.xml" />
|
||||||
|
|
||||||
<!-- Umami Analytics -->
|
|
||||||
<script defer src="https://os.scoble.me/umami/script.js" data-website-id="4a23c88a-5397-4bac-a830-074c6aca7108"></script>
|
|
||||||
|
|
||||||
<!-- Google Fonts -->
|
<!-- Google Fonts -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
title="Page Not Found — Scott Scoble"
|
title="Page Not Found — Scott Scoble"
|
||||||
description="That page doesn't exist."
|
description="That page doesn't exist."
|
||||||
|
pageType="home"
|
||||||
>
|
>
|
||||||
|
|
||||||
<section class="error-page">
|
<section class="error-page">
|
||||||
|
|
@ -12,7 +13,20 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
<span class="error-code">404</span>
|
<span class="error-code">404</span>
|
||||||
<h1>Page not found</h1>
|
<h1>Page not found</h1>
|
||||||
<p>That link doesn't lead anywhere. Maybe it moved, maybe it never existed.</p>
|
<p>That link doesn't lead anywhere. Maybe it moved, maybe it never existed.</p>
|
||||||
<a href="/" class="btn">Go home</a>
|
|
||||||
|
<div class="suggested-links">
|
||||||
|
<h3>Explore the site</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/speaking">Book Scott for Your Event</a></li>
|
||||||
|
<li><a href="/consulting">AI Team Consulting</a></li>
|
||||||
|
<li><a href="/books">Books & Publications</a></li>
|
||||||
|
<li><a href="/about">About Scott</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Can't find what you're looking for? <a href="/contact">Get in touch</a> — I'll get back to you.</p>
|
||||||
|
|
||||||
|
<a href="/" class="btn-home">← Go home</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -30,7 +44,8 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-content {
|
.error-content {
|
||||||
max-width: 500px;
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-code {
|
.error-code {
|
||||||
|
|
@ -57,18 +72,60 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.suggested-links {
|
||||||
|
text-align: left;
|
||||||
|
background: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
margin: 0 0 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggested-links h3 {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggested-links ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggested-links li {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggested-links a {
|
||||||
|
color: var(--color-green);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggested-links a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-content a {
|
||||||
|
color: var(--color-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-home {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background: var(--color-amber);
|
background: var(--color-amber);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0.875rem 2rem;
|
padding: 0.75rem 1.5rem;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:hover {
|
.btn-home:hover {
|
||||||
background: var(--color-amber-light);
|
background: var(--color-amber-light);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
---
|
---
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
|
import SharedForm from '../components/Form/SharedForm.astro';
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
title="Books — Scott Scoble"
|
title="Books — Scott Scoble"
|
||||||
description="Published works on cultivation, mentoring, and the human side of AI."
|
description="Published works on cultivation, mentoring, and the human side of AI."
|
||||||
|
pageType="books"
|
||||||
>
|
>
|
||||||
|
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
|
|
@ -58,7 +60,7 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
<p class="book-description">Formal verification for the AI age. How to audit what your AI actually did.</p>
|
<p class="book-description">Formal verification for the AI age. How to audit what your AI actually did.</p>
|
||||||
<p class="book-quote">"Speckl and the dark provenance system. A new approach to AI accountability."</p>
|
<p class="book-quote">"Speckl and the dark provenance system. A new approach to AI accountability."</p>
|
||||||
|
|
||||||
<a href="#" class="waitlist-btn">Join Waitlist</a>
|
<SharedForm type="waitlist" />
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import PackageCard from '../components/PackageCard.astro';
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
title="Consulting — Scott Scoble"
|
title="Consulting — Scott Scoble"
|
||||||
description="Consulting packages for engineering leaders: Soil Assessment, Cultivation Workshop, and Fractional CTO services."
|
description="Consulting packages for engineering leaders: Soil Assessment, Cultivation Workshop, and Fractional CTO services."
|
||||||
|
pageType="consulting"
|
||||||
>
|
>
|
||||||
|
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
---
|
---
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
|
import SharedForm from '../components/Form/SharedForm.astro';
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
title="Contact — Scott Scoble"
|
title="Contact — Scott Scoble"
|
||||||
description="Get in touch about speaking, consulting, or just to say hello."
|
description="Get in touch about speaking, consulting, or just to say hello."
|
||||||
|
pageType="contact"
|
||||||
>
|
>
|
||||||
|
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
|
|
@ -22,34 +24,7 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
<div class="contact-form-wrapper">
|
<div class="contact-form-wrapper">
|
||||||
<h2>Send a Message</h2>
|
<h2>Send a Message</h2>
|
||||||
|
|
||||||
<form action="https://formspree.io/f/PLACEHOLDER" method="POST">
|
<SharedForm type="contact" />
|
||||||
<div class="form-group">
|
|
||||||
<label for="name">Name *</label>
|
|
||||||
<input type="text" id="name" name="name" required />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="email">Email *</label>
|
|
||||||
<input type="email" id="email" name="email" required />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="topic">Topic</label>
|
|
||||||
<select id="topic" name="topic">
|
|
||||||
<option value="">Select...</option>
|
|
||||||
<option value="speaking">Speaking Inquiry</option>
|
|
||||||
<option value="consulting">Consulting Inquiry</option>
|
|
||||||
<option value="general">General</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="message">Message *</label>
|
|
||||||
<textarea id="message" name="message" rows="5" required></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="submit-btn">Send Message</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Alternative Contact -->
|
<!-- Alternative Contact -->
|
||||||
|
|
@ -63,14 +38,14 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
|
|
||||||
<div class="contact-option">
|
<div class="contact-option">
|
||||||
<h3>📅 Discovery Call</h3>
|
<h3>📅 Discovery Call</h3>
|
||||||
<p><a href="#">Schedule a 20-min call</a> (Calendly link coming soon)</p>
|
<p><a href="#" title="Calendly link coming soon">Schedule a 20-min call</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="contact-option">
|
<div class="contact-option">
|
||||||
<h3>🐦 Social</h3>
|
<h3>🐦 Social</h3>
|
||||||
<p>
|
<p>
|
||||||
<a href="https://linkedin.com/in/scottscoble" target="_blank" rel="noopener">LinkedIn</a> •
|
<a href="https://linkedin.com/in/scottscoble" target="_blank" rel="noopener">LinkedIn</a> •
|
||||||
<a href="https://twitter.com/scottscoble" target="_blank" rel="noopener">X/Twitter</a> •
|
<a href="https://x.com/scottscoble" target="_blank" rel="noopener">X/Twitter</a> •
|
||||||
<a href="https://github.com/sscoble" target="_blank" rel="noopener">GitHub</a>
|
<a href="https://github.com/sscoble" target="_blank" rel="noopener">GitHub</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import PillarCard from '../components/PillarCard.astro';
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
title="Scott Scoble — Human Cultivation Expert for the AI Age"
|
title="Scott Scoble — Human Cultivation Expert for the AI Age"
|
||||||
description="Keynote speaker and consultant helping leaders build AI systems that cultivate humans instead of strip-mining them. Author of The Soil Beneath the Code."
|
description="Keynote speaker and consultant helping leaders build AI systems that cultivate humans instead of strip-mining them. Author of The Soil Beneath the Code."
|
||||||
|
pageType="home"
|
||||||
>
|
>
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,11 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
<p>If you submit a contact form, your email and message are sent to me directly. I don't share this information with third parties.</p>
|
<p>If you submit a contact form, your email and message are sent to me directly. I don't share this information with third parties.</p>
|
||||||
|
|
||||||
<h2>Analytics</h2>
|
<h2>Analytics</h2>
|
||||||
<p>This site does not use Google Analytics or other tracking scripts.</p>
|
<p>This site uses <a href="https://umami.is" target="_blank" rel="noopener">Umami Analytics</a>, a privacy-focused, first-party analytics tool. Umami does not use cookies, does not track you across sites, and does not collect personal data.</p>
|
||||||
|
<p>You can see the privacy policy at <a href="https://umami.is/privacy" target="_blank" rel="noopener">umami.is/privacy</a>.</p>
|
||||||
|
|
||||||
<h2>Cookies</h2>
|
<h2>Cookies</h2>
|
||||||
<p>No cookies are set by this site.</p>
|
<p>No cookies are set by this site. Umami Analytics uses localStorage (not cookies) for anonymous session tracking.</p>
|
||||||
|
|
||||||
<h2>Hosting</h2>
|
<h2>Hosting</h2>
|
||||||
<p>This site is hosted on Storj. Their privacy policy applies to infrastructure-level logging.</p>
|
<p>This site is hosted on Storj. Their privacy policy applies to infrastructure-level logging.</p>
|
||||||
|
|
|
||||||
44
src/pages/sitemap.xml.ts
Normal file
44
src/pages/sitemap.xml.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import { getSortedPosts } from '../utils/posts';
|
||||||
|
|
||||||
|
const posts = await getCollection('blog');
|
||||||
|
const speakingPosts = await getCollection('speaking');
|
||||||
|
|
||||||
|
const pages = [
|
||||||
|
{ url: '/', lastmod: new Date().toISOString() },
|
||||||
|
{ url: '/speaking', lastmod: new Date().toISOString() },
|
||||||
|
{ url: '/consulting', lastmod: new Date().toISOString() },
|
||||||
|
{ url: '/books', lastmod: new Date().toISOString() },
|
||||||
|
{ url: '/contact', lastmod: new Date().toISOString() },
|
||||||
|
...posts.map(post => ({
|
||||||
|
url: `/blog/${post.slug}`,
|
||||||
|
lastmod: post.data.updated || post.data.date,
|
||||||
|
})),
|
||||||
|
...speakingPosts.map(talk => ({
|
||||||
|
url: `/speaking#talk-${talk.slug}`,
|
||||||
|
lastmod: new Date().toISOString(),
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Render the sitemap XML
|
||||||
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
${pages.map(page => `
|
||||||
|
<url>
|
||||||
|
<loc>${new URL(page.url, 'https://scott.scoble.me').href}</loc>
|
||||||
|
<lastmod>${typeof page.lastmod === 'string' ? page.lastmod : new Date(page.lastmod).toISOString().split('T')[0]}</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>${page.url === '/' ? '1.0' : '0.8'}</priority>
|
||||||
|
</url>
|
||||||
|
`).join('')}
|
||||||
|
</urlset>`;
|
||||||
|
|
||||||
|
export const GET = () => {
|
||||||
|
return new Response(xml, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/xml',
|
||||||
|
'Cache-Control': 'public, max-age=3600',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
---
|
---
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
import TalkCard from '../components/TalkCard.astro';
|
import TalkCard from '../components/TalkCard.astro';
|
||||||
|
import SharedForm from '../components/Form/SharedForm.astro';
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
title="Speaking — Scott Scoble"
|
title="Speaking — Scott Scoble"
|
||||||
description="Keynote speaker on AI, leadership, and human cultivation. Book Scott for your next conference or corporate event."
|
description="Keynote speaker on AI, leadership, and human cultivation. Book Scott for your next conference or corporate event."
|
||||||
|
pageType="speaking"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- Hero -->
|
<!-- Hero -->
|
||||||
|
|
@ -108,40 +110,7 @@ import TalkCard from '../components/TalkCard.astro';
|
||||||
<div class="book-form">
|
<div class="book-form">
|
||||||
<h3>Inquiry Form</h3>
|
<h3>Inquiry Form</h3>
|
||||||
|
|
||||||
<form action="https://formspree.io/f/PLACEHOLDER" method="POST">
|
<SharedForm type="speaking" />
|
||||||
<div class="form-group">
|
|
||||||
<label for="name">Name *</label>
|
|
||||||
<input type="text" id="name" name="name" required />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="email">Email *</label>
|
|
||||||
<input type="email" id="email" name="email" required />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="event-date">Event Date</label>
|
|
||||||
<input type="date" id="event-date" name="event-date" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="event-type">Event Type</label>
|
|
||||||
<select id="event-type" name="event-type">
|
|
||||||
<option value="">Select...</option>
|
|
||||||
<option value="conference">Conference</option>
|
|
||||||
<option value="corporate">Corporate Event</option>
|
|
||||||
<option value="workshop">Workshop</option>
|
|
||||||
<option value="other">Other</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="message">Message *</label>
|
|
||||||
<textarea id="message" name="message" rows="4" required></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="submit-btn">Send Inquiry</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue