diff --git a/modules/blox/blox/portfolio/README.md b/modules/blox/blox/portfolio/README.md new file mode 100644 index 000000000..7ca73bb3d --- /dev/null +++ b/modules/blox/blox/portfolio/README.md @@ -0,0 +1,54 @@ +# Hugo Blox Portfolio Block + +A responsive, filterable content grid component for displaying posts, projects, publications, or any Hugo content with advanced filtering and customization options. + +## Key Features + +- **Dynamic Filtering** - Interactive filter buttons by tags or categories with smooth transitions +- **Multiple Views** - article-grid, card, citation, list, compact, and custom layouts +- **Flexible Grid** - 1-4 column responsive layouts with mobile-first design +- **Smart Pagination** - Limit items shown per filter with auto-generated "View All" links +- **Content Filtering** - Filter by folders, tags, categories, authors, dates, publication types, and featured status +- **Customizable Visibility** - Hide/show authors, dates, tags, categories, read time, and images +- **Sorting Options** - Sort by date, title, or custom fields in ascending/descending order +- **Archive Integration** - Automatic links to taxonomy archive pages with item counts +- **Smooth Animations** - Fade and scale transitions when filtering items +- **Type-Safe** - Robust handling of tags/categories with backwards compatibility for old and new filter formats + +## Configuration Options + +### Content + +- `page_type` +- `folders` +- `tags` +- `categories` +- `author` +- `featured_only` +- `count` +- `offset` +- `sort_by` +- `sort_ascending` + +### Design + +- `view` +- `columns` +- `filter_type` +- `filter_items` +- `filter_label` +- `max_posts_per_filter` +- `hide_author` +- `hide_tags` +- `hide_categories` +- `hide_date` +- `show_read_time` +- `fill_image` + +### JavaScript + +Client-side filtering with `portfolioFilter` object handling item visibility, button states, and "View All" link management without page reloads. + +*** + +**Perfect for:** Academic portfolios, project showcases, blog post grids, publication libraries, and any content collection requiring elegant filtering and presentation. diff --git a/modules/blox/blox/portfolio/block-usage b/modules/blox/blox/portfolio/block-usage new file mode 100644 index 000000000..857a7978a --- /dev/null +++ b/modules/blox/blox/portfolio/block-usage @@ -0,0 +1,348 @@ +- block: portfolio + id: portfolio + content: + title: PORTFOLIO + text: '' + filters: + folders: + - post + - courses + - events + - slides + count: 0 + archive: + enable: true + text: 'VIEW ALL POSTS' + text_template: 'VIEW ALL %s POSTS' + design: + view: citation + columns: '1' + # OLD FORMAT - Now supported! + filter_button: + - name: BLOG + tag: '*' + - name: PRACTICE + tag: Events + - name: CLASS + tag: Course + - name: TESTS + tag: Quizzes + filter_type: "categories" + filter_label: "Topics" + max_posts_per_filter: 3 + show_date: false + hide_date: true + show_read_more: true + hide_author: true # ← SET TO TRUE TO HIDE AUTHOR + hide_tags: true # ← SET TO TRUE TO HIDE TAGS + hide_categories: false + spacing: + padding: [0, 0, 0, 0] + css_class: "" + +You can see it in https://innerknowing.xyz/en/ + +Here are complete example Hugo Blox Portfolio block configurations for your `_index.md` file: + +*** + +## **Basic Portfolio Block** + +```yaml +--- +title: My Portfolio +type: landing + +sections: + - block: portfolio + id: projects + content: + title: Featured Projects + subtitle: Selected works and research + text: Browse my portfolio of projects and publications + filters: + folders: ['project'] + count: 12 + sort_by: 'Date' + sort_ascending: false + design: + view: article-grid + columns: '3' + filter_button: + - name: All + tag: '*' + - name: Research + tag: 'research' + - name: Development + tag: 'development' +--- +``` + +*** + +## **Advanced Portfolio with Custom Filters** + +```yaml +--- +title: Research Portfolio +type: landing + +sections: + - block: portfolio + id: publications + content: + title: Publications & Projects + subtitle: Academic research and open-source contributions + text: | + Explore my work in **neuroscience**, **embodied cognition**, and **somatic experience**. + + # Filter content + filters: + folders: ['publication', 'project'] + tags: ['neuroscience', 'somatic-markers', 'NLP'] + exclude_tags: ['draft', 'private'] + featured_only: false + exclude_past: false + exclude_future: false + + # Pagination + count: 9 # Show 9 items per page + offset: 0 + + # Sorting + sort_by: 'Date' + sort_ascending: false + + # Archive page links + archive: + text: "View All Publications" + text_template: "View All %s Posts" + + design: + # Layout + view: article-grid # Options: article-grid, card, citation, list, compact + columns: '3' + + # Filtering + filter_type: tags # Options: tags, categories + filter_items: ['neuroscience', 'somatic-markers', 'NLP', 'research'] + filter_label: "Filter by topic:" + max_posts_per_filter: 6 + + # Visibility controls + hide_author: true + hide_tags: false + hide_categories: true + hide_date: false + show_date: true + show_read_time: true + show_read_more: true + fill_image: true + + # No results message + no_results_title: "No publications found" + no_results_text: "Try selecting a different topic filter." +--- +``` + +*** + +## **Minimal Portfolio (Blog Posts)** + +```yaml +--- +title: Blog +type: landing + +sections: + - block: portfolio + id: blog + content: + title: Latest Posts + filters: + folders: ['blog'] + count: 6 + design: + view: article-grid + columns: '2' + filter_button: + - name: All + tag: '*' + - name: AI + tag: 'artificial-intelligence' + - name: Privacy + tag: 'privacy' +--- +``` + +*** + +## **Card View Portfolio** + +```yaml +--- +title: Projects +type: landing + +sections: + - block: portfolio + id: showcase + content: + title: Project Showcase + text: My open-source and research projects + filters: + folders: ['project'] + featured_only: true + count: 12 + archive: + text: "View Complete Portfolio" + design: + view: card + columns: '2' + filter_type: categories + max_posts_per_filter: 4 + hide_author: true + hide_date: false +--- +``` + +*** + +## **Academic Publications Portfolio** + +```yaml +--- +title: Research +type: landing + +sections: + - block: portfolio + id: publications + content: + title: Research Publications + subtitle: Peer-reviewed articles and conference papers + filters: + folders: ['publication'] + publication_type: '2' # Journal articles + sort_by: 'PublishDate' + sort_ascending: false + count: 20 + design: + view: citation + columns: '1' + filter_button: + - name: All + tag: '*' + - name: Neuroscience + tag: 'neuroscience' + - name: Somatic + tag: 'somatic-experience' + hide_author: false + hide_tags: true + show_date: true +--- +``` + +*** + +## **Multiple Portfolios on Same Page** + +```yaml +--- +title: Complete Portfolio +type: landing + +sections: + # Featured projects + - block: portfolio + id: featured + content: + title: Featured Work + filters: + folders: ['project'] + featured_only: true + count: 3 + design: + view: card + columns: '3' + filter_button: false # Disable filtering + + # All projects with filters + - block: portfolio + id: all-projects + content: + title: All Projects + filters: + folders: ['project'] + count: 12 + design: + view: article-grid + columns: '3' + filter_type: tags + max_posts_per_filter: 6 + + # Publications + - block: portfolio + id: publications + content: + title: Publications + filters: + folders: ['publication'] + count: 10 + design: + view: citation + columns: '1' + filter_type: categories +--- +``` + +*** + +## **Key Configuration Tips** + +**1. Content Folder Structure**: +``` +content/ +├── _index.md # Homepage with portfolio block +├── project/ # Projects folder +│ ├── project-1/ +│ │ └── index.md +│ └── project-2/ +│ └── index.md +└── publication/ # Publications folder + └── paper-1/ + └── index.md +``` + +**2. Individual Item Front Matter** (`content/project/my-project/index.md`): +```yaml +--- +title: "My Project" +date: 2025-11-15 +tags: ['AI', 'research', 'python'] +categories: ['Development'] +featured: true +--- +``` + +**3. View Types**: +- `article-grid` - Standard card grid (default) +- `card` - Larger cards with more detail +- `citation` - Academic citation format (centered) +- `list` - Simple list view +- `compact` - Minimal compact listing + +**4. Filter Types**: +- `tags` - Filter by post tags +- `categories` - Filter by post categories + +**5. Common Folders**: +- `project` - Projects +- `publication` - Academic papers +- `post` or `blog` - Blog posts +- `talk` - Conference talks +- `event` - Events + +*** + +Choose the configuration that best matches your needs and customize the `title`, `subtitle`, `text`, `filters`, and `design` options to fit your content structure! diff --git a/modules/blox/blox/portfolio/block.html b/modules/blox/blox/portfolio/block.html new file mode 100644 index 000000000..9ba501f19 --- /dev/null +++ b/modules/blox/blox/portfolio/block.html @@ -0,0 +1,551 @@ +{{/* Hugo Blox: Portfolio - SUPPORTS BOTH OLD & NEW FILTER FORMATS */}} +{{/* Documentation: https://hugoblox.com/blocks/ */}} +{{/* License: https://github.com/HugoBlox/hugo-blox-builder/blob/main/LICENSE.md */}} + +{{/* Initialise */}} +{{ $page := .wcPage }} +{{ $block := .wcBlock }} +{{ $view := $block.design.view | default "article-grid" }} +{{ $items_offset := $block.content.offset | default 0 }} +{{ $items_count := $block.content.count }} +{{ if eq $items_count 0 }} + {{ $items_count = 65535 }} +{{ else }} + {{ $items_count = $items_count | default 12 }} +{{ end }} + +{{/* Query */}} +{{ $query := site.RegularPages }} +{{ $archive_page := "" }} + +{{/* Filters */}} +{{ if $block.content.page_type }} + {{ $query = where $query "Type" $block.content.page_type }} + {{ $archive_page = site.GetPage "Section" $block.content.page_type }} +{{ end }} +{{ if $block.content.filters.folders }} + {{ $folders := $block.content.filters.folders }} + {{ $query = where $query "Section" "in" $folders }} + {{ $main_folder := index $folders 0 }} + {{ $archive_page = site.GetPage "Section" $main_folder }} +{{ end }} +{{ if $block.content.filters.tags }} + {{ $query = where $query "Params.tags" "intersect" $block.content.filters.tags }} +{{ end }} +{{ if $block.content.filters.exclude_tags }} + {{ $query = $query | symdiff (where site.RegularPages "Params.tags" "intersect" $block.content.filters.exclude_tags) }} +{{ end }} +{{ if $block.content.filters.tag }} + {{ $archive_page = site.GetPage (printf "tags/%s" (urlize $block.content.filters.tag)) }} + {{ $query = $query | intersect $archive_page.Pages }} +{{ end }} +{{ if $block.content.filters.category }} + {{ $archive_page = site.GetPage (printf "categories/%s" (urlize $block.content.filters.category)) }} + {{ $query = $query | intersect $archive_page.Pages }} +{{ end }} +{{ if $block.content.filters.publication_type }} + {{ $archive_page = site.GetPage (printf "publication_types/%s" $block.content.filters.publication_type) }} + {{ $query = $query | intersect $archive_page.Pages }} +{{ end }} +{{ if $block.content.filters.exclude_publication_type }} + {{ $query = $query | complement (site.GetPage (printf "publication_types/%s" $block.content.filters.exclude_publication_type)).Pages }} +{{ end }} +{{ if $block.content.filters.author }} + {{ $archive_page = site.GetPage (printf "authors/%s" (urlize $block.content.filters.author)) }} + {{ $query = $query | intersect $archive_page.Pages }} +{{ end }} +{{ if $block.content.filters.featured_only }} + {{ $query = where $query "Params.featured" "==" true }} +{{ end }} +{{ if $block.content.filters.exclude_featured }} + {{ $query = where $query "Params.featured" "!=" true }} +{{ end }} +{{ if $block.content.filters.exclude_past }} + {{ $query = where $query "Date" ">=" now }} +{{ end }} +{{ if $block.content.filters.exclude_future }} + {{ $query = where $query "Date" "<" now }} +{{ end }} + +{{/* Sort */}} +{{ $sort_by := $block.content.sort_by | default "Date" }} +{{ $sort_by = partial "functions/get_sort_by_parameter" $sort_by }} +{{ $sort_ascending := $block.content.sort_ascending | default false }} +{{ $sort_order := cond $sort_ascending "asc" "desc" }} +{{ $query = sort $query $sort_by $sort_order }} + +{{/* Offset and Limit */}} +{{ if gt $items_offset 0 }} + {{ $query = first $items_count (after $items_offset $query) }} +{{ else }} + {{ $query = first $items_count $query }} +{{ end }} + +{{/* Filter configuration */}} +{{ $filter_type := $block.design.filter_type | default "tags" }} +{{ $max_posts_per_filter := $block.design.max_posts_per_filter | default 6 }} + +{{/* Build filter buttons - SUPPORT BOTH FORMATS */}} +{{ $filter_buttons := slice }} +{{ $use_old_format := false }} + +{{/* Check if using old array format: [{name: "X", tag: "Y"}] */}} +{{ with $block.design.filter_button }} + {{ if reflect.IsSlice . }} + {{ $use_old_format = true }} + {{ $filter_buttons = . }} + {{ end }} +{{ end }} + +{{/* If not old format, build from tags/categories */}} +{{ if not $use_old_format }} + {{ $custom_filter_items := $block.design.filter_items | default slice }} + + {{/* Collect all available categories and tags from queried items */}} + {{ $all_categories := slice }} + {{ $all_tags := slice }} + {{ range $query }} + {{ with .Params.categories }} + {{ if reflect.IsSlice . }} + {{ range . }} + {{ if and . (eq (printf "%T" .) "string") }} + {{ $all_categories = $all_categories | append . }} + {{ end }} + {{ end }} + {{ end }} + {{ end }} + {{ with .Params.tags }} + {{ if reflect.IsSlice . }} + {{ range . }} + {{ if and . (eq (printf "%T" .) "string") }} + {{ $all_tags = $all_tags | append . }} + {{ end }} + {{ end }} + {{ end }} + {{ end }} + {{ end }} + {{ $all_categories = $all_categories | uniq | sort }} + {{ $all_tags = $all_tags | uniq | sort }} + + {{/* Determine which items to show */}} + {{ $filter_items := slice }} + {{ if eq $filter_type "categories" }} + {{ if $custom_filter_items }} + {{ $filter_items = $custom_filter_items }} + {{ else }} + {{ $filter_items = $all_categories }} + {{ end }} + {{ else if eq $filter_type "tags" }} + {{ if $custom_filter_items }} + {{ $filter_items = $custom_filter_items }} + {{ else }} + {{ $filter_items = $all_tags }} + {{ end }} + {{ end }} + + {{/* Convert to button format */}} + {{ range $filter_items }} + {{ $filter_buttons = $filter_buttons | append (dict "name" (. | title) "tag" .) }} + {{ end }} +{{ end }} + +{{/* Configuration for hiding elements */}} +{{ $hide_author := $block.design.hide_author | default true }} +{{ $hide_tags := $block.design.hide_tags | default true }} +{{ $hide_categories := $block.design.hide_categories | default false }} +{{ $hide_date := $block.design.hide_date | default false }} +{{ $show_date := $block.design.show_date | default true }} +{{ $columns := $block.design.columns | default "3" }} +{{ $filter_enabled := true }} +{{ if isset $block.design "filter_button" }} + {{ if not $block.design.filter_button }} + {{ $filter_enabled = false }} + {{ end }} +{{ end }} + +{{/* Generate unique ID for this portfolio block */}} +{{ $portfolio_id := printf "portfolio-%d" now.UnixNano }} + +
+ {{/* Container with max-w-7xl */}} +
+ {{/* Title */}} + {{ if $block.content.title }} +
+
+ {{ $block.content.title | emojify | $page.RenderString }} +
+ {{ with $block.content.text }}

