Initial commit

This commit is contained in:
Bashy 2026-03-27 16:47:50 +01:00
commit 52ae011cb6
16 changed files with 798 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
_config.yml

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 The Hackman Club
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

124
README.md Normal file
View file

@ -0,0 +1,124 @@
# Tapestry-Code Theme
Tapestry-Code is a Hexo theme derrived from Tapestry theme I am using for personal blog.
I am using Tapestry-Code for my [technical blog](https://blog.hackman.club/).
---
## 1. Installation
1. Clone the theme into your Hexo project:
```bash
cd your-hexo-project
git clone https://github.com/HackmanClub/tapestry-code.git themes/tapestry-code
```
1. Set the theme in Hexos main `_config.yml`:
```yaml
theme: tapestry-code
```
---
## 2. Configuration
The theme uses its own `_config.yml` inside `themes/tapestry/`. You will need to copy / rename `_EXAMPLE_config.yml` as a starting point:
```bash
cp themes/tapestry/_EXAMPLE_config.yml themes/tapestry/_config.yml
```
### Important Sections
#### Site Settings
- `avatar`, `banner`, `favicon`: paths to your images.
- `primary_color`: main color for your theme - it's used for links and avatar border.
- `secondary_color`: secondary color for your theme - it's used for links (mostly hover).
- `sidebar`: show or hide the sidebar.
- `scarves`: enable or disable scarves links.
#### Scarves
Scarf links appear at the top right. You can use an image or FontAwesome icon:
```yaml
scarf:
- url: tags/baking
image: images/assets/scarves/scarf.png
icon: false
enable: true
alt: Example
- url: tags/photo
image: false
icon: fa-solid fa-heart
enable: true
alt: Example
```
- `url` link target
- `image` path to scarf image
- `icon` FontAwesome icon class
- `enable` show/hide scarf
- `alt` accessibility text
#### Social Links
Add or remove social links easily:
```yaml
social:
- url: https://www.instagram.com/
icon: fab fa-instagram
relme: true
enable: true
```
If `relme: true` in config, the corresponding social media entry will have `rel="me"`, that you need for verifying sites (eg, mastodon).
---
### Sidebar Menu
Configure links in the sidebar:
```yaml
sidebar_menu:
- enable: true
name: Home
url: /
- enable: true
name: Get Hexo
url: https://hexo.io/docs/setup
```
- `enable` show/hide
- `name` link text
- `url` destination
---
### Blog images
Define image as:
`photos: images/image.png`
> ⚠️ Image shows only if `post.photos` exists, and only first image will be shown.
---
## 4. Running Locally
Start a local Hexo server to preview:
```bash
hexo clean
hexo s
```
- Visit `http://localhost:4000`
- Changes in templates or CSS are reflected immediately.

85
_EXAMPLE_config.yml Normal file
View file

@ -0,0 +1,85 @@
name: Tapestry-Code
version: 1
copy_year: 2026
# Site configuration
avatar: images/assets/avatar.jpg # Avatar image, also fontawesome fallback
banner: images/assets/banner.png
favicon: images/assets/avatar.jpg
fontawesome: https://kit.fontawesome.com/bae8420862.js # Use your own kit here
primary_color: '#ffc107'
secondary_color: '#296126'
sidebar: true # Display sidebar
scarves: true # Display scarves
#Footer
hexo: true # Display "Powered by Hexo" in footer
copyright: true # Display copyright
# Scarves (links on top right of screen) (add more / remove as needed)
# Defaults into FontAwesome if no image provided
# Disables scarf if no image, and font awesome not configured
scarf:
- url: tags/baking # Example Usecase for image
image: images/assets/scarves/scarf.png
icon: false
enable: true
color: false
alt: Example
- url: tags/photo # Example Usecase for FontAwesome
image: false
icon: fa-solid fa-heart
enable: true
color: false
alt: Example
# Enable or disable social links (add more / remove as needed)
social:
- url: https://www.instagram.com/
icon: fab fa-instagram
relme: true
enable: true
- url: https://discord.com/
icon: fab fa-discord
relme: true
enable: true
- url: https://www.twitch.tv/
icon: fab fa-twitch
enable: false
- url: https://mastodon.social
icon: fab fa-mastodon
enable: true
- url: https://www.linkedin.com/
icon: fab fa-linkedin
enable: true
- url: https://www.twitter.com/
icon: fab fa-twitter
enable: true
- url: https://www.facebook.com/
icon: fab fa-facebook
enable: false
- url: https://www.youtube.com/
icon: fab fa-youtube
enable: false
- url: https://www.exapmle.com/
icon: fab fa-hashtag
enable: false
# Sidebar menu configuration (add more / remove as needed)
sidebar_menu:
- enable: true
name: Home
url: /
- enable: true
name: Get Hexo
url: https://hexo.io/docs/setup
- enable: true
name: Get Tapestry
url: https://tapestry.demo.bashynx.dev
- enable: true
name: Bashynx
url: https://bashynx.com
- enable: true
name: The Hackman Club
url: https://blog.hackman.club

19
layout/_partial/date.ejs Normal file
View file

@ -0,0 +1,19 @@
<% {
const posted = new Date(date);
const now = new Date();
const day = ((d => {
if (d > 3 && d < 21) return d + 'th';
switch (d % 10) {
case 1: return d + 'st';
case 2: return d + 'nd';
case 3: return d + 'rd';
default: return d + 'th';
}
})(posted.getDate()));
const options = now.getFullYear() === posted.getFullYear()
? { month: 'long', hour: 'numeric', minute: 'numeric', hour12: true }
: { year: 'numeric', month: 'long'};
%>
<%= day %> <%= posted.toLocaleString('en-US', options).replace(/^(\d+)\s/, '') %>
<% } %>

12
layout/_partial/feed.ejs Normal file
View file

@ -0,0 +1,12 @@
<!-- Default number of columns is two. If 3 is defined, then third column is added-->
<div id="masonry">
<% page.posts.forEach(function(post) { %>
<%- include('post_feed', { post: post }) %>
<% }); %>
</div>
<div class="pagination">
<% if (page.next) { %>
<a href="<%- url_for(page.next_link) %>"></a>
<% } %>
</div>

View file

@ -0,0 +1,32 @@
<!-- This is the template for the post. It is used to display the posts in the feeds. -->
<% if(post.photos && post.photos.length) { %>
<% classTag = "post-with-image" %>
<% } else { %>
<% classTag = "post-without-image" %>
<% } %>
<div class="post-feed border">
<a href='<%= url_for(post.path) %>'><h1><%= post.title || "Untitled" %></h1>
<div class="<%= classTag %>">
<% if(post.photos && post.photos.length) { %>
<a href='<%= url_for(post.path) %>'>
<img class="border" src="<%- config.root %><%- post.photos[0] %>" alt="<%- post.title || 'Untitled' %>">
</a>
<% } %>
<div>
<div class="date smoll">
<a href='<%= url_for(post.path) %>'><%- include('date', { date: post.date.toISOString() }) %></a>
</div>
<p class="post_description"><%= post.description %></p>
<a href='<%= url_for(post.path) %>'><p>Read More...</p></a>
<div class="tags smoll">
<% post.tags.data.forEach(function(tag) { %>
<a href="<%= url_for('/tags/' + tag.slug || tag.name.replace(/\s+/g, '-')) %>">#<%= tag.name %></a>
<% }); %>
</div>
</div>

View file

@ -0,0 +1,17 @@
<div class="scarves">
<% theme.scarf.forEach(function(scarf, index) { %>
<% if (scarf.enable) { %>
<a href="<%= scarf.url %>">
<div class="scarf" title="<%= scarf.alt %>" style="right: <%= index * 30 %>px;
margin-top:-<%= ( (index % 5) * 10) + 40 %>px;
background-color:<%= scarf.color ? scarf.color : theme.primary_color %>;">
<% if(scarf.image) { %>
<img class= "logo" src="<%= url_for(scarf.image) %>" alt="<%= scarf.alt %>">
<% } else if(scarf.icon) { %>
<i class='logo <%= scarf.icon %>'></i>
<% } else%>
</div>
</a>
<% } %>
<% }); %>
</div>

9
layout/archive.ejs Normal file
View file

@ -0,0 +1,9 @@
<% site.tags.forEach(function(tag) { %>
<h2><%= tag.name %></h2>
<ul>
<% tag.posts.forEach(function(post) { %>
<li><a href="<%= post.url %>"><%= post.title %></a></li>
<% }); %>
</ul>
<% }); %>

1
layout/category.ejs Normal file
View file

@ -0,0 +1 @@
<%- include("_partial/feed", { item: page.posts.sort('-date') }) %>

1
layout/index.ejs Normal file
View file

@ -0,0 +1 @@
<%- include("_partial/feed", { item: site.posts.sort('-date') }) %>

133
layout/layout.ejs Normal file
View file

@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Metadata -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="<%= config.description %>">
<meta name="author" content="<%= config.author %>">
<meta name="title" content="<%= config.title %> - <%= config.subtitle %>">
<meta name="theme-color" content="<%= theme.primary_color %>">
<!-- Open Graph Meta Tags -->
<meta property="og:title" content="<%= config.title %>">
<meta property="og:description" content="<%= config.description %>">
<meta property="og:image" content="<%= url_for(theme.banner) %>">
<meta property="og:type" content="website">
<meta property="og:url" content="<%= config.url %>">
<script data-goatcounter="https://bashynx.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>
<link rel="icon" href="<%= url_for(theme.favicon) %>">
<script src="<%= url_for(theme.fontawesome) %>" crossorigin="anonymous"></script>
<title><%= config.title %></title>
<link rel="stylesheet" href="<%= url_for('/css/style.css') %>">
<style>
a {
color: <%= theme.secondary_color %>;
}
a:hover {
color: <%= theme.primary_color %>;
}
.page a,
.post-feed a {
color: <%= theme.primary_color %>;
}
.page a:hover,
.post-feed a:hover {
color: <%= theme.secondary_color %>;
}
.avatar img {
border: 2px solid <%= theme.primary_color %>;
}
.tags a {
color: #6c6f72 !important;
}
</style>
</head>
<body>
<main>
<% if (theme.scarves) { %>
<%- include('_partial/scarves') %>
<% } %>
<% if (theme.sidebar) { %>
<div class="sidebar">
<!-- Banner -->
<a href="/">
<div class="banner">
<img src="<%= url_for(theme.banner) %>" alt="<%= config.author %>">
</div>
</a>
<!-- Sidebar Content -->
<div class="sidebar-content">
<!-- Avatar -->
<div class="avatar">
<img src="<%= url_for(theme.avatar) %>" alt="<%= config.author %>">
</div>
<% if (config.description) { %>
<p><%= config.description %></p>
<% } %>
<!-- Sidebar Menu -->
<nav>
<% theme.sidebar_menu.forEach(function(menu) { %>
<% if (menu.enable) { %>
<a href="<%= menu.url %>">
<%= menu.name %>
</a>
<% } %>
<% }); %>
</nav>
<div class="social">
<% theme.social.forEach(function(social) { %>
<% if (social.enable) { %>
<span class="social-icon">
<% if (social.relme) { %>
<a rel="me" href="<%= social.url %>" target="_blank">
<% } else { %>
<a href="<%= social.url %>" target="_blank">
<% } %>
<% if (theme.fontawesome) { %>
<i class="<%= social.icon %>"></i>
<% } else { %>
<img src='<%= url_for(theme.avatar) %>'></img>
<% } %>
</a>
</span>
<% } %>
<% }); %>
</div>
</div>
<!-- Sidebar Footer -->
<div class="footer smoll">
<% if (theme.hexo) { %>
<div class="hexo footer">
Powered by Hexo, theme <a href="https://tapestry.demo.bashynx.dev" target="_blank">Tapestry Code (WIP)</a><br>
</div>
<% } %>
<% if (theme.copyright) { %>
<div class="copy footer">
&copy; <%= theme.copy_year %> <%= config.author %>
</div>
<% } %>
</div>
</div>
<% } %>
<div class="content">
<%- body %>
</div>
</main>
</body>
</html>

15
layout/page.ejs Normal file
View file

@ -0,0 +1,15 @@
<!-- This is the template for the page -->
<div class="pp page border">
<div class="post-header">
<h1><%= page.title || "Untitled" %></h1>
<% if(page.photos && page.photos.length) { %>
<div>
<img class="border" src="<%- config.root %><%- page.photos[0] %>" alt="<%- page.title || "Untitled" %>">
</div>
<% } %>
<p><%= page.description %></p>
</div>
<div class="post-content">
<%- page.content %>
</div>
</div>

27
layout/post.ejs Normal file
View file

@ -0,0 +1,27 @@
<!-- This is the template for the post -->
<div class="pp post border">
<div class="post-header">
<h1><%= page.title || "Untitled" %></h1>
<% if(page.photos && page.photos.length) { %>
<div>
<img class="border" src="<%- config.root %><%- page.photos[0] %>" alt="<%- page.title || "Untitled" %>">
</div>
<% } %>
<p><%= page.description %></p>
<div class="date smoll">
<a href='<%= url_for(page.path) %>'><%- include('_partial/date', { date: page.date.toISOString() }) %></a>
</div>
</div>
<div class="post-content">
<%- page.content %>
</div>
<div class="post-footer">
<div class="tags smoll">
<% if (page.tags) { %>
<% page.tags.data.forEach(function(tag) { %>
<a href="<%= url_for('/tags/' + tag.name) %>">#<%= tag.name %></a>
<% }); %>
<% } else {console.log(page)} %>
</div>
</div>
</div>

1
layout/tag.ejs Normal file
View file

@ -0,0 +1 @@
<%- include("_partial/feed", { item: page.posts.sort('-date') }) %>

299
source/css/style.css Normal file
View file

@ -0,0 +1,299 @@
body {
font-family: "Helvetica";
margin: auto;
font-size: 14px;
background-color: #343a40;
}
main{
padding: 20px;
}
h1, h2, h3, h4 {
font-weight: lighter;
}
h1, h2 {
text-align: center;
}
a {
text-decoration: none;
transition: color 1s;
}
/* Definition of small fonted text, example: footer */
.smoll {
font-size: 12px !important;
line-height: 12px;
}
/* Sidebar header section */
.sidebar {
margin-right: 1em;
margin-bottom: 0;
flex-shrink: 0;
}
.avatar img {
border-radius: 100%;
width: 50%;
margin: 15px auto;
position: relative;
z-index: 2;
}
/* Sidebar content section */
.sidebar-content {
padding: 1em;
margin-top: 35px;
background-color: #dcdcdf;
color:#343a40;
border-radius: 10px;
}
.sidebar-content p {
line-height: 17px;
font-size: 15px;
}
.social {
justify-content: center;
margin-top: 0.5em;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
}
.social-icon a {
padding: 5px;
box-sizing: border-box;
display:inline-block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 14px;
transition: color 0.3s;
}
.social-icon img {
width: 15px !important;
height: 15px !important;
border-radius: 50%;
}
/* Sidebar menu section */
nav a {
margin: 0.75rem auto;
padding: 0.25rem 0;
display: block;
background-color: #343a40;
color: #dcdcdf !important;
width: 75%;
height: 25px;
text-align: center;
line-height: 25px;
border-radius: 50px;
transition: color 0.3s;
}
/* Footer under the sidebar */
.footer {
color: #dcdcdf !important;
text-align: center;
margin-top: 5px;
}
.footer a {
color: #dcdcdf !important;
}
/* Flags */
.border{
border-radius: 10px;
}
/* Posts and pages */
.post-feed{
padding: 1em calc(1em + 10%);
margin: 0 auto 1em;
width: 60%;
align-items: center;
color: #dcdcdf;
background-color: #2e3237;
}
.post-feed h1{
color:#dcdcdf !important;
}
.pp{
padding: 1em calc(1em + 10%);
box-sizing: border-box;
}
.pp h1{
padding: 1em 0;
}
.post{
background-color: #dcdcdf;
}
.page{
color: #dcdcdf !important;
}
.page-divider {
margin-top: 1em;
border-top: 1px solid #343a40;
}
/* Images */
img {
max-width: 100%;
height: auto;
display: block;
opacity: 1;
transition: all 1s;
}
img:hover {
opacity: 0.95;
}
iframe {
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
}
/* Date */
.date {
display: flex;
}
.date a{
font-style: italic;
margin-left: auto;
text-align: right !important;
}
.post .date a{
color: #343a40 !important;
}
.post-feed .date a {
margin-top:1em;
color: #9fa5aa !important;
}
@media (max-width: 768px) {
.sidebar-content {
display: none;
}
.post_description {
display: none;
}
.footer {
display: none;
}
.content {
width: calc(100% - 1em);
}
}
/* Medium devices (tablets, 576px-768px) */
@media (min-width: 577px) and (max-width: 768px) {
body{
width: 576px;
}
}
@media (min-width: 769px) {
.sidebar {
width: 250px;
height: auto;
position: fixed;
z-index: 1;
}
.content {
width: calc(100% - 250px - 1em);
margin-left: calc(250px + 1em);
}
}
/* Large devices (desktops, 768px-992px) */
@media (min-width: 769px) and (max-width: 992px) {
body{
width: 769px;
}
}
@media (min-width: 993px) and (max-width: 1199px) {
body{
width: 993px;
}
}
@media (min-width: 1200px) {
body{
width: 1200px;
}
}
/* Post - Tags */
.tags {
text-align: right;
margin-bottom: 0em;
}
/* Scarves section */
.scarves {
position:fixed;
right: 0;
margin-right:50px;
z-index: 10;
}
.scarf {
width:25px;
height:100px;
position:absolute;
transition: all 1s, transform 1s;
-moz-transition: all 1s, -moz-transform 1s;
-webkit-transition: all 1s, -webkit-transform 1s;
-o-transition: all 1s, -o-transform 1s;
opacity:1;
}
.scarf .logo {
width:21px;
bottom: 0px;
font-size: 15px;
text-align: center;
padding: 5px 2px;
position: absolute;
object-fit: cover;
}
.scarf:hover {
margin-top:-25px !important;
transition: all 1s, transform 1s;
-moz-transition: all 1s, -moz-transform 1s;
-webkit-transition: all 1s, -webkit-transform 1s;
-o-transition: all 1s, -o-transform 1s;
opacity:0.9;
}
.scarf:hover img {
opacity: 0.75;
transition: opacity 1s;
}