initial: Astro port of ViralMVP with favicon + SEO redirects

This commit is contained in:
2026-04-20 04:52:47 +00:00
commit cdf833d61b
198 changed files with 7220 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
---
import Layout from '../../layouts/Layout.astro';
import { site } from '../../lib/site.js';
import {
getAllPosts,
renderMarkdown,
getExcerpt,
getRelatedPosts,
extractFirstImage,
} from '../../lib/markdown.js';
export function getStaticPaths() {
const posts = getAllPosts();
const reserved = new Set(['about', 'rss.xml', 'robots.txt', 'sitemap.xml', 'sitemap-index.xml']);
return posts
.filter((p) => !reserved.has(p.slug))
.map((post) => ({ params: { slug: post.slug }, props: { post } }));
}
const { post } = Astro.props;
const allPosts = getAllPosts();
const html = renderMarkdown(post.body);
const related = getRelatedPosts(post, allPosts);
const excerpt = getExcerpt(post.body, 160);
const firstImage = extractFirstImage(html);
const fullImage = firstImage ? `${site.url}${firstImage}` : null;
const canonical = `${site.url}/${post.slug}/`;
const structuredData = JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
datePublished: post.date,
dateModified: post.date,
author: { '@type': 'Organization', name: site.name },
publisher: {
'@type': 'Organization',
name: site.name,
url: site.url,
},
description: excerpt,
...(fullImage ? { image: fullImage } : {}),
});
const formattedDate = new Date(post.date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
---
<Layout
title={post.title}
description={excerpt}
canonical={canonical}
image={fullImage}
structuredData={structuredData}
>
<a href="/" class="back-link">← Back to Home</a>
<article class="post-content">
<h1>{post.title}</h1>
<div class="post-meta" style="margin-bottom: 30px;">
{formattedDate}
{post.tags.map((t) => (
<span class="tag"><a href={`/tag/${t}/`}>{t}</a></span>
))}
</div>
<div set:html={html} />
</article>
{related.length > 0 && (
<section class="related-posts">
<h3>Related Posts</h3>
<div class="related-posts-grid">
{related.map((r) => (
<div class="related-post-card">
<a href={`/${r.slug}/`} class="related-post-link">
<h4>{r.title}</h4>
<p class="related-post-excerpt">{r.rawExcerpt}</p>
<span class="related-post-date">
{new Date(r.date).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}
</span>
</a>
</div>
))}
</div>
</section>
)}
</Layout>

View File

@@ -0,0 +1,18 @@
---
import Layout from '../../layouts/Layout.astro';
import { site } from '../../lib/site.js';
import { getPage, renderMarkdown, getExcerpt } from '../../lib/markdown.js';
const page = getPage('about');
const title = page?.title || 'About';
const html = page ? renderMarkdown(page.body) : '';
const description = page ? getExcerpt(page.body, 160) : site.description;
const canonical = `${site.url}/about/`;
---
<Layout title={title} description={description} canonical={canonical}>
<a href="/" class="back-link">← Back to Home</a>
<h1>{title}</h1>
<div class="post-content">
<div set:html={html} />
</div>
</Layout>

28
src/pages/index.astro Normal file
View File

@@ -0,0 +1,28 @@
---
import Layout from '../layouts/Layout.astro';
import PostList from '../components/PostList.astro';
import Pagination from '../components/Pagination.astro';
import { site } from '../lib/site.js';
import { getAllPosts } from '../lib/markdown.js';
const posts = getAllPosts();
const pageSize = site.postsPerPage;
const currentPosts = posts.slice(0, pageSize);
const totalPages = Math.ceil(posts.length / pageSize);
const page = {
currentPage: 1,
lastPage: totalPages,
total: posts.length,
url: {
prev: null,
next: totalPages > 1 ? '/page/2/' : null,
},
};
const canonical = `${site.url}/`;
---
<Layout title={site.name} description={site.description} canonical={canonical}>
<PostList posts={currentPosts} />
<Pagination page={page} />
</Layout>

View File

@@ -0,0 +1,42 @@
---
import Layout from '../../../layouts/Layout.astro';
import PostList from '../../../components/PostList.astro';
import Pagination from '../../../components/Pagination.astro';
import { site } from '../../../lib/site.js';
import { getAllPosts } from '../../../lib/markdown.js';
export function getStaticPaths() {
const posts = getAllPosts();
const pageSize = site.postsPerPage;
const total = Math.ceil(posts.length / pageSize);
const paths = [];
for (let i = 2; i <= total; i++) {
paths.push({ params: { page: String(i) } });
}
return paths;
}
const { page: pageParam } = Astro.params;
const currentPage = parseInt(pageParam);
const posts = getAllPosts();
const pageSize = site.postsPerPage;
const totalPages = Math.ceil(posts.length / pageSize);
const start = (currentPage - 1) * pageSize;
const currentPosts = posts.slice(start, start + pageSize);
const page = {
currentPage,
lastPage: totalPages,
total: posts.length,
url: {
prev: currentPage === 2 ? '/' : `/page/${currentPage - 1}/`,
next: currentPage < totalPages ? `/page/${currentPage + 1}/` : null,
},
};
const canonical = `${site.url}/page/${currentPage}/`;
---
<Layout title={`${site.name} — Page ${currentPage}`} description={site.description} canonical={canonical}>
<PostList posts={currentPosts} />
<Pagination page={page} />
</Layout>

8
src/pages/robots.txt.js Normal file
View File

@@ -0,0 +1,8 @@
import { site } from '../lib/site.js';
export function GET() {
const body = `User-agent: *\nAllow: /\n\nSitemap: ${site.url}/sitemap-index.xml\n`;
return new Response(body, {
headers: { 'Content-Type': 'text/plain' },
});
}

18
src/pages/rss.xml.js Normal file
View File

@@ -0,0 +1,18 @@
import rss from '@astrojs/rss';
import { site } from '../lib/site.js';
import { getAllPosts, renderMarkdown, getExcerpt } from '../lib/markdown.js';
export function GET(context) {
const posts = getAllPosts().slice(0, 20);
return rss({
title: site.name,
description: site.description,
site: context.site ?? site.url,
items: posts.map((post) => ({
title: post.title,
pubDate: new Date(post.date),
description: getExcerpt(post.body, 300),
link: `/${post.slug}/`,
})),
});
}

View File

@@ -0,0 +1,40 @@
---
import Layout from '../../../layouts/Layout.astro';
import PostList from '../../../components/PostList.astro';
import Pagination from '../../../components/Pagination.astro';
import { site } from '../../../lib/site.js';
import { getAllPosts } from '../../../lib/markdown.js';
export function getStaticPaths() {
const posts = getAllPosts();
const tags = new Set();
posts.forEach((p) => p.tags.forEach((t) => tags.add(t)));
return [...tags].map((tag) => ({ params: { tag } }));
}
const { tag } = Astro.params;
const tagPosts = getAllPosts().filter((p) => p.tags.includes(tag));
const pageSize = site.postsPerPage;
const currentPosts = tagPosts.slice(0, pageSize);
const totalPages = Math.ceil(tagPosts.length / pageSize);
const page = {
currentPage: 1,
lastPage: totalPages,
total: tagPosts.length,
url: {
prev: null,
next: totalPages > 1 ? `/tag/${tag}/page/2/` : null,
},
};
const canonical = `${site.url}/tag/${tag}/`;
---
<Layout
title={`Tagged: ${tag} — ${site.name}`}
description={`Posts tagged ${tag}`}
canonical={canonical}
>
<PostList posts={currentPosts} tag={tag} />
<Pagination page={page} />
</Layout>

View File

@@ -0,0 +1,55 @@
---
import Layout from '../../../../../layouts/Layout.astro';
import PostList from '../../../../../components/PostList.astro';
import Pagination from '../../../../../components/Pagination.astro';
import { site } from '../../../../../lib/site.js';
import { getAllPosts } from '../../../../../lib/markdown.js';
export function getStaticPaths() {
const posts = getAllPosts();
const tagMap = new Map();
posts.forEach((p) =>
p.tags.forEach((t) => {
if (!tagMap.has(t)) tagMap.set(t, 0);
tagMap.set(t, tagMap.get(t) + 1);
})
);
const pageSize = site.postsPerPage;
const paths = [];
for (const [tag, count] of tagMap.entries()) {
const total = Math.ceil(count / pageSize);
for (let i = 2; i <= total; i++) {
paths.push({ params: { tag, page: String(i) } });
}
}
return paths;
}
const { tag, page: pageParam } = Astro.params;
const currentPage = parseInt(pageParam);
const tagPosts = getAllPosts().filter((p) => p.tags.includes(tag));
const pageSize = site.postsPerPage;
const totalPages = Math.ceil(tagPosts.length / pageSize);
const start = (currentPage - 1) * pageSize;
const currentPosts = tagPosts.slice(start, start + pageSize);
const page = {
currentPage,
lastPage: totalPages,
total: tagPosts.length,
url: {
prev: currentPage === 2 ? `/tag/${tag}/` : `/tag/${tag}/page/${currentPage - 1}/`,
next: currentPage < totalPages ? `/tag/${tag}/page/${currentPage + 1}/` : null,
},
};
const canonical = `${site.url}/tag/${tag}/page/${currentPage}/`;
---
<Layout
title={`Tagged: ${tag} — Page ${currentPage} — ${site.name}`}
description={`Posts tagged ${tag}, page ${currentPage}`}
canonical={canonical}
>
<PostList posts={currentPosts} tag={tag} />
<Pagination page={page} />
</Layout>