{{ . | emojify | $page.RenderString }}

{{ end }} +
+ {{ end }} + + {{/* Filter Controls - Rounded Button Group */}} + {{ if and $filter_enabled $filter_buttons }} +
+
+ {{ with $block.design.filter_label }} + {{ . }} + {{ end }} +
+ {{/* Collect visible buttons first */}} + {{ $visible_buttons := slice }} + + {{ range $button := $filter_buttons }} + {{ if or (eq $button.tag "*") (eq $button.tag "all") }} + {{ $visible_buttons = $visible_buttons | append $button }} + {{ else }} + {{ $item_count := 0 }} + {{ if eq $filter_type "categories" }} + {{ $item_count = len (where $query "Params.categories" "intersect" (slice $button.tag)) }} + {{ else }} + {{ $item_count = len (where $query "Params.tags" "intersect" (slice $button.tag)) }} + {{ end }} + {{ if gt $item_count 0 }} + {{ $visible_buttons = $visible_buttons | append $button }} + {{ end }} + {{ end }} + {{ end }} + + {{/* Render buttons */}} + {{ $button_count := len $visible_buttons }} + {{ range $index, $button := $visible_buttons }} + {{ $is_first := eq $index 0 }} + {{ $is_last := eq $index (sub $button_count 1) }} + {{ $is_all := or (eq $button.tag "*") (eq $button.tag "all") }} + + {{/* Determine rounding classes */}} + {{ $rounding := "" }} + {{ if $is_first }} + {{ $rounding = "rounded-l-lg" }} + {{ end }} + {{ if $is_last }} + {{ $rounding = printf "%s rounded-r-lg" $rounding }} + {{ end }} + + {{/* Determine border classes */}} + {{ $border_classes := "border" }} + {{ if not $is_first }} + {{ $border_classes = "border-t border-b border-r" }} + {{ end }} + + {{/* Set first button (All) as active by default */}} + {{ $active_class := "" }} + {{ if $is_first }} + {{ $active_class = "bg-primary-700 text-white border-primary-700" }} + {{ else }} + {{ $active_class = "bg-white text-gray-900 border-gray-200 hover:bg-gray-100 hover:text-primary-700 dark:bg-gray-800 dark:text-white dark:border-gray-700 dark:hover:bg-gray-700" }} + {{ end }} + + + {{ end }} +
+
+
+ {{ end }} + + {{/* Portfolio Grid - FIXED: Safe type handling */}} +
+ {{ $config := dict + "columns" ($block.design.columns | default 3) + "len" (len $query) + "fill_image" ($block.design.fill_image | default true) + "show_date" (and $show_date (not $hide_date)) + "show_read_time" ($block.design.show_read_time | default false) + "show_read_more" ($block.design.show_read_more | default true) + }} + +
+ {{/* Render each item once with safe type checking */}} + {{ range $index, $item := $query }} + {{ $item_tags := slice }} + {{ $item_categories := slice }} + + {{/* Safely collect tags - ensure it's a slice of strings */}} + {{ with $item.Params.tags }} + {{ if reflect.IsSlice . }} + {{ range . }} + {{ if and . (eq (printf "%T" .) "string") }} + {{ $item_tags = $item_tags | append (string .) }} + {{ end }} + {{ end }} + {{ end }} + {{ end }} + + {{/* Safely collect categories - ensure it's a slice of strings */}} + {{ with $item.Params.categories }} + {{ if reflect.IsSlice . }} + {{ range . }} + {{ if and . (eq (printf "%T" .) "string") }} + {{ $item_categories = $item_categories | append (string .) }} + {{ end }} + {{ end }} + {{ end }} + {{ end }} + + {{/* Build filter data attributes */}} + {{ $filter_data := slice "*" }} + {{ if eq $filter_type "categories" }} + {{ range $item_categories }} + {{ $filter_data = $filter_data | append . }} + {{ end }} + {{ else }} + {{ range $item_tags }} + {{ $filter_data = $filter_data | append . }} + {{ end }} + {{ end }} + {{ $filter_data = $filter_data | uniq }} + + {{/* Generate CSS classes safely */}} + {{ $js_tag_classes := "" }} + {{ if gt (len $item_tags) 0 }} + {{ $tag_classes := slice }} + {{ range $item_tags }} + {{ $clean_tag := . | string | lower | replaceRE "[^a-z0-9-]" "-" }} + {{ $tag_classes = $tag_classes | append (printf "js-id-%s" $clean_tag) }} + {{ end }} + {{ $js_tag_classes = delimit $tag_classes " " }} + {{ end }} + +
+ {{ partial "functions/render_view" (dict "page" $block "item" $item "view" $view "index" $index "config" $config) }} +
+ {{ end }} +
+ + {{/* View All Links - Per Filter */}} + {{ range $button := $filter_buttons }} + {{ if and (ne $button.tag "*") (ne $button.tag "all") }} + {{ $current_filter := $button.tag }} + {{ $filtered_posts := slice }} + {{ if eq $filter_type "categories" }} + {{ $filtered_posts = where $query "Params.categories" "intersect" (slice $current_filter) }} + {{ else }} + {{ $filtered_posts = where $query "Params.tags" "intersect" (slice $current_filter) }} + {{ end }} + + {{ if gt (len $filtered_posts) $max_posts_per_filter }} + {{ $view_all_link := "" }} + {{ if eq $filter_type "categories" }} + {{ $view_all_link = printf "categories/%s/" ($current_filter | urlize) | relLangURL }} + {{ else }} + {{ $view_all_link = printf "tags/%s/" ($current_filter | urlize) | relLangURL }} + {{ end }} + + {{ $filtered_button_text := printf "View All %s Posts" $button.name }} + {{ if $block.content.archive.text_template }} + {{ $filtered_button_text = printf $block.content.archive.text_template $button.name }} + {{ end }} + + + {{ end }} + {{ end }} + {{ end }} + + {{/* View All link for "All" filter */}} + {{ if and $archive_page (gt (len $query) $max_posts_per_filter) }} + + {{ end }} + + {{/* No results message */}} + +
+
+
+ + + + diff --git a/modules/blox/blox/portfolio/manifest.json b/modules/blox/blox/portfolio/manifest.json new file mode 100644 index 000000000..94400ba6d --- /dev/null +++ b/modules/blox/blox/portfolio/manifest.json @@ -0,0 +1,57 @@ +{ + "id": "portfolio", + "name": "Portfolio", + "version": "1.0.0", + "license": "MIT", + "category": "content", + "tags": [ + "portfolio", + "collection", + "filter", + "grid", + "blog", + "publications", + "projects", + "content-display", + "responsive", + "taxonomy" + ], + "description": "A responsive, filterable content grid component for displaying posts, projects, publications, or any Hugo content with advanced filtering and customization options", + "author": "Hugo Blox", + "homepage": "https://hugoblox.com/blocks/", + "repository": "https://github.com/HugoBlox/kit", + "keywords": [ + "hugo", + "static-site", + "portfolio", + "filter", + "grid", + "responsive", + "content", + "taxonomy", + "pagination", + "tailwind" + ], + "features": [ + "Dynamic filtering by tags or categories", + "Multiple view layouts (article-grid, card, citation, list, compact)", + "Responsive 1-4 column grid layouts", + "Smart pagination with View All links", + "Content filtering by folders, tags, categories, authors, dates", + "Customizable visibility controls", + "Smooth animations and transitions", + "Archive integration with item counts", + "Type-safe filter handling" + ], + "views": [ + "article-grid", + "card", + "citation", + "list", + "compact" + ], + "dependencies": { + "hugo": ">=0.110.0", + "tailwindcss": ">=3.0.0" + } +} diff --git a/modules/blox/blox/portfolio/preview.png b/modules/blox/blox/portfolio/preview.png new file mode 100644 index 000000000..358b86e97 Binary files /dev/null and b/modules/blox/blox/portfolio/preview.png differ diff --git a/modules/blox/blox/portfolio/schema.json b/modules/blox/blox/portfolio/schema.json new file mode 100644 index 000000000..384090d24 --- /dev/null +++ b/modules/blox/blox/portfolio/schema.json @@ -0,0 +1,334 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://hugoblox.com/schemas/blocks/portfolio.json", + "title": "Portfolio Block Schema", + "description": "Schema for the Portfolio block - displays responsive, filterable content grid with advanced filtering and customization options", + "allOf": [ + { + "$ref": "../shared/schemas/base-block.json" + }, + { + "type": "object", + "properties": { + "block": { + "const": "portfolio", + "description": "Block type identifier" + }, + "content": { + "type": "object", + "description": "Content configuration for the portfolio block", + "properties": { + "title": { + "type": "string", + "description": "Section title (supports Markdown and emoji)" + }, + "text": { + "type": "string", + "description": "Section description (supports Markdown and emoji)" + }, + "count": { + "type": "integer", + "description": "Maximum number of items to display (0 means unlimited)", + "minimum": 0, + "default": 12 + }, + "offset": { + "type": "integer", + "description": "Number of items to skip from the start of the query", + "minimum": 0, + "default": 0 + }, + "page_type": { + "type": "string", + "description": "Filter by page type/section (e.g., 'post', 'publication', 'project')", + "examples": ["post", "publication", "project", "event"] + }, + "sort_by": { + "type": "string", + "description": "Field to sort by (e.g., Date, Title, Weight, Lastmod, PublishDate)", + "default": "Date", + "examples": ["Date", "Title", "Weight", "Lastmod", "PublishDate"] + }, + "sort_ascending": { + "type": "boolean", + "description": "Sort in ascending order (false = descending)", + "default": false + }, + "filters": { + "type": "object", + "description": "Advanced content filtering options", + "properties": { + "folders": { + "type": "array", + "description": "Filter by content folders/sections", + "items": { + "type": "string" + }, + "examples": [["post"], ["publication", "preprint"]] + }, + "tags": { + "type": "array", + "description": "Filter by multiple tags (intersection)", + "items": { + "type": "string" + } + }, + "exclude_tags": { + "type": "array", + "description": "Exclude items with these tags", + "items": { + "type": "string" + } + }, + "tag": { + "type": "string", + "description": "Filter by a single tag" + }, + "category": { + "type": "string", + "description": "Filter by a single category" + }, + "publication_type": { + "type": "string", + "description": "Filter by publication type" + }, + "exclude_publication_type": { + "type": "string", + "description": "Exclude specific publication type" + }, + "author": { + "type": "string", + "description": "Filter by author username" + }, + "featured_only": { + "type": "boolean", + "description": "Show only featured content", + "default": false + }, + "exclude_featured": { + "type": "boolean", + "description": "Exclude featured content", + "default": false + }, + "exclude_past": { + "type": "boolean", + "description": "Exclude past-dated content", + "default": false + }, + "exclude_future": { + "type": "boolean", + "description": "Exclude future-dated content", + "default": false + } + }, + "additionalProperties": false + }, + "archive": { + "type": "object", + "description": "Archive link configuration", + "properties": { + "enable": { + "type": "boolean", + "description": "Show archive link even when item count is below limit" + }, + "link": { + "type": "string", + "description": "Override archive URL (defaults to derived archive page)" + }, + "text": { + "type": "string", + "description": "Override archive link text for 'All' filter", + "default": "View All Posts" + }, + "text_template": { + "type": "string", + "description": "Template for per-filter archive links (use %s for filter name)", + "examples": ["View All %s Posts", "See all %s articles"] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "design": { + "type": "object", + "description": "Layout and filtering options for portfolio rendering", + "properties": { + "view": { + "type": "string", + "enum": ["article-grid", "card", "compact", "citation", "list", "masonry"], + "description": "Display view type", + "default": "article-grid" + }, + "columns": { + "type": ["integer", "string"], + "description": "Number of columns for grid-based views (1-4)", + "default": 3, + "minimum": 1, + "maximum": 4 + }, + "fill_image": { + "type": "boolean", + "description": "Fill cards with cover images when available", + "default": true + }, + "show_date": { + "type": "boolean", + "description": "Display published/modified date", + "default": true + }, + "hide_date": { + "type": "boolean", + "description": "Hide date display (overrides show_date)", + "default": false + }, + "show_read_time": { + "type": "boolean", + "description": "Display estimated reading time", + "default": false + }, + "show_read_more": { + "type": "boolean", + "description": "Show a read more link on cards", + "default": true + }, + "hide_author": { + "type": "boolean", + "description": "Hide author information", + "default": true + }, + "hide_tags": { + "type": "boolean", + "description": "Hide tags display", + "default": true + }, + "hide_categories": { + "type": "boolean", + "description": "Hide categories display", + "default": false + }, + "filter_button": { + "oneOf": [ + { + "type": "boolean", + "description": "Enable/disable filtering (false disables)" + }, + { + "type": "array", + "description": "Legacy format: Array of filter button objects", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Display name for the filter button" + }, + "tag": { + "type": "string", + "description": "Tag or category value to filter by (use '*' or 'all' for show all)" + } + }, + "required": ["name", "tag"], + "additionalProperties": false + } + } + ], + "description": "Filter button configuration (supports both old and new formats)" + }, + "filter_type": { + "type": "string", + "enum": ["tags", "categories"], + "description": "Type of taxonomy to use for filtering", + "default": "tags" + }, + "filter_items": { + "type": "array", + "description": "Custom list of filter items (tags or categories) to display", + "items": { + "type": "string" + } + }, + "filter_label": { + "type": "string", + "description": "Label text displayed above filter buttons", + "examples": ["Filter by topic:", "Browse by category:"] + }, + "max_posts_per_filter": { + "type": "integer", + "description": "Maximum items to show per filter before displaying 'View All' link", + "default": 6, + "minimum": 1 + }, + "no_results_title": { + "type": "string", + "description": "Title text for no results message", + "default": "No posts found" + }, + "no_results_text": { + "type": "string", + "description": "Description text for no results message", + "default": "Try selecting a different filter." + } + }, + "additionalProperties": true + } + }, + "required": ["content"], + "additionalProperties": true, + "examples": [ + { + "block": "portfolio", + "content": { + "title": "Recent Projects", + "text": "Explore our latest work and research", + "count": 12, + "page_type": "project", + "sort_by": "Date", + "sort_ascending": false, + "filters": { + "folders": ["project"], + "featured_only": false + }, + "archive": { + "text": "View All Projects", + "text_template": "View All %s Projects" + } + }, + "design": { + "view": "article-grid", + "columns": 3, + "filter_type": "tags", + "filter_label": "Filter by technology:", + "max_posts_per_filter": 6, + "fill_image": true, + "show_date": true, + "show_read_time": false, + "hide_author": true, + "hide_tags": false, + "hide_categories": true + } + }, + { + "block": "portfolio", + "content": { + "title": "Publications", + "count": 0, + "filters": { + "folders": ["publication"], + "exclude_publication_type": "thesis" + } + }, + "design": { + "view": "citation", + "columns": 1, + "filter_type": "categories", + "filter_items": ["Machine Learning", "Neuroscience", "Data Science"], + "hide_author": false, + "hide_date": false + } + } + ] + } + ] +}