From e7ecf98c42508ad5c65282b802356b2b9e588ede Mon Sep 17 00:00:00 2001 From: Innei Date: Thu, 12 Jun 2025 19:48:58 +0800 Subject: [PATCH] feat: add feed sitemap generation plugin and update site URL in config - Introduced a new Vite plugin for generating RSS feeds and sitemaps based on photo data. - Updated the example configuration file to reflect the new site URL format. Signed-off-by: Innei --- apps/web/src/global.d.ts | 1 + apps/web/src/modules/gallery/ActionGroup.tsx | 14 -- .../gallery/MasonryHeaderMasonryItem.tsx | 41 ++++ apps/web/vite.config.ts | 13 ++ config.example.json | 8 +- plugins/vite/feed-sitemap.ts | 188 ++++++++++++++++++ site.config.ts | 6 +- 7 files changed, 248 insertions(+), 23 deletions(-) create mode 100644 plugins/vite/feed-sitemap.ts diff --git a/apps/web/src/global.d.ts b/apps/web/src/global.d.ts index 2780e0f7..62109b48 100644 --- a/apps/web/src/global.d.ts +++ b/apps/web/src/global.d.ts @@ -25,6 +25,7 @@ declare global { const APP_NAME: string const BUILT_DATE: string + const GIT_COMMIT_HASH: string } export {} diff --git a/apps/web/src/modules/gallery/ActionGroup.tsx b/apps/web/src/modules/gallery/ActionGroup.tsx index bd600d21..2de6722e 100644 --- a/apps/web/src/modules/gallery/ActionGroup.tsx +++ b/apps/web/src/modules/gallery/ActionGroup.tsx @@ -1,6 +1,4 @@ -import { siteConfig } from '@config' import { photoLoader } from '@photo-gallery/data' -import { repository } from '@pkg' import { useAtom } from 'jotai' import { useRef, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -50,18 +48,6 @@ export const ActionGroup = () => { return (
- {siteConfig.extra?.accessRepo && ( - - )} - {/* 标签筛选按钮 */} diff --git a/apps/web/src/modules/gallery/MasonryHeaderMasonryItem.tsx b/apps/web/src/modules/gallery/MasonryHeaderMasonryItem.tsx index 3e8cd100..d0d075c8 100644 --- a/apps/web/src/modules/gallery/MasonryHeaderMasonryItem.tsx +++ b/apps/web/src/modules/gallery/MasonryHeaderMasonryItem.tsx @@ -1,5 +1,6 @@ import { siteConfig } from '@config' import { photoLoader } from '@photo-gallery/data' +import { repository } from '@pkg' import * as AvatarPrimitive from '@radix-ui/react-avatar' import { useTranslation } from 'react-i18next' @@ -58,6 +59,34 @@ export const MasonryHeaderMasonryItem = ({ {siteConfig.name} + {/* Social media links */} + {siteConfig.social && ( +
+ {siteConfig.social.github && ( + + + + )} + {siteConfig.social.twitter && ( + + + + )} +
+ )} +

{t('gallery.photos', { count: data?.length || 0 })}

@@ -79,6 +108,18 @@ export const MasonryHeaderMasonryItem = ({ month: 'long', day: 'numeric', })} + + ( + + {GIT_COMMIT_HASH.slice(0, 6)} + + ) +
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 88e59802..13ae8b28 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -1,3 +1,4 @@ +import { execSync } from 'node:child_process' import { rmSync } from 'node:fs' import path from 'node:path' @@ -13,6 +14,7 @@ import tsconfigPaths from 'vite-tsconfig-paths' import PKG from '../../package.json' import { ogImagePlugin } from '../../plugins/og-image-plugin' import { createDependencyChunksPlugin } from '../../plugins/vite/deps' +import { createFeedSitemapPlugin } from '../../plugins/vite/feed-sitemap' import { siteConfig } from '../../site.config' if (process.env.CI) { @@ -56,6 +58,7 @@ export default defineConfig({ siteName: siteConfig.name, siteUrl: siteConfig.url, }), + createFeedSitemapPlugin(siteConfig), createHtmlPlugin({ minify: { collapseWhitespace: true, @@ -81,5 +84,15 @@ export default defineConfig({ APP_DEV_CWD: JSON.stringify(process.cwd()), APP_NAME: JSON.stringify(PKG.name), BUILT_DATE: JSON.stringify(new Date().toLocaleDateString()), + GIT_COMMIT_HASH: JSON.stringify(getGitHash()), }, }) + +function getGitHash() { + try { + return execSync('git rev-parse HEAD').toString().trim() + } catch (e) { + console.error('Failed to get git hash', e) + return '' + } +} diff --git a/config.example.json b/config.example.json index e7771093..f29c7b94 100644 --- a/config.example.json +++ b/config.example.json @@ -2,7 +2,7 @@ "name": "Innei's Photo Gallery", "title": "Innei's Photo Gallery", "description": "Capturing beautiful moments in life, documenting daily warmth and emotions through my lens.", - "url": "https://gallery.innei.in", + "url": "https://afilmory.innei.in/", "accentColor": "#007bff", "author": { "name": "Photo Gallery", @@ -10,9 +10,7 @@ "avatar": "//cdn.jsdelivr.net/gh/Innei/static@master/avatar.png" }, "social": { - "twitter": "@__oQuery" - }, - "extra": { - "accessRepo": true + "twitter": "@__oQuery", + "github": "Innei" } } \ No newline at end of file diff --git a/plugins/vite/feed-sitemap.ts b/plugins/vite/feed-sitemap.ts new file mode 100644 index 00000000..7ba8666b --- /dev/null +++ b/plugins/vite/feed-sitemap.ts @@ -0,0 +1,188 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +import type { Plugin } from 'vite' + +interface PhotoData { + id: string + title: string + description: string + dateTaken: string + views: number + tags: string[] + originalUrl: string + thumbnailUrl: string + blurhash: string + width: number + height: number + aspectRatio: number + s3Key: string + lastModified: string + size: number + exif?: any + isLivePhoto: boolean +} + +interface SiteConfig { + name: string + title: string + description: string + url: string + author: { + name: string + url: string + avatar?: string + } +} + +const __dirname = fileURLToPath(new URL('.', import.meta.url)) + +export function createFeedSitemapPlugin(siteConfig: SiteConfig): Plugin { + return { + name: 'feed-sitemap-generator', + apply: 'build', + generateBundle() { + try { + // Read photos manifest + const manifestPath = resolve( + __dirname, + '../../packages/data/src/photos-manifest.json', + ) + const photosData: PhotoData[] = JSON.parse( + readFileSync(manifestPath, 'utf-8'), + ) + + // Sort photos by date taken (newest first) + const sortedPhotos = photosData.sort( + (a, b) => + new Date(b.dateTaken).getTime() - new Date(a.dateTaken).getTime(), + ) + + // Generate RSS feed + const rssXml = generateRSSFeed(sortedPhotos, siteConfig) + + // Generate sitemap + const sitemapXml = generateSitemap(sortedPhotos, siteConfig) + + // Emit RSS feed + this.emitFile({ + type: 'asset', + fileName: 'feed.xml', + source: rssXml, + }) + + // Emit sitemap + this.emitFile({ + type: 'asset', + fileName: 'sitemap.xml', + source: sitemapXml, + }) + + console.info(`Generated RSS feed with ${sortedPhotos.length} photos`) + console.info(`Generated sitemap with ${sortedPhotos.length + 1} URLs`) + } catch (error) { + console.error('Error generating RSS feed and sitemap:', error) + } + }, + } +} + +function generateRSSFeed(photos: PhotoData[], config: SiteConfig): string { + const now = new Date().toUTCString() + const latestPhoto = photos[0] + const lastBuildDate = latestPhoto + ? new Date(latestPhoto.dateTaken).toUTCString() + : now + + // Take latest 20 photos for RSS feed + const recentPhotos = photos.slice(0, 20) + + const rssItems = recentPhotos + .map((photo) => { + const photoUrl = `${config.url}/${photo.id}` + const pubDate = new Date(photo.dateTaken).toUTCString() + const tags = photo.tags.join(', ') + + let description = photo.description || photo.title + if (tags) { + description += ` | Tags: ${tags}` + } + + return ` + <![CDATA[${escapeXml(photo.title)}]]> + ${photoUrl} + ${photoUrl} + + ${pubDate} + ${photo.tags.map((tag) => ``).join('\n ')} + + ` + }) + .join('\n') + + return ` + + + <![CDATA[${escapeXml(config.title)}]]> + ${config.url} + + zh-CN + ${lastBuildDate} + ${now} + 60 + + ${config.author.name} + ${config.author.name} + Vite RSS Generator + + ${config.author.avatar || `${config.url}/favicon.ico`} + <![CDATA[${escapeXml(config.title)}]]> + ${config.url} + +${rssItems} + +` +} + +function generateSitemap(photos: PhotoData[], config: SiteConfig): string { + const now = new Date().toISOString() + + // Main page + const mainPageXml = ` + ${config.url} + ${now} + daily + 1.0 + ` + + // Photo pages + const photoUrls = photos + .map((photo) => { + const lastmod = new Date( + photo.lastModified || photo.dateTaken, + ).toISOString() + return ` + ${config.url}/${photo.id} + ${lastmod} + monthly + 0.8 + ` + }) + .join('\n') + + return ` + +${mainPageXml} +${photoUrls} +` +} + +function escapeXml(text: string): string { + return text + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", ''') +} diff --git a/site.config.ts b/site.config.ts index 4a319349..92644dcb 100644 --- a/site.config.ts +++ b/site.config.ts @@ -10,7 +10,6 @@ interface SiteConfig { accentColor: string author: Author social?: Social - extra?: Extra } interface Author { name: string @@ -19,9 +18,7 @@ interface Author { } interface Social { twitter: string -} -interface Extra { - accessRepo: boolean + github: string } const defaultConfig: SiteConfig = { @@ -38,6 +35,7 @@ const defaultConfig: SiteConfig = { }, social: { twitter: '@__oQuery', + github: 'Innei', }, } export const siteConfig: SiteConfig = merge(defaultConfig, userConfig) as any