displayAllHeaders but for a single Markdown file - vuepress

I am trying to make a sidebar in VuePress with multiple sidebar groups. However, for one of the elements of the sidebar array, the element is a single file, rather than a group. For example:
module.exports = {
themeConfig: {
sidebar: [
{
title: 'Group 1', // required
path: '/foo/', // optional, which should be a absolute path.
collapsable: false, // optional, defaults to true
sidebarDepth: 1, // optional, defaults to 1
children: [
'/'
]
},
{
title: 'Group 2',
children: [ /* ... */ ]
},
"my_md_file",
]
}
}
This works, but does not allow the user to see the subheadings of the Markdown file when on the children of other groups. I can use the displayAllHeaders to show the subheadings of the page even when not on it, but I only want this effect for a single page.

Insert this snippet instead of the condition in the SidebarLink.vue component on line 46 (I added just the first else if condition):
if (item.type === 'auto') {
return [link, renderChildren(h, item.children, item.basePath, $route, maxDepth)]
} else if (item.frontmatter.displayHeaders && item.headers && !hashRE.test(item.path)) {
const children = groupHeaders(item.headers)
return [link, renderChildren(h, children, item.path, $route)]
} else if ((active || displayAllHeaders) && item.headers && !hashRE.test(item.path)) {
const children = groupHeaders(item.headers)
return [link, renderChildren(h, children, item.path, $route, maxDepth)]
} else {
return link
}
Add this statement to the frontmatter part of your desired markdown file:
displayHeaders: true

Related

Dynamically create pages with Gatsby based on many Contentful references

