Get rid of explicit excerpt on Post
This commit is contained in:
parent
e029c6b61d
commit
4a82426dd5
15 changed files with 85 additions and 83 deletions
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Post" DROP COLUMN "excerpt";
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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() + '...'
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in a new issue