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
title String? @db.VarChar(255) // Optional for microblog posts
content Json? // BlockNote JSON for blog/microblog
excerpt String? @db.Text
// Type-specific fields
linkUrl String? @db.VarChar(500)

View file

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

View file

@ -194,7 +194,7 @@
// Second item is Photos (index 2) - animation handled by individual rect animations
// 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;
transform-origin: center bottom;
}
@ -205,19 +205,19 @@
}
// 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;
}
.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;
}
.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;
}
.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;
}
</style>

View file

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

View file

@ -4,6 +4,16 @@
import type { UniverseItem } from '../../routes/api/universe/+server'
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>
<UniverseCard item={post} type="post">
@ -25,15 +35,13 @@
</div>
{/if}
<div class="post-excerpt">
{#if post.excerpt}
<p>{post.excerpt}</p>
{:else if post.content}
<p>{getContentExcerpt(post.content)}</p>
{/if}
</div>
{#if post.content}
<div class="post-excerpt">
<p>{getContentExcerpt(post.content, 150)}</p>
</div>
{/if}
{#if post.postType === 'essay'}
{#if post.postType === 'essay' && isContentTruncated}
<p>
<a href="/universe/{post.slug}" class="read-more" onclick={(e) => e.preventDefault()} tabindex="-1">Continue reading</a>
</p>
@ -97,7 +105,7 @@
line-height: 1.5;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
-webkit-line-clamp: 2;
overflow: hidden;
}
}

View file

@ -12,7 +12,6 @@
initialData?: {
title: string
slug: string
excerpt: string
content: JSONContent
tags: string[]
status: 'draft' | 'published'
@ -33,7 +32,6 @@
// Form data
let title = $state(initialData?.title || '')
let slug = $state(initialData?.slug || '')
let excerpt = $state(initialData?.excerpt || '')
let content = $state<JSONContent>(initialData?.content || { type: 'doc', content: [] })
let tags = $state<string[]>(initialData?.tags || [])
let status = $state<'draft' | 'published'>(initialData?.status || 'draft')
@ -103,7 +101,6 @@
postType: 'blog', // 'blog' is the database value for essays
status,
content,
excerpt,
tags
}
@ -268,15 +265,6 @@
<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">
<label class="input-label">Tags</label>
<div class="tag-input-wrapper">

View file

@ -125,7 +125,6 @@
.map((tag) => tag.trim())
.filter(Boolean)
: [],
excerpt: generateExcerpt(editorContent)
}
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() {
status = 'published'

View file

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

View file

@ -73,17 +73,27 @@ export const renderEdraContent = (content: any): string => {
// Extract text content from Edra JSON for excerpt
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 => {
// 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
// For nested content
if (node.content && Array.isArray(node.content)) {
return node.content.map(extractText).join(' ')
}
return ''
}
const text = content.content.map(extractText).join(' ').trim()
const text = blocks.map(extractText).join(' ').trim()
if (text.length <= maxLength) return text
return text.substring(0, maxLength).trim() + '...'
}
}

View file

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

View file

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

View file

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

View file

@ -88,7 +88,7 @@ export const GET: RequestHandler = async (event) => {
id: post.id.toString(),
title:
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),
link: `${event.url.origin}/universe/${post.slug}`,
guid: `${event.url.origin}/universe/${post.slug}`,

View file

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