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 }}
+
+
+ {{ $button.name }}
+
+ {{ 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 */}}
+
+
+
+
+
+
{{ $block.design.no_results_title | default "No posts found" }}
+
{{ $block.design.no_results_text | default "Try selecting a different filter." }}
+
+
+
+
+
+
+
+
+
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
+ }
+ }
+ ]
+ }
+ ]
+}