feat: rework single post view for better content hierarchy
- Add post-type header (Reply/Like/Repost/Bookmark/Note) for titleless posts - Add left accent border to user content on interaction posts - Collapse AI Usage box into a compact <details> summary line - Collapse comments section when empty, auto-open when comments exist - Collapse webmention send form behind <details> toggle Confab-Link: http://localhost:8080/sessions/648a550c-4f65-46be-b9a9-6b7e0fd90751
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
{# Comments section — shown on post pages before webmentions #}
|
||||
{# Collapsed when empty, auto-opens when comments exist #}
|
||||
{% set absoluteUrl = site.url + page.url %}
|
||||
|
||||
<is-land on:visible>
|
||||
@@ -6,91 +7,103 @@
|
||||
x-data="commentsSection('{{ absoluteUrl }}')"
|
||||
x-init="init()">
|
||||
|
||||
<h2 class="text-xl font-bold mb-4">Comments</h2>
|
||||
<details class="group" x-bind:open="comments.length > 0 || showForm">
|
||||
<summary class="flex items-center justify-between cursor-pointer list-none [&::-webkit-details-marker]:hidden" @click="showForm = true">
|
||||
<h2 class="text-lg font-semibold text-surface-700 dark:text-surface-300">
|
||||
Comments
|
||||
<span x-show="comments.length > 0" x-text="'(' + comments.length + ')'" class="text-sm font-normal" x-cloak></span>
|
||||
</h2>
|
||||
<svg class="w-4 h-4 text-surface-400 transition-transform group-open:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</summary>
|
||||
|
||||
{# Status messages #}
|
||||
<div x-show="statusMessage" x-cloak
|
||||
x-bind:class="statusType === 'error' ? 'bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400' :
|
||||
statusType === 'success' ? 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400' :
|
||||
'bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400'"
|
||||
class="p-3 rounded-lg mb-4 text-sm">
|
||||
<span x-text="statusMessage"></span>
|
||||
</div>
|
||||
|
||||
{# Sign-in form (shown when not authenticated) #}
|
||||
<div x-show="!user" x-cloak>
|
||||
<p class="text-sm text-surface-600 dark:text-surface-400 mb-3">Sign in with your website to comment:</p>
|
||||
<form x-on:submit.prevent="startAuth()" class="flex gap-2 items-end flex-wrap">
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label for="comment-me" class="block text-sm font-medium mb-1">Your website</label>
|
||||
<input id="comment-me" type="url" x-model="meUrl"
|
||||
placeholder="https://yourdomain.com" required
|
||||
class="w-full px-3 py-2 border rounded-lg dark:bg-surface-800 dark:border-surface-600">
|
||||
<div class="mt-4">
|
||||
{# Status messages #}
|
||||
<div x-show="statusMessage" x-cloak
|
||||
x-bind:class="statusType === 'error' ? 'bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400' :
|
||||
statusType === 'success' ? 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400' :
|
||||
'bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400'"
|
||||
class="p-3 rounded-lg mb-4 text-sm">
|
||||
<span x-text="statusMessage"></span>
|
||||
</div>
|
||||
<button type="submit" class="button" x-bind:disabled="authLoading">
|
||||
<span x-show="!authLoading">Sign In</span>
|
||||
<span x-show="authLoading" x-cloak>Signing in...</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{# Comment form (shown when authenticated) #}
|
||||
<div x-show="user" x-cloak>
|
||||
<div class="flex items-center gap-2 mb-3 text-sm text-surface-600 dark:text-surface-400">
|
||||
<span>Signed in as</span>
|
||||
<a x-bind:href="user?.url" class="font-medium hover:underline" x-text="user?.name || user?.url" target="_blank" rel="noopener"></a>
|
||||
<button x-on:click="signOut()" class="text-xs underline hover:no-underline">Sign out</button>
|
||||
</div>
|
||||
|
||||
<form x-on:submit.prevent="submitComment()">
|
||||
<textarea x-model="commentText" rows="4" required
|
||||
placeholder="Share your thoughts... (supports **bold**, *italic*, and [links](url))"
|
||||
class="w-full px-3 py-2 border rounded-lg mb-2 dark:bg-surface-800 dark:border-surface-600"
|
||||
x-bind:maxlength="maxLength"></textarea>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-surface-500" x-text="commentText.length + '/' + maxLength"></span>
|
||||
<button type="submit" class="button" x-bind:disabled="submitting">
|
||||
<span x-show="!submitting">Post Comment</span>
|
||||
<span x-show="submitting" x-cloak>Posting...</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{# Comment list #}
|
||||
<div class="mt-6 space-y-4">
|
||||
<template x-if="loading">
|
||||
<p class="text-sm text-surface-500">Loading comments...</p>
|
||||
</template>
|
||||
|
||||
<template x-for="comment in comments" x-bind:key="comment.published">
|
||||
<div class="p-4 bg-surface-100 dark:bg-surface-800 rounded-lg">
|
||||
<div class="flex items-start gap-3">
|
||||
<template x-if="comment.author?.photo">
|
||||
<img x-bind:src="comment.author.photo" x-bind:alt="comment.author.name"
|
||||
class="w-8 h-8 rounded-full flex-shrink-0" loading="lazy">
|
||||
</template>
|
||||
<template x-if="!comment.author?.photo">
|
||||
<div class="w-8 h-8 rounded-full bg-surface-200 dark:bg-surface-700 flex-shrink-0 flex items-center justify-center text-xs font-bold"
|
||||
x-text="(comment.author?.name || '?')[0].toUpperCase()">
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<a x-bind:href="comment.author?.url" class="font-medium text-sm hover:underline" target="_blank" rel="noopener"
|
||||
x-text="comment.author?.name || comment.author?.url"></a>
|
||||
<time class="text-xs text-surface-500" x-bind:datetime="comment.published"
|
||||
x-text="new Date(comment.published).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })"></time>
|
||||
</div>
|
||||
<div class="mt-1 text-sm prose dark:prose-invert" x-html="comment.content?.html || comment.content?.text"></div>
|
||||
{# Sign-in form (shown when not authenticated) #}
|
||||
<div x-show="!user" x-cloak>
|
||||
<p class="text-sm text-surface-600 dark:text-surface-400 mb-3">Sign in with your website to comment:</p>
|
||||
<form x-on:submit.prevent="startAuth()" class="flex gap-2 items-end flex-wrap">
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label for="comment-me" class="block text-sm font-medium mb-1">Your website</label>
|
||||
<input id="comment-me" type="url" x-model="meUrl"
|
||||
placeholder="https://yourdomain.com" required
|
||||
class="w-full px-3 py-2 border rounded-lg dark:bg-surface-800 dark:border-surface-600">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="button" x-bind:disabled="authLoading">
|
||||
<span x-show="!authLoading">Sign In</span>
|
||||
<span x-show="authLoading" x-cloak>Signing in...</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="!loading && comments.length === 0">
|
||||
<p class="text-sm text-surface-500">No comments yet. Be the first to share your thoughts!</p>
|
||||
</template>
|
||||
</div>
|
||||
{# Comment form (shown when authenticated) #}
|
||||
<div x-show="user" x-cloak>
|
||||
<div class="flex items-center gap-2 mb-3 text-sm text-surface-600 dark:text-surface-400">
|
||||
<span>Signed in as</span>
|
||||
<a x-bind:href="user?.url" class="font-medium hover:underline" x-text="user?.name || user?.url" target="_blank" rel="noopener"></a>
|
||||
<button x-on:click="signOut()" class="text-xs underline hover:no-underline">Sign out</button>
|
||||
</div>
|
||||
|
||||
<form x-on:submit.prevent="submitComment()">
|
||||
<textarea x-model="commentText" rows="4" required
|
||||
placeholder="Share your thoughts... (supports **bold**, *italic*, and [links](url))"
|
||||
class="w-full px-3 py-2 border rounded-lg mb-2 dark:bg-surface-800 dark:border-surface-600"
|
||||
x-bind:maxlength="maxLength"></textarea>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-surface-500" x-text="commentText.length + '/' + maxLength"></span>
|
||||
<button type="submit" class="button" x-bind:disabled="submitting">
|
||||
<span x-show="!submitting">Post Comment</span>
|
||||
<span x-show="submitting" x-cloak>Posting...</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{# Comment list #}
|
||||
<div class="mt-6 space-y-4">
|
||||
<template x-if="loading">
|
||||
<p class="text-sm text-surface-500">Loading comments...</p>
|
||||
</template>
|
||||
|
||||
<template x-for="comment in comments" x-bind:key="comment.published">
|
||||
<div class="p-4 bg-surface-100 dark:bg-surface-800 rounded-lg">
|
||||
<div class="flex items-start gap-3">
|
||||
<template x-if="comment.author?.photo">
|
||||
<img x-bind:src="comment.author.photo" x-bind:alt="comment.author.name"
|
||||
class="w-8 h-8 rounded-full flex-shrink-0" loading="lazy">
|
||||
</template>
|
||||
<template x-if="!comment.author?.photo">
|
||||
<div class="w-8 h-8 rounded-full bg-surface-200 dark:bg-surface-700 flex-shrink-0 flex items-center justify-center text-xs font-bold"
|
||||
x-text="(comment.author?.name || '?')[0].toUpperCase()">
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<a x-bind:href="comment.author?.url" class="font-medium text-sm hover:underline" target="_blank" rel="noopener"
|
||||
x-text="comment.author?.name || comment.author?.url"></a>
|
||||
<time class="text-xs text-surface-500" x-bind:datetime="comment.published"
|
||||
x-text="new Date(comment.published).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })"></time>
|
||||
</div>
|
||||
<div class="mt-1 text-sm prose dark:prose-invert" x-html="comment.content?.html || comment.content?.text"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="!loading && comments.length === 0">
|
||||
<p class="text-sm text-surface-500">No comments yet. Be the first to share your thoughts!</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
</is-land>
|
||||
|
||||
@@ -175,27 +175,32 @@
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{# Webmention send form #}
|
||||
<section class="webmention-form mt-8 p-4 bg-surface-100 dark:bg-surface-800 rounded-lg">
|
||||
<h3 class="text-sm font-semibold text-surface-700 dark:text-surface-300 mb-2">
|
||||
{# Webmention send form — collapsed by default #}
|
||||
<details class="mt-8">
|
||||
<summary class="text-sm font-semibold text-surface-600 dark:text-surface-400 cursor-pointer hover:text-surface-700 dark:hover:text-surface-300 list-none [&::-webkit-details-marker]:hidden flex items-center gap-1.5">
|
||||
<svg class="w-3.5 h-3.5 transition-transform [[open]>&]:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
Send a Webmention
|
||||
</h3>
|
||||
<p class="text-xs text-surface-600 dark:text-surface-400 mb-3">
|
||||
Have you written a response to this post? Send a webmention by entering your post URL below.
|
||||
</p>
|
||||
<form action="https://webmention.io/{{ site.webmentions.domain }}/webmention" method="post" class="flex gap-2">
|
||||
<input type="hidden" name="target" value="{{ site.url }}{{ page.url }}">
|
||||
<input
|
||||
type="url"
|
||||
name="source"
|
||||
placeholder="https://your-site.com/response"
|
||||
required
|
||||
class="flex-1 px-3 py-2 text-sm bg-surface-50 dark:bg-surface-700 border border-surface-300 dark:border-surface-600 rounded focus:outline-none focus:ring-2 focus:ring-accent-500"
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-accent-600 hover:bg-accent-700 rounded transition-colors">
|
||||
Send
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
</summary>
|
||||
<div class="mt-3 p-4 bg-surface-100 dark:bg-surface-800 rounded-lg">
|
||||
<p class="text-xs text-surface-600 dark:text-surface-400 mb-3">
|
||||
Have you written a response to this post? Send a webmention by entering your post URL below.
|
||||
</p>
|
||||
<form action="https://webmention.io/{{ site.webmentions.domain }}/webmention" method="post" class="flex gap-2">
|
||||
<input type="hidden" name="target" value="{{ site.url }}{{ page.url }}">
|
||||
<input
|
||||
type="url"
|
||||
name="source"
|
||||
placeholder="https://your-site.com/response"
|
||||
required
|
||||
class="flex-1 px-3 py-2 text-sm bg-surface-50 dark:bg-surface-700 border border-surface-300 dark:border-surface-600 rounded focus:outline-none focus:ring-2 focus:ring-accent-500"
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-accent-600 hover:bg-accent-700 rounded transition-colors">
|
||||
Send
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -3,8 +3,20 @@ layout: layouts/base.njk
|
||||
withBlogSidebar: true
|
||||
---
|
||||
<article class="h-entry post" x-data="lightbox" @keydown.window="onKeydown($event)">
|
||||
{# Support both camelCase (Indiekit Eleventy preset) and underscore (legacy) property names #}
|
||||
{% set bookmarkedUrl = bookmarkOf or bookmark_of %}
|
||||
{% set likedUrl = likeOf or like_of %}
|
||||
{% set replyTo = inReplyTo or in_reply_to %}
|
||||
{% set repostedUrl = repostOf or repost_of %}
|
||||
|
||||
{% if title %}
|
||||
<h1 class="p-name text-2xl sm:text-3xl font-bold text-surface-900 dark:text-surface-100 mb-3 sm:mb-4">{{ title }}</h1>
|
||||
{% else %}
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="text-sm font-medium text-surface-500 dark:text-surface-400">
|
||||
{% if replyTo %}↩ Reply{% elif likedUrl %}♥ Like{% elif repostedUrl %}♻ Repost{% elif bookmarkedUrl %}🔖 Bookmark{% else %}✎ Note{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="post-meta mb-4 sm:mb-6">
|
||||
@@ -27,12 +39,6 @@ withBlogSidebar: true
|
||||
|
||||
{# Bridgy syndication content - controls what gets posted to social networks #}
|
||||
{# For interaction types (bookmarks, likes, replies, reposts), include the target URL #}
|
||||
{# Support both camelCase (Indiekit Eleventy preset) and underscore (legacy) property names #}
|
||||
{% set bookmarkedUrl = bookmarkOf or bookmark_of %}
|
||||
{% set likedUrl = likeOf or like_of %}
|
||||
{% set replyTo = inReplyTo or in_reply_to %}
|
||||
{% set repostedUrl = repostOf or repost_of %}
|
||||
|
||||
{% set bridgySummary = description or summary or (content | ogDescription(280)) %}
|
||||
{% set interactionUrl = bookmarkedUrl or likedUrl or replyTo or repostedUrl %}
|
||||
|
||||
@@ -53,44 +59,31 @@ withBlogSidebar: true
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="e-content prose prose-surface dark:prose-invert max-w-none">
|
||||
{% set isInteraction = replyTo or likedUrl or repostedUrl or bookmarkedUrl %}
|
||||
{% set hasContent = content and content | striptags | trim %}
|
||||
<div class="e-content prose prose-surface dark:prose-invert max-w-none{% if isInteraction and hasContent %} border-l-[3px] border-l-accent-500 dark:border-l-accent-400 pl-4{% endif %}">
|
||||
{{ content | safe }}
|
||||
</div>
|
||||
|
||||
{# AI usage disclosure #}
|
||||
{# AI usage disclosure — collapsed by default #}
|
||||
{% set aiTextLevel = aiTextLevel or ai_text_level %}
|
||||
{% set aiCodeLevel = aiCodeLevel or ai_code_level %}
|
||||
{% set aiTools = aiTools or ai_tools %}
|
||||
{% set aiDescription = aiDescription or ai_description %}
|
||||
{% if aiTextLevel or aiCodeLevel or aiTools %}
|
||||
<aside class="mt-6 p-4 rounded-lg bg-surface-50 dark:bg-surface-800/50 border border-surface-200 dark:border-surface-700">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<svg class="w-4 h-4 text-surface-500 dark:text-surface-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<details class="mt-4 text-xs text-surface-500 dark:text-surface-400">
|
||||
<summary class="cursor-pointer hover:text-surface-600 dark:hover:text-surface-300 list-none flex items-center gap-1.5 [&::-webkit-details-marker]:hidden">
|
||||
<svg class="w-3.5 h-3.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2 2 0 01-2 2H7a2 2 0 01-2-2v-2.5"/>
|
||||
</svg>
|
||||
<strong class="text-sm font-semibold text-surface-700 dark:text-surface-300">AI Usage</strong>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3 text-xs text-surface-600 dark:text-surface-400">
|
||||
{% if aiTextLevel %}
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-surface-100 dark:bg-surface-700">
|
||||
Text: {% if aiTextLevel === "0" %}None{% elif aiTextLevel === "1" %}Editorial{% elif aiTextLevel === "2" %}Co-drafted{% elif aiTextLevel === "3" %}AI-generated{% endif %}
|
||||
<span>AI:
|
||||
{% if aiTextLevel %}Text {% if aiTextLevel === "0" %}None{% elif aiTextLevel === "1" %}Editorial{% elif aiTextLevel === "2" %}Co-drafted{% elif aiTextLevel === "3" %}AI-generated{% endif %}{% endif %}{% if aiCodeLevel %}{% if aiTextLevel %} · {% endif %}Code {% if aiCodeLevel === "0" %}Human{% elif aiCodeLevel === "1" %}AI-assisted{% elif aiCodeLevel === "2" %}AI-generated{% endif %}{% endif %}{% if aiTools %}{% if aiTextLevel or aiCodeLevel %} · {% endif %}{{ aiTools }}{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if aiCodeLevel %}
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-surface-100 dark:bg-surface-700">
|
||||
Code: {% if aiCodeLevel === "0" %}Human{% elif aiCodeLevel === "1" %}AI-assisted{% elif aiCodeLevel === "2" %}AI-generated{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if aiTools %}
|
||||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-surface-100 dark:bg-surface-700">
|
||||
Tools: {{ aiTools }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</summary>
|
||||
{% if aiDescription %}
|
||||
<p class="mt-2 text-xs text-surface-500 dark:text-surface-400">{{ aiDescription }}</p>
|
||||
<p class="mt-1 pl-5">{{ aiDescription }}</p>
|
||||
{% endif %}
|
||||
</aside>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
{# Rich reply context with h-cite microformat #}
|
||||
|
||||
@@ -19,6 +19,7 @@ document.addEventListener("alpine:init", () => {
|
||||
statusMessage: "",
|
||||
statusType: "info",
|
||||
maxLength: 2000,
|
||||
showForm: false,
|
||||
|
||||
async init() {
|
||||
await this.checkSession();
|
||||
|
||||
Reference in New Issue
Block a user