I am currently using Gatsby's collection routes API to create pages for a simple blog with data coming from Contentful.
For example, creating a page for each blogpost category :
-- src/pages/categories/{contentfulBlogPost.category}.js
export const query = graphql`
query categoriesQuery($category: String = "") {
allContentfulBlogPost(filter: { category: { eq: $category } }) {
edges {
node {
title
category
description {
description
}
...
}
}
}
}
...
[React component mapping all blogposts from each category in a list]
...
This is working fine.
But now I would like to have multiple categories per blogpost, so I switched to Contentful's references, many content-type, which allows to have multiple entries for a field :
Now the result of my graphQL query on field category2 is an array of different categories for each blogpost :
Query :
query categoriesQuery {
allContentfulBlogPost {
edges {
node {
category2 {
id
name
slug
}
}
}
}
}
Output :
{
"data": {
"allContentfulBlogPost": {
"edges": [
{
"node": {
"category2": [
{
"id": "75b89e48-a8c9-54fd-9742-cdf70c416b0e",
"name": "Test",
"slug": "test"
},
{
"id": "568r9e48-t1i8-sx4t8-9742-cdf70c4ed789vtu",
"name": "Test2",
"slug": "test-2"
}
]
}
},
{
"node": {
"category2": [
{
"id": "75b89e48-a8c9-54fd-9742-cdf70c416b0e",
"name": "Test",
"slug": "test"
}
]
}
},
...
Now that categories are inside an array, I don't know how to :
write a query variable to filter categories names ;
use the slug field as a route to dynamically create the page.
For blogposts authors I was doing :
query authorsQuery($author__slug: String = "") {
allContentfulBlogPost(filter: { author: { slug: { eq: $author__slug } } }) {
edges {
node {
id
author {
slug
name
}
...
}
...
}
And creating pages with src/pages/authors/{contentfulBlogPost.author__slug}.js
I guess I'll have to use the createPages API instead.
You can achieve the result using the Filesystem API, something like this may work:
src/pages/category/{contentfulBlogPost.category2__name}.js
In this case, it seems that this approach may lead to some caveats, since you may potentially create duplicated pages with the same URL (slug) because the posts can contain multiple and repeated categories.
However, I think it's more succinct to use the createPages API as you said, keeping in mind that you will need to treat the categories to avoid duplicities because they are in a one-to-many relationship.
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions
const result = await graphql(`
query {
allContentfulBlogPost {
edges {
node {
category2 {
id
name
slug
}
}
}
}
}
`)
let categories= { slugs: [], names: [] };
result.data.allContentfulBlogPost.edges.map(({node}))=> {
let { name, slug } = node.category2;
// make some checks if needed here
categories.slugs.push(slug);
categories.names.push(name);
return new Set(categories.slugs) && new Set(categories.names);
});
categories.slugs.forEach((category, index) => {
let name = categories.names[index];
createPage({
path: `category/${category}`,
component: path.resolve(`./src/templates/your-category-template.js`),
context: {
name
}
});
});
}
The code's quite self-explanatory. Basically you are defining an empty object (categories) that contains two arrays, slugs and names:
let categories= { slugs: [], names: [] };
After that, you only need to loop through the result of the query (result) and push the field values (name, slug, and others if needed) to the previous array, making the needed checks if you want (to avoid pushing empty values, or that matches some regular expression, etc) and return a new Set to remove the duplicates.
Then, you only need to loop through the slugs to create pages using createPage API and pass the needed data via context:
context: {
name
}
Because of redundancy, this is the same than doing:
context: {
name: name
}
So, in your template, you will get the name in pageContext props. Replace it with the slug if needed, depending on your situation and your use case, the approach is exactly the same.

How to get 'Last Update Date' of a blog post in GATSBY.js

Hello I'm not a dev so may the question will be easy for you guys. I used the advance starter from gatsby site. The blog is working perfect but I need to provide the LAST UPDATED time under my title. Searched for some solutions but none of them worked. Could you Provide some help?
gatsby-node.js
exports.onCreateNode = ({ node, getNode, actions }) => {
const { createNodeField } = actions;
if (node.internal.type !== 'MarkdownRemark') {
return;
}
const fileNode = getNode(node.parent);
createNodeField({
node,
name: 'modifiedTime',
value: fileNode.mtime
});
};
`````````````````````````
PostListing.jsx
class PostListing extends React.Component {
getPostList() {
const postList = [];
this.props.postEdges.forEach(postEdge => {
postList.push({
path: postEdge.node.fields.slug,
tags: postEdge.node.frontmatter.tags,
cover: postEdge.node.frontmatter.cover,
title: postEdge.node.frontmatter.title,
date: postEdge.node.fields.date,
excerpt: postEdge.node.excerpt,
timeToRead: postEdge.node.timeToRead,
modifiedTime:postEdge.node.modifiedTime
});
});
return postList;
}
render() {
const postList = this.getPostList();
return (
<div className='posts'>
{/* Your post list here. */
postList.map(post => (
<Fragment>
<div className='singlePost__date'>
<h4 style={{color:'white'}}> {post.modifiedTime}</h4>
</div>
<div className='singlePost__Title'>
<Link classname='singlePost' to={post.path} key={post.title}>
<h1 className='singlePost__title'>{post.title}</h1>
</Link>
</div>
</Fragment>
))}
</div>
);
}
}
export default PostListing;
I expect something like
TITLE
last updated : 3/2/2019
You can use information stored in Git to get the latest time when a file was modified.
1st approach
Track it manually, but this can be error-prone if you forget to edit the modified time. So I would recommend that as the last option if you can't get others to work.
2nd approach
You can edit your gatsby-node.js to pull information from Git like so:
const { execSync } = require("child_process")
exports.onCreateNode = ({ node, actions }) => {
// ...
if (node.internal.type === "MarkdownRemark") {
const gitAuthorTime = execSync(
`git log -1 --pretty=format:%aI ${node.fileAbsolutePath}`
).toString()
actions.createNodeField({
node,
name: "gitAuthorTime",
value: gitAuthorTime,
})
}
// ...
}
Then, in your template, you can fetch it:
query($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
# ...
fields {
gitAuthorTime
}
# ...
}
}
And, finally, use it in JSX like so:
import React from "react"
const BlogPost = (props) => {
const { gitAuthorTime } = props.data.markdownRemark.fields
render(<p>Updated at: ${gitAuthorTime}</p>)
}
export default BlogPost
3rd approach
Similar to the previous one but it uses a plugin gatsby-transformer-info. It does a similar thing as in the 2nd approach, but you need to access the modified time differently this time. Like so:
query($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
# ...
parent {
... on File {
fields {
gitLogLatestDate
}
}
}
# ...
}
}
I wrote more about this in my blog post "Add Updated At To Your Gatsby Blog" if you want to check it out.
Edit: The answer below is actually wrong, since File. modifiedTime is the modifiedTime of the markdown file itself & not the modifiedTime for your content. For example, if you deploy your blog on say, Netlify, then the modifiedTime of your files there will be different than in your local environment.
I think the right answer is to track it separately. If you're using a CMS like NetlifyCMS, you can create a field that automatically update the date/time on every edit.
Wherever you're querying for your markdown files, you can use the below field:
query {
allMarkdownRemark {
edges {
node {
frontmatter { /* other stuff */ }
parent {
... on File {
modifiedTime(formatString: "MM/DD/YYYY")
}
}
}
}
}
}
And access it in your via postEdge.node.parent.modifiedTime

ESLint moving comments to align with parent, not children

