This commit is contained in:
34
site/src/pages/about.astro
Normal file
34
site/src/pages/about.astro
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import CtaLink from "../components/CtaLink.astro";
|
||||
import { LINKS } from "../lib/links";
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="About | SanthoshJ"
|
||||
description="About SanthoshJ and where to follow."
|
||||
canonicalPath="/about"
|
||||
>
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>About</h2>
|
||||
<span class="muted">Tech, streaming, movies, travel</span>
|
||||
</div>
|
||||
<div class="empty">
|
||||
<p style="margin-top: 0;">
|
||||
This is a lightweight site that aggregates my content so it can be discovered via search and
|
||||
shared cleanly.
|
||||
</p>
|
||||
<div class="cta-row">
|
||||
<CtaLink platform="youtube" placement="about" url={LINKS.youtubeChannel} label="YouTube" />
|
||||
<CtaLink
|
||||
platform="instagram"
|
||||
placement="about"
|
||||
url={LINKS.instagramProfile}
|
||||
label="Instagram"
|
||||
/>
|
||||
<CtaLink platform="podcast" placement="about" url={LINKS.podcast} label="Podcast" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
238
site/src/pages/index.astro
Normal file
238
site/src/pages/index.astro
Normal file
@@ -0,0 +1,238 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import CtaLink from "../components/CtaLink.astro";
|
||||
import ContentCard from "../components/ContentCard.astro";
|
||||
import InstagramEmbed from "../components/InstagramEmbed.astro";
|
||||
import { readContentCache } from "../lib/content/cache";
|
||||
import {
|
||||
newestItems,
|
||||
highPerformingYoutubeVideos,
|
||||
instagramPosts,
|
||||
podcastEpisodes,
|
||||
} from "../lib/content/selectors";
|
||||
import { readFeaturedVideoIds } from "../lib/content/curation";
|
||||
import { LINKS } from "../lib/links";
|
||||
|
||||
const cache = await readContentCache();
|
||||
const featuredIds = await readFeaturedVideoIds();
|
||||
|
||||
const newest = newestItems(cache, 9);
|
||||
const highPerf = highPerformingYoutubeVideos(cache, 6, featuredIds);
|
||||
const ig = instagramPosts(cache).slice(0, 6);
|
||||
const pods = podcastEpisodes(cache)
|
||||
.slice(0, 6)
|
||||
.sort((a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt));
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="SanthoshJ | Tech, streaming, movies, travel"
|
||||
description="A fast, SEO-first home for videos, movie posts, and the Irregular Mind podcast."
|
||||
canonicalPath="/"
|
||||
>
|
||||
<section class="hero">
|
||||
<div>
|
||||
<h1>Fast content. Clear next actions.</h1>
|
||||
<p>
|
||||
I post about technology, game streaming, movies, and travel. This site collects the best of
|
||||
it and points you to the platform you prefer.
|
||||
</p>
|
||||
<div class="cta-row">
|
||||
<CtaLink
|
||||
platform="youtube"
|
||||
placement="hero"
|
||||
url={LINKS.youtubeChannel}
|
||||
label="Subscribe on YouTube"
|
||||
class="primary"
|
||||
/>
|
||||
<CtaLink
|
||||
platform="instagram"
|
||||
placement="hero"
|
||||
url={LINKS.instagramProfile}
|
||||
label="Follow on Instagram"
|
||||
/>
|
||||
<CtaLink
|
||||
platform="podcast"
|
||||
placement="hero"
|
||||
url={LINKS.podcast}
|
||||
label="Listen to the podcast"
|
||||
/>
|
||||
</div>
|
||||
<p class="muted" style="margin-top: 14px;">
|
||||
Last updated: {new Date(cache.generatedAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div class="empty">
|
||||
<strong>Goal:</strong> 10% month-over-month growth in followers and engagement.
|
||||
<br />
|
||||
<span class="muted"
|
||||
>This site is the SEO landing surface that turns search traffic into followers.</span
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>Newest</h2>
|
||||
<a
|
||||
class="muted"
|
||||
href="/videos"
|
||||
data-umami-event="click"
|
||||
data-umami-event-target_id="section_header.newest.browse_all"
|
||||
data-umami-event-placement="section_header"
|
||||
data-umami-event-target_url="/videos"
|
||||
>
|
||||
Browse all →
|
||||
</a>
|
||||
</div>
|
||||
{
|
||||
newest.length > 0 ? (
|
||||
<div class="grid">
|
||||
{newest.map((item) => (
|
||||
<ContentCard item={item} placement="home.newest" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div class="empty">
|
||||
No content cache yet. Run <code>npm run fetch-content</code> in <code>site/</code>.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>High-performing videos</h2>
|
||||
<a
|
||||
class="muted"
|
||||
href="/videos"
|
||||
data-umami-event="click"
|
||||
data-umami-event-target_id="section_header.high_performing.videos"
|
||||
data-umami-event-placement="section_header"
|
||||
data-umami-event-target_url="/videos"
|
||||
>
|
||||
Videos →
|
||||
</a>
|
||||
</div>
|
||||
{
|
||||
highPerf.length > 0 ? (
|
||||
<div class="grid">
|
||||
{highPerf.map((item) => (
|
||||
<ContentCard item={item} placement="home.high_performing" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div class="empty">
|
||||
No video stats found yet. Add <code>YOUTUBE_API_KEY</code> or curate{" "}
|
||||
<code>content/featured-videos.json</code>.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>Instagram</h2>
|
||||
<a
|
||||
class="muted"
|
||||
href={LINKS.instagramProfile}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-umami-event="outbound_click"
|
||||
data-umami-event-target_id="section_header.instagram.profile"
|
||||
data-umami-event-placement="section_header"
|
||||
data-umami-event-target_url={LINKS.instagramProfile}
|
||||
data-umami-event-domain="www.instagram.com"
|
||||
data-umami-event-source="instagram"
|
||||
data-umami-event-ui_placement="section_header"
|
||||
>
|
||||
Profile →
|
||||
</a>
|
||||
</div>
|
||||
{
|
||||
ig.length > 0 ? (
|
||||
<div class="grid">
|
||||
{ig.map((item) => (
|
||||
<InstagramEmbed url={item.url} placement="home.instagram" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div class="empty">
|
||||
Add post URLs to <code>content/instagram-posts.json</code> to show Instagram here.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
|
||||
{
|
||||
ig.length > 0 ? (
|
||||
<script
|
||||
is:inline
|
||||
define:vars={{}}
|
||||
set:html={`
|
||||
(function(){
|
||||
var s = document.createElement('script');
|
||||
s.async = true;
|
||||
s.defer = true;
|
||||
s.src = 'https://www.instagram.com/embed.js';
|
||||
s.onload = function(){
|
||||
try { window.instgrm && window.instgrm.Embeds && window.instgrm.Embeds.process(); } catch(e) {}
|
||||
};
|
||||
document.head.appendChild(s);
|
||||
})();
|
||||
`}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>Podcast: Irregular Mind</h2>
|
||||
<a
|
||||
class="muted"
|
||||
href="/podcast"
|
||||
data-umami-event="click"
|
||||
data-umami-event-target_id="section_header.podcast.episodes"
|
||||
data-umami-event-placement="section_header"
|
||||
data-umami-event-target_url="/podcast"
|
||||
>
|
||||
Episodes →
|
||||
</a>
|
||||
</div>
|
||||
{
|
||||
pods.length > 0 ? (
|
||||
<div class="grid">
|
||||
{pods.map((item) => (
|
||||
<ContentCard item={item} placement="home.podcast" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div class="empty">
|
||||
Set <code>PODCAST_RSS_URL</code> and run <code>npm run fetch-content</code> to populate
|
||||
episodes.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>Follow</h2>
|
||||
<span class="muted">Pick your platform</span>
|
||||
</div>
|
||||
<div class="cta-row">
|
||||
<CtaLink
|
||||
platform="youtube"
|
||||
placement="footer_cta"
|
||||
url={LINKS.youtubeChannel}
|
||||
label="YouTube"
|
||||
/>
|
||||
<CtaLink
|
||||
platform="instagram"
|
||||
placement="footer_cta"
|
||||
url={LINKS.instagramProfile}
|
||||
label="Instagram"
|
||||
/>
|
||||
<CtaLink platform="podcast" placement="footer_cta" url={LINKS.podcast} label="Podcast" />
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
38
site/src/pages/podcast.astro
Normal file
38
site/src/pages/podcast.astro
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import ContentCard from "../components/ContentCard.astro";
|
||||
import { readContentCache } from "../lib/content/cache";
|
||||
import { podcastEpisodes } from "../lib/content/selectors";
|
||||
|
||||
const cache = await readContentCache();
|
||||
const episodes = podcastEpisodes(cache).sort(
|
||||
(a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt),
|
||||
);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Podcast | Irregular Mind"
|
||||
description="Episodes from the Irregular Mind podcast."
|
||||
canonicalPath="/podcast"
|
||||
>
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>Irregular Mind</h2>
|
||||
<span class="muted">{episodes.length} episodes</span>
|
||||
</div>
|
||||
{
|
||||
episodes.length > 0 ? (
|
||||
<div class="grid">
|
||||
{episodes.map((item) => (
|
||||
<ContentCard item={item} placement="podcast.list" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div class="empty">
|
||||
No episodes yet. Set <code>PODCAST_RSS_URL</code> and run{" "}
|
||||
<code>npm run fetch-content</code>.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
</BaseLayout>
|
||||
74
site/src/pages/podcast/[id].astro
Normal file
74
site/src/pages/podcast/[id].astro
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||
import { readContentCache } from "../../lib/content/cache";
|
||||
import { podcastEpisodes } from "../../lib/content/selectors";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const cache = await readContentCache();
|
||||
const eps = podcastEpisodes(cache);
|
||||
return eps.map((e) => ({ params: { id: e.id }, props: { episode: e } }));
|
||||
}
|
||||
|
||||
const { episode } = Astro.props;
|
||||
|
||||
const jsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "PodcastEpisode",
|
||||
name: episode.title,
|
||||
datePublished: episode.publishedAt,
|
||||
url: episode.url,
|
||||
};
|
||||
|
||||
let episodeDomain = "";
|
||||
try {
|
||||
episodeDomain = new URL(episode.url).hostname;
|
||||
} catch {
|
||||
episodeDomain = "";
|
||||
}
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={`${episode.title} | Irregular Mind`}
|
||||
description={`Listen: ${episode.title}`}
|
||||
canonicalPath={`/podcast/${encodeURIComponent(episode.id)}`}
|
||||
ogImageUrl={episode.thumbnailUrl}
|
||||
>
|
||||
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>{episode.title}</h2>
|
||||
<a
|
||||
class="muted"
|
||||
href="/podcast"
|
||||
data-umami-event="click"
|
||||
data-umami-event-target_id="podcast_detail.back_to_podcast"
|
||||
data-umami-event-placement="section_header"
|
||||
data-umami-event-target_url="/podcast"
|
||||
>
|
||||
Back to podcast →
|
||||
</a>
|
||||
</div>
|
||||
<div class="empty">
|
||||
<p style="margin-top: 0;">
|
||||
This page exists for SEO and sharing. Listening happens on your platform.
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
class="cta primary"
|
||||
href={episode.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-umami-event="outbound_click"
|
||||
data-umami-event-target_id={`podcast_detail.open.${episode.id}`}
|
||||
data-umami-event-placement="podcast_detail"
|
||||
data-umami-event-target_url={episode.url}
|
||||
data-umami-event-domain={episodeDomain || "unknown"}
|
||||
data-umami-event-source="podcast"
|
||||
data-umami-event-ui_placement="podcast_detail"
|
||||
>
|
||||
Open episode
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
38
site/src/pages/videos.astro
Normal file
38
site/src/pages/videos.astro
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import ContentCard from "../components/ContentCard.astro";
|
||||
import { readContentCache } from "../lib/content/cache";
|
||||
import { youtubeVideos } from "../lib/content/selectors";
|
||||
|
||||
const cache = await readContentCache();
|
||||
const videos = youtubeVideos(cache).sort(
|
||||
(a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt),
|
||||
);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Videos | SanthoshJ"
|
||||
description="Latest and featured YouTube videos."
|
||||
canonicalPath="/videos"
|
||||
>
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>Videos</h2>
|
||||
<span class="muted">{videos.length} items</span>
|
||||
</div>
|
||||
{
|
||||
videos.length > 0 ? (
|
||||
<div class="grid">
|
||||
{videos.map((item) => (
|
||||
<ContentCard item={item} placement="videos.list" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div class="empty">
|
||||
No videos yet. Configure <code>YOUTUBE_CHANNEL_ID</code> and run{" "}
|
||||
<code>npm run fetch-content</code>.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
</BaseLayout>
|
||||
75
site/src/pages/videos/[id].astro
Normal file
75
site/src/pages/videos/[id].astro
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
import BaseLayout from "../../layouts/BaseLayout.astro";
|
||||
import { readContentCache } from "../../lib/content/cache";
|
||||
import { youtubeVideos } from "../../lib/content/selectors";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const cache = await readContentCache();
|
||||
const videos = youtubeVideos(cache);
|
||||
return videos.map((v) => ({ params: { id: v.id }, props: { video: v } }));
|
||||
}
|
||||
|
||||
const { video } = Astro.props;
|
||||
|
||||
const jsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "VideoObject",
|
||||
name: video.title,
|
||||
uploadDate: video.publishedAt,
|
||||
thumbnailUrl: video.thumbnailUrl ? [video.thumbnailUrl] : undefined,
|
||||
url: video.url,
|
||||
};
|
||||
|
||||
let videoDomain = "";
|
||||
try {
|
||||
videoDomain = new URL(video.url).hostname;
|
||||
} catch {
|
||||
videoDomain = "";
|
||||
}
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={`${video.title} | SanthoshJ`}
|
||||
description={`Watch: ${video.title}`}
|
||||
canonicalPath={`/videos/${encodeURIComponent(video.id)}`}
|
||||
ogImageUrl={video.thumbnailUrl}
|
||||
>
|
||||
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>{video.title}</h2>
|
||||
<a
|
||||
class="muted"
|
||||
href="/videos"
|
||||
data-umami-event="click"
|
||||
data-umami-event-target_id="video_detail.back_to_videos"
|
||||
data-umami-event-placement="section_header"
|
||||
data-umami-event-target_url="/videos"
|
||||
>
|
||||
Back to videos →
|
||||
</a>
|
||||
</div>
|
||||
<div class="empty">
|
||||
<p style="margin-top: 0;">
|
||||
This page exists for SEO and sharing. The canonical watch page is YouTube.
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
class="cta primary"
|
||||
href={video.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-umami-event="outbound_click"
|
||||
data-umami-event-target_id={`video_detail.watch.${video.id}`}
|
||||
data-umami-event-placement="video_detail"
|
||||
data-umami-event-target_url={video.url}
|
||||
data-umami-event-domain={videoDomain || "unknown"}
|
||||
data-umami-event-source="youtube"
|
||||
data-umami-event-ui_placement="video_detail"
|
||||
>
|
||||
Watch on YouTube
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
Reference in New Issue
Block a user