Get rid of explicit excerpt on Post

This commit is contained in:
Justin Edmund 2025-06-02 13:03:03 -07:00
parent e029c6b61d
commit 4a82426dd5
15 changed files with 85 additions and 83 deletions

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Post" DROP COLUMN "excerpt";

View file

@ -46,7 +46,6 @@ model Post {
postType String @db.VarChar(50) // blog, microblog, link, photo, album postType String @db.VarChar(50) // blog, microblog, link, photo, album
title String? @db.VarChar(255) // Optional for microblog posts title String? @db.VarChar(255) // Optional for microblog posts
content Json? // BlockNote JSON for blog/microblog content Json? // BlockNote JSON for blog/microblog
excerpt String? @db.Text
// Type-specific fields // Type-specific fields
linkUrl String? @db.VarChar(500) linkUrl String? @db.VarChar(500)

View file

@ -75,10 +75,6 @@
<div class="post-body"> <div class="post-body">
{@html renderedContent} {@html renderedContent}
</div> </div>
{:else if post.excerpt}
<div class="post-body">
<p>{post.excerpt}</p>
</div>
{/if} {/if}
<footer class="post-footer"> <footer class="post-footer">

View file

@ -194,7 +194,7 @@
// Second item is Photos (index 2) - animation handled by individual rect animations // Second item is Photos (index 2) - animation handled by individual rect animations
// Third item is Labs (index 3) // Third item is Labs (index 3)
.nav-item:nth-of-type(3) :global(svg.animate) { .nav-item:nth-of-type(2) :global(svg.animate) {
animation: tubeRotate 0.6s ease; animation: tubeRotate 0.6s ease;
transform-origin: center bottom; transform-origin: center bottom;
} }
@ -205,19 +205,19 @@
} }
// Specific animation for photo masonry rectangles // Specific animation for photo masonry rectangles
.nav-item:nth-of-type(2) :global(svg.animate rect:nth-child(1)) { .nav-item:nth-of-type(3) :global(svg.animate rect:nth-child(1)) {
animation: masonryRect1 0.6s ease; animation: masonryRect1 0.6s ease;
} }
.nav-item:nth-of-type(2) :global(svg.animate rect:nth-child(2)) { .nav-item:nth-of-type(3) :global(svg.animate rect:nth-child(2)) {
animation: masonryRect2 0.6s ease; animation: masonryRect2 0.6s ease;
} }
.nav-item:nth-of-type(2) :global(svg.animate rect:nth-child(3)) { .nav-item:nth-of-type(3) :global(svg.animate rect:nth-child(3)) {
animation: masonryRect3 0.6s ease; animation: masonryRect3 0.6s ease;
} }
.nav-item:nth-of-type(2) :global(svg.animate rect:nth-child(4)) { .nav-item:nth-of-type(3) :global(svg.animate rect:nth-child(4)) {
animation: masonryRect4 0.6s ease; animation: masonryRect4 0.6s ease;
} }
</style> </style>

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from 'svelte' import type { Snippet } from 'svelte'
import UniverseIcon from '$icons/universe.svg' import UniverseIcon from '$icons/universe.svg'
import PhotosIcon from '$icons/photos.svg'
import { formatDate } from '$lib/utils/date' import { formatDate } from '$lib/utils/date'
import { goto } from '$app/navigation' import { goto } from '$app/navigation'
@ -55,12 +56,18 @@
{formatDate(item.publishedAt)} {formatDate(item.publishedAt)}
</time> </time>
</a> </a>
<UniverseIcon class="universe-icon" /> {#if type === 'album'}
<PhotosIcon class="card-icon" />
{:else}
<UniverseIcon class="card-icon" />
{/if}
</div> </div>
</div> </div>
</article> </article>
<style lang="scss"> <style lang="scss">
@import '../../assets/styles/animations';
.universe-card { .universe-card {
width: 100%; width: 100%;
max-width: 700px; max-width: 700px;
@ -107,7 +114,7 @@
transition: color 0.2s ease; transition: color 0.2s ease;
} }
:global(.universe-icon) { :global(.card-icon) {
width: 16px; width: 16px;
height: 16px; height: 16px;
fill: $grey-40; fill: $grey-40;
@ -120,7 +127,7 @@
color: $red-60; color: $red-60;
} }
:global(.universe-icon) { :global(.card-icon) {
fill: $red-60; fill: $red-60;
transform: rotate(15deg); transform: rotate(15deg);
} }
@ -143,9 +150,32 @@
color: $red-60; color: $red-60;
} }
:global(.universe-icon) { :global(.card-icon) {
fill: $red-60; fill: $red-60;
transform: rotate(15deg); }
:global(.card-icon rect:nth-child(1)) {
transition: all 0.3s ease;
height: 6px;
y: 2px;
}
:global(.card-icon rect:nth-child(2)) {
transition: all 0.3s ease;
height: 10px;
y: 2px;
}
:global(.card-icon rect:nth-child(3)) {
transition: all 0.3s ease;
height: 8px;
y: 10px;
}
:global(.card-icon rect:nth-child(4)) {
transition: all 0.3s ease;
height: 4px;
y: 14px;
} }
:global(.card-title-link) { :global(.card-title-link) {
@ -153,6 +183,11 @@
} }
} }
// Base state for smooth transition back
:global(.card-icon rect) {
transition: all 0.3s ease;
}
:global(.card-title-link) { :global(.card-title-link) {
color: $grey-10; color: $grey-10;
text-decoration: none; text-decoration: none;

View file

@ -4,6 +4,16 @@
import type { UniverseItem } from '../../routes/api/universe/+server' import type { UniverseItem } from '../../routes/api/universe/+server'
let { post }: { post: UniverseItem } = $props() let { post }: { post: UniverseItem } = $props()
// Check if content is truncated
const isContentTruncated = $derived(() => {
if (post.content) {
// Check if the excerpt is shorter than the full content
const excerpt = getContentExcerpt(post.content)
return excerpt.endsWith('...')
}
return false
})
</script> </script>
<UniverseCard item={post} type="post"> <UniverseCard item={post} type="post">
@ -25,15 +35,13 @@
</div> </div>
{/if} {/if}
<div class="post-excerpt"> {#if post.content}
{#if post.excerpt} <div class="post-excerpt">
<p>{post.excerpt}</p> <p>{getContentExcerpt(post.content, 150)}</p>
{:else if post.content} </div>
<p>{getContentExcerpt(post.content)}</p> {/if}
{/if}
</div>
{#if post.postType === 'essay'} {#if post.postType === 'essay' && isContentTruncated}
<p> <p>
<a href="/universe/{post.slug}" class="read-more" onclick={(e) => e.preventDefault()} tabindex="-1">Continue reading</a> <a href="/universe/{post.slug}" class="read-more" onclick={(e) => e.preventDefault()} tabindex="-1">Continue reading</a>
</p> </p>
@ -97,7 +105,7 @@
line-height: 1.5; line-height: 1.5;
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-line-clamp: 4; -webkit-line-clamp: 2;
overflow: hidden; overflow: hidden;
} }
} }

View file

@ -12,7 +12,6 @@
initialData?: { initialData?: {
title: string title: string
slug: string slug: string
excerpt: string
content: JSONContent content: JSONContent
tags: string[] tags: string[]
status: 'draft' | 'published' status: 'draft' | 'published'
@ -33,7 +32,6 @@
// Form data // Form data
let title = $state(initialData?.title || '') let title = $state(initialData?.title || '')
let slug = $state(initialData?.slug || '') let slug = $state(initialData?.slug || '')
let excerpt = $state(initialData?.excerpt || '')
let content = $state<JSONContent>(initialData?.content || { type: 'doc', content: [] }) let content = $state<JSONContent>(initialData?.content || { type: 'doc', content: [] })
let tags = $state<string[]>(initialData?.tags || []) let tags = $state<string[]>(initialData?.tags || [])
let status = $state<'draft' | 'published'>(initialData?.status || 'draft') let status = $state<'draft' | 'published'>(initialData?.status || 'draft')
@ -103,7 +101,6 @@
postType: 'blog', // 'blog' is the database value for essays postType: 'blog', // 'blog' is the database value for essays
status, status,
content, content,
excerpt,
tags tags
} }
@ -268,15 +265,6 @@
<Input label="Slug" bind:value={slug} placeholder="essay-url-slug" /> <Input label="Slug" bind:value={slug} placeholder="essay-url-slug" />
<Input
type="textarea"
label="Excerpt"
helpText="Brief description shown in post lists"
bind:value={excerpt}
rows={3}
placeholder="A brief summary of your essay..."
/>
<div class="tags-field"> <div class="tags-field">
<label class="input-label">Tags</label> <label class="input-label">Tags</label>
<div class="tag-input-wrapper"> <div class="tag-input-wrapper">

View file

@ -125,7 +125,6 @@
.map((tag) => tag.trim()) .map((tag) => tag.trim())
.filter(Boolean) .filter(Boolean)
: [], : [],
excerpt: generateExcerpt(editorContent)
} }
const url = mode === 'edit' ? `/api/posts/${postId}` : '/api/posts' const url = mode === 'edit' ? `/api/posts/${postId}` : '/api/posts'
@ -160,22 +159,6 @@
} }
} }
function generateExcerpt(content: JSONContent): string {
// Extract plain text from editor content for excerpt
if (!content?.content) return ''
let text = ''
const extractText = (node: any) => {
if (node.type === 'text') {
text += node.text
} else if (node.content) {
node.content.forEach(extractText)
}
}
content.content.forEach(extractText)
return text.substring(0, 200) + (text.length > 200 ? '...' : '')
}
async function handlePublish() { async function handlePublish() {
status = 'published' status = 'published'

View file

@ -5,7 +5,6 @@
post: any post: any
postType: 'post' | 'essay' postType: 'post' | 'essay'
slug: string slug: string
excerpt: string
tags: string[] tags: string[]
tagInput: string tagInput: string
triggerElement: HTMLElement triggerElement: HTMLElement
@ -19,7 +18,6 @@
post, post,
postType, postType,
slug = $bindable(), slug = $bindable(),
excerpt = $bindable(),
tags = $bindable(), tags = $bindable(),
tagInput = $bindable(), tagInput = $bindable(),
triggerElement, triggerElement,
@ -32,8 +30,6 @@
function handleFieldUpdate(key: string, value: any) { function handleFieldUpdate(key: string, value: any) {
if (key === 'slug') { if (key === 'slug') {
slug = value slug = value
} else if (key === 'excerpt') {
excerpt = value
} else if (key === 'tagInput') { } else if (key === 'tagInput') {
tagInput = value tagInput = value
} }
@ -48,17 +44,6 @@
label: 'Slug', label: 'Slug',
placeholder: 'post-slug' placeholder: 'post-slug'
}, },
...(postType === 'essay'
? [
{
type: 'textarea' as const,
key: 'excerpt',
label: 'Excerpt',
rows: 3,
placeholder: 'Brief description...'
}
]
: []),
{ {
type: 'tags', type: 'tags',
key: 'tags', key: 'tags',
@ -79,7 +64,6 @@
// Create a reactive data object // Create a reactive data object
let popoverData = $state({ let popoverData = $state({
slug, slug,
excerpt,
tags, tags,
tagInput, tagInput,
createdAt: post.createdAt, createdAt: post.createdAt,
@ -91,7 +75,6 @@
$effect(() => { $effect(() => {
popoverData = { popoverData = {
slug, slug,
excerpt,
tags, tags,
tagInput, tagInput,
createdAt: post.createdAt, createdAt: post.createdAt,

View file

@ -73,17 +73,27 @@ export const renderEdraContent = (content: any): string => {
// Extract text content from Edra JSON for excerpt // Extract text content from Edra JSON for excerpt
export const getContentExcerpt = (content: any, maxLength = 200): string => { export const getContentExcerpt = (content: any, maxLength = 200): string => {
if (!content || !content.content) return '' if (!content) return ''
// Handle both { blocks: [...] } and { content: [...] } formats
const blocks = content.blocks || content.content || []
if (!Array.isArray(blocks)) return ''
const extractText = (node: any): string => { const extractText = (node: any): string => {
// For block-level content
if (node.type && node.content && typeof node.content === 'string') {
return node.content
}
// For inline content with text property
if (node.text) return node.text if (node.text) return node.text
// For nested content
if (node.content && Array.isArray(node.content)) { if (node.content && Array.isArray(node.content)) {
return node.content.map(extractText).join(' ') return node.content.map(extractText).join(' ')
} }
return '' return ''
} }
const text = content.content.map(extractText).join(' ').trim() const text = blocks.map(extractText).join(' ').trim()
if (text.length <= maxLength) return text if (text.length <= maxLength) return text
return text.substring(0, maxLength).trim() + '...' return text.substring(0, maxLength).trim() + '...'
} }

View file

@ -126,7 +126,6 @@ export const POST: RequestHandler = async (event) => {
postType: data.type, postType: data.type,
status: data.status, status: data.status,
content: postContent, content: postContent,
excerpt: data.excerpt,
linkUrl: data.link_url, linkUrl: data.link_url,
linkDescription: data.linkDescription, linkDescription: data.linkDescription,
featuredImage: featuredImageId, featuredImage: featuredImageId,

View file

@ -95,7 +95,6 @@ export const PUT: RequestHandler = async (event) => {
postType: data.type, postType: data.type,
status: data.status, status: data.status,
content: postContent, content: postContent,
excerpt: data.excerpt,
linkUrl: data.link_url, linkUrl: data.link_url,
linkDescription: data.linkDescription, linkDescription: data.linkDescription,
featuredImage: featuredImageId, featuredImage: featuredImageId,

View file

@ -8,7 +8,6 @@ export interface UniverseItem {
type: 'post' | 'album' type: 'post' | 'album'
slug: string slug: string
title?: string title?: string
excerpt?: string
content?: any content?: any
publishedAt: string publishedAt: string
createdAt: string createdAt: string
@ -47,7 +46,6 @@ export const GET: RequestHandler = async (event) => {
postType: true, postType: true,
title: true, title: true,
content: true, content: true,
excerpt: true,
linkUrl: true, linkUrl: true,
linkDescription: true, linkDescription: true,
attachments: true, attachments: true,
@ -96,7 +94,6 @@ export const GET: RequestHandler = async (event) => {
type: 'post' as const, type: 'post' as const,
slug: post.slug, slug: post.slug,
title: post.title || undefined, title: post.title || undefined,
excerpt: post.excerpt || undefined,
content: post.content, content: post.content,
postType: post.postType, postType: post.postType,
linkUrl: post.linkUrl || undefined, linkUrl: post.linkUrl || undefined,
@ -113,7 +110,6 @@ export const GET: RequestHandler = async (event) => {
slug: album.slug, slug: album.slug,
title: album.title, title: album.title,
description: album.description || undefined, description: album.description || undefined,
excerpt: album.description || undefined,
location: album.location || undefined, location: album.location || undefined,
date: album.date?.toISOString(), date: album.date?.toISOString(),
photosCount: album._count.photos, photosCount: album._count.photos,

View file

@ -88,7 +88,7 @@ export const GET: RequestHandler = async (event) => {
id: post.id.toString(), id: post.id.toString(),
title: title:
post.title || `${post.postType.charAt(0).toUpperCase() + post.postType.slice(1)} Post`, post.title || `${post.postType.charAt(0).toUpperCase() + post.postType.slice(1)} Post`,
description: post.excerpt || extractTextSummary(post.content) || '', description: extractTextSummary(post.content) || '',
content: convertContentToHTML(post.content), content: convertContentToHTML(post.content),
link: `${event.url.origin}/universe/${post.slug}`, link: `${event.url.origin}/universe/${post.slug}`,
guid: `${event.url.origin}/universe/${post.slug}`, guid: `${event.url.origin}/universe/${post.slug}`,

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import Page from '$components/Page.svelte' import Page from '$components/Page.svelte'
import DynamicPostContent from '$components/DynamicPostContent.svelte' import DynamicPostContent from '$components/DynamicPostContent.svelte'
import { getContentExcerpt } from '$lib/utils/content'
import type { PageData } from './$types' import type { PageData } from './$types'
let { data }: { data: PageData } = $props() let { data }: { data: PageData } = $props()
@ -8,6 +9,9 @@
const post = $derived(data.post) const post = $derived(data.post)
const error = $derived(data.error) const error = $derived(data.error)
const pageTitle = $derived(post?.title || 'Post') const pageTitle = $derived(post?.title || 'Post')
const description = $derived(
post?.content ? getContentExcerpt(post.content, 160) : `${post?.postType === 'essay' ? 'Essay' : 'Post'} by jedmund`
)
</script> </script>
<svelte:head> <svelte:head>
@ -15,14 +19,14 @@
<title>{pageTitle} - jedmund</title> <title>{pageTitle} - jedmund</title>
<meta <meta
name="description" name="description"
content={post.excerpt || `${post.postType === 'essay' ? 'Essay' : 'Post'} by jedmund`} content={description}
/> />
<!-- Open Graph meta tags --> <!-- Open Graph meta tags -->
<meta property="og:title" content={pageTitle} /> <meta property="og:title" content={pageTitle} />
<meta <meta
property="og:description" property="og:description"
content={post.excerpt || `${post.postType === 'essay' ? 'Essay' : 'Post'} by jedmund`} content={description}
/> />
<meta property="og:type" content="article" /> <meta property="og:type" content="article" />
{#if post.attachments && post.attachments.length > 0} {#if post.attachments && post.attachments.length > 0}