diff --git a/.frontmatter/database/pinnedItemsDb.json b/.frontmatter/database/pinnedItemsDb.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/.frontmatter/database/pinnedItemsDb.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.frontmatter/database/taxonomyDb.json b/.frontmatter/database/taxonomyDb.json new file mode 100644 index 000000000..c6da0bcff --- /dev/null +++ b/.frontmatter/database/taxonomyDb.json @@ -0,0 +1,92 @@ +{ + "taxonomy": { + "tags": [], + "categories": [ + "Announcements", + "Books", + "DevOps", + "Events", + "Graph", + "In Case You Missed It", + "News", + "PowerShell Summit", + "PowerShell for Admins", + "PowerShell for Developers", + "Scripting Games", + "Tips and Tricks", + "Tools", + "Training", + "Tutorials" + ] + }, + "customTaxonomy": [ + { + "id": "authors", + "options": [ + "Aaron Jensen", + "Adam Bertram", + "Adam Platt", + "Alex Aymonier", + "Art Beane", + "Bartek Bielawski", + "Bjorn Houben", + "Boe Prox", + "Brian Bourque", + "Carlo Mancini", + "Chris Martin", + "Cole McDonald", + "Colyn Via", + "Darren Mar-Elia", + "David Jones", + "David Wilson", + "Don Jones", + "Eli Hess", + "Enrique Puig", + "Eric Brookman (scriptingcaveman)", + "Glenn Sizemore", + "Graham Beer", + "Greg Altman", + "Greg Tate", + "Jaap Brasser", + "Jacob Benson", + "Jacob Moran", + "James Petty", + "Jeffery Hicks", + "Joel Newton", + "John Mello", + "Jonas Sommer Nielsen", + "Jonathan Medd", + "Jonathan Walz", + "June Blender", + "Keith Hill", + "Kirk Munro", + "Liam Kemp", + "Mark Kraus (markekraus)", + "Mark Roloff", + "Mark Wragg", + "Matt Laird", + "Matthew Hodgkins", + "Mike F Robbins", + "Mike Kanakos", + "Mike Roberts", + "Missy Januszko", + "Nathaniel Webb (ArtisanByteCrafter)", + "Nick Rimmer", + "Richard Siddaway", + "Robin Dadswell", + "Stephen Moore", + "Stephen Owen", + "Steve Parankewich", + "Steven Murawski", + "Sunny Chakraborty", + "Terri Donahue", + "Thomas Malkewitz", + "Thomas Rayner, MVP", + "Tim Curwick", + "Timothy Warner", + "WeiYen Tan", + "Will Anderson" + ] + } + ] +} diff --git a/.github/ISSUE_TEMPLATE/guest-blog-post.yml b/.github/ISSUE_TEMPLATE/guest-blog-post.yml index 2c0f9eddc..09386c1d6 100644 --- a/.github/ISSUE_TEMPLATE/guest-blog-post.yml +++ b/.github/ISSUE_TEMPLATE/guest-blog-post.yml @@ -36,6 +36,48 @@ body: validations: required: true + - type: input + id: description + attributes: + label: Description + description: "A 1-2 sentence summary used for SEO, social cards, and the article list." + placeholder: "Learn how to enable and customize PSReadLine predictive IntelliSense for a faster console." + validations: + required: true + + - type: dropdown + id: category + attributes: + label: Category + description: "Pick the category that best fits your article." + options: + - "Announcements" + - "Books" + - "DevOps" + - "Events" + - "Graph" + - "In Case You Missed It" + - "News" + - "PowerShell Summit" + - "PowerShell for Admins" + - "PowerShell for Developers" + - "Scripting Games" + - "Tips and Tricks" + - "Tools" + - "Training" + - "Tutorials" + validations: + required: true + + - type: input + id: tags + attributes: + label: Tags (optional) + description: "Comma-separated keywords, e.g. psreadline, console, productivity." + placeholder: "psreadline, console, productivity" + validations: + required: false + - type: textarea id: summary attributes: diff --git a/.github/workflows/add-article.yml b/.github/workflows/add-article.yml new file mode 100644 index 000000000..d1667be23 --- /dev/null +++ b/.github/workflows/add-article.yml @@ -0,0 +1,128 @@ +name: Add Guest Article + +on: + issues: + types: [labeled] + +jobs: + add-article: + if: | + github.event.label.name == 'approved' && + contains(github.event.issue.labels.*.name, 'article') + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Parse issue and create article content file + id: parse + env: + ISSUE_BODY: ${{ github.event.issue.body }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + ISSUE_TITLE: ${{ github.event.issue.title }} + run: | + python3 - <<'PYEOF' + import os, re, datetime, yaml + + # Normalize line endings — issue bodies can contain CRLF. + body = os.environ['ISSUE_BODY'].replace('\r\n', '\n').replace('\r', '\n') + + def strip_html(s): + # Strip raw HTML from scalar front matter values (matches add-event.yml). + return re.sub(r'<[^>]+>', '', s).strip() + + def field(label): + m = re.search(rf'### {re.escape(label)}\s+(.+?)(?=\n###|\Z)', body, re.DOTALL) + if not m: + return '' + val = m.group(1).strip() + return '' if val in ('_No response_', '') else val + + title = strip_html(field('Article Title')) + author = strip_html(field('Author Name')) + description = strip_html(field('Description')) + category = strip_html(field('Category')) + tags_raw = strip_html(field('Tags (optional)')) + content = field('Article Content (Markdown)') + + # The Article Content field is rendered as a ```markdown fenced block — unwrap it, + # tolerating surrounding whitespace/blank lines around the fences. + content = re.sub(r'^\s*```[a-zA-Z]*[ \t]*\n', '', content) + content = re.sub(r'\n```[ \t]*\s*$', '', content).strip() + + if not title: + raise SystemExit('No article title found; aborting.') + if not content: + # A pitch with no draft — nothing to publish yet. + print('No article content provided (pitch only); skipping file creation.') + with open(os.environ['GITHUB_OUTPUT'], 'a') as out: + out.write('skip=true\n') + raise SystemExit(0) + + tags = [t.strip() for t in re.split(r'[,\n]', tags_raw) if t.strip()] + + today = datetime.datetime.now(datetime.timezone.utc) + date_str = today.strftime('%Y-%m-%dT%H:%M:%S+00:00') + + slug = re.sub(r'[^a-z0-9]+', '-', title.lower()).strip('-') + if not slug: + # Title was only punctuation / non-ASCII — fall back to the issue number. + slug = f"article-{os.environ['ISSUE_NUMBER']}" + filename = f"{today.strftime('%Y-%m-%d')}-{slug}.md" + + fm = { + 'title': title, + 'author': author, + 'authors': [author] if author else [], + 'date': date_str, + 'description': description, + } + if category: + fm['categories'] = [category] + if tags: + fm['tags'] = tags + + front = yaml.safe_dump(fm, default_flow_style=False, allow_unicode=True, sort_keys=False) + document = '---\n' + front + '---\n\n' + content + '\n' + + filepath = f'content/articles/{filename}' + os.makedirs('content/articles', exist_ok=True) + with open(filepath, 'w', encoding='utf-8') as f: + f.write(document) + + with open(os.environ['GITHUB_OUTPUT'], 'a') as out: + out.write(f'skip=false\n') + out.write(f'slug={slug}\n') + out.write(f'filepath={filepath}\n') + out.write(f'article_title={title}\n') + + print(f'Created {filepath}') + PYEOF + + - name: Open PR + if: steps.parse.outputs.skip == 'false' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SLUG: ${{ steps.parse.outputs.slug }} + FILEPATH: ${{ steps.parse.outputs.filepath }} + ARTICLE_TITLE: ${{ steps.parse.outputs.article_title }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + run: | + BRANCH="article/issue-${ISSUE_NUMBER}-${SLUG}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" + git add "$FILEPATH" + git commit -m "Add article: ${ARTICLE_TITLE} (closes #${ISSUE_NUMBER})" + git push origin "$BRANCH" + gh pr create \ + --title "Add article: ${ARTICLE_TITLE}" \ + --body "Closes #${ISSUE_NUMBER} + + Auto-generated from guest blog post submission. Please review front matter and content before merging." \ + --base main \ + --head "$BRANCH" diff --git a/.gitignore b/.gitignore index 364fdec1a..70e7efd49 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ public/ + +# Local Netlify folder +.netlify diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..24889420f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "eliostruyf.vscode-front-matter", + "davidanson.vscode-markdownlint", + "yzhang.markdown-all-in-one" + ] +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e5374a55e..ca9c1f096 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,6 +27,7 @@ If you're not comfortable with Git, you can pitch or submit your article through ```yaml --- title: "Your Article Title" + description: "A 1-2 sentence summary used for SEO, social cards, and the article list." author: Your Name authors: - Your Name @@ -41,6 +42,11 @@ If you're not comfortable with Git, you can pitch or submit your article through Your article content in Markdown goes here. ``` + > Tip: If you have the [Front Matter CMS](https://frontmatter.codes/) extension + > installed in VS Code, run **"Create content"** in the `content/articles` + > folder — it scaffolds the file name (`YYYY-MM-DD-slug.md`) and all of the + > front matter fields above for you. + 5. Submit a pull request with a brief description of your article ### Writing Tips diff --git a/archetypes/articles.md b/archetypes/articles.md new file mode 100644 index 000000000..1bf867f56 --- /dev/null +++ b/archetypes/articles.md @@ -0,0 +1,11 @@ +--- +title: '{{ replace .File.ContentBaseName "-" " " | title }}' +description: "" +author: "" +authors: + - "" +date: '{{ .Date }}' +categories: [] +tags: [] +draft: true +--- diff --git a/archetypes/authors.md b/archetypes/authors.md new file mode 100644 index 000000000..76764ce08 --- /dev/null +++ b/archetypes/authors.md @@ -0,0 +1,8 @@ +--- +title: '{{ replace .File.ContentBaseName "-" " " | title }}' +description: "" +layout: "authors" +website: "" +twitter: "" +github: "" +--- diff --git a/archetypes/calendar.md b/archetypes/calendar.md new file mode 100644 index 000000000..e8a78ad3a --- /dev/null +++ b/archetypes/calendar.md @@ -0,0 +1,8 @@ +--- +title: '{{ replace .File.ContentBaseName "-" " " | title }}' +startDate: '{{ .Date | dateFormat "2006-01-02" }}' +endDate: "" +where: "" +externalUrl: "" +virtual: false +--- diff --git a/archetypes/default.md b/archetypes/default.md index 25b67521d..bdca4fcfa 100644 --- a/archetypes/default.md +++ b/archetypes/default.md @@ -1,5 +1,5 @@ -+++ -date = '{{ .Date }}' -draft = true -title = '{{ replace .File.ContentBaseName "-" " " | title }}' -+++ +--- +title: '{{ replace .File.ContentBaseName "-" " " | title }}' +date: '{{ .Date }}' +draft: true +--- diff --git a/archetypes/podcast.md b/archetypes/podcast.md new file mode 100644 index 000000000..36674a171 --- /dev/null +++ b/archetypes/podcast.md @@ -0,0 +1,10 @@ +--- +title: '{{ replace .File.ContentBaseName "-" " " | title }}' +description: "" +author: "" +authors: + - "" +date: '{{ .Date }}' +podcast_url: "" +draft: true +--- diff --git a/frontmatter.json b/frontmatter.json new file mode 100644 index 000000000..3cd53dc29 --- /dev/null +++ b/frontmatter.json @@ -0,0 +1,288 @@ +{ + "$schema": "https://frontmatter.codes/frontmatter.schema.json", + "frontMatter.framework.id": "hugo", + "frontMatter.preview.host": "http://localhost:1313", + "frontMatter.content.publicFolder": "static", + "frontMatter.content.defaultContentType": "page", + "frontMatter.content.dateFormat": "yyyy-MM-dd'T'HH:mm:ssxxx", + "frontMatter.taxonomy.customTaxonomy": [ + { + "id": "authors", + "options": [] + } + ], + "frontMatter.taxonomy.contentTypes": [ + { + "name": "article", + "pageBundle": false, + "previewPath": "/articles", + "fields": [ + { + "title": "Title", + "name": "title", + "type": "string", + "required": true + }, + { + "title": "Description", + "name": "description", + "type": "string", + "description": "1-2 sentence summary used for SEO, social cards, and article list excerpts.", + "required": true + }, + { + "title": "Author", + "name": "author", + "type": "string", + "description": "Display name of the primary author.", + "required": true + }, + { + "title": "Authors", + "name": "authors", + "type": "list", + "taxonomyId": "authors", + "description": "One or more authors. Drives the author taxonomy pages." + }, + { + "title": "Publishing date", + "name": "date", + "type": "datetime", + "default": "{{now}}", + "isPublishDate": true + }, + { + "title": "Categories", + "name": "categories", + "type": "categories" + }, + { + "title": "Tags", + "name": "tags", + "type": "tags" + }, + { + "title": "Aliases", + "name": "aliases", + "type": "list", + "description": "Legacy URLs that should redirect to this article." + }, + { + "title": "Content preview", + "name": "preview", + "type": "image" + }, + { + "title": "Is in draft", + "name": "draft", + "type": "draft" + } + ] + }, + { + "name": "podcast", + "pageBundle": false, + "previewPath": "/podcast", + "fields": [ + { + "title": "Title", + "name": "title", + "type": "string", + "required": true + }, + { + "title": "Description", + "name": "description", + "type": "string", + "description": "1-2 sentence summary of the episode." + }, + { + "title": "Author", + "name": "author", + "type": "string", + "required": true + }, + { + "title": "Authors", + "name": "authors", + "type": "list", + "taxonomyId": "authors" + }, + { + "title": "Publishing date", + "name": "date", + "type": "datetime", + "default": "{{now}}", + "isPublishDate": true + }, + { + "title": "Podcast URL", + "name": "podcast_url", + "type": "string", + "description": "Direct URL to the episode audio file (mp3)." + }, + { + "title": "Aliases", + "name": "aliases", + "type": "list" + }, + { + "title": "Is in draft", + "name": "draft", + "type": "draft" + } + ] + }, + { + "name": "event", + "pageBundle": false, + "previewPath": "/calendar", + "fields": [ + { + "title": "Title", + "name": "title", + "type": "string", + "required": true + }, + { + "title": "Start date", + "name": "startDate", + "type": "datetime", + "dateFormat": "yyyy-MM-dd", + "isPublishDate": true, + "required": true + }, + { + "title": "End date", + "name": "endDate", + "type": "datetime", + "dateFormat": "yyyy-MM-dd" + }, + { + "title": "Where", + "name": "where", + "type": "string", + "description": "Location, e.g. \"Prague, Czech Republic\" or \"Online\"." + }, + { + "title": "External URL", + "name": "externalUrl", + "type": "string", + "description": "Link to the event website or registration page." + }, + { + "title": "Virtual", + "name": "virtual", + "type": "boolean", + "default": false + } + ] + }, + { + "name": "page", + "pageBundle": false, + "previewPath": null, + "fields": [ + { + "title": "Title", + "name": "title", + "type": "string", + "required": true + }, + { + "title": "Description", + "name": "description", + "type": "string" + }, + { + "title": "Layout", + "name": "layout", + "type": "choice", + "description": "Theme layout used to render this section page.", + "choices": [ + "staticpage", + "learning", + "summit", + "plaster", + "authors", + "calendar" + ] + }, + { + "title": "Is in draft", + "name": "draft", + "type": "draft" + } + ] + }, + { + "name": "author", + "pageBundle": false, + "previewPath": "/authors", + "fields": [ + { + "title": "Name", + "name": "title", + "type": "string", + "required": true + }, + { + "title": "Bio", + "name": "description", + "type": "string", + "description": "Short author biography." + }, + { + "title": "Layout", + "name": "layout", + "type": "string", + "default": "authors" + }, + { + "title": "Website", + "name": "website", + "type": "string" + }, + { + "title": "Twitter / X", + "name": "twitter", + "type": "string" + }, + { + "title": "GitHub", + "name": "github", + "type": "string" + } + ] + } + ], + "frontMatter.content.pageFolders": [ + { + "title": "Articles", + "path": "[[workspace]]/content/articles", + "contentTypes": ["article"], + "filePrefix": "{{year}}-{{month}}-{{day}}" + }, + { + "title": "Podcast", + "path": "[[workspace]]/content/podcast", + "contentTypes": ["podcast"], + "filePrefix": "{{year}}-{{month}}-{{day}}" + }, + { + "title": "Events", + "path": "[[workspace]]/content/calendar", + "contentTypes": ["event"] + }, + { + "title": "Authors", + "path": "[[workspace]]/content/authors", + "contentTypes": ["author"] + }, + { + "title": "Pages", + "path": "[[workspace]]/content", + "contentTypes": ["page"] + } + ] +}