I'm running eslint on a project with some promises, and it has strange rules for pre-comment spacing. A sample file (simplified for readability, with exactly the same syntax) is below:
const myFunction = (myUrl) => {
// This comment spacing makes sense
fetch(myUrl)
.then(response => {
// This is the first comment whose spacing is broken
if(response.status === 404) {
// Another strangely spaced comment
window.location.reload();
} else {
// The last strange comment
preformSuccessfulAction(myUrl);
}
})
.catch(() => {
console.error("no internet connection");
});
}
My .eslintrc.json looks like this:
{
"extends": "airbnb",
"env": {
"es6": true
},
"rules": {
"indent": ["error", 4, {"SwitchCase": 1, "ignoreComments": false}],
"spaced-comment": ["error", "always", {"markers": ["TODO"]}]
},
"globals": {
"fetch": true
}
}
I've attempted to fix the files individually and ignore the comment spacing, which does work, but will not ensure consistency across this project. Is there a lint rule which allows for this specific instance of comment alignment?

How to define Gatsby markdown page paths in one file?

Bu default, Gatsby uses frontmatter for defining paths, like:
---
path: /example-page
---
and then works with it via GraphQL.
What is the best way to define those paths for all markdown files by not writing frontmatter part in every file, but in one file, as an instance, like this:
[
{
"title": "Example",
"path": "/example-page.md"
}
]
You can do that by adding the path when the page is created.
Add this in gatsby-node :
const { createFilePath } = require(`gatsby-source-filesystem`);
exports.onCreateNode = ({ node, getNode, boundActionCreators }) => {
const { createNodeField } = boundActionCreators
if (node.internal.type === `MarkdownRemark`) {
const slug = createFilePath({
node,
getNode,
basePath: `pages`
})
createNodeField({
node,
name: `slug`,
value: `/pages${slug}`
})
}
};
createFilePath turn markdown files in pages directory into /pages/slug.
createNodeField creates new query'able field with name of 'slug'.
Now in graphql you can access the slug :
{
allMarkdownRemark {
edges {
node {
fields {
slug
}
}
}
}
}
Then you can create your pages as usual using the new slug field as page path.
With that you can add your title and all you want in data accessible in graphql.
Example here : https://www.gatsbyjs.org/tutorial/part-seven/

Extjs validate in separate files

I'm trying to validate fields in my form, but I keep getting an error message.
Here is my code:
Ext.define('ExtDoc.views.extfields.FieldsValidator',{
valEng: function(val) {
var engTest = /^[a-zA-Z0-9\s]+$/;
Ext.apply(Ext.form.field.VTypes, {
eng: function(val, field) {
return engTest.test(val);
},
engText: 'Write it in English Please',
// vtype Mask property: The keystroke filter mask
engMask: /[a-zA-Z0-9_\u0600-\u06FF\s]/i
});
}
});
And I define my field as follow:
{
"name": "tik_moed_chasifa",
"type": "ExtDoc.views.extfields.ExtDocTextField",
"label": "moed_hasifa",
"vtype": "eng",
"msgTarget": "under"
}
The first snippet is in a separate js file, and I have it in my fields js file as required.
When I start typing text in the text field, I keep seeing the following error msg in the explorer debugger:
"SCRIPT438: Object doesn't support property or method 'eng' "
What could it be? Have I declared something wrong?
You have defined your own class with a function valEng(val), but you don't instantiate it, neither do you call the function anywhere.
Furthermore, your function valEng(val) does not require a parameter, because you are not using that parameter anywhere.
It would be far easier and more readable, would you remove the Ext.define part and create the validators right where you need them. For instance if you need them inside an initComponent function:
initComponent:function() {
var me = this;
Ext.apply(Ext.form.field.VTypes, {
mobileNumber:function(val, field) {
var numeric = /^[0-9]+$/
if(!Ext.String.startsWith(val,'+')) return false;
if(!numeric.test(val.substring(1))) return false;
return true;
},
mobileNumberText:'This is not a valid mobile number'
});
Ext.apply(me,{
....
items: [{
xtype:'fieldcontainer',
items:[{
xtype: 'combobox',
vtype: 'mobileNumber',
Or, you could add to your Application.js, in the init method, if you need it quite often at different levels of your application:
Ext.define('MyApp.Application', {
extend: 'Ext.app.Application',
views: [
],
controllers: [
],
stores: [
],
init:function() {
Ext.apply(Ext.form.field.VTypes, {
mobileNumber:function(val, field) {
var numeric = /^[0-9]+$/
if(!Ext.String.startsWith(val,'+')) return false;
if(!numeric.test(val.substring(1))) return false;
return true;
},
mobileNumberText:'This is not a valid mobile number'
});
}

Resources